Architecture : Design Patterns
The solution generated by Scaffolder.net implements a cohesive set of design patterns to ensure separation of concerns, testability, and extensibility. Each pattern is implemented via templates and automatically registered in the dependency injection container by reflection.
Result Pattern
The Result pattern replaces exceptions for expected business errors. The Result class exposes Ok() and Fail() factory methods, and the generic Result<T> class encapsulates a typed return value. Both classes enforce the invariant that a success never contains errors and a failure always contains at least one.
Mediator Pattern
The Mediator pattern decouples request dispatch from request handling. The IMediator interface exposes a SendAsync method that dynamically locates the corresponding handler via the DI container. The Mediator implementation uses compiled and cached expression trees to invoke handlers without runtime reflection, ensuring optimal performance.
CQRS (Command Query Responsibility Segregation)
- Commands :
ICommand<TResponse>records encapsulate modification intents (Create, Edit, Delete). Each command has a dedicated handler that validates the request, executes business logic via the domain entity, persists via the repository, and returns aResult<TResponse>. - Queries :
IQuery<TResponse>records encapsulate read requests (GetList, GetDetails, GetEdit, GetDelete). Each query has a handler that reads data via the query repository and returns aResult<TResponse>without modifying state.
Pipeline Behavior (Chain of Responsibility)
The Pipeline Behavior pattern allows cross-cutting behaviors (logging, validation) to be added around each handler without modifying its code. The IPipelineBehavior<TRequest, TResponse> interface intercepts the request before the handler and can short-circuit the chain. The LoggingPipelineBehavior measures and logs the execution time of each request. Behaviors are chained by the Mediator via Aggregate on delegates, applying the Chain of Responsibility pattern.
Repository Pattern
- Domain Repository :
I{Entity.NameSingular}DomainRepositoryinterface in the Domain, implemented in Infrastructure.Persistence. Responsible for uniqueness checks (ExistsWithId, ExistsWithPropertyName). - Command Repository :
I{Entity.NameSingular}CommandRepositoryinterface in the Application, implemented in Infrastructure.Persistence. Responsible for write operations (Add, Update, Remove) via theCommandRepository<TDbContext, TEntity, TEntityId>base class. - Query Repository :
I{Entity.NameSingular}QueryRepositoryinterface in the Application, implemented in Infrastructure.Persistence. Responsible for read operations with projections via theQueryRepository<TDbContext>base class.
Unit of Work
The IUnitOfWork interface exposes a SaveChangesAsync method to coordinate transactional persistence. The UnitOfWork<TDbContext> implementation creates a DbContext on demand via IDbContextFactory and implements IAsyncDisposable to properly release resources. Command handlers inject IUnitOfWork to ensure that modifications are only persisted after entity validation.
Domain Events
- Raise : The entity calls
RaiseDomainEventinherited fromEntity<TEntityId>to collect events (Created, Edited, Deleted) in an internal list. - Collect : The EF Core interceptor
PublishDomainEventsInterceptorcollects and clears domain events from entities tracked by the ChangeTracker beforeSaveChangesAsync. - Publish : After
SaveChangesAsync, theDomainEventPublisherdispatches each event to allIDomainEventHandler<TDomainEvent>registered in the DI container. - Handle : Domain event handlers (e.g.,
{Entity.NameSingular}CreatedDomainEventHandler) consume events to log, synchronize external systems, or trigger side effects.
Factory Method
Domain entities expose static factory methods (CreateAsync) and instance methods (EditAsync, Delete) instead of public constructors. CreateAsync builds the entity, validates via FluentValidation, checks uniqueness constraints via the domain service, and raises the Created domain event. EditAsync validates new values and raises the Edited domain event. Delete raises the Deleted domain event. This pattern ensures that any created or modified entity respects business invariants.
Proxy Pattern
The CommandHandlerProxy and QueryHandlerProxy implement the same interface as the handlers but delegate execution to an HTTP API via HttpClient. This pattern allows Blazor WebAssembly to execute commands and queries via REST calls to the Minimal API, replacing the local implementation with a transparent remote implementation. Proxies are registered only if Blazor WebAssembly or Minimal API is enabled.
Options Pattern
The Options pattern is used to configure strongly typed settings via IOptions<T>. The LocalizationOption class defines the default culture and supported cultures. Each option has a FluentValidation validator (LocalizationOptionValidator) and a SectionName constant for binding from appsettings.json. The pattern is also used for CorsOption and WebApiOption in the presentation projects.
Interceptor (EF Core)
The PublishDomainEventsInterceptor inherits from the Entity Framework Core SaveChangesInterceptor. It intercepts SavingChangesAsync to collect domain events from tracked entities, then SavedChangesAsync to publish them via IDomainEventPublisher. Events are only published after a successful persistence, ensuring that no event is emitted for a failed transaction. Publish methods are cached in a ConcurrentDictionary to avoid repeated reflection.
Marker Interface + Reflection Registration
Marker interfaces (IDomainRepository, IDomainService, ICommandRepository, IQueryRepository, ICommandService, IQueryService) expose no members but serve to identify classes to automatically register in the DI container. The RegisterServicesByMarkerType and RegisterServicesByGenericType extension methods scan assemblies via reflection, identify concrete classes implementing these markers, and register them with their corresponding interfaces. Each project has a sealed AssemblyReference class that serves as an anchor point to identify the assembly to scan.
Dependency Injection (Composition Root)
Each layer exposes an Add{Layer}Services extension method that serves as a Composition Root. Registration is hierarchical: the Presentation calls AddApplicationServices, which calls AddMediator, AddCommandHandlers, AddQueryHandlers, AddDomainEventHandlers, etc. The Infrastructure calls AddDbContextFactoryWithDomainEvents, AddUnitOfWork, AddCommandRepositories, AddQueryRepositories, AddDomainRepositories and AddDomainServices. This centralized composition ensures that all services are registered consistently and reproducibly.
Template Method
The base classes CommandRepository<TDbContext, TEntity, TEntityId>, QueryRepository<TDbContext> and DomainRepository<TDbContext> define virtual methods (Add, Update, Remove, GetEntityAsync, ExistsWithPropertyNameAsync) that concrete repositories can override. Generated repositories inherit from these base classes and only add entity-specific methods, avoiding duplication of common data access code.
Facade (Service Layer)
The CommandService and QueryService act as facades between the presentation layer and the Mediator. Instead of calling the Mediator directly with commands and queries, the presentation injects application services that encapsulate request creation and dispatch via IMediator.SendAsync. This pattern simplifies the API consumed by the presentation and hides the complexity of the CQRS pattern.
Strategy (EF Core Configurations)
Each entity has its own EF Core configuration class {Entity.NameSingular}Configuration implementing IEntityTypeConfiguration<TEntity>. This Strategy approach allows defining each entity's mapping (primary keys, indexes, uniqueness constraints, properties, relationships) in isolation without grouping all configurations in the DbContext. The DbContext automatically applies all configurations via modelBuilder.ApplyConfigurationsFromAssembly.
Best Practices
- Sealed classes : All generated concrete classes (entities, handlers, services, repositories, validators, errors) are
sealedto prevent unintended inheritance and enable compiler inlining. - Immutable records : Commands and queries are
sealed recordtypes ensuring immutability of requests and responses. - Private set on entity properties : Entity properties use
private setto force modification through business methods (Create, Edit, Delete). - Static readonly validators : FluentValidation validators are declared as
static readonlyto avoid allocation on each use. - Internal handlers : Command and query handlers are
internal sealedto avoid exposing execution infrastructure to external consumers. - Async end-to-end : All data access and orchestration methods are asynchronous with
CancellationTokenpropagated systematically. - EF Core resilient : The DbContext factory configures
EnableRetryOnFailurefor SQL Server transient faults. - Scoped lifetime : All services, handlers and repositories are registered as
Scopedto ensure transaction consistency per request.