mcpField
C
clean-architecture

clean-architecture

github.com/codewithmukesh开发7933.2k 次安装12 周前更新
代码仓库

关于 clean-architecture

面向采用 C# 和 .NET 的开发者,提供分层 Clean Architecture 的设计原则与实现模式。内容涵盖依赖边界、用例组织和精简 API 端点,并说明常见反模式。

主要功能

  1. 01依赖反转与分层边界
  2. 02领域层业务规则建模
  3. 03按用例组织命令与查询
  4. 04以接口隔离基础设施实现
  5. 05端点组自动发现
  6. 06反模式与架构决策指南

适用场景

  1. 01规划中大型领域系统的项目结构
  2. 02将业务规则与数据库和 HTTP 解耦
  3. 03按用例实现命令与查询处理器

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill clean-architecture

安装后在 Claude Code 中输入/clean-architecture确认已加载。

详细介绍

整洁架构

核心原则

  1. 依赖倒置是基础——所有依赖都指向内层。Domain 没有任何项目引用。Application 只引用 Domain。Infrastructure 引用 Application 和 Domain。Api 引用其他所有层,但依赖的是抽象。这由编译器通过项目引用强制执行。
  2. Domain 负责规则——业务逻辑位于 Domain 层,以实体方法、领域服务或规约的形式实现。Domain 层不了解数据库、HTTP 或任何框架,只使用纯 C# 和 .NET 基础类型。
  3. 用例是工作单元——每个用例(命令或查询)都是 Application 层中的一个独立类。它负责协调领域对象,通过抽象持久化,并返回结果。不要创建包含 20 个方法的“服务”类。
  4. Infrastructure 是插件——EF Core、外部 API、邮件发送器、文件存储都位于 Infrastructure 中,并实现由 Application 或 Domain 定义的接口。替换实现时无需触及业务逻辑。
  5. 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