Domain层必须零外部依赖且可独立编译测试,Application层仅编排业务逻辑,Infrastructure实现具体技术细节,WebAPI仅做HTTP适配,各层严格遵循依赖倒置与分层隔离原则。

Domain 层不能引用任何外部 NuGet 包,连 System.Text.Json 都不该出现 —— 这是判断你项目是否“真干净”的唯一硬指标。
Domain 项目必须是纯 .NET Standard / .NET 6+ 类库,且零依赖
很多团队在 Domain 里加了 Microsoft.EntityFrameworkCore 或者 AutoMapper,结果一升级框架就编译失败,或者单元测试跑不起来。根本原因是违反了“内层不依赖外层”原则。
检查方法很简单:打开 Domain 项目的 .csproj,里面只能有 <TargetFramework>,不能有任何 <PackageReference>;再用 Visual Studio 的“查看调用层次结构”右键点击任意实体类,确认没有箭头指向 Infrastructure 或 Application 的命名空间。
- 实体(
Product)、值对象(Money)、领域异常(InsufficientStockException)全用public,但不带任何 ORM 特性(比如别写[Key]) - 仓储接口(
IProductRepository)只定义领域语言的方法名,例如FindActiveBySku(string sku),而不是GetById(int id) - 泛型仓储(
IRepository<T>)可以存在,但具体实现必须放在 Infrastructure,Domain 只保留契约
Application 层别写数据访问和序列化逻辑
常见错误是把 JsonSerializer.Serialize()、Mapper.Map<>() 或 EF Core 的 AsNoTracking() 直接塞进 CreateProductCommandHandler。这会让 Application 层变成“技术胶水”,失去编排业务逻辑的本意。
Application 层只该做三件事:接收 Request(如 CreateProductCommand),调用 Domain 接口(如 IProductDomainService.Create()),返回 Response(如 CreateProductResponse)。所有 DTO 都是它自己定义的,和 Domain 实体完全隔离。
- 事务控制通过
IUnitOfWork接口声明,但SaveChangesAsync()实现必须在 Infrastructure - 数据库异常(如
DbUpdateConcurrencyException)不能在 Application 层 catch —— 应由 Infrastructure 捕获后转成领域异常(如ProductAlreadyExistsException)再往上抛 - 不要在 Handler 里 new 任何具体类型,所有依赖都走构造函数注入,且类型必须来自 Domain 或 Application 自己的接口
Program.cs 里绝对不能出现 Infrastructure 的具体类型
ASP.NET Core 6+ 的 Program.cs 是整个 Clean Architecture 的“胶水注入点”,这里错一点,依赖就漏气。最常踩的坑是直接 new AppDbContext 或在 services.AddScoped<>() 里传 Infrastructure 的实现类。
正确做法是:只注册接口,实现类的绑定留在 Infrastructure 层内部完成(比如在 Infrastructure 的 DependencyInjection.cs 里调用 services.AddDbContext<AppDbContext>()),然后让 Program.cs 只引用 Infrastructure 的扩展方法(如 services.AddInfrastructure(Configuration))。
-
AppDbContext类型不能在Program.cs中 using,也不能出现在任何services.调用的泛型参数里 - 配置项(如连接字符串)从
IConfiguration读取,但解析逻辑(比如options.UseSqlServer())必须封装在 Infrastructure - 如果用了 MediatR,
IMediator注册在 Program.cs,但所有IRequestHandler<>实现类必须在 Infrastructure 或 Application 中定义,不能跨层混放
WebAPI 层只负责 HTTP 协议适配,不做业务判断
Controller 不该验证库存是否充足、价格是否合法 —— 这些全是 Domain 的事。它只干两件事:把 HTTP 请求转成 Command/Query,把 Response 转成 HTTP 响应。否则 Controller 会越来越胖,单元测试难写,API 版本升级时容易误伤业务逻辑。
比如 ProductsController.Post() 应该只调用 mediator.Send(new CreateProductCommand(...)),然后 return Ok(result)。所有校验、默认值填充、状态转换,都在 CommandHandler 或 Domain Service 里完成。
- 不要在 Controller 里手动 new ViewModel 或 DTO,全部交给 Application 层返回
- HTTP 状态码(如 400、404、409)由 Application 层的 Result 类型(如
Result<T>)携带语义,Controller 统一映射 - Swagger 注释、授权(
[Authorize])、限流等横切关注点,用中间件或过滤器处理,不侵入 Controller 方法体
最容易被忽略的是:Domain 层的“干净”不是靠目录名体现的,而是靠编译时能不能脱离整个解决方案单独构建并测试。如果你删掉 Infrastructure 和 WebAPI 项目后,Domain 项目还能成功 dotnet build 并跑通所有单元测试,那才真正立住了。


















