بنقرة واحدة
clean-architecture-dotnet
// Use when domain logic leaks into API/Infrastructure, project references violate layer boundaries, or CQRS handlers/buses need implementation.
// Use when domain logic leaks into API/Infrastructure, project references violate layer boundaries, or CQRS handlers/buses need implementation.
| name | clean-architecture-dotnet |
| description | Use when domain logic leaks into API/Infrastructure, project references violate layer boundaries, or CQRS handlers/buses need implementation. |
| metadata | {"version":2,"authors":["Sébastien DEGODEZ"]} |
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.
| Use | When |
|---|---|
sealed class Foo : AggregateRoot + factory method | The object has domain invariants, raises events, or owns child entities |
Plain sealed record Foo(...) (POCO) | Pure data carrier, no invariants, used only inside Application or API (DTOs, ViewModels) |
sealed class Bar : ValueObject | Immutable, identity-by-value, represents a domain concept (e.g., PolicyNumber, Horsepower) |
Rule of thumb: if you're tempted to add an if or throw to protect the object's state — it's a Domain object (aggregate or value object), not a POCO.
digraph clean_arch {
"New Feature?" [shape=diamond];
"Define Logic in Domain" [shape=box];
"Create Handler in Application" [shape=box];
"Inject Handler in API" [shape=box];
"New Feature?" -> "Define Logic in Domain" [label="Business Rule"];
"New Feature?" -> "Create Handler in Application" [label="Orchestration Only"];
"Define Logic in Domain" -> "Create Handler in Application";
"Create Handler in Application" -> "Inject Handler in API";
}
Commands (writes) and Queries (reads) separate side-effects:
// Application/Features/Orders/PlaceOrderCommandHandler.cs
public sealed class PlaceOrderCommandHandler : ICommandHandler<PlaceOrderCommand, OrderId>
{
private readonly IOrderRepository _repository; // Interface defined in Application/Domain
public async Task<OrderId> HandleAsync(PlaceOrderCommand cmd, CancellationToken ct)
{
var order = Order.Create(cmd.OrderId, cmd.CustomerName);
await _repository.AddAsync(order, ct);
return order.Id;
}
}
// API/OrdersEndpoints.cs — inject ICommandBus (never ICommandHandler<,> directly)
app.MapPost("/orders", async (PlaceOrderCommand cmd, ICommandBus bus)
=> Results.Created($"/orders/{await bus.PublishAsync<PlaceOrderCommand, OrderId>(cmd)}"));
// Queries follow the same rule — inject IQueryBus
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 | Purpose | Allowed Dependencies |
|---|---|---|
| SharedKernel (multi-context only) | Generic interfaces + base classes only — zero domain logic | None |
| Domain | Pure business logic, aggregates | SharedKernel (optional) |
| Application | Use cases, Handler orchestration | Domain, SharedKernel (optional) |
| Infrastructure | Database, DI registration, CQRS Bus | Application, Domain |
| API | Endpoints, JSON mapping | Infrastructure (Transitive: Application, Domain) |
Use a SharedKernel project when two or more Bounded Contexts need to share handler interfaces or base classes. It must contain zero domain logic.
SharedKernel/
├── Abstractions/
│ ├── ICommandHandler.cs ← generic interface only
│ ├── IQueryHandler.cs
│ ├── ValueObject.cs ← base class, no business logic
│ └── AggregateRoot.cs ← base class, exposes DomainEvents collection
└── Events/
└── DomainEvent.cs ← abstract base for all domain events
Dependency rule for SharedKernel: it depends on nothing. Domain and Application reference it, not the other way around.
// SharedKernel/Abstractions/ICommandHandler.cs — pure interface, no NuGet beyond System
public interface ICommandHandler<in TCommand, TResult>
{
Task<TResult> HandleAsync(TCommand command, CancellationToken ct = default);
}
// Each context's Application layer implements it:
// Orders.Application/Features/PlaceOrder/PlaceOrderCommandHandler.cs
using SharedKernel.Abstractions;
public sealed class PlaceOrderCommandHandler
: ICommandHandler<PlaceOrderCommand, OrderId> { }
What NEVER goes in SharedKernel: concrete value objects like Money, Address (each context defines its own); aggregate logic; context-specific event types.
Rule: Domain events are declared in the Domain layer and dispatched in the Application layer (from the handler, after the aggregate operation succeeds).
// Domain/Orders/Events/OrderPlacedEvent.cs — sealed, in Domain
public sealed class OrderPlacedEvent : DomainEvent // DomainEvent base from SharedKernel (or Domain if single-context)
{
public OrderId OrderId { get; }
public OrderPlacedEvent(OrderId orderId) => OrderId = orderId;
}
// Domain/Orders/Order.cs — aggregate raises the event
public sealed class Order : AggregateRoot
{
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, OrderId>
{
private readonly IOrderRepository _repository;
private readonly IDomainEventDispatcher _dispatcher; // interface in Application/Domain
public async Task<OrderId> HandleAsync(PlaceOrderCommand cmd, CancellationToken ct)
{
var order = Order.Create(cmd.OrderId, cmd.CustomerId);
await _repository.AddAsync(order, ct);
await _dispatcher.DispatchAsync(order.DomainEvents, ct); // ← dispatched in Application
return order.Id;
}
}
Anti-pattern: Never dispatch events from inside a Domain aggregate method — Domain has no dependency on dispatch infrastructure.
The architecture follows a strict outward-in dependency flow: API → Infrastructure → Application → Domain
CRITICAL: Just because a layer can see another via transitive reference doesn't mean it should use its concrete types.
| Excuse | Reality |
|---|---|
| "Injecting ICommandHandler<,> directly is simpler" | Always use ICommandBus / IQueryBus — consistent indirection, easier to intercept (logging, validation). |
| "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. |
using MyApp.Application; inside API layer filesusing MyApp.Infrastructure; inside Domain layer filesICommandHandler<,> or IQueryHandler<,> directly in API endpoints — use ICommandBus / IQueryBus| 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 via DI |
Guid used in handler return type | Use strongly typed ID (OrderId, ProductId) |
Read result named ProductDto | Name it ProductViewModel to distinguish from transfer objects |
typeof(Product).Assembly in tests | Use typeof(IApplicationMarker).Assembly for reliable discovery |
| Non-sealed Domain classes | All Domain classes must be sealed (enforced by NetArchTest) |
| SharedKernel references Domain | SharedKernel must depend on nothing — if it references Domain, invert: Domain references SharedKernel |
Handler contains if/business logic | Delegate to Domain aggregate methods — handlers orchestrate only |
./init-project.sh MyApp)