
这次我们来看 .NET 8 和 ASP.NET Core 中的依赖注入。这不是一个新概念但 .NET 8 带来了一些关键的优化和更清晰的实践模式。对于开发者来说核心问题不是“它是什么”而是“怎么用好它”——如何配置更简洁、如何管理生命周期、如何避免内存泄漏以及如何应对复杂场景。本文将直接切入 .NET 8 中依赖注入的核心机制重点关注其开箱即用的能力、服务注册与解析的多种方式、生命周期管理的实战细节以及如何构建可测试、可维护的服务架构。我们会从创建一个基础的 Web API 项目开始逐步深入到高级场景如工厂模式、选项模式、第三方容器集成等并提供可运行的代码示例和常见问题的排查方法。无论你是刚接触 ASP.NET Core还是希望升级现有项目到 .NET 8 并优化其 DI 设计这篇文章都能提供直接的、可落地的指导。1. 核心能力速览能力项说明技术栈.NET 8, ASP.NET Core核心功能内置的轻量级 IoC (控制反转) 容器用于管理对象的创建和生命周期。启动方式在Program.cs中通过WebApplicationBuilder.Services属性进行服务注册框架自动完成依赖解析。主要接口IServiceCollection(注册),IServiceProvider(解析)服务生命周期瞬态 (Transient)、作用域 (Scoped)、单例 (Singleton)关键特性与配置系统、日志系统、选项模式深度集成支持泛型、工厂、委托等多种注册方式。适合场景构建松耦合、可测试的 ASP.NET Core Web API、MVC、Blazor、微服务及后台服务应用。性能与资源容器本身开销极低内存占用主要取决于注册的服务实例数量和生命周期。单例服务需注意线程安全与内存泄漏。2. 适用场景与使用边界依赖注入是 ASP.NET Core 的基石几乎适用于所有场景。它最适合解决以下问题解耦与可测试性将类之间的直接依赖替换为接口依赖便于单元测试使用 Mock 框架。管理对象生命周期框架自动管理服务的创建与销毁开发者无需手动处理new和Dispose对于实现了IDisposable的作用域/瞬态服务容器会负责调用Dispose。统一配置与集成方便地将配置IConfiguration、日志ILogger、数据库上下文等基础设施组件注入到任何需要的地方。实现模式是实现选项模式Options、健康检查Health Checks、身份认证Authentication等框架功能的基础。需要注意的边界不是银弹对于极其简单的工具类或值对象如DateTime,string直接实例化可能更简单。避免服务定位器模式应尽量避免在构造函数外通过HttpContext.RequestServices或IServiceProvider.GetService来解析服务服务定位器反模式这会使依赖关系不明确降低可测试性。循环依赖构造函数循环依赖会导致运行时异常。需要通过设计引入接口、使用IServiceProvider延迟解析来避免。单例服务中的 Scoped 服务禁止将生命周期短的服务Scoped/Transient注入到生命周期长的服务Singleton中这可能导致 Scoped 服务行为异常或内存泄漏。3. 环境准备与前置条件开始之前请确保你的开发环境已就绪。操作系统Windows 10/11, macOS, 或 Linux 发行版如 Ubuntu。SDK安装 .NET 8 SDK 或更高版本。在终端运行dotnet --version确认版本为8.x.x。开发工具任选其一即可。Visual Studio 2022(v17.8)社区版免费。Visual Studio Code安装 C# 扩展。JetBrains Rider商业 IDE体验优秀。项目类型我们将以 ASP.NET Core Web API 项目为例。控制台应用或类库中使用 DI 需要手动创建ServiceCollection。4. 安装部署与启动方式在 ASP.NET Core 中依赖注入容器是内置的无需额外安装 NuGet 包。所有操作从创建项目开始。第一步创建新项目打开终端导航到你的工作目录执行以下命令dotnet new webapi -n DiDemo -f net8.0 cd DiDemo这个命令创建了一个名为DiDemo的 .NET 8 Web API 项目。核心文件是Program.cs。第二步理解启动流程打开Program.cs你会看到类似以下代码var builder WebApplication.CreateBuilder(args); // 在这里添加服务到容器DI 注册 builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app builder.Build(); // 配置 HTTP 请求管道 if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();关键点是builder.Services它的类型是IServiceCollection这就是我们注册所有服务的地方。WebApplication.CreateBuilder方法已经为我们注册了大量框架自带的服务如配置、日志、主机等。第三步注册自定义服务假设我们有一个服务接口IMyService和其实现MyService。// 在 Program.cs 文件顶部添加命名空间引用如果类在别的文件 // 定义接口和实现类 public interface IMyService { string GetData(); } public class MyService : IMyService { public string GetData() Data from MyService; }现在在builder.Services.AddControllers();这行之后添加服务注册builder.Services.AddScopedIMyService, MyService();第四步在控制器中注入使用打开Controllers/WeatherForecastController.cs或新建一个控制器在构造函数中声明依赖using Microsoft.AspNetCore.Mvc; namespace DiDemo.Controllers; [ApiController] [Route([controller])] public class WeatherForecastController : ControllerBase { private readonly IMyService _myService; // 依赖通过构造函数注入 public WeatherForecastController(IMyService myService) { _myService myService; } [HttpGet(Name GetWeatherForecast)] public IActionResult Get() { var data _myService.GetData(); return Ok(new { Message data }); } }第五步运行测试在终端运行项目dotnet run访问https://localhost:5001/WeatherForecast或http://localhost:5000你应该能看到返回的 JSON 数据中包含Data from MyService。至此一个最基本的依赖注入流程就跑通了。5. 功能测试与效果验证深入服务生命周期理解并正确使用三种服务生命周期是避免 Bug 的关键。我们来设计测试验证它们的行为差异。5.1 创建测试服务首先创建一个用于跟踪实例 ID 和创建时间的服务。public interface IOperation { string OperationId { get; } } public class Operation : IOperation { public string OperationId { get; } Guid.NewGuid().ToString()[^4..]; // 取后4位便于观察 }5.2 注册不同生命周期的服务在Program.cs中同时注册三种生命周期的服务// 瞬态每次请求都创建新实例 builder.Services.AddTransientIOperation, Operation(); // 作用域在同一作用域如一次 Web 请求内是同一个实例 builder.Services.AddScopedIOperation(sp new Operation()); // 使用工厂方式注册 // 单例整个应用生命周期内是同一个实例 builder.Services.AddSingletonIOperation(new Operation()); // 直接传入实例5.3 创建测试控制器创建一个新的控制器LifecycleController.cs来验证using Microsoft.AspNetCore.Mvc; namespace DiDemo.Controllers; [ApiController] [Route([controller])] public class LifecycleController : ControllerBase { private readonly IOperation _transientOp; private readonly IOperation _scopedOp; private readonly IOperation _singletonOp; private readonly IOperation _transientOp2; private readonly IOperation _scopedOp2; private readonly IOperation _singletonOp2; // 通过构造函数注入多个同接口的不同实例需要起不同名字 public LifecycleController( IOperation transientOp, IOperation scopedOp, IOperation singletonOp, IOperation transientOp2, IOperation scopedOp2, IOperation singletonOp2) { _transientOp transientOp; _scopedOp scopedOp; _singletonOp singletonOp; _transientOp2 transientOp2; _scopedOp2 scopedOp2; _singletonOp2 singletonOp2; } [HttpGet] public IActionResult Get() { var data new { Transient1 _transientOp.OperationId, Transient2 _transientOp2.OperationId, Scoped1 _scopedOp.OperationId, Scoped2 _scopedOp2.OperationId, Singleton1 _singletonOp.OperationId, Singleton2 _singletonOp2.OperationId, }; return Ok(data); } }注意上面的注册方式会导致IOperation有多个实现容器无法区分。我们需要使用不同的接口或泛型来区分。更清晰的测试方法是创建三个不同的服务类。这里为了简化我们调整注册和控制器逻辑调整注册Program.cs:// 为每种生命周期创建独立的接口和类更清晰这里用泛型作为演示 builder.Services.AddTransientITransientOperation, TransientOperation(); builder.Services.AddScopedIScopedOperation, ScopedOperation(); builder.Services.AddSingletonISingletonOperation, SingletonOperation();创建三个接口和类:public interface ITransientOperation : IOperation { } public interface IScopedOperation : IOperation { } public interface ISingletonOperation : IOperation { } public class TransientOperation : Operation, ITransientOperation { } public class ScopedOperation : Operation, IScopedOperation { } public class SingletonOperation : Operation, ISingletonOperation { }调整控制器LifecycleController.cs:public class LifecycleController : ControllerBase { private readonly ITransientOperation _transientOp1; private readonly ITransientOperation _transientOp2; private readonly IScopedOperation _scopedOp1; private readonly IScopedOperation _scopedOp2; private readonly ISingletonOperation _singletonOp1; private readonly ISingletonOperation _singletonOp2; public LifecycleController( ITransientOperation transientOp1, ITransientOperation transientOp2, IScopedOperation scopedOp1, IScopedOperation scopedOp2, ISingletonOperation singletonOp1, ISingletonOperation singletonOp2) { _transientOp1 transientOp1; _transientOp2 transientOp2; _scopedOp1 scopedOp1; _scopedOp2 scopedOp2; _singletonOp1 singletonOp1; _singletonOp2 singletonOp2; } [HttpGet] public IActionResult Get() { var data new { Transient1 _transientOp1.OperationId, Transient2 _transientOp2.OperationId, Scoped1 _scopedOp1.OperationId, Scoped2 _scopedOp2.OperationId, Singleton1 _singletonOp1.OperationId, Singleton2 _singletonOp2.OperationId, }; return Ok(data); } }5.4 运行并观察结果启动应用访问https://localhost:5001/Lifecycle。刷新页面几次观察结果Transient1 和 Transient2每次刷新两者的 ID 都不同且彼此也不同。证明每次解析都创建新实例。Scoped1 和 Scoped2在同一次页面刷新同一次请求中两者的 ID 相同。刷新页面后ID 会改变但新的请求内两者 ID 依然相同。证明在同一作用域内是同一实例。Singleton1 和 Singleton2无论刷新多少次页面两者的 ID 始终相同且不变。证明整个应用生命周期只有一个实例。这个测试清晰地验证了三种生命周期的核心区别这是正确使用 DI 的基础。6. 接口 API 与批量任务高级注册与解析模式依赖注入不仅用于控制器更是构建复杂服务层的基础。我们来看几种高级用法。6.1 工厂模式注册当服务的创建逻辑复杂需要根据运行时参数决定时可以使用工厂委托。public interface IReportService { string Generate(string format); } public class PdfReportService : IReportService { public string Generate(string format) $Generated PDF report.; } public class ExcelReportService : IReportService { public string Generate(string format) $Generated Excel report.; } // 在 Program.cs 中注册 builder.Services.AddTransientPdfReportService(); builder.Services.AddTransientExcelReportService(); builder.Services.AddTransientIReportService(serviceProvider { // 可以从其他服务获取配置或状态 var config serviceProvider.GetRequiredServiceIConfiguration(); var defaultFormat config[ReportFormat] ?? PDF; // 根据条件返回不同的实现 return defaultFormat.ToUpper() EXCEL ? serviceProvider.GetRequiredServiceExcelReportService() : serviceProvider.GetRequiredServicePdfReportService(); });6.2 选项模式集成选项模式是 .NET 中管理配置的推荐方式与 DI 深度集成。定义选项类public class ApiSettings { public const string SectionName ApiSettings; public string BaseUrl { get; set; } string.Empty; public int TimeoutSeconds { get; set; } 30; public bool EnableCache { get; set; } true; }在appsettings.json中配置{ Logging: { ... }, ApiSettings: { BaseUrl: https://api.example.com, TimeoutSeconds: 60, EnableCache: false } }在 Program.cs 中绑定并注册// 方式1直接绑定到 IOptionsApiSettings builder.Services.ConfigureApiSettings(builder.Configuration.GetSection(ApiSettings.SectionName)); // 方式2注册为 Singleton 服务可直接注入 ApiSettings 实例 (推荐 .NET 8) builder.Services.AddOptionsApiSettings() .Bind(builder.Configuration.GetSection(ApiSettings.SectionName)) .ValidateDataAnnotations() // 可选使用数据注解验证 .ValidateOnStart(); // 可选应用启动时验证配置在服务中注入使用public class MyApiClient { private readonly ApiSettings _settings; // 也可以注入 IOptionsApiSettings 或 IOptionsSnapshotApiSettings public MyApiClient(IOptionsApiSettings options) { _settings options.Value; // 注意IOptions 的值在应用生命周期内不会变 } // 或者直接注入 ApiSettings 实例如果使用了 AddOptions 注册 // public MyApiClient(ApiSettings settings) { _settings settings; } public async Task CallApiAsync() { using var client new HttpClient(); client.BaseAddress new Uri(_settings.BaseUrl); client.Timeout TimeSpan.FromSeconds(_settings.TimeoutSeconds); // ... 调用 API } }6.3 批量注册与约定式注册对于大量遵循类似模式的服务如所有 Repository 或所有I*Service实现可以使用 Scrutor 等库或自己实现扫描。使用 Scrutor (需安装 NuGet 包Scrutor)// 注册所有实现 ITransientService 接口的类为 Transient builder.Services.Scan(scan scan .FromAssemblyOfITransientService() .AddClasses(classes classes.AssignableToITransientService()) .AsImplementedInterfaces() .WithTransientLifetime() ); // 注册所有名称以 Repository 结尾的类为 Scoped builder.Services.Scan(scan scan .FromAssemblies(typeof(Program).Assembly) .AddClasses(classes classes.Where(c c.Name.EndsWith(Repository))) .AsSelf() .WithScopedLifetime() );7. 资源占用与性能观察依赖注入容器本身非常轻量性能开销主要在于服务实例的创建和销毁。以下几点需要关注单例服务的内存占用单例服务会一直存在于内存中直到应用关闭。如果单例服务持有大量数据或缓存需要监控内存使用。确保单例服务是线程安全的。瞬态服务的创建开销频繁创建和销毁复杂的瞬态服务如包含数据库连接、HTTP 客户端会有性能成本。对于这类资源密集型对象考虑使用池化技术如IHttpClientFactory或将其注册为 Scoped/Singleton如果逻辑允许。作用域服务的正确释放对于实现了IDisposable接口的作用域服务如DbContext容器会在请求结束时自动调用Dispose。切勿在代码中手动对注入的服务调用Dispose()。服务解析复杂度如果一个服务的构造函数依赖链非常深A 依赖 BB 依赖 CC 依赖 D...首次解析时可能会有可察觉的延迟。这在应用启动时如首次请求是正常的。可以通过预加载关键服务或审查设计来优化。诊断工具日志设置LogLevel为Debug或Trace可以看到容器解析服务的详细日志。性能分析器使用 Visual Studio 的性能分析器或 JetBrains dotTrace 来定位由服务解析引起的性能瓶颈。一个简单的性能观察方法是在Program.cs的app.Run()之前添加一个中间件记录请求处理时间和解析的服务数量仅用于开发调试app.Use(async (context, next) { var stopwatch System.Diagnostics.Stopwatch.StartNew(); await next(); stopwatch.Stop(); var logger context.RequestServices.GetRequiredServiceILoggerProgram(); logger.LogInformation(Request to {Path} completed in {ElapsedMs}ms, context.Request.Path, stopwatch.ElapsedMilliseconds); });8. 常见问题与排查方法问题现象可能原因排查方式解决方案启动时报错InvalidOperationException: Unable to resolve service for type X1. 服务X未在IServiceCollection中注册。2. 注册的服务生命周期与使用场景不匹配如在中间件中尝试解析 Scoped 服务。3. 有多个X的实现但未使用TryAdd或未明确指定要解析哪个。1. 检查Program.cs中是否有AddX或AddScopedX等注册语句。2. 检查错误堆栈看是在哪里尝试解析X。确保在正确的作用域内解析 Scoped 服务。3. 使用GetServicesX查看所有注册的实现。1. 在Program.cs中添加缺失的服务注册。2. 避免在单例或Configure方法中直接解析 Scoped/Transient 服务。使用IServiceScopeFactory创建作用域。3. 使用TryAdd系列方法避免重复注册或使用具体的接口/泛型参数来区分。内存泄漏服务实例数量持续增长1. 单例服务持有对瞬态或作用域服务的引用导致后者无法被垃圾回收。2. 事件Event订阅未取消导致对象无法释放。3. 缓存无限增长。1. 使用内存分析工具如 dotMemory查看对象保留路径。2. 检查单例服务的依赖项确保没有引用短生命周期对象。3. 审查代码中的事件处理程序。1. 遵循生命周期准则不要将短生命周期服务注入长生命周期服务。2. 对于需要从单例中使用 Scoped 服务的场景使用IServiceScopeFactory.CreateScope在每次操作时创建独立的作用域。3. 为缓存设置大小或时间限制。循环依赖错误两个或多个服务在构造函数中相互依赖。查看异常信息找到形成循环的类链。1.重构设计提取公共逻辑到第三个服务。2.使用属性注入不推荐破坏构造函数注入的纯粹性。3.使用IServiceProvider延迟解析将其中一个依赖改为注入IServiceProvider在方法内部需要时再解析。配置IOptions在运行时不会更新使用了IOptionsT而不是IOptionsSnapshotT或IOptionsMonitorT。检查注入的是哪个接口。如果需要读取最新的配置值如在请求中应注入IOptionsSnapshotTScoped 生命周期或IOptionsMonitorTSingleton 生命周期。在后台服务IHostedService中解析 Scoped 服务失败后台服务是单例的无法直接解析 Scoped 服务。错误信息会提示作用域问题。在后台服务的ExecuteAsync或StartAsync方法中使用IServiceScopeFactory创建作用域csharp brusing var scope _scopeFactory.CreateScope();brvar scopedService scope.ServiceProvider.GetRequiredServiceIMyScopedService();br多个实现时无法确定注入哪一个为同一接口注册了多个实现但尝试直接解析该接口。使用GetServicesT查看所有注册项。1. 为每个实现注册不同的接口。2. 使用工厂模式在注册时根据条件返回特定实现。3. 注入IEnumerableT来获取所有实现。9. 最佳实践与使用建议面向接口编程始终针对接口而非具体类进行服务注册和注入。这是实现松耦合和可测试性的第一步。明确生命周期无状态、轻量级服务使用Transient。需要请求上下文或单位工作如 DbContext的服务使用Scoped。全局共享、线程安全、开销大的服务如配置、缓存、日志器使用Singleton。构造函数注入为主这是 ASP.NET Core 默认且推荐的方式它使依赖关系明确。避免服务定位器Service Locator尽量不要在方法内部使用HttpContext.RequestServices或IServiceProvider.GetService()。这会使代码的依赖关系变得隐晦难以测试和维护。善用选项模式将配置信息封装成强类型对象并通过 DI 注入。使用IOptionsSnapshotT来获取支持更新的配置。管理IDisposable资源对于实现了IDisposable的瞬态和作用域服务容器会自动处理释放。不要在消费代码中对其调用Dispose()。在Program.cs中组织注册将服务注册逻辑按功能模块分组可以使用扩展方法让Program.cs保持整洁。// 扩展方法 public static class ServiceCollectionExtensions { public static IServiceCollection AddMyBusinessServices(this IServiceCollection services) { services.AddScopedIOrderService, OrderService(); services.AddScopedIProductService, ProductService(); // ... 更多注册 return services; } } // 在 Program.cs 中使用 builder.Services.AddMyBusinessServices();进行单元测试利用 DI 可以轻松地用 Mock 对象替换真实依赖。使用如 Moq、NSubstitute 等框架来编写测试。谨慎使用第三方容器内置容器能满足绝大多数场景。仅在确有高级需求如动态代理、子容器等时再考虑集成 Autofac、DryIoc 等第三方容器。10. 总结与下一步.NET 8 和 ASP.NET Core 的依赖注入框架是一个强大而优雅的基础设施。它的核心价值在于让代码更清晰、更可测试、更易于管理。通过本文的实践你应该已经掌握了从基础注册、生命周期管理到高级模式使用的完整路径。要真正掌握它下一步可以深入源码阅读Microsoft.Extensions.DependencyInjection的源代码理解ServiceProvider的构建和解析过程。探索高级场景研究拦截器AOP、装饰器模式如何与 DI 结合或者如何集成更复杂的第三方容器。应用于架构在整洁架构、领域驱动设计DDD或垂直切片架构中思考如何利用 DI 来组织领域层、应用层和基础设施层。性能调优对于高性能应用了解服务描述符缓存、表达式树编译等内部机制以优化首次请求的响应时间。从今天开始在你的下一个 ASP.NET Core 项目中有意识地运用这些原则和模式你会发现构建和维护复杂应用变得前所未有的顺畅。