$npx -y skills add wshaddix/dotnet-skills --skill dotnet-architecture-patternsOrganizing APIs at scale. Vertical slices, request pipelines, caching, error handling, idempotency.
| 1 | # dotnet-architecture-patterns |
| 2 | |
| 3 | Modern architecture patterns for .NET applications. Covers practical approaches to organizing minimal APIs at scale, vertical slice architecture, request pipeline composition, validation strategies, caching, error handling, and idempotency/outbox patterns. |
| 4 | |
| 5 | **Out of scope:** DI container mechanics and async/await patterns -- see [skill:dotnet-csharp-dependency-injection] and [skill:dotnet-csharp-async-patterns]. Project scaffolding and file layout -- see [skill:dotnet-scaffold-project]. Testing strategies -- see [skill:dotnet-testing-strategy] for decision guidance and [skill:dotnet-integration-testing] for WebApplicationFactory patterns. |
| 6 | |
| 7 | Cross-references: [skill:dotnet-csharp-dependency-injection] for service registration and lifetimes, [skill:dotnet-csharp-async-patterns] for async pipeline patterns, [skill:dotnet-csharp-configuration] for Options pattern in configuration, [skill:dotnet-solid-principles] for SOLID/DRY design principles governing class and interface design. |
| 8 | |
| 9 | --- |
| 10 | |
| 11 | ## Vertical Slice Architecture |
| 12 | |
| 13 | Organize code by feature (vertical slice) rather than by technical layer (controllers, services, repositories). Each slice owns its endpoint, handler, validation, and data access. |
| 14 | |
| 15 | ### Directory Structure |
| 16 | |
| 17 | ``` |
| 18 | Features/ |
| 19 | Orders/ |
| 20 | CreateOrder/ |
| 21 | CreateOrderEndpoint.cs |
| 22 | CreateOrderHandler.cs |
| 23 | CreateOrderRequest.cs |
| 24 | CreateOrderValidator.cs |
| 25 | GetOrder/ |
| 26 | GetOrderEndpoint.cs |
| 27 | GetOrderHandler.cs |
| 28 | ListOrders/ |
| 29 | ListOrdersEndpoint.cs |
| 30 | ListOrdersHandler.cs |
| 31 | Products/ |
| 32 | GetProduct/ |
| 33 | ... |
| 34 | ``` |
| 35 | |
| 36 | ### Why Vertical Slices |
| 37 | |
| 38 | - **Low coupling**: changing one feature does not ripple through shared layers |
| 39 | - **Easy navigation**: everything for a feature is in one place |
| 40 | - **Independent testability**: each slice has a clear input/output contract |
| 41 | - **Team scalability**: different developers can work on different features without merge conflicts |
| 42 | |
| 43 | ### Slice Anatomy |
| 44 | |
| 45 | Each slice typically contains: |
| 46 | |
| 47 | 1. **Request/Response DTOs** -- the contract |
| 48 | 2. **Validator** -- input validation rules |
| 49 | 3. **Handler** -- business logic |
| 50 | 4. **Endpoint** -- HTTP mapping (route, method, status codes) |
| 51 | |
| 52 | ```csharp |
| 53 | // Features/Orders/CreateOrder/CreateOrderRequest.cs |
| 54 | public sealed record CreateOrderRequest( |
| 55 | string CustomerId, |
| 56 | List<OrderLineRequest> Lines); |
| 57 | |
| 58 | public sealed record OrderLineRequest( |
| 59 | string ProductId, |
| 60 | int Quantity); |
| 61 | |
| 62 | // Features/Orders/CreateOrder/CreateOrderResponse.cs |
| 63 | public sealed record CreateOrderResponse( |
| 64 | string OrderId, |
| 65 | decimal Total, |
| 66 | DateTimeOffset CreatedAt); |
| 67 | ``` |
| 68 | |
| 69 | --- |
| 70 | |
| 71 | ## Minimal API Organization at Scale |
| 72 | |
| 73 | ### Route Group Pattern |
| 74 | |
| 75 | Use `MapGroup` to organize related endpoints and apply shared filters: |
| 76 | |
| 77 | ```csharp |
| 78 | // Program.cs |
| 79 | var app = builder.Build(); |
| 80 | |
| 81 | app.MapGroup("/api/orders") |
| 82 | .WithTags("Orders") |
| 83 | .MapOrderEndpoints(); |
| 84 | |
| 85 | app.MapGroup("/api/products") |
| 86 | .WithTags("Products") |
| 87 | .MapProductEndpoints(); |
| 88 | |
| 89 | app.Run(); |
| 90 | ``` |
| 91 | |
| 92 | ```csharp |
| 93 | // Features/Orders/OrderEndpoints.cs |
| 94 | public static class OrderEndpoints |
| 95 | { |
| 96 | public static RouteGroupBuilder MapOrderEndpoints(this RouteGroupBuilder group) |
| 97 | { |
| 98 | group.MapPost("/", CreateOrderEndpoint.Handle) |
| 99 | .WithName("CreateOrder") |
| 100 | .Produces<CreateOrderResponse>(StatusCodes.Status201Created) |
| 101 | .ProducesValidationProblem(); |
| 102 | |
| 103 | group.MapGet("/{id}", GetOrderEndpoint.Handle) |
| 104 | .WithName("GetOrder") |
| 105 | .Produces<OrderResponse>() |
| 106 | .ProducesProblem(StatusCodes.Status404NotFound); |
| 107 | |
| 108 | group.MapGet("/", ListOrdersEndpoint.Handle) |
| 109 | .WithName("ListOrders") |
| 110 | .Produces<PagedResult<OrderSummary>>(); |
| 111 | |
| 112 | return group; |
| 113 | } |
| 114 | } |
| 115 | ``` |
| 116 | |
| 117 | ### Endpoint Classes |
| 118 | |
| 119 | Keep each endpoint in its own static class with a single `Handle` method: |
| 120 | |
| 121 | ```csharp |
| 122 | public static class CreateOrderEndpoint |
| 123 | { |
| 124 | public static async Task<IResult> Handle( |
| 125 | CreateOrderRequest request, |
| 126 | IValidator<CreateOrderRequest> validator, |
| 127 | IOrderService orderService, |
| 128 | CancellationToken ct) |
| 129 | { |
| 130 | var validation = await validator.ValidateAsync(request, ct); |
| 131 | if (!validation.IsValid) |
| 132 | { |
| 133 | return Results.ValidationProblem(validation.ToDictionary()); |
| 134 | } |
| 135 | |
| 136 | var order = await orderService.CreateAsync(request, ct); |
| 137 | |
| 138 | return Results.Created($"/api/orders/{order.OrderId}", order); |
| 139 | } |
| 140 | } |
| 141 | ``` |
| 142 | |
| 143 | --- |
| 144 | |
| 145 | ## Request Pipeline Composition |
| 146 | |
| 147 | ### Endpoint Filters (Middleware for Endpoints) |
| 148 | |
| 149 | Use endpoint filters for cross-cutting concerns scoped to specific routes: |
| 150 | |
| 151 | ```csharp |
| 152 | // Validation filter applied to a route group |
| 153 | public sealed class ValidationFilter<TRequest> : IEndpointFilter |
| 154 | where TRequest : class |
| 155 | { |
| 156 | public async ValueTask<object?> InvokeAsync( |
| 157 | EndpointFilterInvocationContext context, |
| 158 | E |