news 2026/9/13 7:42:38

CleanArchitecture 模板的 ADR-001:在 Application 层直接使用 EF Core,而不是引入仓储层

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CleanArchitecture 模板的 ADR-001:在 Application 层直接使用 EF Core,而不是引入仓储层

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(暴露DbSet<T>),命令/查询处理器直接使用它;Infrastructure 层实现该接口;在两者之间引入仓储层

这一决策的适用前提是:模板的默认路线不预设 DDD。它回答了一个在 Clean Architecture 实践中反复出现的问题——Application 层到底该不该接触 ORM?该不该用仓储把 EF Core 藏起来?

背景:依赖方向与 ORM 的位置

Clean Architecture 的核心约束是"内层不依赖外层"。ORM 这类框架通常被归入 Infrastructure 层,于是出现一个设计岔路口:

  • 方案 A:Application 层定义一个自己拥有的接口,直接通过该接口访问 EF Core;
  • 方案 B:在 Application 与 EF Core 之间放一层仓储(Repository),由仓储封装所有数据访问细节。

模板选择了方案 A。原因在 ADR 的 Rationale 部分给出,下面结合源码逐一展开。

决策落地:IApplicationDbContextApplicationDbContext

接口由 Application 层定义并拥有

接口定义在 Application 层,见 IApplicationDbContext.cs:

public interface IApplicationDbContext { DbSet<TodoList> TodoLists { get; } DbSet<TodoItem> TodoItems { get; } Task<int> SaveChangesAsync(CancellationToken cancellationToken); }

它只暴露了模板中真实存在的两个聚合入口DbSet<TodoList>DbSet<TodoItem>以及持久化提交方法SaveChangesAsync,没有任何数据库提供程序(provider)相关的类型。

实现位于 Infrastructure 层

ApplicationDbContext.cs 在 Infrastructure 层实现该接口:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>, IApplicationDbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } public DbSet<TodoList> TodoLists => Set<TodoList>(); public DbSet<TodoItem> TodoItems => Set<TodoItem>(); protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); } }

实现细节全部停留在 Infrastructure:它继承IdentityDbContext<ApplicationUser>以集成 ASP.NET Core Identity,实体配置通过ApplyConfigurationsFromAssembly从 Configurations 目录 自动装载,数据库提供程序的选择(SQLite / SQL Server / PostgreSQL)则由 Infrastructure/DependencyInjection.cs 中的编译期开关决定。

注册:把具体 DbContext 以接口身份注入

依赖注入的关键一步在 Infrastructure/DependencyInjection.cs:

builder.Services.AddDbContext<ApplicationDbContext>((sp, options) => { options.AddInterceptors(sp.GetServices<ISaveChangesInterceptor>()); #if (UsePostgreSQL) options.UseNpgsql(connectionString); #elif (UseSqlServer) options.UseSqlServer(connectionString); #else options.UseSqlite(connectionString); #endif ... }); builder.Services.AddScoped<IApplicationDbContext>(provider => provider.GetRequiredService<ApplicationDbContext>());

ApplicationDbContext作为具体类型注册并完成数据库配置,随后以IApplicationDbContext的身份按 Scoped 生命周期对外暴露。这样 Application 层只认接口,所有数据库相关的装配都被隔离在 Infrastructure 层内部。

数据访问链路

ADR 明确给出了从处理器到数据库的完整链路:

handler -> IApplicationDbContext -> ApplicationDbContext

以 CreateTodoItem.cs 的命令处理器为例,它构造TodoItem实体后直接调用_context.TodoItems.Add(...)_context.SaveChangesAsync(...),全程没有经过任何仓储方法。查询侧同样直接面对 EF Core 的完整表达能力,例如 GetTodos.cs 中链式使用AsNoTracking()、AutoMapper 的ProjectTo<TodoListDto>()OrderByToListAsync完成投影排序。这正是 ADR 在 Consequences 中强调的"全表达力可用"——投影、Include、原生 SQL、编译查询都不需要绕过仓储接口去迁就抽象。

理由一:依赖倒置已经满足,而非"零框架引用"

这是整个 ADR 最容易被误解的地方。Application 层确实持有对 EF Core 抽象类型(DbSet<T>IQueryable<T>)的编译期引用:

  • 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 层实现,那么:

  1. 耦合并未消失,只是被隐藏——仓储实现内部仍然使用 EF Core;
  2. ORM 语义会从接口里"漏出来"——"加载哪些关联实体、如何过滤、如何投影"这类意图最终会以GetByIdWithItemsGetActiveOrderedByName之类的自定义方法形态出现在仓储接口上,本质上是对 EF Core 查询模式的复刻,却失去了 EF Core 的语法与工具链支持;
  3. 代价是真实的——多了一层需要设计、实现、并随需求演进而持续同步的间接层,可发现性(discoverability)反而下降。

结论是:IApplicationDbContext直接暴露DbSet<T>的做法,比仓储更诚实地反映了实际发生的事情。

理由三:功能测试倾向针对真实数据库

ADR 明确采纳了"针对真实数据库做功能测试"的策略,并指出这是 Microsoft EF Core 官方推荐的测试策略 所提倡的方向(原文链接,本文不展开外部内容)。真实数据库能暴露内存假实现与 mock 仓储无法发现的问题:provider 特有的查询行为、索引冲突、事务语义、约束强制

模板的测试工程完整落地了这条策略:

  • 测试基础设施(TestApp.cs、WebApiFactory.cs)通过 Aspire 编排拉起真实数据库;
  • DatabaseResetter.cs 使用Respawn在用例之间快速清空数据,按编译开关分别选择NpgsqlConnection/SqlConnection/SqliteConnection,与生产环境的 provider 保持同构;
  • 功能测试直接通过TestApp.SendAsync(...)走完整的命令/查询管线,例如 CreateTodoItemTests.cs 会真实落库并断言CreatedByCreatedLastModified等审计字段,这些断言依赖 AuditableEntityInterceptor 在SaveChanges时写入,而它只能被真实持久化链路触发。

与此同时,纯领域逻辑与 Application 层校验的单元测试(如 Application.UnitTests 与 Domain.UnitTests)完全不需要数据库,两类测试的分工边界清晰。

拦截器与 SaveChanges 的配合

值得注意:正因为处理器直接调用IApplicationDbContext.SaveChangesAsync,Infrastructure 层注册的ISaveChangesInterceptor(见 DependencyInjection.cs)才能统一介入每次提交。例如 DispatchDomainEventsInterceptor.cs 在SavingChangesAsync阶段从ChangeTracker中收集领域事件并经IMediator.Publish派发,随后才真正提交。这验证了 ADR 的一个隐含收益:移除仓储层之后,EF Core 自身的拦截器机制直接成为横切逻辑(审计、领域事件)的落点,无需在仓储方法里手写重复样板。

理由四:ORM 可替换性是 YAGNI

"用接口包住 ORM,将来可以随时换实现"是最常见的反驳。ADR 的回应是:绿地项目一旦选用 EF Core,实践中几乎不会更换 ORM。为一个几乎不发生的场景,让整个 Application 层围绕它做抽象,是用当下的真实复杂度去解决一个理论问题。对这句话的落地印证同样在 Application.csproj:Application 层仅引用Microsoft.EntityFrameworkCore这一个数据访问包,而数据库 provider(Npgsql、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)为粒度,方法如GetByIdSaveFindByCustomer使用领域语言而非 EF Core 语言表达;
  • Infrastructure 层负责实现这些领域接口;
  • 这与"为了把 DbContext 藏起来而在 Application 层包一层仓储"有本质区别——后者正是本 ADR 反对的做法。

本仓库的配套决策 ADR-003-MediatR-Contracts-In-Domain 与本决策一同勾勒出"模板默认风格"的边界。判断是否切换路线,关键看你的项目是否需要"富领域模型、聚合边界、领域服务、在领域内部维护不变量"——如果答案是否定的,直接使用IApplicationDbContext就是更简洁的选择;如果是肯定的,请把仓储放回领域层。

小结

ADR-001 的实质是三点:

  1. 依赖倒置看方向,不看引用清单:Application 层可以引用 EF Core 抽象,只要数据库 provider 与具体 DbContext 都留在 Infrastructure,依赖箭头就始终指向内层;
  2. 仓储不是免费的:在没有 DDD 的前提下,它只是把 EF Core 的语义换一种形态再暴露一遍,多一层间接、少一分诚实;
  3. 真实数据库测试是配套前提:既然 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 7:42:17

Proteus仿真C51单片机十字路口交通灯设计与状态机实现

简介&#xff1a;基于C51与Proteus的经典交通灯控制系统完整工程资源&#xff0c;面向嵌入式初学者与单片机课程设计人群&#xff0c;演示AT89C51控制红绿黄灯定时切换的实现思路。压缩包共24个文件&#xff0c;约120KB&#xff0c;包含Keil工程文件&#xff08;.uvproj/.uvopt…

作者头像 李华
网站建设 2026/9/13 7:41:24

因子信号回测漂亮、实盘失灵?IC 与 Rank IC 这样选

因子信号回测漂亮、实盘失灵&#xff1f;IC 与 Rank IC 这样选 【免费下载链接】gs-quant Python toolkit for quantitative finance 项目地址: https://gitcode.com/GitHub_Trending/gs/gs-quant 你大概率遇到过这种情况&#xff1a;某个因子回测里 IC 曲线一路向上&am…

作者头像 李华
网站建设 2026/9/13 7:40:41

OpenClaw CLI 命令行工具使用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 7:38:37

Vue+SpringBoot+MyBatis简历管理系统技术栈解析

简介&#xff1a;这是一套面向计算机专业本科生的毕业设计实战项目资源&#xff0c;基于VueSSMSpringBoot技术栈构建完整简历管理系统&#xff0c;适用于Java全栈开发入门到进阶的学习与毕设参考。资源包含可直接运行的源码、管理员与前台双端演示视频、结构规范的毕业论文及答…

作者头像 李华