Architecture and Patterns
|
This section documents ASP.NET Core on .NET 10 (LTS), the current release — the minimal hosting model, the middleware pipeline, dependency injection, Minimal APIs, MVC & Razor Pages, Blazor with the current render modes, SignalR and gRPC, EF Core, ASP.NET Core Identity and policy-based authorization, output caching, rate limiting, and Native-AOT-aware building — as described by the official documentation at Microsoft Learn, which is the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. .NET ships a major release every November and its APIs continue to evolve: the examples here target .NET 10 / C# 14. This section’s bibliography lists the reference material consulted while preparing these pages. |
This page is orientation, not a patterns textbook: a short description, a small snippet, and a link for each idea. It is informed by the architecture chapters of the books in this section’s bibliography, but everything is verified against the current guidance at .NET application architecture guides.
Principles
Separation of concerns, DRY, KISS, YAGNI, and SOLID show up concretely in ASP.NET Core:
-
Single responsibility — thin controllers/endpoints that delegate to services.
-
Open/closed — add behavior via new
IEndpointFilter/IAuthorizationHandlerimplementations, not edits. -
Liskov — any registered
IPaymentGatewaymust be substitutable. -
Interface segregation — small service interfaces, easy to fake in tests.
-
Dependency inversion — depend on abstractions; the DI container supplies implementations.
// controller depends on an abstraction; wiring lives in Program.cs
public sealed class CheckoutController(IPaymentGateway gateway) : ControllerBase { /* ... */ }
builder.Services.AddScoped<IPaymentGateway, StripeGateway>();
Layering and Clean Architecture
Split the solution so dependencies point inward: the domain knows nothing about the web or the database.
Shop.Domain // entities, value objects, domain services -- no dependencies
Shop.Application // use cases, ports (interfaces), DTOs -> references Domain
Shop.Infrastructure // EF Core, HTTP clients, implementations of ports -> references Application
Shop.Web // controllers/endpoints, DI wiring -> references Application (+ Infrastructure at composition root)
controller / endpoint"] P --> A["Application
use-case handler + port interface"] A --> D["Domain
entity behaviour + invariants"] A -. resolved at runtime .-> I["Infrastructure
EF Core repository implements the port"] I --> DB[(Database)]
DTOs and object mapping
Map between wire DTOs, application models, and domain entities at the edges. Options: hand-written mapping (zero magic), Mapperly (source-generated, compile-time checked), or AutoMapper (convention-based, reflection).
[Mapper]
public partial class OrderMapper
{
public partial OrderDto ToDto(Order order); // generated at build time
}
See Mapperly and AutoMapper.
Cross-cutting patterns in DI
// Strategy: many implementations + a selector
builder.Services.AddKeyedScoped<IShipping, DhlShipping>("dhl");
builder.Services.AddKeyedScoped<IShipping, UpsShipping>("ups");
// Decorator: wrap a registration (Scrutor)
builder.Services.AddScoped<IOrderRepo, SqlOrderRepo>();
builder.Services.Decorate<IOrderRepo, CachingOrderRepo>();
// Factory: build per-call instances
builder.Services.AddSingleton<Func<string, IShipping>>(sp => key => sp.GetRequiredKeyedService<IShipping>(key));
The Operation Result pattern (see
Error handling) pairs naturally with
ProblemDetails: the application layer returns Result<T>, the endpoint maps failure to a problem response.
CQRS and the mediator pattern
Separate write commands from read queries; a mediator decouples the endpoint from the handler and gives a place for pipeline behaviors (validation, logging, transactions).
public sealed record PlaceOrder(int CustomerId, CartDto Cart) : IRequest<Result<int>>;
public sealed class PlaceOrderHandler(IOrderRepo repo) : IRequestHandler<PlaceOrder, Result<int>>
{
public async Task<Result<int>> Handle(PlaceOrder cmd, CancellationToken ct) { /* ... */ }
}
// endpoint
app.MapPost("/orders", (PlaceOrder cmd, ISender mediator) => mediator.Send(cmd));
A mediator is overkill for a small app with a handful of endpoints. See MediatR.
Vertical slices and larger structure
-
Vertical Slice Architecture / REPR (Request-EndPoint-Response): organise by feature folder, one endpoint + its request/response/handler per file. Less indirection than layered/onion for feature-heavy apps.
-
Modular monolith vs. microservices: start with modules (clear boundaries, in-process calls); split a module into a service only when it needs independent scaling or deployment. The Backend-for-Frontend pattern gives each client its own tailored API. Event-driven integration (a message broker) decouples modules/services that must not call each other synchronously.