
这次我们来看 .NET 8 和 ASP.NET Core 中的依赖注入。对于任何构建现代、可测试、可维护的 .NET 应用程序来说依赖注入都是一个核心且必须掌握的技术。它不是新概念但在 .NET 8 和 ASP.NET Core 的框架演进下其内置容器的能力、性能优化和最佳实践都有了新的变化。如果你关心如何高效地组织服务、管理对象生命周期、进行单元测试或者想知道在 .NET 8 中 DI 有哪些新特性和性能提升这篇文章可以直接收藏。本文将带你快速理解 .NET 8 中依赖注入的核心机制并通过一个完整的 ASP.NET Core Web API 项目从零开始演示如何注册服务、注入依赖、管理生命周期以及如何利用新特性优化应用。我们会重点关注框架内置容器的使用方式、不同生命周期的选择策略、面向接口编程的优势以及如何避免常见的 DI 陷阱。无论你是刚开始接触 .NET 依赖注入还是希望了解 .NET 8 下的最新实践都能从中获得可落地的操作指南。1. 核心能力速览在深入细节之前我们先通过一个表格快速了解 .NET 8 和 ASP.NET Core 中依赖注入的核心能力概览这有助于你判断其是否适合你的项目以及需要关注的重点。能力项说明技术栈.NET 8, ASP.NET Core, C#核心机制内置的轻量级、高性能 IoC (控制反转) 容器主要功能服务注册、依赖解析、生命周期管理瞬时、作用域、单例、构造函数注入启动方式在Program.cs或Startup.cs中通过IServiceCollection配置随应用启动自动初始化。硬件门槛无特殊要求依赖注入是框架特性不额外消耗显存/GPU。接口能力支持通过IServiceProvider或构造函数直接获取服务实例是框架内部通信的基础。批量任务支持通过IEnumerableTService注入同一接口的所有实现实现批量处理或插件模式。适合场景构建松耦合、可测试的 ASP.NET Core Web API、MVC、Blazor、微服务及任何 .NET 应用程序。关键优势开箱即用、与框架深度集成、性能优化.NET 8 有改进、支持复杂场景如工厂、选项模式。2. 适用场景与使用边界依赖注入并非银弹理解其适用场景和边界能让你更有效地使用它。它最适合解决以下问题解耦与可测试性将类之间的直接依赖转换为对接口的依赖使得单元测试时可以用 Mock 对象轻松替换真实实现。管理对象生命周期清晰定义服务实例是每次创建新对象、每个请求共享一个还是全局唯一避免内存泄漏和状态混乱。配置与选项管理优雅地将配置如appsettings.json绑定到强类型选项类并通过 DI 注入到需要使用的地方。集成框架组件ASP.NET Core 框架本身如控制器、中间件、DbContext、HttpClientFactory重度依赖 DI 来提供各种服务。实现插件/策略模式通过注入IEnumerableIPlugin或IServiceProvider来动态选择或使用多个实现。它可能不是最佳选择或需要注意的场景简单的控制台应用或脚本如果应用非常简单没有复杂的依赖关系或测试需求手动new对象可能更直接。性能极度敏感的代码路径虽然 .NET 8 的 DI 容器性能很好但在每秒数百万次调用的热点路径上手动管理或使用更轻量的容器可能有一丝优势需实测。循环依赖DI 容器通常无法处理构造函数循环依赖。遇到此问题需要重构设计或考虑使用属性注入需谨慎、IServiceProvider延迟解析等模式。过度设计避免为每个简单的类都创建接口和注入只为那些确实需要替换实现、进行测试或管理生命周期的服务使用 DI。3. 环境准备与前置条件开始编码前确保你的开发环境已就绪。依赖注入是 .NET SDK 的一部分因此环境准备相对简单。操作系统Windows 10/11, macOS, 或 Linux 发行版如 Ubuntu。.NET SDK必须安装.NET 8.0 SDK或更高版本。你可以从 .NET 官方网站 下载安装。 验证安装打开终端CMD, PowerShell, Bash并运行dotnet --version应输出8.0.x或更高。开发工具任选其一Visual Studio 2022社区版免费确保安装了“.NET 8 开发”工作负载。Visual Studio Code免费需安装 C# 扩展。JetBrains Rider商业 IDE提供卓越的 .NET 开发体验。项目类型我们将以ASP.NET Core Web API项目为例因为它最典型地展示了 DI 的用法。其他项目类型如 MVC、Blazor、Worker Service原理相通。4. 安装部署与启动方式依赖注入无需“安装”它是框架内置的。我们的“部署”就是创建项目并配置服务。下面从零开始创建一个演示项目。第一步创建新项目打开终端导航到你的工作目录执行以下命令# 创建一个名为 DependencyInjectionDemo 的 Web API 项目 dotnet new webapi -n DependencyInjectionDemo # 进入项目目录 cd DependencyInjectionDemo第二步理解启动配置.NET 6 风格打开项目根目录下的Program.cs文件。在 .NET 6 及更高版本中Program.cs使用了顶级语句是配置服务的入口。// Program.cs (初始状态) var builder WebApplication.CreateBuilder(args); // 在此处添加服务到容器这就是依赖注入配置的核心区域 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所有服务的注册都通过它来完成。服务注册必须在var app builder.Build();这行代码之前完成。第三步启动服务在终端中运行项目# 在项目目录下执行 dotnet run启动后控制台会输出应用监听的地址如https://localhost:7294; http://localhost:5256。打开浏览器访问https://localhost:7294/swagger即可看到 Swagger UI这证明你的基础 Web API 项目已成功运行并且框架内置的 DI 容器已经工作例如控制器本身就是通过 DI 激活的。5. 功能测试与效果验证现在我们通过创建自定义服务并注入来验证 DI 的核心功能。5.1 定义服务与接口首先在项目中创建一个Services文件夹并添加以下两个文件模拟一个简单的天气数据服务。IWeatherService.cs (接口)namespace DependencyInjectionDemo.Services; public interface IWeatherService { IEnumerableWeatherForecast GetForecast(); } public record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) { public int TemperatureF 32 (int)(TemperatureC / 0.5556); }WeatherService.cs (实现)using System; namespace DependencyInjectionDemo.Services; public class WeatherService : IWeatherService { private static readonly string[] Summaries new[] { Freezing, Bracing, Chilly, Cool, Mild, Warm, Balmy, Hot, Sweltering, Scorching }; public IEnumerableWeatherForecast GetForecast() { return Enumerable.Range(1, 5).Select(index new WeatherForecast ( DateOnly.FromDateTime(DateTime.Now.AddDays(index)), Random.Shared.Next(-20, 55), Summaries[Random.Shared.Next(Summaries.Length)] )) .ToArray(); } }5.2 注册服务到容器回到Program.cs在builder.Services.AddControllers();之后添加服务注册。// Program.cs var builder WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // 注册我们自定义的服务 // AddScoped 表示该服务的作用域生命周期每个HTTP请求创建一个实例 builder.Services.AddScopedIWeatherService, WeatherService(); var app builder.Build(); // ... 其余代码不变这里使用了AddScoped方法这是三种核心生命周期之一。5.3 在控制器中注入并使用服务打开Controllers/WeatherForecastController.cs项目模板自带的修改它以注入我们的IWeatherService。using DependencyInjectionDemo.Services; using Microsoft.AspNetCore.Mvc; namespace DependencyInjectionDemo.Controllers; [ApiController] [Route([controller])] public class WeatherForecastController : ControllerBase { // 通过构造函数注入依赖 private readonly IWeatherService _weatherService; private readonly ILoggerWeatherForecastController _logger; // 框架的DI容器会自动解析并传入这两个参数 public WeatherForecastController( IWeatherService weatherService, ILoggerWeatherForecastController logger) { _weatherService weatherService; _logger logger; } [HttpGet(Name GetWeatherForecast)] public IEnumerableWeatherForecast Get() { _logger.LogInformation(Getting weather forecast from service.); // 使用注入的服务 return _weatherService.GetForecast(); } }5.4 验证效果确保项目仍在运行 (dotnet run)。打开浏览器访问 Swagger UI 页面 (如https://localhost:7294/swagger)。找到WeatherForecast端点点击Try it out-Execute。你应该能看到返回的 JSON 数据其中包含了由WeatherService生成的随机天气数据。成功标准API 成功返回数据且控制台日志中能看到Getting weather forecast from service.这条信息。这证明IWeatherService接口的WeatherService实现已被成功注入到控制器中。6. 深入理解服务生命周期生命周期管理是 DI 的核心错误的选择会导致 Bug如状态泄露、多用户数据混乱。.NET DI 支持三种主要生命周期生命周期注册方法实例创建时机适用场景瞬时 (Transient)AddTransientTService, TImplementation()每次从服务容器请求时都创建一个新实例。无状态服务轻量级且开销小的服务。例如一个简单的数据转换器IDataFormatter。作用域 (Scoped)AddScopedTService, TImplementation()在每个作用域如一个 HTTP 请求内创建一个实例在该作用域内多次请求会得到同一个实例。需要在一个“工作单元”内保持状态一致的服务。最常用于 Web 应用如DbContext(EF Core), 自定义业务逻辑服务。单例 (Singleton)AddSingletonTService, TImplementation()在应用程序首次请求时创建之后所有请求都使用这同一个实例。全局共享、无状态或线程安全的服务。例如配置对象、缓存服务、日志器工厂。测试不同生命周期创建一个简单的IOperationIdService来演示差异。// Services/IOperationIdService.cs 和 OperationIdService.cs public interface IOperationIdService { string Id { get; } } public class OperationIdService : IOperationIdService { public string Id { get; } Guid.NewGuid().ToString()[..8]; // 生成一个短ID }在Program.cs中用三种方式分别注册builder.Services.AddTransientIOperationIdService, OperationIdService(); // 方式1 // builder.Services.AddScopedIOperationIdService, OperationIdService(); // 方式2 // builder.Services.AddSingletonIOperationIdService, OperationIdService(); // 方式3创建一个测试控制器或端点在同一个请求内多次解析IOperationIdService并观察Id是否相同。再发起多个请求观察不同请求间的Id是否相同。通过这个简单的测试你可以直观地理解三种生命周期的区别。7. 高级注册与解析技巧7.1 多种注册方式除了基本的Add{Lifecycle}Interface, Implementation()容器还支持其他模式注册实现类本身services.AddScopedWeatherService();不推荐不利于测试和替换。注册实例services.AddSingletonIWeatherService(new WeatherService());直接提供一个现有实例。注册工厂services.AddScopedIWeatherService(sp new WeatherService());通过工厂函数创建可以在创建时访问其他服务 (sp)。尝试注册services.TryAddScopedIWeatherService, WeatherService();仅当没有其他IWeatherService注册时才注册避免重复。7.2 注入多个实现与IEnumerableT当同一个接口有多个实现时可以通过注入IEnumerableTService来获取所有实现。// 注册多个实现 builder.Services.AddScopedINotificationService, EmailNotificationService(); builder.Services.AddScopedINotificationService, SmsNotificationService(); // 在消费者中注入 public class OrderProcessor { private readonly IEnumerableINotificationService _notificationServices; public OrderProcessor(IEnumerableINotificationService notificationServices) { _notificationServices notificationServices; } public void ProcessOrder() { // 遍历所有通知服务 foreach (var service in _notificationServices) { service.SendNotification(Order processed); } } }7.3 使用IServiceProvider(服务定位器模式)有时无法通过构造函数注入如在静态方法或中间件中可以注入IServiceProvider然后手动解析服务。谨慎使用因为它会隐藏类依赖不利于测试。public class SomeService { private readonly IServiceProvider _serviceProvider; public SomeService(IServiceProvider serviceProvider) { _serviceProvider serviceProvider; } public void DoWork() { using (var scope _serviceProvider.CreateScope()) { var scopedService scope.ServiceProvider.GetRequiredServiceIMyScopedService(); // 使用 scopedService } } }关键点对于作用域服务必须创建新的作用域 (CreateScope) 来解析否则可能遇到生命周期错误。8. .NET 8 中的依赖注入增强.NET 8 对依赖注入容器进行了一些底层优化虽然 API 基本保持稳定但性能和功能有所提升基于源的生成器 (Source Generator) 优化对于使用AddSingleton,AddScoped,AddTransient等扩展方法注册的服务.NET 8 的编译器能进行更多优化减少运行时反射的使用从而提升服务解析速度尤其是在应用启动时。IKeyedServiceProvider和键控服务.NET 8 引入了更正式的方式来注册和解析“键控”服务即给同一个接口的不同实现起一个“键名”。这比之前依赖第三方库或自定义工厂的方式更规范。// 注册键控服务 builder.Services.AddKeyedSingletonICache, DistributedCache(distributed); builder.Services.AddKeyedSingletonICache, MemoryCache(memory); // 通过 [FromKeyedServices] 特性注入 public class MyController( [FromKeyedServices(memory)] ICache cache) { // 使用特定的缓存实现 }IServiceProviderIsService改进这个接口用于检查某个服务类型是否已被注册在 .NET 8 中其实现更加高效。这些增强使得在复杂场景下如多租户、插件系统使用 DI 更加方便和高效。9. 集成 ASP.NET Core 框架服务ASP.NET Core 框架本身注册了大量服务你可以直接注入使用IConfiguration访问应用配置。IWebHostEnvironment/IHostEnvironment获取环境信息开发、生产等。ILoggerT记录日志。IHttpClientFactory创建和管理HttpClient实例强烈推荐此方式而非直接new HttpClient()。DbContext(EF Core)数据库上下文默认注册为 Scoped 生命周期。例如在自定义服务中注入配置和日志public class MyBackgroundService { private readonly IConfiguration _configuration; private readonly ILoggerMyBackgroundService _logger; public MyBackgroundService(IConfiguration configuration, ILoggerMyBackgroundService logger) { _configuration configuration; _logger logger; } public void DoWork() { var setting _configuration[MySetting]; _logger.LogInformation(Using setting: {Setting}, setting); } } // 在 Program.cs 中注册 builder.Services.AddHostedServiceMyBackgroundService(); // 注册为后台服务10. 常见问题与排查方法在开发过程中你可能会遇到一些典型的 DI 相关问题。下表列出了常见现象、原因及解决方案。问题现象可能原因排查方式解决方案启动时抛出InvalidOperationException无法解析服务1. 服务未注册。2. 服务已注册但生命周期不匹配如在根容器解析 Scoped 服务。3. 构造函数参数有循环依赖。1. 检查Program.cs中是否有对应的AddXxx调用。2. 检查是否在Main或Build()之前尝试解析 Scoped/Transient 服务。3. 检查类之间的构造函数引用关系。1. 确保在builder.Build()前完成所有服务注册。2. 确保在正确的作用域内解析服务如在 Controller 或 Middleware 中。3. 重构设计打破循环依赖或使用IServiceProvider延迟解析。服务行为异常状态在不同请求间共享将本应注册为Transient或Scoped的服务错误地注册为了Singleton。检查服务的注册生命周期 (AddSingleton)。回顾服务是否包含不应共享的成员变量。根据服务用途将其改为AddScoped对于每个请求独立或AddTransient每次使用都新建。DbContext出现线程安全问题或数据混乱将DbContext注册为Singleton或Transient。EF Core DbContext 默认不是线程安全的。检查 DbContext 的注册代码。始终将DbContext注册为Scoped生命周期 (AddDbContext默认就是 Scoped)。注入IEnumerableT为空或缺少实现1. 没有为接口T注册任何实现。2. 实现类注册为了自身类型而非接口。1. 检查是否有AddXxxTInterface, TImpl的调用。2. 检查是否错误地使用了AddXxxTImpl()。1. 确保至少注册了一个接口实现。2. 确保注册时指定了接口类型。使用IServiceProvider.GetService返回null请求的服务类型未被注册。GetService在找不到服务时返回null。使用GetRequiredService替代它会在服务未注册时直接抛出异常便于快速定位问题。优先使用构造函数注入。如需手动解析考虑使用GetRequiredService并确保服务已正确注册。性能问题应用启动慢注册了过多服务尤其是 Singleton 服务在启动时初始化复杂。使用性能分析工具查看启动时间消耗。检查是否有 Singleton 服务在构造函数中执行了耗时操作如读取大文件、网络请求。1. 将非立即需要的初始化逻辑延迟执行如移到StartAsync或首次使用时。2. 考虑使用AddXxx的工厂重载进行懒加载。11. 最佳实践与使用建议遵循这些实践能让你的代码更健壮、更易维护面向接口编程始终针对接口而非具体类进行注册和注入。这是实现松耦合和可测试性的基石。构造函数注入为主这是 ASP.NET Core 推荐的方式它明确声明了类的依赖且依赖不可变 (readonly)。谨慎选择生命周期默认使用 Scoped对于大多数自定义业务服务AddScoped是最安全、最合理的选择。无状态服务用 Transient如果服务非常简单、无状态且创建开销小可以使用AddTransient。深思熟虑后用 Singleton确保 Singleton 服务是线程安全的并且不依赖 Scoped 服务除非你非常清楚自己在做什么。避免服务定位器模式尽量避免在业务代码中直接使用IServiceProvider.GetService。它使依赖关系变得隐晦难以理解和测试。构造函数注入是更优解。利用选项模式 (Options Pattern)管理配置不要直接注入IConfiguration并在各处读取字符串。使用services.ConfigureMyOptions(Configuration.GetSection(MySection))和注入IOptionsMyOptions这是类型安全且易于管理的方式。为单元测试设计依赖注入的核心价值之一是便于测试。确保你的服务可以轻松地用 Mock 对象替换。避免在服务构造函数中执行逻辑保持构造函数简单。保持 Startup/Program 整洁当服务注册很多时可以使用扩展方法分组注册。// 在 Program.cs 中 builder.Services.AddApplicationServices(); // 自定义扩展方法 builder.Services.AddInfrastructureServices(); builder.Services.AddDomainServices();及时释放资源如果服务实现了IDisposable接口容器会在其生命周期结束时自动调用Dispose。但对于你手动从IServiceProvider解析的IDisposable对象你需要负责释放通常使用using语句或在作用域结束时自动释放。依赖注入是构建现代化 .NET 应用的骨架。在 .NET 8 和 ASP.NET Core 中它已经高度成熟和优化。掌握其核心概念——注册、解析、生命周期并理解其背后的设计哲学远比记忆所有 API 细节更重要。先从面向接口、构造函数注入和正确使用 Scoped 生命周期开始实践你就能解决开发中 90% 的依赖管理问题。当遇到更复杂的场景如装饰器、动态代理、多实现选择时再深入探索高级特性或考虑功能更丰富的第三方容器如 Autofac, DryIoc。现在你可以回到你的项目审视现有的服务注册尝试将其重构为更清晰的依赖关系并为其编写单元测试这是巩固理解的最佳方式。