clean-architecture
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseClean Architecture
整洁架构(Clean Architecture)
Core Principles
核心原则
- Dependency inversion is the foundation — All dependencies point inward. Domain has zero project references. Application references only Domain. Infrastructure references Application and Domain. Api references all but depends on abstractions. The compiler enforces this via project references.
- Domain owns the rules — Business logic lives in the Domain layer as entity methods, domain services, or specifications. The Domain layer has no knowledge of databases, HTTP, or any framework — only pure C# and .NET primitives.
- Use cases are the unit of work — Each use case (command or query) is a single class in the Application layer. It orchestrates domain objects, persists through abstractions, and returns a result. No "service" classes with 20 methods.
- Infrastructure is a plugin — EF Core, external APIs, email senders, file storage — all live in Infrastructure and implement interfaces defined in Application or Domain. Swap implementations without touching business logic.
- The API layer is thin — Endpoints map HTTP to use cases and use cases to HTTP responses. No business logic in endpoints.
- 依赖倒置是基础 — 所有依赖都指向内部。Domain层无任何项目引用。Application层仅引用Domain层。Infrastructure层引用Application和Domain层。Api层引用所有层,但依赖抽象。编译器通过项目引用强制实施这一规则。
- 领域层拥有规则 — 业务逻辑以实体方法、领域服务或规格的形式存在于Domain层。Domain层不了解数据库、HTTP或任何框架 — 仅包含纯C#和.NET原语。
- 用例是工作单元 — 每个用例(命令或查询)是Application层中的一个独立类。它编排领域对象,通过抽象实现持久化,并返回结果。不存在包含20个方法的“服务”类。
- 基础设施是插件 — EF Core、外部API、邮件发送器、文件存储 — 所有这些都位于Infrastructure层,并实现Application或Domain层定义的接口。无需修改业务逻辑即可替换实现。
- API层很精简 — 端点将HTTP请求映射到用例,并将用例结果转换为HTTP响应。端点中不包含业务逻辑。
Patterns
模式
Project Layout
项目布局
src/
MyApp.Domain/
Entities/
Order.cs # Entity with behavior
OrderItem.cs
Enums/
OrderStatus.cs
Exceptions/
DomainException.cs # Base domain exception
Interfaces/
IOrderRepository.cs # Only if query needs go beyond DbSet
Common/
Entity.cs # Base entity with Id
Result.cs # Result pattern type
MyApp.Application/
Common/
Behaviors/
ValidationBehavior.cs # Mediator pipeline behavior
Interfaces/
IAppDbContext.cs # DbContext abstraction (preferred over repository)
Orders/
Commands/
CreateOrder/
CreateOrderCommand.cs
CreateOrderHandler.cs
CreateOrderValidator.cs
Queries/
GetOrder/
GetOrderQuery.cs
GetOrderHandler.cs
OrderDto.cs
MyApp.Infrastructure/
Persistence/
AppDbContext.cs # Implements IAppDbContext
Configurations/
OrderConfiguration.cs
Migrations/
Services/
EmailSender.cs # Implements IEmailSender from Application
DependencyInjection.cs # AddInfrastructure extension
MyApp.Api/
Endpoints/
OrderEndpoints.cs # Thin, maps HTTP ↔ use cases
Program.cssrc/
MyApp.Domain/
Entities/
Order.cs # 带行为的实体
OrderItem.cs
Enums/
OrderStatus.cs
Exceptions/
DomainException.cs # 领域异常基类
Interfaces/
IOrderRepository.cs # 仅当查询需要超出DbSet的能力时使用
Common/
Entity.cs # 带Id的基础实体
Result.cs # 结果模式类型
MyApp.Application/
Common/
Behaviors/
ValidationBehavior.cs # Mediator管道行为
Interfaces/
IAppDbContext.cs # DbContext抽象(优先于仓库模式)
Orders/
Commands/
CreateOrder/
CreateOrderCommand.cs
CreateOrderHandler.cs
CreateOrderValidator.cs
Queries/
GetOrder/
GetOrderQuery.cs
GetOrderHandler.cs
OrderDto.cs
MyApp.Infrastructure/
Persistence/
AppDbContext.cs # 实现IAppDbContext
Configurations/
OrderConfiguration.cs
Migrations/
Services/
EmailSender.cs # 实现Application层的IEmailSender
DependencyInjection.cs # AddInfrastructure扩展方法
MyApp.Api/
Endpoints/
OrderEndpoints.cs # 精简,映射HTTP ↔ 用例
Program.csDbContext Abstraction (Preferred Over Repository)
DbContext抽象(优先于仓库模式)
Define a minimal interface in Application; implement in Infrastructure:
csharp
// Application/Common/Interfaces/IAppDbContext.cs
public interface IAppDbContext
{
DbSet<Order> Orders { get; }
DbSet<Product> Products { get; }
Task<int> SaveChangesAsync(CancellationToken ct = default);
}
// Infrastructure/Persistence/AppDbContext.cs
public class AppDbContext(DbContextOptions<AppDbContext> options)
: DbContext(options), IAppDbContext
{
public DbSet<Order> Orders => Set<Order>();
public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
}Why IAppDbContext over IRepository? EF Core's DbSet already IS a repository. Adding another abstraction on top adds indirection without value in most cases.
在Application层定义最小接口;在Infrastructure层实现:
csharp
// Application/Common/Interfaces/IAppDbContext.cs
public interface IAppDbContext
{
DbSet<Order> Orders { get; }
DbSet<Product> Products { get; }
Task<int> SaveChangesAsync(CancellationToken ct = default);
}
// Infrastructure/Persistence/AppDbContext.cs
public class AppDbContext(DbContextOptions<AppDbContext> options)
: DbContext(options), IAppDbContext
{
public DbSet<Order> Orders => Set<Order>();
public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
}为什么用IAppDbContext而不是IRepository?EF Core的DbSet本身就是一个仓库。在大多数情况下,添加另一层抽象只会增加间接性而没有价值。
Use Case Handler (Command)
用例处理器(命令)
csharp
// Application/Orders/Commands/CreateOrder/CreateOrderCommand.cs
public record CreateOrderCommand(
string CustomerId,
List<OrderItemDto> Items) : IRequest<Result<Guid>>;
public record OrderItemDto(string ProductId, int Quantity, decimal UnitPrice);
// Application/Orders/Commands/CreateOrder/CreateOrderHandler.cs — uses Mediator (source-generated, MIT)
internal sealed class CreateOrderHandler(
IAppDbContext db,
TimeProvider clock) : IRequestHandler<CreateOrderCommand, Result<Guid>>
{
public async ValueTask<Result<Guid>> Handle(CreateOrderCommand request, CancellationToken ct)
{
var order = Order.Create(
request.CustomerId,
request.Items.Select(i => new OrderItem(i.ProductId, i.Quantity, i.UnitPrice)),
clock.GetUtcNow());
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
return Result.Success(order.Id);
}
}
// Application/Orders/Commands/CreateOrder/CreateOrderValidator.cs
public class CreateOrderValidator : AbstractValidator<CreateOrderCommand>
{
public CreateOrderValidator()
{
RuleFor(x => x.CustomerId).NotEmpty();
RuleFor(x => x.Items).NotEmpty();
RuleForEach(x => x.Items).ChildRules(item =>
{
item.RuleFor(x => x.ProductId).NotEmpty();
item.RuleFor(x => x.Quantity).GreaterThan(0);
item.RuleFor(x => x.UnitPrice).GreaterThan(0);
});
}
}csharp
// Application/Orders/Commands/CreateOrder/CreateOrderCommand.cs
public record CreateOrderCommand(
string CustomerId,
List<OrderItemDto> Items) : IRequest<Result<Guid>>;
public record OrderItemDto(string ProductId, int Quantity, decimal UnitPrice);
// Application/Orders/Commands/CreateOrder/CreateOrderHandler.cs — 使用Mediator(源代码生成,MIT协议)
internal sealed class CreateOrderHandler(
IAppDbContext db,
TimeProvider clock) : IRequestHandler<CreateOrderCommand, Result<Guid>>
{
public async ValueTask<Result<Guid>> Handle(CreateOrderCommand request, CancellationToken ct)
{
var order = Order.Create(
request.CustomerId,
request.Items.Select(i => new OrderItem(i.ProductId, i.Quantity, i.UnitPrice)),
clock.GetUtcNow());
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
return Result.Success(order.Id);
}
}
// Application/Orders/Commands/CreateOrder/CreateOrderValidator.cs
public class CreateOrderValidator : AbstractValidator<CreateOrderCommand>
{
public CreateOrderValidator()
{
RuleFor(x => x.CustomerId).NotEmpty();
RuleFor(x => x.Items).NotEmpty();
RuleForEach(x => x.Items).ChildRules(item =>
{
item.RuleFor(x => x.ProductId).NotEmpty();
item.RuleFor(x => x.Quantity).GreaterThan(0);
item.RuleFor(x => x.UnitPrice).GreaterThan(0);
});
}
}Use Case Handler (Query)
用例处理器(查询)
csharp
// Application/Orders/Queries/GetOrder/GetOrderQuery.cs
public record GetOrderQuery(Guid OrderId) : IRequest<Result<OrderDto>>;
public record OrderDto(Guid Id, string CustomerId, decimal Total, string Status, DateTimeOffset CreatedAt);
// Application/Orders/Queries/GetOrder/GetOrderHandler.cs
internal sealed class GetOrderHandler(IAppDbContext db) : IRequestHandler<GetOrderQuery, Result<OrderDto>>
{
public async ValueTask<Result<OrderDto>> Handle(GetOrderQuery request, CancellationToken ct)
{
var order = await db.Orders
.Where(o => o.Id == request.OrderId)
.Select(o => new OrderDto(o.Id, o.CustomerId, o.Total, o.Status.ToString(), o.CreatedAt))
.FirstOrDefaultAsync(ct);
return order is not null
? Result.Success(order)
: Result.Failure<OrderDto>("Order not found");
}
}csharp
// Application/Orders/Queries/GetOrder/GetOrderQuery.cs
public record GetOrderQuery(Guid OrderId) : IRequest<Result<OrderDto>>;
public record OrderDto(Guid Id, string CustomerId, decimal Total, string Status, DateTimeOffset CreatedAt);
// Application/Orders/Queries/GetOrder/GetOrderHandler.cs
internal sealed class GetOrderHandler(IAppDbContext db) : IRequestHandler<GetOrderQuery, Result<OrderDto>>
{
public async ValueTask<Result<OrderDto>> Handle(GetOrderQuery request, CancellationToken ct)
{
var order = await db.Orders
.Where(o => o.Id == request.OrderId)
.Select(o => new OrderDto(o.Id, o.CustomerId, o.Total, o.Status.ToString(), o.CreatedAt))
.FirstOrDefaultAsync(ct);
return order is not null
? Result.Success(order)
: Result.Failure<OrderDto>("Order not found");
}
}Domain Entity with Behavior
带行为的领域实体
csharp
// Domain/Entities/Order.cs
public class Order : Entity
{
private readonly List<OrderItem> _items = [];
private Order() { } // EF Core
public string CustomerId { get; private set; } = null!;
public OrderStatus Status { get; private set; }
public decimal Total { get; private set; }
public DateTimeOffset CreatedAt { get; private set; }
public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
public static Order Create(string customerId, IEnumerable<OrderItem> items, DateTimeOffset now)
{
var order = new Order
{
Id = Guid.CreateVersion7(),
CustomerId = customerId,
Status = OrderStatus.Pending,
CreatedAt = now
};
foreach (var item in items)
order.AddItem(item);
return order;
}
public void AddItem(OrderItem item)
{
_items.Add(item);
Total = _items.Sum(i => i.Quantity * i.UnitPrice);
}
public Result Cancel()
{
if (Status is not OrderStatus.Pending)
return Result.Failure("Only pending orders can be cancelled");
Status = OrderStatus.Cancelled;
return Result.Success();
}
}csharp
// Domain/Entities/Order.cs
public class Order : Entity
{
private readonly List<OrderItem> _items = [];
private Order() { } // EF Core需要
public string CustomerId { get; private set; } = null!;
public OrderStatus Status { get; private set; }
public decimal Total { get; private set; }
public DateTimeOffset CreatedAt { get; private set; }
public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
public static Order Create(string customerId, IEnumerable<OrderItem> items, DateTimeOffset now)
{
var order = new Order
{
Id = Guid.CreateVersion7(),
CustomerId = customerId,
Status = OrderStatus.Pending,
CreatedAt = now
};
foreach (var item in items)
order.AddItem(item);
return order;
}
public void AddItem(OrderItem item)
{
_items.Add(item);
Total = _items.Sum(i => i.Quantity * i.UnitPrice);
}
public Result Cancel()
{
if (Status is not OrderStatus.Pending)
return Result.Failure("Only pending orders can be cancelled");
Status = OrderStatus.Cancelled;
return Result.Success();
}
}Thin Endpoint Wiring (IEndpointGroup Auto-Discovery)
精简端点连接(IEndpointGroup自动发现)
Every endpoint group implements and is auto-discovered via . Program.cs never changes when adding new endpoints. See the minimal-api skill for the full interface and setup.
IEndpointGroupapp.MapEndpoints()IEndpointGroupEndpointExtensionscsharp
// Api/Endpoints/OrderEndpoints.cs
public sealed class OrderEndpoints : IEndpointGroup
{
public void Map(IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/orders").WithTags("Orders");
group.MapPost("/", CreateOrder)
.WithName("CreateOrder");
group.MapGet("/{id:guid}", GetOrder)
.WithName("GetOrder");
group.MapGet("/", ListOrders)
.WithName("ListOrders");
}
private static async Task<IResult> CreateOrder(
CreateOrderCommand command, ISender sender, CancellationToken ct)
{
var result = await sender.Send(command, ct);
return result.IsSuccess
? TypedResults.Created($"/api/orders/{result.Value}", result.Value)
: result.ToProblemDetails();
}
private static async Task<IResult> GetOrder(
Guid id, ISender sender, CancellationToken ct)
{
var result = await sender.Send(new GetOrderQuery(id), ct);
return result.IsSuccess
? TypedResults.Ok(result.Value)
: TypedResults.NotFound();
}
private static async Task<IResult> ListOrders(
[AsParameters] ListOrdersQuery query, ISender sender, CancellationToken ct)
{
var result = await sender.Send(query, ct);
return TypedResults.Ok(result);
}
}每个端点组都实现,并通过自动发现。添加新端点时无需修改Program.cs。查看minimal-api技能获取完整的接口和设置。
IEndpointGroupapp.MapEndpoints()IEndpointGroupEndpointExtensionscsharp
// Api/Endpoints/OrderEndpoints.cs
public sealed class OrderEndpoints : IEndpointGroup
{
public void Map(IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/orders").WithTags("Orders");
group.MapPost("/", CreateOrder)
.WithName("CreateOrder");
group.MapGet("/{id:guid}", GetOrder)
.WithName("GetOrder");
group.MapGet("/", ListOrders)
.WithName("ListOrders");
}
private static async Task<IResult> CreateOrder(
CreateOrderCommand command, ISender sender, CancellationToken ct)
{
var result = await sender.Send(command, ct);
return result.IsSuccess
? TypedResults.Created($"/api/orders/{result.Value}", result.Value)
: result.ToProblemDetails();
}
private static async Task<IResult> GetOrder(
Guid id, ISender sender, CancellationToken ct)
{
var result = await sender.Send(new GetOrderQuery(id), ct);
return result.IsSuccess
? TypedResults.Ok(result.Value)
: TypedResults.NotFound();
}
private static async Task<IResult> ListOrders(
[AsParameters] ListOrdersQuery query, ISender sender, CancellationToken ct)
{
var result = await sender.Send(query, ct);
return TypedResults.Ok(result);
}
}Infrastructure DI Registration
基础设施层DI注册
csharp
// Infrastructure/DependencyInjection.cs
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration config)
{
services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(config.GetConnectionString("DefaultConnection")));
services.AddScoped<IAppDbContext>(sp => sp.GetRequiredService<AppDbContext>());
return services;
}
}csharp
// Infrastructure/DependencyInjection.cs
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration config)
{
services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(config.GetConnectionString("DefaultConnection")));
services.AddScoped<IAppDbContext>(sp => sp.GetRequiredService<AppDbContext>());
return services;
}
}Anti-patterns
反模式
Anemic Domain Model
贫血领域模型
csharp
// BAD — entity is just a data bag, all logic in handler
public class Order
{
public Guid Id { get; set; }
public string CustomerId { get; set; } = null!;
public decimal Total { get; set; }
public List<OrderItem> Items { get; set; } = [];
}
// Handler sets everything directly
order.Total = order.Items.Sum(i => i.Quantity * i.UnitPrice);
order.Status = OrderStatus.Pending;
// GOOD — entity encapsulates its own rules (see Domain Entity pattern above)
var order = Order.Create(customerId, items, clock.GetUtcNow());csharp
// 错误示例 — 实体只是数据容器,所有逻辑都在处理器中
public class Order
{
public Guid Id { get; set; }
public string CustomerId { get; set; } = null!;
public decimal Total { get; set; }
public List<OrderItem> Items { get; set; } = [];
}
// 处理器直接设置所有属性
order.Total = order.Items.Sum(i => i.Quantity * i.UnitPrice);
order.Status = OrderStatus.Pending;
// 正确示例 — 实体封装自身规则(见上文的领域实体模式)
var order = Order.Create(customerId, items, clock.GetUtcNow());DbContext in Domain Layer
Domain层引用DbContext
csharp
// BAD — Domain references EF Core
// Domain/Services/OrderService.cs
public class OrderService(AppDbContext db) { } // Domain depends on Infrastructure!
// GOOD — Domain defines interfaces, Infrastructure implements
// Domain/Interfaces/IOrderRepository.cs (only if you need query abstraction beyond DbSet)
// Application/Common/Interfaces/IAppDbContext.cs (preferred)csharp
// 错误示例 — Domain层引用EF Core
// Domain/Services/OrderService.cs
public class OrderService(AppDbContext db) { } // Domain层依赖Infrastructure层!
// 正确示例 — Domain层定义接口,Infrastructure层实现
// Domain/Interfaces/IOrderRepository.cs(仅当你需要超出DbSet的查询抽象时使用)
// Application/Common/Interfaces/IAppDbContext.cs(优先选择)Fat Endpoints
臃肿端点
csharp
// BAD — business logic in the endpoint
app.MapPost("/orders", async (CreateOrderRequest req, AppDbContext db) =>
{
var order = new Order { CustomerId = req.CustomerId };
foreach (var item in req.Items)
{
order.Items.Add(new OrderItem { ProductId = item.ProductId, Quantity = item.Quantity });
}
order.Total = order.Items.Sum(i => i.Quantity * i.UnitPrice);
db.Orders.Add(order);
await db.SaveChangesAsync();
return TypedResults.Created($"/orders/{order.Id}", order);
});
// GOOD — endpoint delegates to a use case
app.MapPost("/orders", async (CreateOrderCommand command, ISender sender, CancellationToken ct) =>
{
var result = await sender.Send(command, ct);
return result.IsSuccess
? TypedResults.Created($"/orders/{result.Value}", result.Value)
: result.ToProblemDetails();
});csharp
// 错误示例 — 业务逻辑在端点中
app.MapPost("/orders", async (CreateOrderRequest req, AppDbContext db) =>
{
var order = new Order { CustomerId = req.CustomerId };
foreach (var item in req.Items)
{
order.Items.Add(new OrderItem { ProductId = item.ProductId, Quantity = item.Quantity });
}
order.Total = order.Items.Sum(i => i.Quantity * i.UnitPrice);
db.Orders.Add(order);
await db.SaveChangesAsync();
return TypedResults.Created($"/orders/{order.Id}", order);
});
// 正确示例 — 端点委托给用例
app.MapPost("/orders", async (CreateOrderCommand command, ISender sender, CancellationToken ct) =>
{
var result = await sender.Send(command, ct);
return result.IsSuccess
? TypedResults.Created($"/orders/{result.Value}", result.Value)
: result.ToProblemDetails();
});Repository for Every Entity
每个实体对应一个仓库
csharp
// BAD — repository per entity duplicates DbSet functionality
public interface IOrderRepository { Task<Order?> GetByIdAsync(Guid id); }
public interface IProductRepository { Task<Product?> GetByIdAsync(Guid id); }
public interface ICustomerRepository { Task<Customer?> GetByIdAsync(Guid id); }
// GOOD — use IAppDbContext with DbSet<T> directly
// Only create a repository interface when you have complex query logic
// that you want to test in isolation or reuse across multiple use casescsharp
// 错误示例 — 每个实体对应一个仓库,重复DbSet的功能
public interface IOrderRepository { Task<Order?> GetByIdAsync(Guid id); }
public interface IProductRepository { Task<Product?> GetByIdAsync(Guid id); }
public interface ICustomerRepository { Task<Customer?> GetByIdAsync(Guid id); }
// 正确示例 — 直接使用IAppDbContext和DbSet<T>
// 仅当你有复杂的查询逻辑,且希望独立测试或在多个用例中复用,才创建仓库接口Decision Guide
决策指南
| Scenario | Recommendation |
|---|---|
| When to use CA over VSA | Medium+ domain complexity, long-lived system, team familiar with layers |
| When to add a Domain layer | Business rules involve invariants across entity groups |
| IAppDbContext vs repositories | Prefer IAppDbContext; add repository only for complex reusable queries |
| Mediator vs raw handlers in CA | Mediator for pipeline behaviors (validation, logging); raw handlers for simplicity |
| When to add Domain events | When side effects (notifications, audit) should be decoupled from the main flow |
| Evolving from VSA to CA | When handlers start needing shared domain logic that does not belong in Common/ |
| 场景 | 推荐方案 |
|---|---|
| 何时用整洁架构替代VSA | 中等及以上领域复杂度、长期维护系统、团队熟悉分层架构 |
| 何时添加Domain层 | 业务规则涉及跨实体组的不变量时 |
| IAppDbContext vs 仓库模式 | 优先选择IAppDbContext;仅为复杂的可复用查询添加仓库 |
| 整洁架构中使用Mediator还是原生处理器 | 用Mediator实现管道行为(验证、日志);原生处理器用于简化场景 |
| 何时添加领域事件 | 当副作用(通知、审计)需要与主流程解耦时 |
| 从VSA演进到整洁架构 | 当处理器开始需要不属于Common/的共享领域逻辑时 |