CleanArchitecture 模板的 ADR-001在 Application 层直接使用 EF Core而不是引入仓储层【免费下载链接】CleanArchitectureClean Architecture Solution Template for ASP.NET Core项目地址: https://gitcode.com/GitHub_Trending/cle/CleanArchitecture本篇技术指南以本仓库Clean Architecture Solution Template for ASP.NET Core的 ADR-001 决策记录 为骨架系统讲解该模板为何让 Application 层通过自有的IApplicationDbContext接口直接操作 EF Core而不是在两者之间再插入一层仓储Repository。读完本文你将理解依赖倒置与零框架引用的区别、仓储模式在非 DDD 场景下的真实成本以及该模板如何在命令/查询处理器、功能测试与 Aspire 编排中落地这一决策并能据此判断自己的项目是否适合沿用这条默认路径。ADR-001 决策概览条目内容文档docs/decisions/ADR-001-Use-EFCore-In-Application-Layer.md状态Accepted已采纳日期2024-02-29核心结论Application 层定义IApplicationDbContext暴露DbSetT命令/查询处理器直接使用它Infrastructure 层实现该接口不在两者之间引入仓储层这一决策的适用前提是模板的默认路线不预设 DDD。它回答了一个在 Clean Architecture 实践中反复出现的问题——Application 层到底该不该接触 ORM该不该用仓储把 EF Core 藏起来背景依赖方向与 ORM 的位置Clean Architecture 的核心约束是内层不依赖外层。ORM 这类框架通常被归入 Infrastructure 层于是出现一个设计岔路口方案 AApplication 层定义一个自己拥有的接口直接通过该接口访问 EF Core方案 B在 Application 与 EF Core 之间放一层仓储Repository由仓储封装所有数据访问细节。模板选择了方案 A。原因在 ADR 的 Rationale 部分给出下面结合源码逐一展开。决策落地IApplicationDbContext与ApplicationDbContext接口由 Application 层定义并拥有接口定义在 Application 层见 IApplicationDbContext.cspublic interface IApplicationDbContext { DbSetTodoList TodoLists { get; } DbSetTodoItem TodoItems { get; } Taskint SaveChangesAsync(CancellationToken cancellationToken); }它只暴露了模板中真实存在的两个聚合入口DbSetTodoList、DbSetTodoItem以及持久化提交方法SaveChangesAsync没有任何数据库提供程序provider相关的类型。实现位于 Infrastructure 层ApplicationDbContext.cs 在 Infrastructure 层实现该接口public class ApplicationDbContext : IdentityDbContextApplicationUser, IApplicationDbContext { public ApplicationDbContext(DbContextOptionsApplicationDbContext options) : base(options) { } public DbSetTodoList TodoLists SetTodoList(); public DbSetTodoItem TodoItems SetTodoItem(); protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); } }实现细节全部停留在 Infrastructure它继承IdentityDbContextApplicationUser以集成 ASP.NET Core Identity实体配置通过ApplyConfigurationsFromAssembly从 Configurations 目录 自动装载数据库提供程序的选择SQLite / SQL Server / PostgreSQL则由 Infrastructure/DependencyInjection.cs 中的编译期开关决定。注册把具体 DbContext 以接口身份注入依赖注入的关键一步在 Infrastructure/DependencyInjection.csbuilder.Services.AddDbContextApplicationDbContext((sp, options) { options.AddInterceptors(sp.GetServicesISaveChangesInterceptor()); #if (UsePostgreSQL) options.UseNpgsql(connectionString); #elif (UseSqlServer) options.UseSqlServer(connectionString); #else options.UseSqlite(connectionString); #endif ... }); builder.Services.AddScopedIApplicationDbContext(provider provider.GetRequiredServiceApplicationDbContext());ApplicationDbContext作为具体类型注册并完成数据库配置随后以IApplicationDbContext的身份按 Scoped 生命周期对外暴露。这样 Application 层只认接口所有数据库相关的装配都被隔离在 Infrastructure 层内部。数据访问链路ADR 明确给出了从处理器到数据库的完整链路handler - IApplicationDbContext - ApplicationDbContext以 CreateTodoItem.cs 的命令处理器为例它构造TodoItem实体后直接调用_context.TodoItems.Add(...)与_context.SaveChangesAsync(...)全程没有经过任何仓储方法。查询侧同样直接面对 EF Core 的完整表达能力例如 GetTodos.cs 中链式使用AsNoTracking()、AutoMapper 的ProjectToTodoListDto()、OrderBy与ToListAsync完成投影排序。这正是 ADR 在 Consequences 中强调的全表达力可用——投影、Include、原生 SQL、编译查询都不需要绕过仓储接口去迁就抽象。理由一依赖倒置已经满足而非零框架引用这是整个 ADR 最容易被误解的地方。Application 层确实持有对 EF Core 抽象类型DbSetT、IQueryableT的编译期引用GlobalUsings.cs 中声明了global using Microsoft.EntityFrameworkCore;Application.csproj 中引用了Microsoft.EntityFrameworkCore包但 ADR 明确指出引用一个程序集assembly不等于依赖一个具体实现。依赖倒置关心的是依赖箭头的方向而不是追求内层对框架引用的绝对为零。真实方向是IApplicationDbContext定义在 Application 层内层ApplicationDbContext在 Infrastructure 层实现该接口外层实现内层契约Application 层不知道ApplicationDbContext、不知道数据库提供程序、不引用任何 Infrastructure 类型。因此箭头从 Infrastructure 指向 Application依赖倒置成立。理由二仓储在此场景下只是加了间接层没有有意义的抽象ADR 对方案 B 的批评非常具体如果为了隐藏 EF Core 而在 Application 层定义仓储接口、在 Infrastructure 层实现那么耦合并未消失只是被隐藏——仓储实现内部仍然使用 EF CoreORM 语义会从接口里漏出来——加载哪些关联实体、如何过滤、如何投影这类意图最终会以GetByIdWithItems、GetActiveOrderedByName之类的自定义方法形态出现在仓储接口上本质上是对 EF Core 查询模式的复刻却失去了 EF Core 的语法与工具链支持代价是真实的——多了一层需要设计、实现、并随需求演进而持续同步的间接层可发现性discoverability反而下降。结论是IApplicationDbContext直接暴露DbSetT的做法比仓储更诚实地反映了实际发生的事情。理由三功能测试倾向针对真实数据库ADR 明确采纳了针对真实数据库做功能测试的策略并指出这是 Microsoft EF Core 官方推荐的测试策略 所提倡的方向原文链接本文不展开外部内容。真实数据库能暴露内存假实现与 mock 仓储无法发现的问题provider 特有的查询行为、索引冲突、事务语义、约束强制。模板的测试工程完整落地了这条策略测试基础设施TestApp.cs、WebApiFactory.cs通过 Aspire 编排拉起真实数据库DatabaseResetter.cs 使用Respawn在用例之间快速清空数据按编译开关分别选择NpgsqlConnection/SqlConnection/SqliteConnection与生产环境的 provider 保持同构功能测试直接通过TestApp.SendAsync(...)走完整的命令/查询管线例如 CreateTodoItemTests.cs 会真实落库并断言CreatedBy、Created、LastModified等审计字段这些断言依赖 AuditableEntityInterceptor 在SaveChanges时写入而它只能被真实持久化链路触发。与此同时纯领域逻辑与 Application 层校验的单元测试如 Application.UnitTests 与 Domain.UnitTests完全不需要数据库两类测试的分工边界清晰。拦截器与 SaveChanges 的配合值得注意正因为处理器直接调用IApplicationDbContext.SaveChangesAsyncInfrastructure 层注册的ISaveChangesInterceptor见 DependencyInjection.cs才能统一介入每次提交。例如 DispatchDomainEventsInterceptor.cs 在SavingChangesAsync阶段从ChangeTracker中收集领域事件并经IMediator.Publish派发随后才真正提交。这验证了 ADR 的一个隐含收益移除仓储层之后EF Core 自身的拦截器机制直接成为横切逻辑审计、领域事件的落点无需在仓储方法里手写重复样板。理由四ORM 可替换性是 YAGNI用接口包住 ORM将来可以随时换实现是最常见的反驳。ADR 的回应是绿地项目一旦选用 EF Core实践中几乎不会更换 ORM。为一个几乎不发生的场景让整个 Application 层围绕它做抽象是用当下的真实复杂度去解决一个理论问题。对这句话的落地印证同样在 Application.csprojApplication 层仅引用Microsoft.EntityFrameworkCore这一个数据访问包而数据库 providerNpgsql、SqlServer、Sqlite全部出现在 Infrastructure 层切换数据库根本不需要触碰 Application 代码——这也说明框架引用与具体实现依赖在分层设计里是两回事。后果评估更简单与更困难ADR 对采纳决策的后果做了坦诚的权衡更简单Easier处理器可以直接使用 EF Core 的完整表达力——投影、Include、原生 SQL、编译查询无需变通或透过仓储接口泄露意图不存在需要设计、实现、并随处理器需求演化而持续同步的仓储层数据访问路径直观handler - IApplicationDbContext - ApplicationDbContext可追踪性强。更困难Harder功能测试必须跑在真实数据库上没有轻量替代品模板用 Aspire Respawn 承担这一成本见上文若日后真要更换 ORM需要改动 Application 处理器而不仅是 Infrastructure。鉴于这种情况在实践中极为罕见这是一个被接受的取舍。边界什么时候这个决策不适用ADR 明确划出了适用范围——默认模板不预设 DDD本决策只适用于这条默认路线。如果你在 Clean Architecture 之上叠加 DDD仓储模式反而是正确选择但理由与持久化抽象无关DDD 中的仓储是领域概念定义在领域层以聚合Aggregate为粒度方法如GetById、Save、FindByCustomer使用领域语言而非 EF Core 语言表达Infrastructure 层负责实现这些领域接口这与为了把 DbContext 藏起来而在 Application 层包一层仓储有本质区别——后者正是本 ADR 反对的做法。本仓库的配套决策 ADR-003-MediatR-Contracts-In-Domain 与本决策一同勾勒出模板默认风格的边界。判断是否切换路线关键看你的项目是否需要富领域模型、聚合边界、领域服务、在领域内部维护不变量——如果答案是否定的直接使用IApplicationDbContext就是更简洁的选择如果是肯定的请把仓储放回领域层。小结ADR-001 的实质是三点依赖倒置看方向不看引用清单Application 层可以引用 EF Core 抽象只要数据库 provider 与具体 DbContext 都留在 Infrastructure依赖箭头就始终指向内层仓储不是免费的在没有 DDD 的前提下它只是把 EF Core 的语义换一种形态再暴露一遍多一层间接、少一分诚实真实数据库测试是配套前提既然 Application 直接面对 EF Core功能测试就必须用 Aspire 拉起真实数据库、用 Respawn 管理状态模板的测试工程已把这套成本内置。对于模板用户而言这意味着添加新的命令/查询处理器时只需注入IApplicationDbContext并直接编写 EF Core 查询不需要创建任何仓储接口只有当你的项目真正进入 DDD 路线时才需要把仓储作为领域概念引入并停止在处理器中直接使用IApplicationDbContext。【免费下载链接】CleanArchitectureClean Architecture Solution Template for ASP.NET Core项目地址: https://gitcode.com/GitHub_Trending/cle/CleanArchitecture创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考