dotnet-architecture
.NET Architecture (DDD/CQRS)
Architecture using Domain-Driven Design (DDD) and CQRS patterns for clean, maintainable solutions.
Core Architectural Patterns
Domain-Driven Design (DDD)
Structure code around the business domain, not technical concerns.
- Focus on domain first: Design models that reflect business semantics, not database structures.
- Ubiquitous Language: Use business terminology consistently.
- Bounded Contexts: Partition system into logical domains with clear boundaries.
For detailed guidance on Entities, Aggregates, Value Objects, Domain Services, and lifecycle patterns: → See references/ddd-concepts.md
CQRS (Command Query Responsibility Segregation)
Separate read and write operations at the architectural level.
- Commands: Represent intent to change state → Return void or result object.
- Queries: Request data with no side effects → Return DTOs.
- Orchestration: Use MediatR to dispatch commands and queries to handlers.
For detailed command/query patterns, handler examples, and DTOs: → See references/cqrs-patterns.md
Layer Architecture
Four distinct layers with clear responsibilities:
Domain Layer
Pure business logic, no infrastructure dependencies.
- Contains: Entities, Aggregates, Value Objects, Domain Services.
- No I/O, database access, or external calls.
- Encapsulates business rules and aggregate consistency.
public class Order // Aggregate Root
{
public Guid OrderId { get; private set; }
private readonly List<OrderLine> _lines = new();
public void AddLine(Product product, int quantity)
{
if (Status != OrderStatus.Pending)
throw new InvalidOperationException();
_lines.Add(new OrderLine(product, quantity));
}
}
Application Layer
Orchestration and command/query handling.
- Contains: Command/Query Handlers (CQRS).
- Defines interfaces (repositories, external services).
- Contains DTOs and mapping logic.
- Handlers are thin; delegate to Domain.
public record CreateOrderCommand(Guid CustomerId, List<OrderLineDto> Lines) : IRequest<Guid>;
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>
{
public async Task<Guid> Handle(CreateOrderCommand command, CancellationToken ct)
{
var order = Order.CreateNew(customer, orderLines);
await _orderRepository.AddAsync(order, ct);
await _unitOfWork.SaveChangesAsync(ct);
return order.OrderId;
}
}
Infrastructure Layer
Technical concerns and external integrations.
- Implements repositories and external service interfaces.
- Handles database persistence (EF Core).
- Manages external integrations (APIs, brokers, caching).
- Configures DI composition via extension methods.
Presentation Layer (API)
HTTP entry/exit points.
- Controllers receive requests and delegate to MediatR.
- No business logic in controllers.
- Return DTOs, not domain models.
- Handle HTTP concerns: routing, versioning, error responses.
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IMediator _mediator;
[HttpPost]
public async Task<IActionResult> CreateOrder(CreateOrderCommand command)
{
var orderId = await _mediator.Send(command);
return CreatedAtAction(nameof(GetOrder), new { id = orderId }, orderId);
}
}
Quick Reference
When to Use Each DDD Concept
- Entity: Object with unique identity (User, Order, Product).
- Value Object: Immutable object defined by attributes (Money, Email, Address).
- Aggregate: Cluster of objects with a root entity (Order with OrderLines).
- Domain Service: Logic spanning multiple aggregates or too complex for one entity.
CQRS Guidelines
- Commands: Use imperative verbs (
CreateOrderCommand,CancelOrderCommand). - Queries: Use nouns (
GetOrderByIdQuery,ListOrdersQuery). - DTOs: Never expose domain models directly to clients.
- AsNoTracking: Always use for queries to optimize performance.
// Query example with AsNoTracking
var orders = await _context.Orders
.AsNoTracking()
.Where(o => o.CustomerId == customerId)
.Select(o => new OrderDto(o.OrderId, o.Status, o.Total))
.ToListAsync();
Infrastructure Composition
Organize DI configuration using extension methods in Infrastructure/Extensions.
Required Extension Methods
Create these in separate files under Infrastructure/Extensions:
- AddPersistence - Database context and Unit of Work
- AddRepositories - All repository implementations
- AddServices - Domain and application services
- AddExternalServices - HTTP clients, RabbitMQ, Redis, etc.
- AddConfigurations - Configuration objects as singletons
- AddKeyVault (IConfigurationBuilder) - Azure Key Vault setup
Program.cs Example
var builder = WebApplication.CreateBuilder(args);
// Configure Key Vault
builder.Configuration.AddAzureKeyVault(builder.Configuration);
// Add services via extension methods
builder.Services.AddPersistence(builder.Configuration);
builder.Services.AddRepositories();
builder.Services.AddServices();
builder.Services.AddExternalServices(builder.Configuration);
builder.Services.AddConfigurations(builder.Configuration);
// Add MediatR
builder.Services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();
For complete extension method implementations with examples: → See references/infrastructure-composition.md
Best Practices
Domain Layer
- Keep domain pure; no infrastructure dependencies.
- Enforce invariants within aggregates.
- Use value objects to make implicit concepts explicit.
- Keep aggregates focused and reasonably sized.
Application Layer
- Keep handlers thin; complex logic belongs in domain.
- Use Unit of Work for transactional consistency.
- Validate commands but enforce business rules in domain.
- Map domain models to DTOs for external consumption.
Infrastructure Layer
- Use extension methods for clean DI composition.
- Prioritize environment variables over appsettings for secrets.
- Validate required configuration at startup.
- Use Polly for resilience (retries, circuit breakers).
CQRS
- Separate command and query concerns strictly.
- Optimize queries with projections and AsNoTracking.
- Return identifiers from commands, not full objects.
- Use DTOs as contracts between layers.