CleanArchitecture 模板 ADR-001:用 .NET 内置 DI 替换 Autofac 的决策记录与源码级落地
【免费下载链接】CleanArchitectureClean Architecture Solution Template: A proven Clean Architecture Template for ASP.NET Core 10项目地址: https://gitcode.com/GitHub_Trending/cl/CleanArchitecture
本文以 CleanArchitecture 解决方案模板(Clean Architecture Solution Template,面向 ASP.NET Core 10 的 .NET 模板)中第一份正式的架构决策记录 ADR 001: Replace Autofac with .NET DI 为主体,完整继承其 Status、Context、Decision、Consequences 四段内容,并结合模板中真实的 DI 注册代码(组合根、分层扩展方法、Mediator 源生成器配置)逐条印证该决策在代码库中的落点,帮助读者掌握在 Clean Architecture 分层项目中仅用 .NET 内置 DI 完成全部依赖注册的完整做法。
决策记录(ADR 001)的核心内容
ADR 001 是该模板docs/content/architecture-decisions/目录下的第一份架构决策记录(目录索引见 architecture-decisions/_index.md)。其结构与结论如下:
- Status(状态):Accepted(已接受)。
- Decision(决策):基于社区反馈和对项目需求的审视,模板决定移除 Autofac,全面转向 .NET 内置依赖注入基础设施。目标是简化代码库、减少外部依赖。
- Consequences(后果/收益),原文列出三点:
- Simplified Codebase(简化代码库):移除 Autofac 后代码库更干净、更易维护,且遵循 .NET 标准;
- Reduced Complexity(降低复杂度):过渡消除了 Autofac 专属的额外文件与配置,新贡献者更容易理解项目;
- Standardization(标准化):采用 .NET 内置 DI 与其他 .NET 项目保持一致,熟悉框架的开发者可以更顺畅地参与贡献。
背景(Context):为什么当初用 Autofac,又为什么换掉
ADR 文档完整保留了这段决策的历史脉络,这也是理解本决策最重要的部分:
当初采用 Autofac 的原因(在其被引入的年代):
- 支持 decorator(装饰器)和 module(模块)等高级场景,且这些扩展代码可以就近放在对应实现旁边;
- 稳定性与成熟度经过验证——在 .NET Core 内置 DI 功能完善之前,它已在大量项目中使用。
后来切换的原因:随着 .NET 生态演进,内置 DI 容器开始满足项目需求,不再需要引入 Autofac 带来的额外复杂度。文档指出 .NET DI 框架已显著成熟,对典型场景提供了足够的能力,包括:
- 通过扩展方法改进的配置支持(Improved support for configuration through extension methods);
- 对熟悉 .NET 惯例的新贡献者而言更平缓的学习曲线。
原文档还在 References 一节引用了触发本次决策的社区讨论(Issue #649,主题即"为什么这个仓库用 Autofac 而不是 .NET 自带的 DI 基础设施?"),以及一篇关于 ADR 最佳实践的外部文章。本文不展开外部链接,但建议把"决策由真实社区问题驱动"这一点视为 ADR 写作的示范:先有问题,再谈决策。
决策落地(一):组合根只做编排,不做具体注册
决策本身是一句话("去掉 Autofac,用内置 DI"),真正能检验它的是模板里所有 DI 注册代码的写法。组合根入口是 Web 项目 Program.cs:
var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults() // This sets up OpenTelemetry logging .AddLoggerConfigs(); // This adds Serilog for console formatting using var loggerFactory = LoggerFactory.Create(config => config.AddConsole()); var startupLogger = loggerFactory.CreateLogger<Program>(); startupLogger.LogInformation("Starting web host"); builder.Services.AddOptionConfigs(builder.Configuration, startupLogger, builder); builder.Services.AddServiceConfigs(startupLogger, builder); builder.Services.AddFastEndpoints() .SwaggerDocument(/* ... v1 文档配置 ... */); var app = builder.Build(); await app.UseAppMiddlewareAndSeedDatabase(); app.MapDefaultEndpoints(); // Aspire health checks and metrics app.Run();值得对照 ADR 意图观察的三个细节:
- 没有任何
ContainerBuilder、Populate()、UseProvider()等 Autofac 痕迹——整个启动流程只围绕builder.Services(即IServiceCollection)展开,这正是 ADR "消除 Autofac 专属额外文件与配置"一条的直接体现; - 组合根只调用三个扩展方法:
AddOptionConfigs(绑定 Options)、AddServiceConfigs(注册业务服务)与AddFastEndpoints(API 层)。注册细节全部下沉到 Configurations 目录下的扩展方法中,符合 ADR 中"简化、标准化"的表述; - 文件末尾特意保留
public partial class Program { },注释说明是为了让集成测试引用正确的程序集来构建宿主——内置 DI 配合WebApplicationFactory的测试集成方式同样受益于此。
决策落地(二):Options 模式替代 Autofac 模块的配置能力
OptionConfigs.cs 展示了 ADR Context 中提到的"通过扩展方法改进的配置支持"在模板中的具体形态:
public static IServiceCollection AddOptionConfigs(this IServiceCollection services, IConfiguration configuration, Microsoft.Extensions.Logging.ILogger logger, WebApplicationBuilder builder) { services.Configure<MailserverConfiguration>(configuration.GetSection("Mailserver")) // Configure Web Behavior .Configure<CookiePolicyOptions>(options => { options.CheckConsentNeeded = context => true; options.MinimumSameSitePolicy = SameSiteMode.None; }); if (builder.Environment.IsDevelopment()) { // 仅开发环境:注册 ListStartupServices,用于诊断已注册的服务 services.Configure<ServiceConfig>(config => { config.Services = new List<ServiceDescriptor>(builder.Services); config.Path = "/listservices"; }); } logger.LogInformation("{Project} were configured", "Options"); return services; }几个与 ADR 主题强相关的点:
services.Configure<T>(section)就是 .NET 内置 DI 的Options 模式——用一行代码把配置节绑定为类型化对象并注册到容器,消费端通过IOptions<T>解析。这正是 ADR 所说的"extension methods 改进配置支持"的实际例子,替代了过去靠 Autofac Module 承载配置的职责;builder.Environment.IsDevelopment()的条件注册(只在开发环境挂载Ardalis.ListStartupServices的/listservices诊断端点)是内置 DI 完全原生支持的写法。这为读者提供了一个可验证手段:在开发环境运行 Web 项目后访问/listservices,即可看到全部已注册服务清单,直接检验"仅靠内置 DI 是否注册齐全"。
决策落地(三):Infrastructure 层的注册与依赖倒置
InfrastructureServiceExtensions.cs 是 ADR 收益"减少外部依赖"最能被验证的地方——全部注册均使用内置IServiceCollectionAPI:
public static IServiceCollection AddInfrastructureServices( this IServiceCollection services, ConfigurationManager config, ILogger logger) { // 连接串按优先级解析: // 1. "cleanarchitecture" - Aspire 通过 .WithReference(cleanArchDb) 提供 // 2. "DefaultConnection" - SQL Server(默认仅 Windows,可用 USE_SQL_SERVER=true 强制) // 3. "SqliteConnection" - 回退到 SQLite bool isWindows = OperatingSystem.IsWindows(); bool forceSqlServer = Environment.GetEnvironmentVariable("USE_SQL_SERVER") == "true"; string? connectionString = config.GetConnectionString("cleanarchitecture") ?? ((isWindows || forceSqlServer) ? config.GetConnectionString("DefaultConnection") : null) ?? config.GetConnectionString("SqliteConnection"); Guard.Against.Null(connectionString); services.AddScoped<EventDispatchInterceptor>(); services.AddScoped<IDomainEventDispatcher, MediatorDomainEventDispatcher>(); services.AddDbContext<AppDbContext>((provider, options) => { var eventDispatchInterceptor = provider.GetRequiredService<EventDispatchInterceptor>(); // Aspire 或 Windows/强制下的 DefaultConnection 用 SQL Server,否则 SQLite options.UseSqlServer(connectionString); // 或 options.UseSqlite(connectionString) options.AddInterceptors(eventDispatchInterceptor); }); services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)) .AddScoped(typeof(IReadRepository<>), typeof(EfRepository<>)) .AddScoped<IListContributorsQueryService, ListContributorsQueryService>() .AddScoped<IDeleteContributorService, DeleteContributorService>(); return services; }从源码结构看,这段代码集中演示了内置 DI 覆盖 Autofac 时代常见诉求的三种手法:
- 开放泛型注册:
AddScoped(typeof(IRepository<>), typeof(EfRepository<>))一条语句完成"接口→实现"的泛型映射,无需 Autofac 的泛型委托配置; - 工厂函数中按需解析协作对象:
AddDbContext的注册委托接收ServiceProvider provider参数,在其中GetRequiredService<EventDispatchInterceptor>()拿到拦截器再options.AddInterceptors(...)挂到 EF Core 上。这种"注册时依赖另一个已注册服务"的场景过去常被列举为 Autofac 的卖点,此处表明内置 DI 通过工厂委托同样可以表达; - Core 层接口 + Infrastructure 层实现的倒置:
IListContributorsQueryService、IDeleteContributorService等接口定义在 Core 项目,实现在 Infrastructure 项目并在此注册——Clean Architecture 的依赖方向不因 DI 容器的选择而改变。
决策落地(四):Mediator 源生成器同样只依赖 IServiceCollection
模板的 UseCases 层使用 Mediator 源生成器替代 MediatR(背景可见 v10→v11 迁移指南中"MediatR Replacement"一节)。其注册在 MediatorConfig.cs 中,依旧完全基于内置 DI:
services.AddMediator(options => { // Lifetime: Singleton 最快(见文档);也支持 Scoped/Transient options.ServiceLifetime = ServiceLifetime.Scoped; // 每个待扫描程序集提供任一类型即可(生成器从类型定位程序集) options.Assemblies = [ typeof(Contributor), // Core typeof(CreateContributorCommand), // UseCases typeof(InfrastructureServiceExtensions), // Infrastructure typeof(MediatorConfig) // Web ]; // 流水线 Behavior(顺序敏感) options.PipelineBehaviors = [ typeof(LoggingBehavior<,>) ]; }); // 备选:不走 AOT 时也可自行通过 DI 注册 Behavior // services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));这里的options.ServiceLifetime = ServiceLifetime.Scoped直接复用了 .NET DI 的ServiceLifetime枚举作为 Mediator 的生命周期开关,注释中列出的两种备选注册方式(AddScoped(typeof(IPipelineBehavior<,>), ...))也都是标准内置 DI API。可以说,"Mediator 换成源生成器 + 内置 DI"与"移除 Autofac"是同一方向上的两次依赖精简,二者共同构成了当前模板的 DI 全貌。
环境相关的条件注册:ServiceConfigs 的组合方式
ServiceConfigs.cs 把上述各块串起来,并展示了环境区分注册(开发环境用本地邮件服务器,生产用MimeKitEmailSender):
public static IServiceCollection AddServiceConfigs(this IServiceCollection services, Microsoft.Extensions.Logging.ILogger logger, WebApplicationBuilder builder) { services.AddInfrastructureServices(builder.Configuration, logger) .AddMediatorSourceGen(logger); if (builder.Environment.IsDevelopment()) { // 使用本地测试邮件服务器(由 Aspire 配置) services.AddScoped<IEmailSender, MimeKitEmailSender>(); // 否则使用:services.AddScoped<IEmailSender, FakeEmailSender>(); } else { services.AddScoped<IEmailSender, MimeKitEmailSender>(); } logger.LogInformation("{Project} services registered", "Mediator Source Generator and Email Sender"); return services; }调用顺序体现了清晰的分层:Web 层的AddServiceConfigs→ Infrastructure 层的AddInfrastructureServices→ Mediator 配置,每步返回IServiceCollection形成链式注册,且每一步都通过logger.LogInformation记录"谁注册了什么",便于排查。
如何复现与验证
- 生成一个使用本模板的新解决方案(v11 模板),按 v10-to-v11 迁移指南所述流程可用
dotnet new clean-arch -o Your.ProjectName创建新工程,再与旧工程做差异比对; - 检查新生成的项目中是否存在 Autofac 相关包引用(
Directory.Packages.props中不应再有 Autofac 条目)、组合根中是否只有builder.Services相关调用; - 以开发环境运行 Web 项目,访问
/listservices端点(由 OptionConfigs.cs 中Ardalis.ListStartupServices提供),逐项核对服务注册清单。
小结
ADR 001 的核心判断是:当 .NET 内置 DI 已经能覆盖 Options 绑定、条件注册、开放泛型注册、工厂委托内按需解析等场景时,Autofac 带来的复杂度不再物有所值。从源码结构看,模板中 Program.cs、OptionConfigs.cs、ServiceConfigs.cs、MediatorConfig.cs 与 InfrastructureServiceExtensions.cs 构成了完整的对照样本:所有注册均出自IServiceCollection标准 API,没有第三方容器的痕迹。对读者而言,这份 ADR 的价值不仅是"换了一个 DI 容器",更示范了一个可操作的判断框架——当内置能力与外部库的功能差距缩小到小于维护成本时,就应以 ADR 形式记录并执行"去依赖化"决策。
【免费下载链接】CleanArchitectureClean Architecture Solution Template: A proven Clean Architecture Template for ASP.NET Core 10项目地址: https://gitcode.com/GitHub_Trending/cl/CleanArchitecture
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考