The Standard Architecture
The Standard Architecture
What this skill is
This skill operationalizes the architecture chapters of The Standard. It governs how systems are decomposed, how responsibilities are assigned, and how dependencies flow. It includes the architecture of models, brokers, services, aggregation, exposers, communication protocols, APIs, and user interfaces.
Explicit coverage map
This skill explicitly covers:
-
0.1.2 Modeling
- Data Carrier Models
- Primary Models
- Secondary Models
- Relational Models
- Hybrid Models
- Operational Models
- Integration Models (Brokers)
- Processing Models (Services)
- Exposure Models (Exposers)
- Configuration Models
- Data Carrier Models
-
1 Brokers
- Introduction
- On The Map
- Characteristics
- Implements a Local Interface
- No Flow Control
- No Exception Handling
- Own Their Configurations
- Natives from Primitives
- Naming Conventions
- Language
- Up & Sideways
- One Resource, One Broker
- Organization
- Broker Types
- Entity Brokers
- Support Brokers
- Implementation
- Asynchronization Abstraction
- FAQs / Clarifications
- Standardized Provider Abstraction Libraries (SPAL)
- Extensibility
- Configurability
- Distributability
- External Mockability (Cloud-Foreign)
- Local to Global
-
2 Services
- Introduction
- Services Operations
- Validations
- Processing
- Integration
- Services Types
- Validators
- Orchestrators
- Aggregators
- Overall Rules
- Do or Delegate
- Two-Three (Florance Pattern)
- Single Exposure Point
- Same or Primitives I/O Model
- Every Service for Itself
- Flow Forward
- For APIs
- Foundation Services (Broker-Neighboring)
- Introduction
- On The Map
- Characteristics
- Pure-Primitive
- Single Entity Integration
- Business Language
- Responsibilities
- Abstraction
- Validation
- Circuit-Breaking Validations
- Continuous Validations
- Upsertable Exceptions
- Dynamic Rules
- Rules & Validations Collector
- Hybrid Continuous Validations
- Structural Validations
- Logical Validations
- External Validations
- Dependency Validations
- Mapping
- Non-Local Models
- Exception Handling
- Exceptions Mappings
- Localise External Exceptions
- Caragorize Exceptions
- Logging
- Exceptions Mappings
- Processing Services (Higher-Order Business Logic)
- Introduction
- On The Map
- Characteristics
- Language
- Functions Language
- Pass-Through
- Class-Level Language
- Dependencies
- One-Foundation
- Used-Data-Only Validations
- Responsibilities
- Higher-Order Logic
- Shifters
- Combinations
- Signature Mapping
- Non-Exception Local Models
- Exception Handling
- Unwrap and Localise Foundation Exceptions
- Caragorize Exceptions
- Logging
- Orchestration Services (Complex Higher Order Logic)
- Introduction
- On The Map
- Characteristics
- Language
- Functions Language
- Pass-Through
- Class-Level Language
- Dependencies
- Dependency Balance (Florance Pattern)
- Two-Three
- Full-Normalization
- Semi-Normalization
- No-Normalization
- Meaningful Breakdown
- Contracts
- Physical Contracts
- Virtual Contracts
- Cul-De-Sac
- Responsibilities
- Advanced Logic
- Flow Combinations
- Call Order
- Natural Order
- Enforced Order
- Exception Handling
- Unwrap and Localise Foundation Exceptions
- Caragorize Exceptions
- Logging
- Variations
- Coordination
- Management
- Uber Management
- Unit of Work
- Aggregation Services (The Knot)
- Introduction
- On The Map
- Characteristics
- No Dependency Limitation
- No Order Validation
- Basic Validations
- Pass-Through
- Optionality
- Routine-Level Aggregation
- Pure Dependency Contracts
- Responsibilities
- Abstraction
- Exceptions Aggregation
-
3 Exposers
- Introduction
- Purpose
- Pure Mapping
- Types of Exposure Components
- Communication Protocols
- User Interfaces
- I/O Components
- Single Point of Contact
- Summary
-
3.1 Communication Protocols
- Principles & Rules
- Results Communication
- Error Reports
- RESTful APIs
- Language
- Beyond CRUD Routines
- Similar Verbs
- Route Conventions
- Nouns & Verbs
- Controller Routes
- Routine/Method-Specific Routes
- Plural-Singular-Plural
- Query Parameters & OData
- X-WWW-Form-UrlEncoded Parameters
- Codes & Responses
- Success Codes (2xx)
- User Error Codes (4xx)
- System Error Codes (5xx)
- All Codes
- Single Dependency
- Single Contract
- Organization
- Home Controller
- Principles & Rules
-
3.2 User Interfaces
- Principles & Rules
- Progress (Loading)
- Basic Progress
- Remaining Progress
- Detailed Progress
- Results
- Simple
- Partial Details
- Full Details
- Error Reports
- Informational
- Referential / Implicit Actions
- Actionable
- Single Dependency
- Anatomy
- Bases
- Components
- Containers
- UI Component Types
- Web Applications
- Mobile Applications
- Other Types
- Web Applications
- Anatomy
- Base Component
- Implementation
- Utilization
- Restrictions
- Core Component
- Elements
- Existence
- Property Assignment
- Searching by Id
- General Search
- Properties
- Actions
- Existence
- Styles
- Actions
- Restrictions
- Elements
- Pages
- Unobtrusiveness
- Organization
- Base Component
- Progress (Loading)
The following topics are governed by dedicated skills that extend this skill. See
## Related skillsfor activation guidance:- CulDeSac event brokers, event services, publish/subscribe, DI registration, and startup activation →
the-standard-events - Release versioning, file versioning, API versioning, and deprecation →
the-standard-versioning
- Principles & Rules
When to use
Use this skill for system design, architecture review, decomposition, refactoring, API design, UI architecture, dependency-flow design, or when deciding where behavior belongs.
Related skills
This skill defines the structural rules for the entire system. When a specific architectural pattern is chosen, a dedicated skill governs its detailed implementation. Activate the relevant skill as soon as the pattern is identified -- do not wait until implementation is underway.
| Pattern | Skill | Activate when |
|---|---|---|
| CulDeSac eventing: event broker, event service, publish/subscribe, DI registration, startup activation | the-standard-events |
An orchestration service needs to publish a domain event or subscribe to one without a synchronous return |
| Release versioning, file versioning, API versioning, deprecation | the-standard-versioning |
A model, service, or API contract change is introduced, a release is being cut, or existing code must be deprecated |
System-wide architecture rules
- Every system must be decomposed into purpose, dependency, and exposure.
- At low level, that means:
- Brokers = dependencies
- Services = purpose
- Exposers = exposure
- Keep the flow forward:
- Exposer -> Service -> Broker -> External resource
- Never reverse the flow.
- Never blur layer boundaries.
- Never hide architecture behind magical abstractions.
Modeling rules
Data-carrier models
- Primary models are pillars of the system.
- They are physically self-sufficient.
- Secondary models depend on primary models.
- Relational models connect primary models and should mainly hold references.
- Hybrid models are allowed only when a relationship itself must carry state or details.
- Keep physical models anemic and flat.
- Use virtual contracts only when orchestration truly needs them.
Operational models
- Integration models are brokers.
- Processing models are services.
- Exposure models are exposers.
- Configuration models compose the system and manage startup, DI, middleware, or platform-specific configuration.
Broker rules
- A broker is a liaison between business logic and the outside world.
- Brokers must implement a local interface.
- Brokers must contain no business logic.
- Brokers must contain no flow control.
- No if statements for business decisions.
- No loops for business decisions.
- No switch cases for business decisions.
- Brokers must not handle exceptions.
- Let native exceptions propagate to broker-neighboring services.
- Brokers must own their configurations.
- Brokers may construct native models from primitive inputs.
- Brokers must speak the language of their technology.
- Storage -> Insert / Select / Update / Delete
- Queue -> Enqueue / Dequeue
- REST -> Get / Post / Put / Delete / custom HTTP method when needed
- Brokers cannot call other brokers.
- Brokers cannot depend on services.
- One resource, one broker.
- Use support brokers for generic capabilities such as time and logging.
- Use entity brokers / api brokers for resource- or entity-specific integrations.
- Prefer partial interfaces and partial classes to organize multi-entity brokers.
- Prefer generic helper methods in broker root partials so entity partials do not touch native clients directly.
- Use asynchronous abstractions consistently.
- Prefer ValueTask in Standard examples and abstractions when that aligns with the implementation profile.
- Brokers live under Brokers/ and their namespaces.
- Broker configurations live in appsettings.json or equivalent configuration stores
- Broker configuration classes live under Brokers/ and their namespaces.
Asynchronization Abstraction (§1.5.1)
Every publicly exposed interface method — on brokers, services, and exposers — must
return ValueTask or ValueTask<T>, even if the current implementation does not
internally await anything.
This is The Standard §1.5.1: the public contract is uniformly async so callers never need to change if an implementation later becomes truly asynchronous.
| Pattern | Verdict |
|---|---|
public async ValueTask LogWarningAsync(string message) => this.logger.LogWarning(message); |
Correct — async keyword, direct call |
public ValueTask<IQueryable<Student>> SelectAllStudentsAsync() => ValueTask.FromResult(...) |
Correct — ValueTask.FromResult wraps sync result |
public async ValueTask<IQueryable<Student>> SelectAllStudentsAsync() => await this.SelectAllAsync<Student>(); |
Correct — async/await through generic helper |
public IQueryable<Student> SelectAllStudents() => this.Students.AsNoTracking(); |
WRONG — synchronous, no ValueTask |
public ValueTask LogWarningAsync(string message) => new ValueTask(Task.Run(() => ...)); |
WRONG — Task.Run wraps a synchronous call needlessly |
Consequence for services: CreateAndLog* helpers must be async too, because
ILoggingBroker.LogErrorAsync returns ValueTask. Catch blocks must use throw await:
// WRONG
private StudentValidationException CreateAndLogValidationException(Xeption exception)
{
...
this.loggingBroker.LogError(studentValidationException); // no such sync method
return studentValidationException;
}
// CORRECT
private async ValueTask<StudentValidationException> CreateAndLogValidationException(
Xeption exception)
{
var studentValidationException = new StudentValidationException(...);
await this.loggingBroker.LogErrorAsync(studentValidationException);
return studentValidationException;
}
// CORRECT call site in TryCatch
catch (NullStudentException nullStudentException)
{
throw await CreateAndLogValidationException(nullStudentException);
}
Broker clarifications
- Brokers are broader than repositories.
- Providers are not the same as brokers.
- Native exceptions may leak through brokers by design; foundation services localize them.
- Partialization is preferred for multi-entity brokers because configuration ownership stays centralized.
- Suppress warnings at the project level when truly needed; otherwise fix them.
SPAL rules
- Standardized Provider Abstraction Libraries must be extensible.
- They must be configurable.
- They must be distributable.
- They must support external mockability for local / airplane-mode operation.
- They should move from local need to global reusable library when possible.
- They are subsystems and should themselves follow Brokers / Services / Exposers.
Service-wide rules
- Services contain business logic.
- Service operations break into validations, processing, and integration.
- Service types break into validators, orchestrators, and aggregators.
- Do or delegate, but not both.
- Enforce Florance Pattern where applicable.
- Exposure layers must have a single point of contact with business logic.
- Services should accept and return the same contract or primitives/aggregations of that contract.
- Methods that has primitive inputs must consider using a contract model count exceed three.
- Every service validates its own inputs and outputs.
- Services cannot call other services at the same level.
- Service methods cannot call other service methods at the same level.
- If shared logic exists, extract it to a private method that both public methods can call.
- Public APIs cannot call public APIs at the same level.
- Flow forward only.
Foundation service rules
- Foundation services are broker-neighboring services.
- Their purpose is validation, abstraction, mapping, and primitive business language.
- They must remain pure-primitive.
- No multi-step higher-order business logic.
- They must integrate with one entity broker only.
- Support brokers like logging and time are allowed.
- They must translate technology language to business language.
- Insert -> Add
- Select -> Retrieve
- Update -> Modify
- Delete -> Remove
- They must wrap logic in TryCatch / exception-noise-cancellation.
- They must perform validation before dependency calls.
- They are the last abstraction layer before core business logic.
- Foundation services live under Services/Foundations.
- Fondation service models live under Models/Foundations/{Entity Plural}/{Entity}.cs
- Fondation service exceptionmodels live under Models/Foundations/{Entity Plural}/Exceptions/
Validation rules
- Validation order is mandatory:
- Structural
- Logical
- External
- Circuit-breaking validations exit immediately.
- Continuous validations accumulate errors, then break the circuit.
- Use upsertable exceptions for accumulated validation data.
- Use dynamic rules that include both condition and human-readable message.
- Use a rule collector to aggregate and then throw.
- Use hybrid continuous validation for nested models.
- Validate outgoing data when the current routine uses it.
- Map native failures into local exception models.
Exception rules for foundation services
- Localize native exceptions.
- The data dictionary from native exception must be assigned to the localised exception's data dictionary.
- The inner exception of the localised exception must be the native exception when possible.
- Categorize exceptions into validation, dependency validation, dependency, and service exceptions.
- Preserve inner localized exceptions when moving upstream.
- Log at the appropriate severity.
- Critical dependency failures are infrastructure/configuration failures.
- Do not leak raw native exception semantics into upstream pure business logic.
Processing service rules
- Processing services hold higher-order single-entity business logic.
- They may combine primitive operations from one foundation service.
- They may use utility brokers.
- They may not use entity brokers directly.
- They may depend on one and only one foundation service.
- Their naming must include the entity and the Processing suffix.
- They validate only what they use from the input.
- They may shift outcomes to primitives such as bool or int.
- They may combine multiple primitive routines into one higher-order routine.
- They map exceptions from foundation layer to processing-layer exception categories.
- They must localise and categorise exceptions from the foundation layer.
- Processing services live under Services/Processings.
- Processing service virtual models live under Models/Processings/{Entity Plural}/{Entity}.cs
- Processing service exceptionmodels live under Models/Processings/{Entity Plural}/Exceptions/
Orchestration service rules
- Orchestration services combine multi-entity operations.
- They may depend on foundation services or processing services, but not a mixed set of both for entity/business dependencies.
- Utility brokers are allowed in addition.
- Florance Pattern is mandatory.
- Two-Three rule is mandatory for entity/business service dependencies.
- If dependencies exceed the rule, normalize.
- Full normalization
- Semi-normalization
- No-normalization only as the last option
- Every breakdown must be meaningful.
- Orchestration services may be pass-through when contract purity is preserved.
- Orchestration services may expose physical contracts or virtual contracts.
- They are responsible for mapping and branching.
- They are responsible for calling dependencies in the proper order.
- Natural order is preferred when dependencies require it.
- Enforced order must be tested when the call dependency is not naturally encoded in input/output relationships.
- Cul-De-Sac orchestration is valid for event/listener scenarios.
- Variants include coordination, management, and uber-management services.
- Keep a unit-of-work mindset.
- Prefer eventing when it reduces orchestration complexity safely.
- They map exceptions from foundation layer or processing layer to orchestration-layer exception categories.
- They must localise and categorise exceptions from the foundation layer or processing layer.
- Orchestration services live under Services/Orchestrations.
- Orchestration service virtual models live under Models/Orchestrations/{Entity Plural}/{Entity}.cs
- Orchestration service exceptionmodels live under Models/Orchestrations/{Entity Plural}/Exceptions/
- When Cul-De-Sac eventing is chosen (rules 13 and 16), activate
the-standard-eventsskill -- it governs the event broker, event service, validation, exception handling, DI lifetime, and startup activation for the event layer.
Aggregation service rules
- Aggregation services are the knot at the border of core business logic.
- They provide a single exposure point when many same-variation services share the same contract.
- They do not add business logic.
- They can have many same-variation dependencies.
- They do not validate call order.
- They only perform basic structural validations.
- They may aggregate by multi-call routine or by pass-through methods.
- They are optional.
- Their dependencies must share the same contract family.
- They must aggregate exceptions the same way orchestration-like services do.
- They must localise and categorise exceptions the same way orchestration-like services do.
- Aggregation services lives under Services/Aggregations.
- Aggregation service exceptionmodels live under Models/Aggregations/{Entity Plural}/Exceptions/
Exposer rules
- Exposers are disposable mapping layers.
- Their purpose is duplex mapping in and out of the core business logic.
- They are pure mapping only.
- They may not talk to brokers.
- They may not contain business logic.
- They may have one and only one service dependency.
- That dependency must be a service, not a broker.
- They must provide a single point of contact.
Exposure component types
- Communication protocols
- User interfaces
- I/O components / background daemons
Communication protocol rules
- Return core-business results in the protocol’s form.
- Return error reports faithfully and with standardized codes.
- Support result communication and error reporting.
- Keep mapping thin and explicit.
RESTful API rules
- Controllers speak HTTP verb language.
- Post / Get / Put / Delete
- Custom verbs are allowed when the business operation goes beyond CRUD and the standard verbs do not fit.
- Similar verbs are allowed across different routines when names and routes differentiate them.
- Routes must never contain verbs.
- Routes must use nouns.
- Enforce the single-noun principle.
- Prefer resource intersections to noun-collisions.
- Controller classes are plural.
- Controller routes should usually follow api/[controller].
- Method-specific routes extend the controller route.
- Follow plural-singular-plural route patterns for nested resource intersections.
- Use query parameters and OData where appropriate for queryable reads.
- x-www-form-urlencoded is allowed for form-style endpoint inputs.
- Controller methods should not accept more than three parameters; beyond that, design a model.
- Controllers have one service dependency.
- Controllers honor a single contract family.
- Controllers live under Controllers.
- Every system should have a HomeController heartbeat endpoint.
- HomeController should not require security and should only indicate aliveness.
REST response code rules
- Success responses:
- 200 OK for successful GET / PUT / DELETE style outcomes
- 201 Created for successful POSTs
- 202 Accepted for delegated or eventual-consistency submissions
- User error responses:
- 400 BadRequest for validation and dependency-validation issues when mapped to user-correctable input/domain problems
- 404 NotFound for not-found scenarios
- 409 Conflict for already-exists scenarios
- 423 Locked for locked-resource scenarios
- 424 FailedDependency for invalid-reference scenarios
- System error responses:
- 500 InternalServerError for dependency or service failures
- 507 InsufficientStorage for internal storage issues when applicable
- Preserve security boundaries.
- Do not expose internal details from dependency/service failures unless the protocol requires a sanitized representation.
User interface rules
- UI exposers must map progress, results, and errors.
- Never fake progress.
- Choose progress reporting level intentionally:
- Basic progress
- Remaining progress
- Detailed progress
- Choose results reporting level intentionally:
- Simple
- Partial details
- Full details
- It is a violation to redirect users after submission with no indication of what happened.
- Error reports must tell users what happened, why it happened, and the next course of action when possible.
- Error report types:
- Informational
- Referential / implicit action
- Actionable
- Translate technical error language into user-appropriate language.
- UI exposers have one single dependency.
- UI anatomy must separate:
- Bases
- Components
- Containers
- Base components wrap native or third-party UI elements.
- Core components integrate with one and only one view service.
- Containers/pages orchestrate UI components and routes only.
- Containers may not contain UI logic.
- Containers may not use base components directly.
- Base components may not wrap more than one non-local component when avoidable.
- Base components do not contain business logic, exception handling, validations, or calculations.
- Pages are route containers and do not require business logic.
- Separate markup, code-behind, and style files.
- Organize UI under Views/Bases, Views/Components, and Views/Pages.
- Use domain-driven UI organization.
Web-application specifics
- Base components behave like brokers.
- Core components behave like service/controller hybrids.
- Core components are test-driven.
- Core components are built from:
- Elements
- Styles
- Actions
- Element testing must cover:
- Existence
- Properties
- Actions
- Existence testing may use:
- Property assignment
- Searching by id
- General search
- Styles belong primarily to core components, not bases.
- Pages compose components and represent routes.
- Pages typically do not require unit tests.
- Keep UI unobtrusive; do not place CSS, C#, and markup in the same file.
- Core components do not call other core components at the same level.
- One view service corresponds to one core component and one view model.
Versioning and breaking changes
This skill governs what the correct structure is at each layer.
When a structural change breaks an existing contract -- a model property added or removed, a service signature changed, an API route or response shape altered -- it is no longer purely an architecture decision.
Activate the-standard-versioning skill at that point.
The versioning skill governs:
- When and how to increment the release version
- How to introduce new model versions under
Vnsubfolders without overwriting earlier versions - How to introduce new service versions alongside earlier ones
- How to version API routes so earlier consumers are not broken
- How to signal deprecation on models, services, and routes
The architecture skill and the versioning skill are complementary:
- Architecture answers: is the shape correct?
- Versioning answers: how do we change it safely?
Architecture review checklist
When reviewing or generating architecture, verify all of the following:
- Purpose is explicit.
- Models are scoped to the purpose.
- Dependencies and exposure are separate from purpose.
- Broker responsibilities are thin and disposable.
- Services own business logic.
- Validation order is correct.
- Service-layer flow is forward only.
- Same-level services are not calling each other.
- Florance Pattern is honored.
- Exposure layers have single point of contact.
- APIs honor route and status-code rules.
- UI honors single dependency, unobtrusiveness, and anatomy rules.
- Architecture remains readable, autonomous, and rewritable.
- If Cul-De-Sac eventing is used,
the-standard-eventsskill has been activated and its rules are satisfied. - If a model, service, or API contract has changed or a release is being cut,
the-standard-versioningskill has been activated and its rules are satisfied.
.NET implementation profile included from the supplied implementation specification
The following addendum preserves the supplied implementation profile so the skill can enforce both the abstract Standard and the concrete .NET implementation style you attached.
1. Overview
The Standard is an opinionated software engineering standard authored by Hassan Habib. It prescribes a tri-nature architecture consisting of:
| Layer | Responsibility |
|---|---|
| Brokers | Thin abstraction over any external dependency (DB, API, logs) |
| Services | All business logic — validation, orchestration, coordination |
| Exposers | Entry points that expose services (Controllers, Endpoints) |
This project currently implements Brokers and Foundation Services for the
LegacyUser entity (storage-based) and the Person entity (API-based), as well as
an API Broker for sending Person entities to an external API.
2. Project Structure
RedRhino.Core.Synchronizer/
├── Brokers/
│ ├── Apis/
│ │ ├── IModernApiBroker.cs (partial interface — base)
│ │ ├── IModernApiBroker.Persons.cs (partial interface — entity)
│ │ ├── ModernApiBroker.cs (partial class — base)
│ │ └── ModernApiBroker.Persons.cs (partial class — entity)
│ ├── Loggings/
│ │ ├── ILoggingBroker.cs
│ │ └── LoggingBroker.cs
│ └── Storages/
│ ├── IStorageBroker.cs (partial interface — base)
│ ├── IStorageBroker.LegacyUsers.cs (partial interface — entity)
│ ├── StorageBroker.cs (partial class — base)
│ └── StorageBroker.LegacyUsers.cs (partial class — entity)
├── Models/
│ ├── Foundations/
│ ├── Persons/
│ │ ├── Person.cs
│ │ ├── PersonType.cs
│ │ ├── PersonRecordState.cs
│ │ └── Exceptions/
│ │ ├── NullPersonException.cs
│ │ ├── InvalidPersonException.cs
│ │ ├── AlreadyExistsPersonException.cs
│ │ ├── FailedPersonDependencyException.cs
│ │ ├── FailedPersonServiceException.cs
│ │ ├── PersonValidationException.cs
│ │ ├── PersonDependencyException.cs
│ │ ├── PersonDependencyValidationException.cs
│ │ └── PersonServiceException.cs
│ └── LegacyUsers/
│ ├── LegacyUser.cs
│ └── Exceptions/
│ ├── NullLegacyUserException.cs
│ ├── InvalidLegacyUserException.cs
│ ├── AlreadyExistsLegacyUserException.cs
│ ├── FailedStorageLegacyUserDependencyException.cs
│ ├── FailedLegacyUserServiceException.cs
│ ├── LegacyUserValidationException.cs
│ ├── LegacyUserDependencyException.cs
│ ├── LegacyUserDependencyValidationException.cs
│ └── LegacyUserServiceException.cs
├── Services/
│ └── Foundations/
│ ├── LegacyUsers/
│ │ ├── ILegacyUserService.cs
│ │ ├── LegacyUserService.cs (partial — logic)
│ │ ├── LegacyUserService.Validations.cs (partial — validations)
│ │ └── LegacyUserService.Exceptions.cs (partial — exception handling)
│ └── Persons/
│ ├── IPersonService.cs
│ ├── PersonService.cs (partial — logic)
│ ├── PersonService.Validations.cs (partial — validations)
│ └── PersonService.Exceptions.cs (partial — exception handling)
└── Program.cs
RedRhino.Core.Synchronizer.Tests.Units/
└── Services/
└── Foundations/
├── LegacyUsers/
│ ├── LegacyUserServiceTests.cs (partial — setup & helpers)
│ ├── LegacyUserServiceTests.Logic.{Method}.cs (partial — happy-path tests)
│ ├── LegacyUserServiceTests.Validations.{Method}.cs (partial — validation tests)
│ └── LegacyUserServiceTests.Exceptions.{Method}.cs (partial — exception tests)
└── Persons/
├── PersonServiceTests.cs (partial — setup & helpers)
├── PersonServiceTests.Logic.{Method}.cs (partial — happy-path tests)
├── PersonServiceTests.Validations.{Method}.cs (partial — validation tests)
└── PersonServiceTests.Exceptions.{Method}.cs (partial — exception tests)
3. Brokers
Brokers are thin wrappers around external resources. They contain zero business logic.
Rule — Generic Helpers: Every broker base partial must expose private generic helper methods (e.g.,
InsertAsync<T>,PostAsync<T>) that encapsulate the underlying client calls. Entity partial files never reference the private client member (e.g.,this.apiClient,this.Entry(...)) directly — they delegate to the generic helpers instead. This keeps entity partials decoupled from the concrete client and allows swapping the underlying implementation in a single place.
3.1 Storage Broker
| Aspect | Implementation |
|---|---|
| Base class | EFxceptionsContext (from the EFxceptions library — wraps DbContext with meaningful EF exceptions) |
| Interface | partial interface IStorageBroker — split per entity |
| Class | partial class StorageBroker — split per entity |
| Generic CRUD helpers | Private helpers in base partial: InsertAsync<T>, SelectAllAsync<T>, SelectAsync<T>, UpdateAsync<T>, DeleteAsync<T> |
| Configuration | Reads DefaultConnection from IConfiguration; calls this.Database.Migrate() at construction |
Base partial — StorageBroker.cs
This file owns: IConfiguration, OnConfiguring, the constructor (with Database.Migrate()),
and private generic CRUD helpers. It owns nothing else.
arch-014 — No
DbSet<>in the base partial.DbSet<Student> Studentslives inStorageBroker.Students.cs, not here. The base partial must never declare entity-specific members.
public partial class StorageBroker : EFxceptionsContext, IStorageBroker
{
// No DbSet<> properties here. Each entity partial declares its own.
private readonly IConfiguration configuration;
public StorageBroker(IConfiguration configuration)
{
this.configuration = configuration;
// arch-011: Must be called — applies pending migrations at startup.
// Omitting this means the schema is never applied.
this.Database.Migrate();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
string connectionString =
this.configuration.GetConnectionString("DefaultConnection");
optionsBuilder.UseSqlServer(connectionString);
}
// arch-012: All EF operations live here. Entity partials call these helpers only.
private async ValueTask<T> InsertAsync<T>(T entity) where T : class
{
this.Entry(entity).State = EntityState.Added;
await this.SaveChangesAsync();
return entity;
}
// SelectAllAsync<T> wraps IQueryable in a ValueTask so all entity methods
// follow the uniform async/await pattern. AsNoTracking() is applied here.
private ValueTask<IQueryable<T>> SelectAllAsync<T>() where T : class =>
ValueTask.FromResult<IQueryable<T>>(this.Set<T>().AsNoTracking());
private async ValueTask<T> SelectAsync<T>(Guid entityId) where T : class =>
await this.FindAsync<T>(entityId);
private async ValueTask<T> UpdateAsync<T>(T entity) where T : class
{
this.Entry(entity).State = EntityState.Modified;
await this.SaveChangesAsync();
return entity;
}
private async ValueTask<T> DeleteAsync<T>(T entity) where T : class
{
this.Entry(entity).State = EntityState.Deleted;
await this.SaveChangesAsync();
return entity;
}
}
Entity partial — StorageBroker.Students.cs
arch-014:
DbSet<Student>is declared here — in the entity partial — not inStorageBroker.cs.
public partial class StorageBroker
{
// DbSet<Student> belongs in this entity partial file, not in StorageBroker.cs.
public DbSet<Student> Students { get; set; }
public async ValueTask<Student> InsertStudentAsync(Student student) =>
await this.InsertAsync(student);
// arch-009: SelectAll* must be async ValueTask<IQueryable<T>>.
// WRONG: public IQueryable<Student> SelectAllStudents() => this.Students.AsNoTracking();
// WRONG: public IQueryable<Student> SelectAllStudents() => this.Set<Student>();
// CORRECT: delegate to SelectAllAsync<T>() — never touch DbSet or EF members directly.
public async ValueTask<IQueryable<Student>> SelectAllStudentsAsync() =>
await this.SelectAllAsync<Student>();
public async ValueTask<Student> SelectStudentByIdAsync(Guid studentId) =>
await this.SelectAsync<Student>(studentId);
public async ValueTask<Student> UpdateStudentAsync(Student student) =>
await this.UpdateAsync(student);
public async ValueTask<Student> DeleteStudentAsync(Student student) =>
await this.DeleteAsync(student);
}
Rule: Each entity gets its own partial file for both the interface and the class.
Rule — scope before scaffolding (arch-013): A branch named
BROKERS-student-insertimplements onlyInsertStudentAsync. If the prompt is ambiguous about which operations are needed, the agent must ask before creating the branch or writing any code. A single broker branch = a single operation.Rule — branch action language (prac-016): Broker branch actions use infrastructure verbs —
insert,select-all,select-by-id,update,delete. Never use business verbs (add,retrieve,modify,remove) in aBROKERSbranch name.
3.2 API Broker
| Aspect | Implementation |
|---|---|
| HTTP client | IRESTFulApiFactoryClient (from the RESTFulSense library — wraps HttpClient) |
| Interface | partial interface IModernApiBroker — split per entity |
| Class | partial class ModernApiBroker — split per entity |
| Generic HTTP helpers | Private PostAsync<T>() on the base partial for reuse across entities |
| Configuration | Reads ApiConfigurations:Url from IConfiguration to set HttpClient.BaseAddress |
Base partial — ModernApiBroker.cs
public partial class ModernApiBroker : IModernApiBroker
{
private readonly IRESTFulApiFactoryClient apiClient;
public ModernApiBroker(IConfiguration configuration)
{
var httpClient = new HttpClient()
{
BaseAddress =
new Uri(configuration.GetValue<string>("ApiConfigurations:Url"))
};
this.apiClient = new RESTFulApiFactoryClient(httpClient);
}
private async ValueTask<T> PostAsync<T>(string relativeUrl, T content) =>
await this.apiClient.PostContentAsync<T>(relativeUrl, content);
}
Entity partial — ModernApiBroker.Persons.cs
public partial class ModernApiBroker
{
private const string PersonsRelativeUrl = "api/persons";
public async ValueTask<Person> PostPersonAsync(Person person) =>
await PostAsync(PersonsRelativeUrl, person);
}
Rule: Entity partials delegate to the generic helpers (
PostAsync<T>) — they never callthis.apiClientdirectly.
3.3 Logging Broker
A dedicated abstraction over ILogger<T>. The broker exposes only async ValueTask methods
that correspond to standard log levels:
| Method | Log Level |
|---|---|
LogInformationAsync |
Information |
LogTraceAsync |
Trace |
LogDebugAsync |
Debug |
LogWarningAsync |
Warning |
LogErrorAsync |
Error |
LogCriticalAsync |
Critical |
LoggingBroker.cs
public class LoggingBroker : ILoggingBroker
{
private readonly ILogger<LoggingBroker> logger;
public LoggingBroker(ILogger<LoggingBroker> logger) =>
this.logger = logger;
public async ValueTask LogInformationAsync(string message) =>
this.logger.LogInformation(message);
public async ValueTask LogTraceAsync(string message) =>
this.logger.LogTrace(message);
public async ValueTask LogDebugAsync(string message) =>
this.logger.LogDebug(message);
public async ValueTask LogWarningAsync(string message) =>
this.logger.LogWarning(message);
public async ValueTask LogErrorAsync(Exception exception) =>
this.logger.LogError(exception, exception.Message);
public async ValueTask LogCriticalAsync(Exception exception) =>
this.logger.LogCritical(exception, exception.Message);
}
ILoggingBroker.cs
public interface ILoggingBroker
{
ValueTask LogInformationAsync(string message);
ValueTask LogTraceAsync(string message);
ValueTask LogDebugAsync(string message);
ValueTask LogWarningAsync(string message);
ValueTask LogErrorAsync(Exception exception);
ValueTask LogCriticalAsync(Exception exception);
}
Rule — async expression body, no Task.Run: Each method uses the
asynckeyword and delegates directly toILogger<T>. Never wrap the call inTask.Run()ornew ValueTask(Task.Run(...)).ILogger<T>is synchronous; wrapping it inTask.Run()introduces unnecessary thread-pool overhead and produces an inefficient heap-allocatedValueTask.Wrong:
public ValueTask LogWarningAsync(string message) => new ValueTask(Task.Run(() => this.logger.LogWarning(message)));Correct:
public async ValueTask LogWarningAsync(string message) => this.logger.LogWarning(message);
Rule — DI Registration: The logging broker must be explicitly registered in
Program.cs.AddLogging()(or the host builder's default) must also be present so thatILogger<LoggingBroker>resolves correctly.builder.Services.AddLogging(); builder.Services.AddTransient<ILoggingBroker, LoggingBroker>();Omitting either line causes a runtime DI resolution failure.
4. Models
4.1 Entity Model
Entity classes are plain POCOs residing under Models/{Service Type}/{Entity}/. They contain
no behavior — only properties. Domain comments on properties capture validation intent
(e.g., nullable rules, email format, phone format).
The LegacyUser class resides under Models/Foundations/LegacyUsers/.
The Person class resides under Models/Foundations/Persons/.
4.2 Exception Models
Exception models live in Models/{Service Type}/{Entity}/Exceptions/ and form a two-tier exception
hierarchy per The Standard.
The LegacyUser class resides under Models/Foundations/LegacyUsers/Exceptions/.
The Person class resides under Models/Foundations/Persons/Exceptions/.
The inner/outer exception hierarchy varies by the type of broker the Foundation Service
consumes. Storage-based services (e.g., LegacyUserService) handle SQL/EF exceptions, while
API-based services (e.g., PersonService) handle RESTFulSense HTTP exceptions.
4.2.1 Storage-Based Exceptions (LegacyUser)
Inner (Local) Exceptions
| Exception | Purpose |
|---|---|
NullLegacyUserException |
Entity is null |
InvalidLegacyUserException |
One or more property-level validation fails |
AlreadyExistsLegacyUserException |
Duplicate key detected |
FailedStorageLegacyUserDependencyException |
SQL / storage-level failure |
FailedLegacyUserServiceException |
Unexpected runtime failure |
Outer (Categorical) Exceptions
| Exception | Category | Wrapped Inner(s) |
|---|---|---|
LegacyUserValidationException |
Validation | NullLegacyUserException, InvalidLegacyUserException |
LegacyUserDependencyException |
Dependency | FailedStorageLegacyUserDependencyException |
LegacyUserDependencyValidationException |
DependencyValidation | AlreadyExistsLegacyUserException, InvalidLegacyUserException (from DbUpdateException) |
LegacyUserServiceException |
Service | FailedLegacyUserServiceException |
4.2.2 API-Based Exceptions (Person)
Inner (Local) Exceptions
| Exception | Purpose |
|---|---|
NullPersonException |
Entity is null |
InvalidPersonException |
One or more property-level validation fails, or BadRequest from API |
AlreadyExistsPersonException |
Conflict (409) from API |
FailedPersonDependencyException |
HTTP-level failure (any HTTP error or HttpRequestException) |
FailedPersonServiceException |
Unexpected runtime failure |
Outer (Categorical) Exceptions
| Exception | Category | Wrapped Inner(s) |
|---|---|---|
PersonValidationException |
Validation | NullPersonException, InvalidPersonException |
PersonDependencyValidationException |
DependencyValidation | InvalidPersonException (from BadRequest), AlreadyExistsPersonException (from Conflict) |
PersonDependencyException |
Dependency (Critical) | FailedPersonDependencyException (from Unauthorized, Forbidden, NotFound, UrlNotFound, HttpRequestException) |
PersonDependencyException |
Dependency (Non-Critical) | FailedPersonDependencyException (from InternalServerError, ServiceUnavailable) |
PersonServiceException |
Service | FailedPersonServiceException |
All exceptions derive from Xeption (from the Xeption NuGet package), which provides
UpsertDataList and ThrowIfContainsErrors for aggregated validation data.
5. Services — Foundations
A Foundation Service sits directly on top of Brokers and is the only consumer of them.
5.1 Partial Class Layout
The service is split into three partial files:
| Partial file | Concern |
|---|---|
{Entity}Service.cs |
Constructor, DI fields, public business logic |
{Entity}Service.Validations.cs |
All Validate* and IsInvalid* methods |
{Entity}Service.Exceptions.cs |
TryCatch delegate pattern, CreateAndLog* helpers |
5.2 Dependency Injection
A Foundation Service depends only on Brokers — never on other services.
Storage-based service (LegacyUserService):
public partial class LegacyUserService : ILegacyUserService
{
private readonly IStorageBroker storageBroker;
private readonly ILoggingBroker loggingBroker;
public LegacyUserService(
IStorageBroker storageBroker,
ILoggingBroker loggingBroker) { ... }
}
API-based service (PersonService):
public partial class PersonService : IPersonService
{
private readonly IModernApiBroker modernApiBroker;
private readonly ILoggingBroker loggingBroker;
public PersonService(
IModernApiBroker modernApiBroker,
ILoggingBroker loggingBroker) { ... }
}
5.3 Business Logic — TryCatch Pattern
Every public method delegates to a TryCatch wrapper that catches, wraps, logs, and
re-throws categorised exceptions:
Storage-based:
public ValueTask<LegacyUser> AddLegacyUserAsync(LegacyUser legacyUser) =>
TryCatch(async () =>
{
ValidateLegacyUser(legacyUser);
return await this.storageBroker.InsertLegacyUserAsync(legacyUser);
});
API-based:
public ValueTask<Person> AddPersonAsync(Person person) =>
TryCatch(async () =>
{
ValidatePerson(person);
return await this.modernApiBroker.PostPersonAsync(person);
});
5.4 Validations
Validations use a dynamic rule + aggregate pattern:
Validate(
(IsInvalid(legacyUser.Id), nameof(LegacyUser.Id)),
(IsInvalid(legacyUser.UserPK), nameof(LegacyUser.UserPK)),
(IsInvalid(legacyUser.UserName), nameof(LegacyUser.UserName)),
...);
Each IsInvalid overload returns an anonymous object with Condition (bool) and Message (string).
The Validate method aggregates all failures into a single InvalidLegacyUserException via
UpsertDataList and calls ThrowIfContainsErrors().
Custom validators exist for domain-specific rules:
| Validator | Rule |
|---|---|
IsInvalid(Guid) |
Must not be Guid.Empty |
IsInvalid(int) |
Must not be default (0) |
IsInvalid(string) |
Must not be null/empty/whitespace |
IsInvalidLob(int) |
Must be greater than 0 |
IsInvalidEmail |
Regex-based email format check |
6. Exception Handling
The TryCatch method in each {Entity}Service.Exceptions.cs implements The Standard's
exception mapping pattern. The specific catch blocks differ based on the broker type
the service consumes.
6.1 Storage-Based Exception Mapping (LegacyUser)
Native / External Exception → Inner (Local) Exception → Outer (Categorical) Exception
──────────────────────────────────────────────────────────────────────────────────────────────────
(null input) → NullLegacyUserException → LegacyUserValidationException
(validation rules fail) → InvalidLegacyUserException → LegacyUserValidationException
SqlException → FailedStorage...Exception → LegacyUserDependencyException (Critical)
DuplicateKeyException → AlreadyExists...Exception → LegacyUserDependencyValidationException
DbUpdateException → InvalidLegacy...Exception → LegacyUserDependencyValidationException
Exception (catch-all) → FailedService...Exception → LegacyUserServiceException
6.2 API-Based Exception Mapping (Person)
For services that call external APIs through RESTFulSense, the exception mapping covers
all HTTP response exceptions. The ordering of catch blocks in TryCatch matters — more
specific exceptions must appear before their base classes.
Native / External Exception → Inner (Local) Exception → Outer (Categorical) Exception Log Level
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
(null input) → NullPersonException → PersonValidationException LogError
(validation rules fail) → InvalidPersonException → PersonValidationException LogError
HttpResponseBadRequestException → InvalidPersonException → PersonDependencyValidationException LogError
HttpResponseConflictException → AlreadyExistsPersonException → PersonDependencyValidationException LogError
HttpResponseUnauthorizedException → FailedPersonDependencyException → PersonDependencyException LogCritical
HttpResponseForbiddenException → FailedPersonDependencyException → PersonDependencyException LogCritical
HttpResponseNotFoundException → FailedPersonDependencyException → PersonDependencyException LogCritical
HttpResponseUrlNotFoundException → FailedPersonDependencyException → PersonDependencyException LogCritical
HttpResponseInternalServerErrorException → FailedPersonDependencyException → PersonDependencyException LogError
HttpResponseServiceUnavailableException → FailedPersonDependencyException → PersonDependencyException LogError
HttpRequestException → FailedPersonDependencyException → PersonDependencyException LogCritical
Exception (catch-all) → FailedPersonServiceException → PersonServiceException LogError
Rule — Critical vs Non-Critical Dependency: Exceptions that indicate the API endpoint is unreachable or inaccessible (
Unauthorized,Forbidden,NotFound,UrlNotFound,HttpRequestException) are logged atLogCriticalAsyncbecause they signal configuration or infrastructure failures. Server-side errors (InternalServerError,ServiceUnavailable) are logged atLogErrorAsyncbecause they may be transient.
Each CreateAndLog* helper:
- Wraps the inner exception in the categorical outer exception.
- Logs via the
ILoggingBrokerat the appropriate level (LogErrorAsyncorLogCriticalAsync). - Returns the outer exception so
TryCatchcanthrowit.
7. Dependency Registration (Exposers)
All wiring is done in Program.cs following The Standard's explicit registration style:
builder.Services.AddDbContext<StorageBroker>();
builder.Services.AddTransient<IStorageBroker, StorageBroker>();
builder.Services.AddTransient<IModernApiBroker, ModernApiBroker>();
builder.Services.AddTransient<ILoggingBroker, LoggingBroker>();
builder.Services.AddTransient<ILegacyUserService, LegacyUserService>();
builder.Services.AddTransient<IPersonService, PersonService>();
Brokers and Foundation Services are registered as Transient.
10. Naming Conventions
| Element | Pattern | Example |
|---|---|---|
| Broker interface | I{Resource}Broker |
IStorageBroker, IModernApiBroker, ILoggingBroker |
| Broker class | {Resource}Broker |
StorageBroker, ModernApiBroker, LoggingBroker |
| Broker method | {Action}{Entity}Async |
InsertLegacyUserAsync, PostPersonAsync |
| Service interface | I{Entity}Service |
ILegacyUserService |
| Service class | {Entity}Service |
LegacyUserService |
| Service method | Add{Entity}Async |
AddLegacyUserAsync |
| Inner exception | {Adjective}{Entity}Exception |
NullLegacyUserException |
| Outer exception | {Entity}{Category}Exception |
LegacyUserValidationException |
| Test class | {Entity}ServiceTests |
LegacyUserServiceTests |
| Test method | Should{Action}Async / ShouldThrow{Exception}On{Action}… |
ShouldAddLegacyUserAsync |