2026/9/13 9:43:18

CleanArchitecture 模板的 ADR 实践:从决策模板到架构约束的落地指南

CleanArchitecture 模板的 ADR 实践:从决策模板到架构约束的落地指南 CleanArchitecture 模板的 ADR 实践从决策模板到架构约束的落地指南【免费下载链接】CleanArchitectureClean Architecture Solution Template for ASP.NET Core项目地址: https://gitcode.com/GitHub_Trending/cle/CleanArchitecture导读本文围绕 CleanArchitecture 模板仓库中的 架构决策记录ADR机制 展开先从 ADR-000 模板 讲清一份合格 ADR 应该具备的结构骨架再以仓库内三份已接受的 ADRADR-001、ADR-002、ADR-003为真实范例逐节拆解它们如何把EF Core 直连应用层Aspire 编排与测试Domain 引用 MediatR.Contracts等反直觉的架构选择讲透并落到仓库源码与测试中验证。读完本文你将掌握ADR 各章节的写作要点、如何用 ADR 论证一个看似违反 Clean Architecture 原则的决策以及如何用源码、测试与配置反向印证 ADR 的每一处论断。一、什么是 ADR让架构决策可追溯、可争论、可复用ADRArchitecture Decision Record架构决策记录是一种以文档形式固化重要架构决策的实践。在 docs/decisions/README.md 中CleanArchitecture 模板对其给出了明确定义An ADR captures a significant architectural decision: the context that led to it, the decision itself, the rationale behind it, and its consequences.即一份 ADR 需要完整回答四个问题Context背景是什么情境促使了这个决策存在哪些技术约束、团队需求与既有模式Decision决策明确、祈使地陈述选择了什么仅读这一节的人应能确切知道结果。Rationale理由为什么选它而非其它方案这是 ADR 的核心要指名道姓地列出被否定的备选方案及其被拒原因。Consequences后果决策带来了哪些更容易和更困难的代价让后人清楚地知道该为这个决策付出什么。该目录目前收录了三份已被接受Accepted的 ADR作为模板实践的直接范例ADR标题日期状态ADR-001Use EF Core in the Application Layer2024-02-29AcceptedADR-002Aspire for Orchestration and Testing2026-03-12AcceptedADR-003MediatR.Contracts Reference in Domain2026-03-16Accepted命名与编号遵循ADR-NNN-短横线标题.md的约定新记录直接拷贝 ADR-000 模板 开始填写。二、解剖 ADR 模板每个章节在回答什么问题模板docs/decisions/ADR-000-template.md并不长但每个占位注释背后都有明确的写作意图。下面逐节解读并对照三份真实 ADR 看它们如何作答。2.1 Status 与 Date先交代决策的生命周期模板要求填Accepted | Deprecated | Superseded by ADR-NNN并要求给出日期。仓库中的三份 ADR 全部为Accepted日期从 2024-02-29 到 2026-03-16时间跨度近两年说明该模板是持续在用的活文档而非一次性产物。写作要点Status 一栏决定了读者对该决策的信任级别——Deprecated或Superseded必须链向替代它的新 ADR防止后人继续按旧决策行事。2.2 Context只摆事实不下结论模板注释要求 Context factual and neutral客观中立、陈述事实是什么情境、什么力量技术约束、团队需求、既有模式推动了这次决策。看 ADR-001 的背景写法The Application layer needs to read and write data. Clean Architecture restricts inner layers from depending on outer layers, and frameworks like ORMs are typically placed in the Infrastructure layer. The question is whether the Application layer should access EF Core directly through an interface it owns, or whether a repository layer should sit between them.它没有直接说我们用 EF Core 直连而是先把矛盾双方摆出来Clean Architecture 要求内层不依赖外层、ORM 通常放 Infrastructure 层于是自然产生了一个真问题——应用层是通过自有的接口直连 EF Core还是中间再垫一层 Repository。一个好的 Context 应当让读者自行得出这里确实需要做个决策的结论。2.3 Decision一句祈使句自包含、无歧义模板要求 Decision 是 a clear, imperative statement且 someone reading only this section should know exactly what was chosen——只读这一节就应知道选了啥。ADR-001 的 Decision 是一段高度自包含的话应用层定义IApplicationDbContext暴露DbSetT属性并在 command/query handler 中直接使用Infrastructure 层的ApplicationDbContext实现该接口两者之间不设 Repository 层。它连没有做什么都写清楚了。ADR-002 的 Decision 同样精准Aspire 默认进入所有解决方案变体作为本地开发与测试的编排层提供AppHost编排全栈与TestAppHost仅编排数据库供功能测试使用两个项目。ADR-003 的 Decision 则直接到包引用粒度Domain 项目引用MediatR.ContractsBaseEvent直接实现INotification无需任何适配器或映射层。2.4 RationaleADR 的灵魂摆出备选方案并逐一否定模板注释点明 This is the core of the ADR. Name the alternatives that were considered and explain why they were rejected. 三份 ADR 在这一点上都做得很扎实ADR-001 用四个小节论证依赖倒置已满足、Repository 只是徒增间接层、测试应面向真实数据库、ORM 可替换性是 YAGNIADR-002 把三个备选方案无编排、Docker Compose、直接用 Testcontainers逐一列出并给出被拒理由再对比 Aspire 的覆盖范围ADR-003 正面回应了Domain 层零外部依赖这一 Clean Architecture 理想并解释为何在INotification语义完全等价的情况下为一个纯标记接口维护适配层得不偿失。2.5 Consequences诚实地记下代价模板要求用Easier / Harder两栏分别列出决策带来的收益与代价。ADR-001 承认切换 ORM 时需要改动 Application handler 而非仅改 Infrastructure这一硬伤ADR-002 承认 Aspire 把模板耦合到微软专属编排框架、且 PostgreSQL/SQL Server 变体必须依赖 DockerADR-003 承认 Domain.csproj 从此多了一个 NuGet 依赖。承认代价正是 ADR 区别于普通博客文章的价值所在——后续维护者不会因为读到一份全是好处的决策而踩坑。三、范例一ADR-001——应用层直连 EF Core如何自圆其说这是模板中论证密度最高的一份也是最能体现ADR 用于挑战教条价值的案例。它要对抗的直觉是Clean Architecture 的层层依赖规则难道不是要求应用层必须通过 Repository 抽象访问数据吗3.1 依赖倒置DIP看的是箭头方向不是引用数量ADR-001 的核心论点是A reference to an assembly (Microsoft.EntityFrameworkCore) is not the same as a dependency on a concrete implementation. Dependency inversion is about the direction of the dependency arrow, not about achieving zero framework references in inner layers.也就是说应用层虽然编译期引用了 EF Core 的抽象类型DbSetT、IQueryableT但它不知道具体的DbContext、不知道数据库提供程序、不认识任何 Infrastructure 类型。真正的依赖箭头是Infrastructure 依赖 Application而非相反。这个论断可以在源码中直接验证。应用层定义的接口 IApplicationDbContext.cs 只暴露了两个DbSetT属性和一个SaveChangesAsync方法完全是 EF Core 的抽象public interface IApplicationDbContext { DbSetTodoList TodoLists { get; } DbSetTodoItem TodoItems { get; } Taskint SaveChangesAsync(CancellationToken cancellationToken); }而基础设施层的 ApplicationDbContext.cs 继承IdentityDbContextApplicationUser并实现该接口DbSetT属性用SetT()惰性获取——具体的数据库提供程序SQLite / PostgreSQL / SQL Server只出现在 Infrastructure 的注册代码中public class ApplicationDbContext : IdentityDbContextApplicationUser, IApplicationDbContext { public ApplicationDbContext(DbContextOptionsApplicationDbContext options) : base(options) { } public DbSetTodoList TodoLists SetTodoList(); public DbSetTodoItem TodoItems SetTodoItem(); ... }3.2 Handler 里的实际用法接口注入、直接查询决策说command 和 query handler 直接使用IApplicationDbContext这一点在 CreateTodoList.cs 中有最直观的体现——handler 构造函数注入的是应用层自有的接口而非任何 Repositorypublic class CreateTodoListCommandHandler : IRequestHandlerCreateTodoListCommand, int { private readonly IApplicationDbContext _context; public CreateTodoListCommandHandler(IApplicationDbContext context) { _context context; } public async Taskint Handle(CreateTodoListCommand request, CancellationToken cancellationToken) { var entity new TodoList { Title request.Title, Colour Colour.From(request.Colour ?? Colour.Grey) }; _context.TodoLists.Add(entity); await _context.SaveChangesAsync(cancellationToken); return entity.Id; } }由此可以看到 ADR-001 所说的数据访问路径一目了然handler →IApplicationDbContext→ApplicationDbContext。同时Repository 接口会退化成 EF Core 查询模式的镜像这一论点也有据可查——IApplicationDbContext里只有DbSetT与SaveChangesAsync没有GetByIdWithItems、GetActiveOrderedByName这类为隐藏 EF Core 而生的自定义方法Application 层依赖注入的装配见 DependencyInjection.cs也只需注册 MediatR、AutoMapper 与 FluentValidation没有任何仓储注册。3.3 测试策略用真实数据库替代 MockADR-001 在 Rationale 中明确Functional tests use Aspire to spin up a real database and Respawn to reset state between tests并援引微软官方推荐的测试策略。测试代码证实了这一点WebApiFactory.cs 通过UseSetting(ConnectionStrings:CleanArchitectureDb, connectionString)把真实连接字符串注入宿主再以ConfigureTestServices替换IUser的实现功能测试项目里还有 DatabaseResetter.cs 负责用例间重置数据库状态。3.4 边界条款这套决策什么时候不适用ADR-001 用一节 When this decision does not apply 划清了适用范围模板默认不引入 DDD一旦项目演进为领域驱动设计富领域模型、聚合边界、领域服务、域内不变量Repository 就应当作为领域概念定义在 Domain 层、以聚合为粒度表达GetById/Save/FindByCustomer出现而不是为了对 Application 隐藏DbContext而存在。这种主动给决策划边界的写法非常值得新写 ADR 时模仿。四、范例二ADR-002——Aspire 为何成为本地开发与测试的默认编排层ADR-002 解决的是Getting Started摩擦在 Aspire 之前跑Application.FunctionalTests需要自己装数据库服务器并手写连接字符串跑Web.AcceptanceTests需要手动依次启动数据库、后端与前端 dev server模板也没有对可观测性、服务发现、HTTP 韧性给出任何有主见的基线。4.1 决策内容双宿主编排Decision 引入了两个项目AppHostsrc/AppHost/Program.cs编排完整技术栈供本地开发与验收测试使用TestAppHosttests/TestAppHost/仅编排数据库供Application.FunctionalTests使用。AppHost 的实际编排逻辑与 ADR 的描述完全一致Program.cs 按编译符号区分三种数据库变体——UsePostgreSQL走AddAzurePostgresFlexibleServer(...).RunAsContainer(...)、UseSqlServer走AddAzureSqlServer(...).RunAsContainer(...)、默认SQLite走AddSqlite(...)随后把 Web 项目WithReference(databaseServer)并WaitFor(databaseServer)非 API-Only 变体下还通过AddJavaScriptApp把前端./../Web/ClientApp一并纳入编排var databaseServer builder .AddSqlite(Services.Database); var web builder.AddProjectProjects.Web(Services.WebApi) .WithReference(databaseServer) .WaitFor(databaseServer) .WithExternalHttpEndpoints() .WithAspNetCoreEnvironment(); if (builder.ExecutionContext.IsRunMode) { builder.AddJavaScriptApp(Services.WebFrontend, ./../Web/ClientApp) .WithRunScript(start) .WithReference(web) .WaitFor(web) .WithHttpEndpoint(env: PORT) .WithExternalHttpEndpoints(); }服务名统一定义在 src/Shared/Services.cs这正是 ADR-002 所说的服务之间按名称互相引用而非硬编码 URL 或端口。4.2 备选方案对比为什么不是 Docker Compose 或裸 TestcontainersADR-002 逐一否定了三个备选无编排旧方案每个开发者手动安装配置依赖测试前需要外部准备是上手摩擦的最大来源Docker Compose能编排容器但没有可观测性、没有服务发现、没有健康检查集成、无法从测试代码程序化控制且要在 .NET 解决方案之外维护一份 YAML直接使用 Testcontainers适合在测试中拉起数据库容器但只覆盖测试场景无助于本地开发编排、可观测性与服务默认值。而 Aspire 通过ServiceDefaults项目src/ServiceDefaults/Extensions.cs自动注入 OpenTelemetry、服务发现、HTTP 韧性resilience与健康检查等生产级默认值一套机制同时覆盖本地开发与测试。4.3 后果自白绑定微软框架 Docker 依赖Easier 一侧是dotnet test完全自包含无需安装数据库、无需手写连接字符串、无需启动外部进程、本地开发开箱即得 Dashboard/结构化日志/分布式追踪/健康检查、服务按名引用。Harder 一侧则诚实承认Aspire 是重大依赖把模板与微软专属编排框架绑定PostgreSQL 与 SQL Server 变体必须依赖 Docker或 Podman仅默认的 SQLite 变体不需要 Docker。五、范例三ADR-003——Domain 层引用 MediatR.Contracts 的取舍ADR-003 面对的矛盾最尖锐Clean Architecture 要求 Domain 层零外部依赖但领域事件要接入 MediatR 的发布/订阅系统应用层在SaveChangesAsync后派发事件BaseEvent就必须满足INotification标记接口。5.1 决策与源码印证Decision 很干脆Domain 项目引用MediatR.ContractsBaseEvent直接实现INotification。BaseEvent.cs 证实了这一点整个类只有一行实质内容public abstract class BaseEvent : INotification { }而BaseEvent通过 BaseEntity.cs 挂到每个聚合实体上——BaseEntity持有_domainEvents列表并提供AddDomainEvent/RemoveDomainEvent/ClearDomainEvents方法。5.2 派发链路拦截器里的IMediator.Publish事件在何处被真正发布DispatchDomainEventsInterceptor.cs 展示了完整链路EF Core 的SaveChangesInterceptor在SavingChanges/SavingChangesAsync钩子中扫描ChangeTracker里携带领域事件的实体清空事件列表后逐个await _mediator.Publish(domainEvent)public async Task DispatchDomainEvents(DbContext? context) { if (context null) return; var entities context.ChangeTracker .EntriesBaseEntity() .Where(e e.Entity.DomainEvents.Any()) .Select(e e.Entity); var domainEvents entities.SelectMany(e e.DomainEvents).ToList(); entities.ToList().ForEach(e e.ClearDomainEvents()); foreach (var domainEvent in domainEvents) await _mediator.Publish(domainEvent); }正是因为BaseEvent直接实现了INotification_mediator.Publish(domainEvent)才能以零转换直接派发——这就是 ADR-003 所说的省掉一整层适配代码。领域事件的实际消费端例子在 LogTodoItemCompleted.cs对应TodoItemCompletedEventTodoItemCompletedEvent.cs。5.3 备选方案与代价被否定的方案是在 Domain 定义本地标记接口如IDomainEvent再在 Application 层适配为INotification。ADR-003 的论证是这保留了Domain 零外部依赖的理想但适配层只是为了弥合两个语义完全等价的接口缝隙纯属维护负担。MediatR.Contracts只含接口定义、无实现、无传递依赖、跨 MediatR 主版本稳定因此耦合真实但极小。代价也在 Consequences 中写明Domain.csproj 增加了一个 NuGet 依赖零 NuGet 依赖的理想被有意放宽。六、如何为这个仓库新增一份 ADR实操步骤结合模板与三份范例新增 ADR 的推荐流程如下拷贝模板将 ADR-000-template.md 复制为docs/decisions/ADR-NNN-你的标题.mdNNN 取当前最大编号加一例如下一份为 ADR-004填 Status 与 Date新记录先标Accepted写明YYYY-MM-DD格式的日期写 Context用 24 句话陈述触发决策的情境与约束保持中立、不预设立场写 Decision用祈使句给出自包含的结论确保只看这一节的读者也能复述选了啥、没选啥写 Rationale至少列出 23 个真实考虑过的备选方案逐一说明被拒原因——这是全文的论证核心写 Consequences用 Easier / Harder 两栏诚实记录收益与代价必要时像 ADR-001 那样增加本决策何时不适用一节明确适用范围边界更新索引在 docs/decisions/README.md 的表格中追加一行保持编号、标题、日期、状态与文件一致。写作时的两条质量红线一是每个论断最好能在仓库中找到对应的源码、配置或测试证据如本文第三节的对照方式二是宁可承认代价也不要写成只讲好处的单边文档。七、总结ADR 是架构约束的活的契约回到本仓库的语境三份 ADR 合在一起勾勒出了模板的架构立场应用层以自有接口直连 EF CoreIApplicationDbContext.cs、以 Aspire 统一本地开发与测试编排AppHost/Program.cs、以MediatR.Contracts让领域事件成为一等公民BaseEvent.cs。这三条决策都主动偏离了 Clean Architecture 的某种理想形态Repository 抽象、手动搭环境、Domain 零依赖却都以充分的 Rationale 论证了偏离是划算的这正是 ADR 实践的价值它不禁止打破规则只要求打破规则时留下可追溯、可争论、可复查的书面理由。对读者而言ADR-000 模板 是如何写三份已接受 ADR 是如何写好而仓库源码与测试则是如何验证。当你在自己的项目中遇到这个决策反直觉的时刻不妨照此模板写下第一份 ADR——它会成为团队里最有说服力的架构文档。【免费下载链接】CleanArchitectureClean Architecture Solution Template for ASP.NET Core项目地址: https://gitcode.com/GitHub_Trending/cle/CleanArchitecture创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考