关于 clean-architecture
面向采用 C# 和 .NET 的开发者,提供分层 Clean Architecture 的设计原则与实现模式。内容涵盖依赖边界、用例组织和精简 API 端点,并说明常见反模式。
主要功能
- 01依赖反转与分层边界
- 02领域层业务规则建模
- 03按用例组织命令与查询
- 04以接口隔离基础设施实现
- 05端点组自动发现
- 06反模式与架构决策指南
适用场景
- 01规划中大型领域系统的项目结构
- 02将业务规则与数据库和 HTTP 解耦
- 03按用例实现命令与查询处理器
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ codewithmukesh/ dotnet-claude-kit --skill clean-architecture安装后在 Claude Code 中输入/clean-architecture确认已加载。
详细介绍
整洁架构
核心原则
- 依赖倒置是基础——所有依赖都指向内层。Domain 没有任何项目引用。Application 只引用 Domain。Infrastructure 引用 Application 和 Domain。Api 引用其他所有层,但依赖的是抽象。这由编译器通过项目引用强制执行。
- Domain 负责规则——业务逻辑位于 Domain 层,以实体方法、领域服务或规约的形式实现。Domain 层不了解数据库、HTTP 或任何框架,只使用纯 C# 和 .NET 基础类型。
- 用例是工作单元——每个用例(命令或查询)都是 Application 层中的一个独立类。它负责协调领域对象,通过抽象持久化,并返回结果。不要创建包含 20 个方法的“服务”类。
- Infrastructure 是插件——EF Core、外部 API、邮件发送器、文件存储都位于 Infrastructure 中,并实现由 Application 或 Domain 定义的接口。替换实现时无需触及业务逻辑。
- API 层应保持精简——端点负责将 HTTP 映射到用例,再将用例映射为 HTTP 响应。端点中不包含业务逻辑。
模式
项目布局
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.cs
DbContext 抽象(优先于 Repository)
在 Application 中定义一个最小接口,并在 Infrastructure 中实现:
// 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 本身就是 Repository。在此基础上再增加一层抽象,在大多数情况下只会增加间接层,不会带来价值。
用例处理程序(命令)
// 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);
});
}
}
用例处理程序(查询)
// 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/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();
}
}
精简的端点接线(IEndpointGroup 自动发现)
每个端点组都实现 IEndpointGroup,并通过 app.MapEndpoints() 自动发现。添加新端点时,无需更改 Program.cs。完整的 IEndpointGroup 接口和 EndpointExtensions 配置请参见 minimal-api 技能。
// 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 注册
// 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;
}
}
反模式
贫血领域模型
// 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());
Domain 层中的 DbContext
// 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)
臃肿的端点
// 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();
});
为每个实体创建 Repository
// 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 cases
决策指南
| 场景 | 建议 |
|---|---|
| 何时选择 CA 而非 VSA | 领域复杂度达到中等或更高、系统生命周期长、团队熟悉分层架构 |
| 何时添加 Domain 层 | 业务规则涉及跨实体组的不变量 |
| IAppDbContext 与 Repository 的选择 | 优先使用 IAppDbContext;仅在存在复杂且可复用的查询时添加 Repository |
| CA 中选择 Mediator 还是原始处理程序 | 需要管道行为(验证、日志记录)时使用 Mediator;追求简洁时使用原始处理程序 |
| 何时添加领域事件 | 需要将副作用(通知、审计)与主流程解耦时 |
| 从 VSA 演进到 CA | 当处理程序开始需要不适合放在 Common/ 中的共享领域逻辑时 |
评论
支持 Markdown