在 .NET 8 和 ASP.NET Core 项目中,依赖注入(Dependency Injection, DI)是构建松耦合、可测试、可维护应用程序的基石。很多开发者虽然每天都在使用AddScoped、AddSingleton,但对于其背后的生命周期管理、服务注册的多种方式、高级场景下的自定义容器等细节,往往只停留在“会用”层面。当项目规模扩大,遇到作用域服务注入单例、工厂模式使用不当、或者需要集成第三方容器时,问题就会接踵而至。本文将系统性地拆解 .NET 8 和 ASP.NET Core 中的依赖注入,从核心概念、基础用法到高级技巧和常见陷阱,提供一套完整的实战指南。无论你是刚接触 DI 的新手,还是希望优化现有项目架构的资深开发者,都能从中找到清晰的路径和可复用的代码。
1. 依赖注入的核心概念与价值
在深入代码之前,我们必须理解依赖注入要解决的根本问题,以及它在现代 .NET 开发中的核心地位。
1.1 什么是依赖注入?
依赖注入是一种设计模式,也是实现控制反转(Inversion of Control, IoC)原则的一种具体技术。它的核心思想是:一个类不应该自己创建它所依赖的对象,而应该由外部容器(在 ASP.NET Core 中就是IServiceProvider)来提供这些依赖。
让我们看一个典型的反面例子,即“控制耦合”或“紧耦合”:
// 紧耦合的代码 - 不推荐 public class OrderService { private readonly EmailNotifier _notifier; public OrderService() { // OrderService 自己创建了 EmailNotifier,形成了紧耦合 _notifier = new EmailNotifier(); } public void ProcessOrder(Order order) { // 处理订单逻辑... _notifier.SendEmail(order.CustomerEmail, "您的订单已处理"); } } public class EmailNotifier { public void SendEmail(string to, string message) { // 发送邮件逻辑 } }这段代码的问题在于:
- 可测试性差:在单元测试
OrderService时,无法模拟(Mock)EmailNotifier的行为,因为它被硬编码在构造函数里。 - 灵活性低:如果想更换通知方式(例如改用短信通知),必须修改
OrderService的源代码。 - 生命周期管理复杂:如果
EmailNotifier本身又依赖其他资源(如配置、数据库连接),其创建和销毁逻辑会散落在各个调用者中。
依赖注入通过将依赖关系的创建责任“反转”给外部容器来解决这些问题。
1.2 依赖注入的三种主要方式
ASP.NET Core 内置的 DI 容器支持三种注入方式:
构造函数注入(最常用、最推荐):依赖项通过类的构造函数参数传入。
public class OrderService { private readonly INotifier _notifier; // 依赖通过构造函数注入 public OrderService(INotifier notifier) { _notifier = notifier; } // ... 其他方法 }方法注入:依赖项作为特定方法的参数传入。这在某些框架生命周期方法(如 Minimal API 的处理器)中很常见。
app.MapGet("/orders/{id}", (int id, IOrderRepository repository) => { // `repository` 通过方法注入 return repository.GetOrderById(id); });属性注入(较少使用):依赖项通过公共属性设置。ASP.NET Core 内置容器不直接支持属性注入,通常需要配合第三方容器或特定场景(如 Razor Page 的
[Inject]属性)使用。一般不推荐,因为它破坏了对象的不可变性和初始化完整性。
1.3 为什么 ASP.NET Core 重度依赖 DI?
从 ASP.NET Core 的第一个版本开始,DI 就被设计为框架的核心组成部分,而不仅仅是一个可选的插件。这是因为 DI 带来了诸多架构上的优势:
- 促进松耦合:组件之间通过接口(Abstraction)通信,而不是具体实现(Concretion)。这使得替换实现(如将 SQL Server 仓库换成 PostgreSQL 仓库)变得异常简单。
- 提升可测试性:可以轻松地用模拟对象(Mock)替换真实依赖,从而对单个组件进行隔离单元测试。
- 统一生命周期管理:容器负责管理服务的创建和销毁,特别是对于像数据库上下文(DbContext)这样的稀缺资源,可以确保其以正确的作用域被使用和释放。
- 简化配置和集成:框架和第三方库可以通过标准的
IServiceCollection接口添加自己的服务,使得应用程序的配置清晰、集中。
理解了这些核心价值,我们就能更好地运用 DI,而不是仅仅将其视为一个“魔法黑箱”。
2. 环境准备与项目结构
在开始实战之前,我们需要搭建一个标准的 ASP.NET Core 项目环境。本文示例将基于 .NET 8 SDK 和 Visual Studio 2022 或 VS Code 进行演示,但核心概念适用于所有 .NET Core/5+ 版本。
2.1 创建项目
打开终端,使用以下命令创建一个新的 Web API 项目:
dotnet new webapi -n DependencyInjectionDemo -f net8.0 cd DependencyInjectionDemo这个命令会创建一个使用最小 API 和控制器两种风格的入门项目。为了全面演示,我们将主要使用控制器风格。
2.2 项目结构概览
创建完成后,你的项目结构应类似于:
DependencyInjectionDemo/ ├── Controllers/ │ └── WeatherForecastController.cs ├── Program.cs # 应用程序入口和主要配置 ├── appsettings.json # 配置文件 ├── DependencyInjectionDemo.csproj └── WeatherForecast.cs # 模型类对于依赖注入,最关键的文件是Program.cs。在 .NET 6 及更高版本中,Main方法和启动配置都集中在这个文件里。
2.3 理解 Program.cs 中的服务容器
打开Program.cs,你会看到类似以下的内容:
var builder = WebApplication.CreateBuilder(args); // 添加服务到容器。 builder.Services.AddControllers(); // 学习:Swagger/OpenAPI 服务也是通过 DI 容器注册的 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。这是 ASP.NET Core 内置的依赖注入容器配置接口。所有通过AddXXX()方法添加的服务,最终都会被注册到这个容器中。
在builder.Build()被调用后,IServiceCollection会被用来构建一个IServiceProvider(服务提供者),也就是实际的 DI 容器实例,它负责在运行时解析和提供我们注册的服务。
3. 服务注册:从基础到进阶
服务注册是使用 DI 的第一步。你需要告诉容器:“当有人请求IService时,请提供ServiceImpl的实例。” ASP.NET Core 提供了多种注册方式,对应不同的生命周期和场景。
3.1 三种服务生命周期
生命周期决定了服务实例在容器中存在多久,以及何时被创建和销毁。这是 DI 中最容易出错的概念之一。
| 生命周期 | 注册方法 | 描述 | 典型场景 |
|---|---|---|---|
| 瞬时 | AddTransient<TService, TImplementation>() | 每次从服务容器请求时都会创建一个新的实例。 | 无状态服务,轻量级且开销小的服务。例如:一个简单的数值计算器ICalculator。 |
| 作用域 | AddScoped<TService, TImplementation>() | 在同一个Web 请求(HTTP Request)范围内,每次请求都返回同一个实例。对于不同的请求,则创建不同的实例。 | 需要在一个请求内保持状态一致性的服务。最经典的例子是 Entity Framework Core 的DbContext。 |
| 单例 | AddSingleton<TService, TImplementation>() | 在整个应用程序生命周期内,只创建一个实例。所有请求共享该实例。 | 全局配置、缓存、连接池(如HttpClient的最佳实践是使用IHttpClientFactory,而非直接注册单例HttpClient)。 |
重要警告:生命周期不匹配是常见错误根源。例如,将一个Scoped服务(如DbContext)注入到一个Singleton服务中,会导致DbContext在多个请求间被错误地共享,引发并发和数据混乱问题。
3.2 基础注册模式
3.2.1 接口映射实现(最推荐)
这是最常用、最符合面向接口编程原则的方式。
// 1. 定义接口 public interface IMyService { string GetData(); } // 2. 实现接口 public class MyService : IMyService { public string GetData() => "Data from MyService"; } // 3. 在 Program.cs 中注册 builder.Services.AddScoped<IMyService, MyService>();使用:在控制器或其他服务中,通过构造函数注入IMyService。
3.2.2 自注册(直接注册具体类)
当不需要抽象接口,或者类本身就是一个稳定的实现时使用。
public class UtilityService { public void DoWork() { } } // 注册具体类,容器会将该类同时作为“服务类型”和“实现类型” builder.Services.AddTransient<UtilityService>(); // 等价于:builder.Services.AddTransient<UtilityService, UtilityService>();使用:直接注入UtilityService。
3.2.3 注册现有实例
如果你已经有一个创建好的实例,可以将其注册为单例。
var myConfig = new AppConfig { ApiKey = "secret-key" }; builder.Services.AddSingleton(myConfig); // 注册这个特定实例注意:容器不会管理这个实例的生命周期(除了持有引用),也不会在它上面调用Dispose。
3.2.4 使用工厂方法注册
当服务的创建逻辑比较复杂,或者需要根据运行时条件决定如何创建时,可以使用工厂方法。
builder.Services.AddScoped<IMyService>(serviceProvider => { // 可以从容器中解析其他服务来辅助构建 var logger = serviceProvider.GetRequiredService<ILogger<MyService>>(); var config = serviceProvider.GetRequiredService<IConfiguration>(); string connectionString = config.GetConnectionString("Default"); return new MyService(logger, connectionString); });3.3 进阶注册技巧
3.3.1 批量注册(程序集扫描)
对于有大量遵循约定(如都以Service结尾)的类需要注册,手动逐个添加非常繁琐。可以使用Scrutor等第三方库,或者自己实现反射逻辑。
使用 Scrutor(需安装 NuGet 包Scrutor):
using Scrutor; // 需要引入命名空间 builder.Services.Scan(scan => scan .FromAssemblyOf<IMyService>() // 从某个程序集开始 .AddClasses(classes => classes.Where(t => t.Name.EndsWith("Service"))) // 筛选类 .AsImplementedInterfaces() // 以其实现的接口注册 .WithScopedLifetime() // 指定生命周期 );3.3.2 泛型服务注册
可以注册开放的泛型接口和实现。
// 定义泛型仓储接口和实现 public interface IRepository<T> where T : class { T GetById(int id); } public class EfCoreRepository<T> : IRepository<T> where T : class { // 实现... } // 注册开放泛型 builder.Services.AddScoped(typeof(IRepository<>), typeof(EfCoreRepository<>));使用:在需要的地方注入IRepository<Product>或IRepository<Order>,容器会自动为你提供EfCoreRepository<Product>或EfCoreRepository<Order>的实例。
3.3.3 条件注册与装饰器模式
有时需要根据环境(开发/生产)或配置来决定注册哪个实现。可以使用TryAdd系列方法避免重复注册,或使用工厂方法进行条件判断。
// 仅当之前没有注册过 IMyService 时才注册 builder.Services.TryAddScoped<IMyService, DefaultService>(); // 根据环境注册不同实现 if (builder.Environment.IsDevelopment()) { builder.Services.AddScoped<IMyService, MockService>(); } else { builder.Services.AddScoped<IMyService, ProductionService>(); }装饰器模式(Decorator Pattern)可以通过嵌套注册来实现,为现有服务添加额外行为(如日志、缓存)而不修改其本身。
// 1. 注册核心服务 builder.Services.AddScoped<IMyService, CoreService>(); // 2. 使用装饰器(需要 Scrutor 等库简化,或手动用工厂方法实现) // 假设有 LoggingDecorator : IMyService,它在内部调用 _innerService 并记录日志 builder.Services.Decorate<IMyService, LoggingDecorator>();4. 服务解析:获取依赖的实例
注册服务后,如何在需要的地方获取它们?这个过程称为“服务解析”或“依赖注入”。ASP.NET Core 框架在多个环节自动为我们处理了解析。
4.1 构造函数注入(自动解析)
这是最主流的方式。当框架需要创建某个类(如控制器、Middleware、Razor Page)的实例时,它会查看其构造函数,然后尝试从IServiceProvider中解析每一个参数类型对应的服务。
// 在控制器中使用 [ApiController] [Route("[controller]")] public class OrdersController : ControllerBase { private readonly IOrderService _orderService; private readonly ILogger<OrdersController> _logger; // 框架会自动解析 IOrderService 和 ILogger<OrdersController> public OrdersController(IOrderService orderService, ILogger<OrdersController> logger) { _orderService = orderService; _logger = logger; } [HttpGet] public IActionResult Get() { var orders = _orderService.GetAllOrders(); return Ok(orders); } }关键点:
- 构造函数中的参数应该通常是接口或抽象类,以实现松耦合。
- 使用
readonly字段来保存注入的依赖,确保它们在对象生命周期内不变。 - 框架支持多个构造函数,但只会选择其中一个(参数最多且都能从容器中解析的那个)。最佳实践是只定义一个构造函数。
4.2 从 HttpContext 手动解析
在某些无法使用构造函数注入的场合(如静态方法、属性设置器、或是在程序启动早期),可以通过HttpContext来手动请求服务。
// 在 Minimal API 端点或中间件中 app.MapGet("/manual", (HttpContext context) => { // 从 HttpContext.RequestServices 获取服务提供者 var myService = context.RequestServices.GetRequiredService<IMyService>(); return myService.GetData(); }); // 注意:在中间件的 Invoke/InvokeAsync 方法中,IMyService 可以作为参数直接注入。注意:应尽量避免手动解析(称为“服务定位器模式”),因为它会使代码依赖隐藏,降低可测试性,并可能掩盖了设计上的问题(如类承担了过多职责)。构造函数注入是首选。
4.3 在 Program.cs 中早期解析
有时需要在应用程序管道构建完成之前就使用某些服务(例如配置验证、数据初始化)。可以在app.Build()之后,app.Run()之前解析。
var app = builder.Build(); // 构建一个临时的作用域来解析 Scoped 服务 using (var scope = app.Services.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); dbContext.Database.EnsureCreated(); // 例如:确保数据库已创建 // 或者运行种子数据 // SeedData.Initialize(dbContext); } // ... 后续配置中间件管道 app.UseHttpsRedirection(); // ...重要:对于Scoped服务,必须像上面这样创建一个作用域(CreateScope)来解析。直接使用app.Services.GetRequiredService<T>()解析Scoped服务会导致它实际上被提升为Singleton,引发潜在问题。
5. 实战案例:构建一个简单的电商订单处理系统
让我们通过一个模拟的电商订单处理流程,将上述概念串联起来。我们将创建以下组件:
IOrderRepository- 数据访问层抽象。IInventoryService- 库存服务抽象。INotificationService- 通知服务抽象。OrderProcessingService- 核心业务逻辑,依赖上述三个服务。OrdersController- Web API 控制器,依赖OrderProcessingService。
5.1 定义接口和模型
首先,创建模型和接口。
// Models/Order.cs namespace DependencyInjectionDemo.Models; public class Order { public int Id { get; set; } public string CustomerEmail { get; set; } public List<OrderItem> Items { get; set; } = new(); public DateTime OrderDate { get; set; } } public class OrderItem { public int ProductId { get; set; } public string ProductName { get; set; } public int Quantity { get; set; } public decimal UnitPrice { get; set; } }// Services/IOrderRepository.cs namespace DependencyInjectionDemo.Services; public interface IOrderRepository { Task<Order> GetOrderAsync(int id); Task SaveOrderAsync(Order order); }// Services/IInventoryService.cs namespace DependencyInjectionDemo.Services; public interface IInventoryService { Task<bool> CheckStockAsync(int productId, int quantity); Task ReduceStockAsync(int productId, int quantity); }// Services/INotificationService.cs namespace DependencyInjectionDemo.Services; public interface INotificationService { Task SendOrderConfirmationAsync(string customerEmail, Order order); }5.2 实现具体服务
我们为每个接口创建简单的模拟实现。
// Services/InMemoryOrderRepository.cs using DependencyInjectionDemo.Models; namespace DependencyInjectionDemo.Services; public class InMemoryOrderRepository : IOrderRepository { private static readonly Dictionary<int, Order> _orders = new(); private static int _nextId = 1; public Task<Order> GetOrderAsync(int id) { _orders.TryGetValue(id, out var order); return Task.FromResult(order); } public Task SaveOrderAsync(Order order) { if (order.Id == 0) { order.Id = _nextId++; } _orders[order.Id] = order; return Task.CompletedTask; } }// Services/MockInventoryService.cs namespace DependencyInjectionDemo.Services; public class MockInventoryService : IInventoryService { private readonly ILogger<MockInventoryService> _logger; public MockInventoryService(ILogger<MockInventoryService> logger) { _logger = logger; // 演示服务本身也可以依赖其他服务(如ILogger) } public Task<bool> CheckStockAsync(int productId, int quantity) { _logger.LogInformation($"Checking stock for product {productId}, quantity {quantity}"); // 模拟库存充足 return Task.FromResult(true); } public Task ReduceStockAsync(int productId, int quantity) { _logger.LogInformation($"Reducing stock for product {productId} by {quantity}"); return Task.CompletedTask; } }// Services/EmailNotificationService.cs using DependencyInjectionDemo.Models; namespace DependencyInjectionDemo.Services; public class EmailNotificationService : INotificationService { private readonly ILogger<EmailNotificationService> _logger; public EmailNotificationService(ILogger<EmailNotificationService> logger) { _logger = logger; } public Task SendOrderConfirmationAsync(string customerEmail, Order order) { _logger.LogInformation($"Sending order confirmation email to {customerEmail} for order #{order.Id}"); // 模拟发送邮件 return Task.CompletedTask; } }5.3 实现核心业务服务
OrderProcessingService将协调仓储、库存和通知服务。
// Services/OrderProcessingService.cs using DependencyInjectionDemo.Models; namespace DependencyInjectionDemo.Services; public class OrderProcessingService { private readonly IOrderRepository _orderRepository; private readonly IInventoryService _inventoryService; private readonly INotificationService _notificationService; private readonly ILogger<OrderProcessingService> _logger; // 通过构造函数注入所有依赖 public OrderProcessingService( IOrderRepository orderRepository, IInventoryService inventoryService, INotificationService notificationService, ILogger<OrderProcessingService> logger) { _orderRepository = orderRepository; _inventoryService = inventoryService; _notificationService = notificationService; _logger = logger; } public async Task<Order> ProcessOrderAsync(Order order) { _logger.LogInformation($"Processing order for {order.CustomerEmail}"); // 1. 检查库存 foreach (var item in order.Items) { var inStock = await _inventoryService.CheckStockAsync(item.ProductId, item.Quantity); if (!inStock) { throw new InvalidOperationException($"Product {item.ProductId} is out of stock."); } } // 2. 扣减库存 foreach (var item in order.Items) { await _inventoryService.ReduceStockAsync(item.ProductId, item.Quantity); } // 3. 保存订单 order.OrderDate = DateTime.UtcNow; await _orderRepository.SaveOrderAsync(order); _logger.LogInformation($"Order #{order.Id} saved."); // 4. 发送通知 await _notificationService.SendOrderConfirmationAsync(order.CustomerEmail, order); return order; } }5.4 注册服务并创建控制器
现在,在Program.cs中注册所有服务。
// Program.cs var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // --- 依赖注入注册 --- // 仓储:使用作用域生命周期,模拟每个请求一个“数据上下文” builder.Services.AddScoped<IOrderRepository, InMemoryOrderRepository>(); // 库存服务:同样使用作用域 builder.Services.AddScoped<IInventoryService, MockInventoryService>(); // 通知服务:这里我们注册为瞬时,因为可能每次发送都是独立的连接 builder.Services.AddTransient<INotificationService, EmailNotificationService>(); // 核心业务服务:它依赖上面的服务,也注册为作用域 builder.Services.AddScoped<OrderProcessingService>(); // 这里直接注册具体类 // ILogger 是框架默认注册的,我们无需手动注册 // --- 注册结束 --- var app = builder.Build(); // ... 中间件配置保持不变 if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();创建控制器来暴露 API。
// Controllers/OrdersController.cs using DependencyInjectionDemo.Models; using DependencyInjectionDemo.Services; using Microsoft.AspNetCore.Mvc; namespace DependencyInjectionDemo.Controllers; [ApiController] [Route("api/[controller]")] public class OrdersController : ControllerBase { private readonly OrderProcessingService _orderProcessingService; private readonly ILogger<OrdersController> _logger; public OrdersController( OrderProcessingService orderProcessingService, ILogger<OrdersController> logger) { _orderProcessingService = orderProcessingService; _logger = logger; } [HttpPost] public async Task<ActionResult<Order>> CreateOrder([FromBody] Order order) { try { var processedOrder = await _orderProcessingService.ProcessOrderAsync(order); return CreatedAtAction(nameof(GetOrder), new { id = processedOrder.Id }, processedOrder); } catch (InvalidOperationException ex) { _logger.LogWarning(ex, "Order processing failed due to stock issue."); return BadRequest(ex.Message); } catch (Exception ex) { _logger.LogError(ex, "An unexpected error occurred while processing order."); return StatusCode(500, "An internal server error occurred."); } } [HttpGet("{id}")] public async Task<ActionResult<Order>> GetOrder(int id, [FromServices] IOrderRepository repository) { // 演示方法注入:[FromServices] 特性 var order = await repository.GetOrderAsync(id); if (order == null) { return NotFound(); } return order; } }5.5 运行与测试
- 运行项目 (
dotnet run或 F5)。 - 打开 Swagger UI (通常是
https://localhost:xxxx/swagger)。 - 使用
POST /api/orders端点创建一个订单。请求体示例:{ "customerEmail": "test@example.com", "items": [ { "productId": 1, "productName": "Laptop", "quantity": 1, "unitPrice": 999.99 } ] } - 观察控制台日志,你会看到
MockInventoryService和EmailNotificationService记录的日志,证明依赖链被正确解析和执行。 - 使用
GET /api/orders/{id}端点获取刚创建的订单。
这个案例完整演示了从接口定义、服务实现、依赖注册到控制器使用的完整闭环。通过依赖注入,OrdersController和OrderProcessingService完全不知道其依赖的具体实现,只关心抽象契约,这使得替换实现(例如将EmailNotificationService换成SmsNotificationService)或进行单元测试变得非常简单。
6. 常见问题、陷阱与排查思路
即使理解了原理,在实际开发中仍会遇到各种 DI 相关的问题。下面是一些典型场景和解决方案。
6.1 服务解析失败:InvalidOperationException
错误信息:Cannot resolve scoped service 'X' from root provider.或Unable to resolve service for type 'X' while attempting to activate 'Y'.
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
在Program.cs的app.Build()之前,尝试从builder.Services或app.Services解析一个Scoped服务。 | builder.Services是配置集合,不是容器。app.Services是根容器(Root Provider),它不能直接解析Scoped服务。 | 使用using (var scope = app.Services.CreateScope()) { ... }创建作用域,然后从scope.ServiceProvider解析。 |
在Main方法或后台服务(如IHostedService)的构造函数中注入Scoped服务。 | 这些组件的生命周期可能是Singleton,导致Scoped服务被不当提升。 | 避免在Singleton服务中直接依赖Scoped服务。如果必须使用,通过IServiceScopeFactory在每次需要时创建新的作用域。 |
忘记在Program.cs中注册服务。 | 容器不知道如何创建该类型的实例。 | 检查Program.cs,确保所有需要注入的服务都已正确注册。使用TryAdd可以避免重复注册错误。 |
尝试注入一个内部类(internal)或私有嵌套类。 | DI 容器默认只能解析public访问级别的类。 | 将类改为public,或者配置服务注册时使用工厂方法。 |
6.2 生命周期不匹配导致的 Bug
症状:数据在不同请求间混乱;DbContext出现并发异常;单例服务中持有的Scoped服务状态异常。
根本原因:将生命周期短的服务注入到生命周期长的服务中。例如:
Singleton依赖Scoped(错误!)Singleton依赖Transient(通常可以,但Transient实例会被Singleton长期持有,可能不符合预期)
解决方案:
- 重新评估生命周期:这个服务真的需要是
Singleton吗?Scoped是否更合适? - 使用
IServiceScopeFactory:在Singleton服务内部,当需要执行一个操作时,临时创建一个作用域来解析Scoped服务。public class MySingletonService { private readonly IServiceScopeFactory _scopeFactory; public MySingletonService(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } public async Task DoWorkAsync() { using (var scope = _scopeFactory.CreateScope()) { var scopedService = scope.ServiceProvider.GetRequiredService<IMyScopedService>(); await scopedService.PerformTaskAsync(); } // 作用域结束,Scoped 服务被释放 } } - 将依赖改为方法参数:如果可能,将
Scoped依赖从构造函数移到方法参数中,由调用者(通常是框架)在正确的上下文中提供。
6.3 循环依赖(Circular Dependency)
错误信息:A circular dependency was detected.
原因:ClassA依赖ClassB,同时ClassB又依赖ClassA,形成死循环。
排查与解决:
- 检查构造函数注入:这是循环依赖最常见的原因。
- 重新设计:循环依赖通常是糟糕设计的信号。考虑是否可以将共享逻辑提取到第三个服务
ClassC中,让ClassA和ClassB都依赖ClassC。 - 使用属性注入或方法注入(谨慎!):在某些极端情况下,可以使用
[FromServices]属性注入或IServiceProvider延迟解析来打破构造函数循环,但这只是掩盖了设计问题。 - 使用
Lazy<T>或Func<T>:通过工厂委托延迟初始化依赖。public class ClassA { private readonly Lazy<ClassB> _classB; public ClassA(Lazy<ClassB> classB) => _classB = classB; public void Method() => _classB.Value.DoSomething(); } // 注册:builder.Services.AddTransient<ClassA>(); // builder.Services.AddTransient<ClassB>(); // builder.Services.AddTransient(sp => new Lazy<ClassB>(sp.GetRequiredService<ClassB>));
6.4 多实现与命名服务
场景:为同一个接口注册了多个实现,如何根据需要选择其中一个?
解决方案:
- 使用
IEnumerable<TService>:注入所有实现。public class ReportGenerator { private readonly IEnumerable<IDataExporter> _exporters; public ReportGenerator(IEnumerable<IDataExporter> exporters) => _exporters = exporters; // 可以遍历 _exporters 执行所有导出 } - 使用工厂模式或自定义解析:根据运行时条件(如配置、用户输入)决定使用哪个实现。这通常需要更复杂的注册逻辑,可能涉及自定义工厂或使用第三方容器的高级功能。
- 使用命名/键控服务:ASP.NET Core 内置容器不支持。需要借助第三方库(如
Autofac、DryIoc)或自己用Dictionary包装。
6.5 诊断与日志
当 DI 行为不符合预期时,启用日志是强大的排查工具。
// 在 appsettings.Development.json 中增加日志级别 { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning", "Microsoft.Extensions.DependencyInjection": "Debug" // 启用 DI 相关调试日志 } } }容器在构建和解析服务时会输出详细日志,帮助你了解服务的注册关系和解析过程。
7. 高级主题与最佳实践
掌握了基础之后,了解以下高级主题和最佳实践能让你的应用更加健壮和优雅。
7.1 选项模式(Options Pattern)与配置注入
选项模式是 ASP.NET Core 中管理配置的推荐方式,它本身重度依赖 DI。
// 1. 定义强类型选项类 public class ExternalApiOptions { public const string SectionName = "ExternalApi"; public string BaseUrl { get; set; } public string ApiKey { get; set; } public int TimeoutSeconds { get; set; } = 30; } // 2. 在 appsettings.json 中配置 // { // "ExternalApi": { // "BaseUrl": "https://api.example.com", // "ApiKey": "your-secret-key", // "TimeoutSeconds": 60 // } // } // 3. 在 Program.cs 中注册并绑定配置 builder.Services.Configure<ExternalApiOptions>( builder.Configuration.GetSection(ExternalApiOptions.SectionName)); // 4. 在服务中注入 IOptions<T> 或 IOptionsSnapshot<T> public class ApiClientService { private readonly ExternalApiOptions _options; // IOptionsSnapshot 支持配置热更新(Scoped 生命周期) public ApiClientService(IOptionsSnapshot<ExternalApiOptions> optionsSnapshot) { _options = optionsSnapshot.Value; } // 或者使用 IOptionsMonitor 进行更细粒度的控制 }最佳实践:始终使用选项模式来管理配置,而不是直接注入IConfiguration并在各处使用魔法字符串键。
7.2 集成第三方 DI 容器
ASP.NET Core 内置的 DI 容器功能足够应对大多数场景,但如果你需要更高级的功能(如属性注入、子容器、更灵活的命名注册、装饰器自动注册等),可以替换为第三方容器,如Autofac、DryIoc或Grace。
以Autofac为例:
- 安装
Autofac.Extensions.DependencyInjectionNuGet 包。 - 在
Program.cs中使用UseServiceProviderFactory。builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory()); - 在
ConfigureContainer方法中配置 Autofac 模块。builder.Host.ConfigureContainer<ContainerBuilder>(containerBuilder => { // Autofac 特有的注册语法 containerBuilder.RegisterType<MyService>().As<IMyService>().InstancePerLifetimeScope(); containerBuilder.RegisterAssemblyTypes(typeof(Program).Assembly) .Where(t => t.Name.EndsWith("Repository")) .AsImplementedInterfaces(); });
注意:替换容器会增加复杂性,确保你确实需要那些高级功能。
7.3 面向接口设计与单元测试
依赖注入最大的好处之一是便于单元测试。通过注入接口,你可以轻松地用模拟对象(Mock)替换真实实现。
// 使用 Moq 框架进行测试 [Test] public async Task ProcessOrder_Should_Call_NotificationService() { // 1. Arrange var mockNotifier = new Mock<INotificationService>(); var mockRepo = new Mock<IOrderRepository>(); var mockInventory = new Mock<IInventoryService>(); var logger = new Mock<ILogger<OrderProcessingService>>(); var service = new OrderProcessingService( mockRepo.Object, mockInventory.Object, mockNotifier.Object, logger.Object ); var testOrder = new Order { CustomerEmail = "test@test.com", Items = new List<OrderItem>() }; // 2. Act await service.ProcessOrderAsync(testOrder); // 3. Assert mockNotifier.Verify(n => n.SendOrderConfirmationAsync("test@test.com", It.IsAny<Order>()), Times.Once); }7.4 释放 IDisposable 资源
对于实现了IDisposable或IAsyncDisposable接口的服务(如DbContext、HttpClient),容器会自动管理其释放。
- Singleton:在应用程序关闭时(
IServiceProvider被释放时)释放。 - Scoped:在作用域结束时(通常是 HTTP 请求结束时)释放。
- Transient:容器不会跟踪和释放瞬时服务!如果
Transient服务实现了IDisposable,你需要手动管理其生命周期,或者避免将其注册为Transient。通常,需要释放资源的服务更适合注册为Scoped。
最佳实践:让框架管理资源释放。对于自定义的IDisposable服务,确保其生命周期与使用场景匹配(通常是Scoped),并在其中正确实现Dispose或DisposeAsync方法。
7.5 在 Minimal API 中使用依赖注入
.NET 6 引入的 Minimal API 同样完美支持 DI。
// 在 Program.cs 中 app.MapGet("/products/{id}", async (int id, IProductRepository repository) => { // `repository` 通过方法注入自动解析 var product = await repository.GetByIdAsync(id); return product is not null ? Results.Ok(product) : Results.NotFound(); }); app.MapPost("/products", async (Product product, IProductRepository repository) => { await repository.AddAsync(product); return Results.Created($"/products/{product.Id}", product); }); // 你也可以使用 [FromServices] 特性,但通常参数注入更简洁 app.MapGet("/manual", ([FromServices] IMyService service) => service.GetData());依赖注入是 .NET 和 ASP.NET Core 现代应用开发的支柱。从理解三种生命周期开始,到熟练运用接口注册、工厂方法、选项模式,再到规避生命周期陷阱和循环依赖,这是一个逐步深入的过程。本文通过概念梳理、代码示例和实战案例,旨在为你构建一个清晰、实用的知识框架。记住,良好的依赖注入设计意味着你的代码更灵活、更可测、更易于维护。在下一个项目中,尝试从设计接口开始,思考每个服务的职责和生命周期,你会发现自己正在编写更优雅、更专业的 .NET 代码。如果在实践中遇到本文未覆盖的特定场景,查阅官方文档和社区资源通常是下一步的最佳选择。