clean-architecture-dotnet · diff
git:20261006.b2df96a to git:20261007.413caa5
1 added, 1 removed. Audit A to A.
---
name: clean-architecture-dotnet
- description: Use when domain logic leaks into API/Infrastructure, project references violate layer boundaries, or you need to decide between CQS (always), CQRS bus (complex domains), and DDD patterns (invariants and events).
+ description: Use when laying out or extending a .NET / ASP.NET Core solution split into Domain, Application, Infrastructure and Api projects — which project a class belongs in, which project may reference which, how an endpoint reaches a use case, and whether a feature needs plain CQS, a CQRS bus or DDD aggregates.
---
# Clean Architecture in .NET
## Overview
Clean Architecture organizes code into independent layers (Domain → Application → Infrastructure → API).
**The Iron Law**: Violating layer boundaries is a failure. If API references Application or Domain references Infrastructure, delete the change and start over.
> **Concrete violations that break the Iron Law:**
> - `using MyApp.Infrastructure;` in a Domain class — Infrastructure must never flow inward.
> - `using MyApp.Application;` in an API endpoint file — API must only interact via `ICommandBus` / `IQueryBus` or an injected use case interface.
> - `new AppDbContext()` inside a Controller — bypasses both Infrastructure and Application layers.
> - `HttpClient` injected directly into a Domain or Application class — HTTP belongs in Infrastructure behind an interface.
>
> **Fix:** move the dependency to the correct outer layer and expose it through an interface pointing inward.
## When to Use
- Business logic is leaking into Controllers or Repositories
- Circular dependencies occur between projects
- Need to isolate core business rules from external frameworks (EF Core, APIs)
- Autonomy is required: sealed Domain objects, strongly-typed IDs
**When NOT to add CQRS or DDD:**
- The feature is pure CRUD with no business rules, no invariants, and no domain events → use a CQS Application service (see Pattern A below)
- A query only reads data with no domain logic → inject the repository or a read service directly in the Application handler; no `IQueryBus` required
- You have fewer than 3 use cases with no cross-cutting concerns (logging pipeline, validation pipeline) → a CQRS bus adds ceremony without value
## CQS vs CQRS vs DDD — Complexity Threshold
These three patterns are independent tools. Choose based on what the codebase actually needs.
| Pattern | Rule | Add it when… | Skip it when… |
|---------|------|--------------|---------------|
| **CQS** *(always)* | Every handler/method is either a command (void) or a query (returns value) — never both | Always — zero exceptions | Never skip |
| **CQRS + Bus** *(optional)* | Separate `ICommandBus` / `IQueryBus` routing commands and queries through a pipeline | Cross-cutting concerns (validation, logging pipelines), large teams writing many use cases, read/write model asymmetry | Simple CRUD; < 3 use cases; no cross-cutting pipeline needed |
| **DDD** *(optional)* | Aggregates, factory methods, value objects, domain events | Domain has invariants (rules that must be enforced on every state change), state machines, rich business logic | Entities are pure data (no `if`/`throw` to protect state); CRUD-only feature |
**Quick rule of thumb:**
> "Does this feature have a business rule that could be violated?"
> — No → CQS Application Service (Pattern A).
> — Yes → Domain Aggregate + CQS or CQRS depending on scale (Pattern B).
## Implementation Flow
```dot
digraph clean_arch {
"Domain has invariants?" [shape=diamond];
"Pattern A: CQS App Service" [shape=box];
"Pattern B: DDD + CQRS?" [shape=diamond];
"Define Aggregate in Domain" [shape=box];
"Create Handler in Application" [shape=box];
"Add CQRS Bus? (optional)" [shape=diamond];
"Route via ICommandBus/IQueryBus" [shape=box];
"Inject handler directly" [shape=box];
"Expose in API" [shape=box];
"Domain has invariants?" -> "Pattern A: CQS App Service" [label="No (CRUD)"];
"Domain has invariants?" -> "Pattern B: DDD + CQRS?" [label="Yes"];
"Pattern A: CQS App Service" -> "Expose in API";
"Pattern B: DDD + CQRS?" -> "Define Aggregate in Domain";
"Define Aggregate in Domain" -> "Create Handler in Application";
"Create Handler in Application" -> "Add CQRS Bus? (optional)";
"Add CQRS Bus? (optional)" -> "Route via ICommandBus/IQueryBus" [label="Yes (cross-cutting)"];
"Add CQRS Bus? (optional)" -> "Inject handler directly" [label="No"];
"Route via ICommandBus/IQueryBus" -> "Expose in API";
"Inject handler directly" -> "Expose in API";
}
```
## Core Patterns
### Pattern A: CQS Application Use Case (Simple / CRUD)
Use when the feature has no business invariants. Layer boundaries are maintained — no CQRS bus needed.
**CQS rule:** each method is either a command (void / `Task`) or a query (returns a value) — never both.
**Naming:** call it `*UseCase`, not `*Service`. "Service" implies stateful or cross-cutting infrastructure; a use case class orchestrates one bounded interaction.
```csharp
// Application/Contacts/ContactUseCase.cs — plain use case, no bus
public sealed class ContactUseCase
{
private readonly IContactRepository _repository; // interface defined in Application (see Interface Placement)
public ContactUseCase(IContactRepository repository)
{
_repository = repository;
}
// Command — void (CQS)
public async Task CreateAsync(Guid id, string name, CancellationToken ct)
{
await _repository.AddAsync(new Contact(new ContactId(id), name), ct);
}
// Query — maps domain object to ViewModel in the use case (Option 1)
public async Task<ContactViewModel?> GetByIdAsync(Guid id, CancellationToken ct)
{
var contact = await _repository.FindAsync(new ContactId(id), ct);
return contact is null ? null : new ContactViewModel(contact.Id.Value, contact.Name);
}
}
// API/ContactsEndpoints.cs — inject the use case directly (no bus)
app.MapPost("/contacts", async (CreateContactRequest req, ContactUseCase uc) =>
{
var id = Guid.NewGuid();
await uc.CreateAsync(id, req.Name, default);
return Results.Created($"/contacts/{id}", null);
});
app.MapGet("/contacts/{id:guid}", async (Guid id, ContactUseCase uc) =>
await uc.GetByIdAsync(id, default) is { } vm ? Results.Ok(vm) : Results.NotFound());
```
**Option 2 — read-optimized query:** for read-heavy paths, skip the domain object entirely. Define a dedicated read interface in Application; Infrastructure implements it directly (e.g., a raw SQL/EF projection).
```csharp
// Application/Contacts/IContactReadService.cs — interface in Application, no domain object involved
public interface IContactReadService
{
Task<ContactViewModel?> GetByIdAsync(ContactId id, CancellationToken ct = default);
}
// Infrastructure/Contacts/ContactReadService.cs — implementation maps directly from DB row to ViewModel
public sealed class ContactReadService : IContactReadService
{
private readonly AppDbContext _db;
public ContactReadService(AppDbContext db)
{
_db = db;
}
public async Task<ContactViewModel?> GetByIdAsync(ContactId id, CancellationToken ct)
{
return await _db.Contacts
.Where(c => c.Id == id.Value)
.Select(c => new ContactViewModel(c.Id, c.Name))
.FirstOrDefaultAsync(ct);
}
}
```
> **Rule of thumb:** `IContactRepository` (write path) returns domain objects. `IContactReadService` (read path) returns ViewModels. Both interfaces live in Application; implementations live in Infrastructure.
### Pattern B: CQRS + DDD (Complex Domains)
Use when the domain has invariants, state transitions, or events. CQRS bus is optional even here — add it when you need a cross-cutting pipeline.
> **Add `ICommandBus` / `IQueryBus` only when ALL of the following are true:**
> 1. You need a **cross-cutting pipeline** for ALL commands/queries (e.g., logging every command, input validation via pipeline behaviours, performance metrics).
> 2. You have **multiple use cases** (>3) that all benefit from this pipeline — the overhead is justified by scale.
> 3. Your read and write models are **structurally different** (CQRS read-model separation), or your team is large enough that different people own the command and query paths.
>
> If only one condition is true, use a decorator or middleware instead — don't build a bus for a single use case.
**CQS rule for commands:** a command must be void — it does not return a value. The caller is responsible for generating the ID and including it in the command.
```csharp
// Application/Features/Orders/PlaceOrderCommand.cs — ID is part of the command
public sealed record PlaceOrderCommand(OrderId OrderId, string CustomerName);
// Application/Features/Orders/PlaceOrderCommandHandler.cs
public sealed class PlaceOrderCommandHandler : ICommandHandler<PlaceOrderCommand>
{
private readonly IOrderRepository _repository; // IOrderRepository defined in Domain (write path) — see Interface Placement
public async Task HandleAsync(PlaceOrderCommand cmd, CancellationToken ct)
{
var order = Order.Create(cmd.OrderId, cmd.CustomerName);
await _repository.AddAsync(order, ct);
}
}
// API/OrdersEndpoints.cs — caller generates the ID, sends it via ICommandBus (optional)
app.MapPost("/orders", async (PlaceOrderCommand cmd, ICommandBus bus) =>
{
await bus.PublishAsync(cmd);
return Results.Created($"/orders/{cmd.OrderId}");
});
// Queries follow the same rule — inject IQueryBus if using CQRS bus
app.MapGet("/orders/{id}", async (Guid id, IQueryBus bus)
=> Results.Ok(await bus.SendAsync<GetOrderQuery, OrderViewModel>(new GetOrderQuery(new OrderId(id)))));
// Both buses resolve via Infrastructure DI. API never references Application assembly.
```
## Layer Responsibilities
| Layer | Purpose | Allowed Dependencies |
|-------|---------|----------------------|
| **Domain** | Pure business logic, aggregates | None |
| **Application** | Use cases, Handler orchestration | Domain |
| **Infrastructure** | Database, DI registration, CQRS Bus | Application, Domain |
| **API** | Endpoints, JSON mapping | **Infrastructure** (Transitive: Application, Domain) |
## Domain Events
**Rule:** Domain events are **declared in the Domain layer** and **dispatched in the Application layer** (from the handler, after the aggregate operation succeeds).
```csharp
// Domain/Orders/Events/OrderPlacedEvent.cs — sealed, in Domain
public sealed class OrderPlacedEvent : DomainEvent // DomainEvent base in Domain
{
public OrderId OrderId { get; }
public OrderPlacedEvent(OrderId orderId)
{
OrderId = orderId;
}
}
// Domain/Orders/Order.cs — aggregate raises the event
public sealed class Order : AggregateRoot
{
public OrderId Id { get; }
public CustomerId CustomerId { get; }
// Private constructor — only the factory method can create an instance
private Order(OrderId id, CustomerId customerId)
{
Id = id;
CustomerId = customerId;
}
public static Order Create(OrderId id, CustomerId customerId)
{
var order = new Order(id, customerId);
order.AddDomainEvent(new OrderPlacedEvent(id)); // ← raised in Domain
return order;
}
}
// Application/Features/PlaceOrder/PlaceOrderCommandHandler.cs — handler dispatches
public sealed class PlaceOrderCommandHandler : ICommandHandler<PlaceOrderCommand>
{
private readonly IOrderRepository _repository;
private readonly IDomainEventDispatcher _dispatcher; // interface in Application/Domain
public async Task HandleAsync(PlaceOrderCommand cmd, CancellationToken ct)
{
var order = Order.Create(cmd.OrderId, cmd.CustomerId); // ← ID comes from the command
await _repository.AddAsync(order, ct);
await _dispatcher.DispatchAsync(order.DomainEvents, ct); // ← dispatched in Application
}
}
```
**Anti-pattern:** Never dispatch events from inside a Domain aggregate method — Domain has no dependency on dispatch infrastructure.
## The Dependency Chain (Transitive Access)
The architecture follows a strict outward-in dependency flow:
**API** → **Infrastructure** → **Application** → **Domain**
- **API** has access to everything below it (Infrastructure, Application, Domain).
- **Infrastructure** has access to Application and Domain.
- **Application** has access to Domain.
- **Domain** remains pure with **zero** project dependencies.
**CRITICAL**: Just because a layer *can* see another via transitive reference doesn't mean it *should* use its concrete types.
- API should only use **Interfaces** from Application/Domain.
- Always follow the **Iron Law** and **Red Flags** below.
## Interface Placement — Repository, Authorization, and Authentication
See [Interface Placement](references/interface-placement.md) for full decision tables and examples.
| Contract | Define in | Key question |
|----------|-----------|------|
| Write repo for a Domain aggregate | **Domain** | Does it persist an aggregate? |
| Write repo for CRUD entity / read service | **Application** | No aggregate — Application owns the contract |
| `ICurrentUser` / `IUserContext` | **Application** | Use cases need the caller's identity |
| Authentication (token/role gate) | **Infrastructure** | Technical filter, no business logic |
> **Authorization quick rule:** read access gate → Application use case policy. Mutation invariant → Domain aggregate method. Token/role → Infrastructure middleware.
## Rationalization Table
| Excuse | Reality |
|--------|---------|
| "Injecting ICommandHandler<,> directly is simpler" | If you're using a CQRS bus, always route through `ICommandBus` / `IQueryBus` — consistent indirection, easier to intercept. But if you're not using a bus at all (Pattern A), injecting the Application service directly is correct. |
| "It's just one small service" | Small leaks become circular dependency nightmares. |
| "Referencing Application in API is faster" | It bypasses the Bus/Handler pattern and couples contract to implementation. |
| "Domain needs this NuGet package" | If it's not a primitive/System lib, it doesn't belong in Domain. |
| "Every handler needs a CQRS bus" | Only if you need a cross-cutting pipeline or read/write model separation. Simple CRUD with a CQS Application service is valid and cleaner. |
## Red Flags - STOP and Start Over
- `using MyApp.Application;` inside API layer files
- `using MyApp.Infrastructure;` inside Domain layer files
- Injecting `ICommandHandler<>` or `IQueryHandler<,>` directly in API endpoints when using a CQRS bus — route through `ICommandBus` / `IQueryBus`
- Command handler returns a domain ID — commands must be void (`Task`); the ID must be part of the incoming command
- Handlers performing HTTP calls directly (use an Infrastructure service via interface)
- Adding `ICommandBus` / `IQueryBus` to a simple CRUD API with no domain logic, invariants, or cross-cutting pipeline — use a CQS Application use case (Pattern A) instead
- Application class named `*Service` (e.g., `OrderService`) — rename to `*UseCase`; "Service" implies infrastructure or shared state, not a single bounded interaction
## Common Mistakes
| Mistake | Fix |
|---------|-----|
| Handler not found by DI | Handler must be listed explicitly in `AddInfrastructure()` via `AddHandler<T>()` |
| `using MyApp.Application;` in API | Remove it — inject `ICommandBus` / `IQueryBus` (CQRS) or Application service (CQS) via DI |
| Command handler returns an ID | Commands are void — the caller generates the ID and passes it in the command |
| Read result named `ProductDto` | Name it `ProductViewModel` to distinguish from transfer objects |
| `typeof(Product).Assembly` in tests | Use `typeof(IApplicationMarker).Assembly` for reliable discovery |
| Handler contains `if`/domain invariant logic | Delegate to Domain aggregate methods — handlers orchestrate only. Exception: Application use cases may contain access policy checks (`if (resource.OwnerId != _currentUser.Id) throw`) — that is use-case policy, not domain invariant logic |
| Adding CQRS bus for < 3 use cases with no cross-cutting concerns | Use Pattern A (CQS Application service) — less indirection, same layer safety |
---
## References
- [Architecture Layers](references/architecture-layers.md) — Dependency rules, marker interfaces, layer tests, and layer validation with NetArchTest
- [CQRS Patterns](references/cqrs-patterns.md) — Handler interfaces, examples, DI discovery, and complete CQRS implementation patterns
- [Convention-Based DI](references/convention-based-di.md) — Auto-discovery, Bus interfaces, full DI setup
- [Layer Responsibilities](references/layer-responsibilities.md) — Full rules for each layer with DDD context
- [Project Structure](references/project-structure.md) — File/folder conventions and layer organization guidance
- [NetArchTest Rules](references/netarchtest-rules.md) — Automated boundary enforcement
- [DDD Patterns](references/ddd.md) — Aggregates, factory methods, value objects, and domain events (optional)
- [Interface Placement](references/interface-placement.md) — Repository, authorization, and ICurrentUser placement with GetOrder (read policy) + CancelOrder (domain invariant) examples
- [Init Script](scripts/init-project.sh) — Bootstrap script for new project setup (`./init-project.sh MyApp`)
- [ArchitectureTests Template](templates/IntegrationTests/ArchitectureTests.cs) — Drop-in architecture test template