引用链接:https://mp.weixin.qq.com/s/0o6WTPjKDx-DE-vvawTBhg?scene=1&click_id=1
一、引言:为什么需要依赖注入?
在现代软件开发中,构建可维护、可测试、可扩展的应用程序是核心目标之一。随着应用程序复杂度的增加,类与类之间的依赖关系变得错综复杂。传统的“硬编码”依赖方式(即在类内部直接 new 一个依赖对象)导致了高耦合、低内聚,给单元测试、重构和维护带来了巨大挑战。
依赖注入(Dependency Injection, DI)是一种设计模式,它通过将依赖项从类的内部创建转移到外部注入,实现了“控制反转”(Inversion of Control, IoC),从而降低了类之间的耦合度,提高了代码的灵活性和可测试性。
.NET Core 从设计之初就内置了强大的依赖注入容器,使得开发者可以轻松地实现依赖管理。本文将深入探讨 .NET Core 中的依赖注入机制,涵盖服务生命周期、注册方式、使用场景,并通过详细示例展示构造函数注入、属性注入、方法注入等常见模式
二、.NET Core 依赖注入基础概念
1. 什么是依赖注入?
依赖注入是一种实现控制反转(IoC)的技术。在传统编程中,一个类如果需要使用另一个类的功能,通常会自己创建该类的实例:
public class OrderService{private readonly EmailService _emailService;public OrderService(){_emailService = new EmailService(); // 硬编码依赖}public void ProcessOrder(Order order){// 处理订单逻辑_emailService.SendConfirmation(order.CustomerEmail);}}
这种方式的问题在于:
-
OrderService与EmailService紧耦合。 -
无法在测试中替换
EmailService为模拟对象(Mock)。 -
如果
EmailService的构造函数发生变化,OrderService也需要修改。
使用依赖注入后:
public class OrderService{private readonly IEmailService _emailService;public OrderService(IEmailService emailService){_emailService = emailService;}public void ProcessOrder(Order order){_emailService.SendConfirmation(order.CustomerEmail);}}
依赖由外部注入,OrderService 不再关心 IEmailService 的具体实现。
2. 依赖注入的三种主要方式
-
构造函数注入(Constructor Injection):最常用、最推荐的方式。依赖通过构造函数传入。
-
属性注入(Property Injection):依赖通过公共属性注入。
-
方法注入(Method Injection):依赖通过方法参数传入。
3. 服务生命周期
.NET Core DI 容器支持三种服务生命周期:
|
|
|
|
|---|---|---|
| Singleton |
|
|
| Scoped |
|
|
| Transient |
|
|
注意:在 ASP.NET Core 应用中,
Scoped通常对应一个 HTTP 请求的生命周期。
三、.NET Core DI 容器配置
1. 配置服务容器
在 Program.cs(.NET 6+)或 Startup.cs(.NET 5 及更早)中配置服务:
// Program.cs (.NET 6+)var builder = WebApplication.CreateBuilder(args);// 添加服务到 DI 容器builder.Services.AddTransient<IEmailService, EmailService>();builder.Services.AddScoped<IOrderService, OrderService>();builder.Services.AddSingleton<ILogger, Logger>();var app = builder.Build();// 配置中间件管道app.MapGet("/", () => "Hello World!");app.Run();
2. 服务注册方法
-
AddTransient<TService, TImplementation>():注册瞬态服务。 -
AddScoped<TService, TImplementation>():注册作用域服务。 -
AddSingleton<TService, TImplementation>():注册单例服务。 -
AddSingleton<TService>(instance):注册一个已创建的单例实例。 -
AddSingleton<TService>(factory):通过工厂方法创建单例。
四、构造函数注入(Constructor Injection)
这是最推荐、最常用的注入方式。
示例
// 服务接口public interface IEmailService{void SendConfirmation(string email);}// 服务实现public class EmailService : IEmailService{public void SendConfirmation(string email){Console.WriteLine($"发送确认邮件到: {email}");}}// 使用服务的类public class OrderService{private readonly IEmailService _emailService;// 构造函数注入public OrderService(IEmailService emailService){_emailService = emailService;}public void ProcessOrder(Order order){// 处理订单Console.WriteLine("订单处理中...");_emailService.SendConfirmation(order.CustomerEmail);}}// Order 类public class Order{public int Id { get; set; }public string CustomerEmail { get; set; }}
在 Program.cs 中注册服务:
builder.Services.AddTransient<IEmailService, EmailService>();builder.Services.AddScoped<IOrderService, OrderService>();
优点
-
依赖关系清晰,易于理解。
-
所有依赖在对象创建时必须提供,避免了空引用异常。
-
易于进行单元测试(可通过 Mock 框架注入模拟对象)。
五、属性注入(Property Injection)
属性注入允许通过公共属性注入依赖。虽然 .NET Core 内置的 DI 容器不直接支持属性注入,但我们可以通过以下方式实现:
1. 使用 IServiceProvider 手动解析(不推荐)
public class OrderService{public IEmailService EmailService { get; set; }public void ProcessOrder(Order order){// 手动从容器解析(不推荐,破坏了 DI 原则)EmailService ??= ServiceProvider.GetService<IEmailService>();EmailService.SendConfirmation(order.CustomerEmail);}// 需要外部设置 ServiceProviderpublic IServiceProvider ServiceProvider { get; set; }}
这种方式破坏了依赖注入的初衷,不推荐使用。
2. 使用第三方容器(如 Autofac)
Autofac 等第三方 DI 容器原生支持属性注入。
安装 Autofac.Extensions.DependencyInjection
dotnet add package Autofac.Extensions.DependencyInjection
配置 Autofac
// Program.csusing Autofac;using Autofac.Extensions.DependencyInjection;var builder = WebApplication.CreateBuilder(args);// 使用 Autofac 作为容器builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());// 注册服务(Autofac 方式)builder.Host.ConfigureContainer<ContainerBuilder>(containerBuilder =>{containerBuilder.RegisterType<EmailService>().As<IEmailService>().InstancePerLifetimeScope();containerBuilder.RegisterType<OrderService>().PropertiesAutowired(); // 启用属性注入});var app = builder.Build();app.Run();
启用属性注入的类
public class OrderService{// [Dependency] 特性标记属性需要注入public IEmailService EmailService { get; set; }public void ProcessOrder(Order order){EmailService.SendConfirmation(order.CustomerEmail);}}
属性注入的优缺点
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
建议:尽量使用构造函数注入。只有在确实需要可选依赖或与第三方框架集成时才考虑属性注入。
六、方法注入(Method Injection)
方法注入通过方法参数传入依赖。适用于某些特定场景,如策略模式、工厂方法等。
示例
public interface IPaymentProcessor{void ProcessPayment(decimal amount);}public class CreditCardProcessor : IPaymentProcessor{public void ProcessPayment(decimal amount){Console.WriteLine($"使用信用卡支付 {amount:C}");}}public class PayPalProcessor : IPaymentProcessor{public void ProcessPayment(decimal amount){Console.WriteLine($"使用 PayPal 支付 {amount:C}");}}public class OrderService{// 方法注入:依赖通过参数传入public void ProcessOrder(Order order, IPaymentProcessor paymentProcessor){Console.WriteLine("订单处理中...");paymentProcessor.ProcessPayment(order.Amount);}}
在 Program.cs 中:
builder.Services.AddTransient<IPaymentProcessor, CreditCardProcessor>();// 或注册多个实现
调用时:
var orderService = app.Services.GetRequiredService<OrderService>();var paymentProcessor = app.Services.GetRequiredService<IPaymentProcessor>();orderService.ProcessOrder(order, paymentProcessor);
优点
-
灵活性高,可以根据上下文选择不同的实现。
-
适合动态决策场景。
缺点
-
调用方需要知道并获取依赖。
-
增加了调用复杂度。
七、服务生命周期详解
1. Singleton(单例)
builder.Services.AddSingleton<ILogger, Logger>();
-
整个应用程序生命周期中只有一个实例。
-
所有请求共享同一个实例。
-
注意:避免在单例中持有作用域或瞬态服务的引用,否则会导致“捕获服务”问题。
2. Scoped(作用域)
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
-
在每个作用域(如 HTTP 请求)内创建一个实例。
-
同一请求内的所有服务共享同一个实例。
-
在非 Web 应用中,需要手动创建作用域。
using (var scope = app.Services.CreateScope()){var service = scope.ServiceProvider.GetRequiredService<IOrderService>();// 使用 service}
3. Transient(瞬态)
builder.Services.AddTransient<IValidator, OrderValidator>();
-
每次请求服务时都创建新实例。
-
适合无状态、轻量级服务。
生命周期选择建议
-
Singleton:全局配置、缓存、日志记录器。
-
Scoped:数据库上下文(如
DbContext)、用户会话服务。 -
Transient:验证器、转换器、工具类。
八、高级用法
1. 注册多个实现
builder.Services.AddTransient<INotificationService, EmailNotificationService>();builder.Services.AddTransient<INotificationService, SmsNotificationService>();
注入所有实现:
public class NotificationService{private readonly IEnumerable<INotificationService> _notificationServices;public NotificationService(IEnumerable<INotificationService> notificationServices){_notificationServices = notificationServices;}public void NotifyAll(string message){foreach (var service in _notificationServices){service.Send(message);}}}
2. 工厂模式注入
builder.Services.AddSingleton<Func<string, INotificationService>>(provider => key =>{return key switch{"email" => provider.GetService<EmailNotificationService>(),"sms" => provider.GetService<SmsNotificationService>(),_ => throw new ArgumentException("Unknown notification type")};});
使用:
public class OrderService{private readonly Func<string, INotificationService> _notificationFactory;public OrderService(Func<string, INotificationService> notificationFactory){_notificationFactory = notificationFactory;}public void ProcessOrder(Order order){var emailService = _notificationFactory("email");emailService.Send($"订单 {order.Id} 已处理");}}
3. 命名注册与 Keyed Services(.NET 8+)
.NET 8 引入了 Keyed Services,支持按名称或键注册服务。
// 注册builder.Services.AddKeyedSingleton<INotificationService, EmailNotificationService>("email");builder.Services.AddKeyedSingleton<INotificationService, SmsNotificationService>("sms");// 注入public class OrderService{private readonly INotificationService _emailService;private readonly INotificationService _smsService;public OrderService([FromKeyedServices("email")] INotificationService emailService,[FromKeyedServices("sms")] INotificationService smsService){_emailService = emailService;_smsService = smsService;}}
九、完整 Demo 项目
DemoApp/
├── Services/
│ ├── IEmailService.cs
│ ├── EmailService.cs
│ ├── IOrderService.cs
│ └── OrderService.cs
├── Models/
│ └── Order.cs
├── Controllers/
│ └── OrderController.cs
└── Program.cs
代码实现
Models/Order.cs
public class Order{public int Id { get; set; }public string CustomerName { get; set; }public string CustomerEmail { get; set; }public decimal Amount { get; set; }}
Services/IEmailService.cs
public interface IEmailService{void SendConfirmation(string email, string orderId);}
Services/EmailService.cs
public class EmailService : IEmailService{private readonly ILogger<EmailService> _logger;public EmailService(ILogger<EmailService> logger){_logger = logger;}public void SendConfirmation(string email, string orderId){_logger.LogInformation($"发送订单确认邮件到 {email},订单号:{orderId}");// 模拟发送邮件Console.WriteLine($"邮件已发送至 {email},订单 {orderId}");}}
Services/IOrderService.cs
public interface IOrderService{Order CreateOrder(string customerName, string customerEmail, decimal amount);void ProcessOrder(int orderId);}
Services/OrderService.cs
public class OrderService : IOrderService{private readonly IEmailService _emailService;private readonly ILogger<OrderService> _logger;private static List<Order> _orders = new();public OrderService(IEmailService emailService, ILogger<OrderService> logger){_emailService = emailService;_logger = logger;}public Order CreateOrder(string customerName, string customerEmail, decimal amount){var order = new Order{Id = _orders.Count + 1,CustomerName = customerName,CustomerEmail = customerEmail,Amount = amount};_orders.Add(order);_logger.LogInformation($"创建订单:{order.Id}");return order;}public void ProcessOrder(int orderId){var order = _orders.FirstOrDefault(o => o.Id == orderId);if (order == null){_logger.LogWarning($"订单 {orderId} 未找到");return;}_logger.LogInformation($"处理订单 {orderId}");// 模拟处理逻辑Thread.Sleep(100); // 模拟耗时操作_emailService.SendConfirmation(order.CustomerEmail, order.Id.ToString());_logger.LogInformation($"订单 {orderId} 处理完成");}}
Controllers/OrderController.cs
[][]public class OrderController : ControllerBase{private readonly IOrderService _orderService;public OrderController(IOrderService orderService){_orderService = orderService;}[]public IActionResult CreateOrder([FromBody] CreateOrderRequest request){var order = _orderService.CreateOrder(request.CustomerName, request.CustomerEmail, request.Amount);return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);}[]public IActionResult GetOrder(int id){var order = _orderService.GetOrder(id); // 假设 IOrderService 有此方法if (order == null) return NotFound();return Ok(order);}[]public IActionResult ProcessOrder(int id){_orderService.ProcessOrder(id);return Ok(new { message = "订单处理已启动" });}}public class CreateOrderRequest{public string CustomerName { get; set; }public string CustomerEmail { get; set; }public decimal Amount { get; set; }}
Program.cs
var builder = WebApplication.CreateBuilder(args);// 添加服务builder.Services.AddControllers();builder.Services.AddEndpointsApiExplorer();builder.Services.AddSwaggerGen();// 注册自定义服务builder.Services.AddTransient<IEmailService, EmailService>();builder.Services.AddScoped<IOrderService, OrderService>();// 添加日志服务(内置)builder.Services.AddLogging();var app = builder.Build();// 配置中间件if (app.Environment.IsDevelopment()){app.UseSwagger();app.UseSwaggerUI();}app.UseHttpsRedirection();app.UseAuthorization();app.MapControllers();app.Run();
运行与测试
-
启动应用。
-
使用 Swagger 或 Postman 测试:
-
POST /api/order创建订单。 -
POST /api/order/{id}/process处理订单,观察控制台输出的邮件发送信息。
十、最佳实践与常见陷阱
最佳实践
-
优先使用构造函数注入。
-
合理选择服务生命周期。
-
接口隔离:定义细粒度接口。
-
避免在构造函数中执行耗时操作。
-
使用
ILogger<T>而不是静态日志。
常见陷阱
-
服务捕获(Service Capturing):
-
错误:在 Singleton 中注入 Scoped 服务。
-
后果:Scoped 服务被提升为 Singleton,导致状态污染。
-
解决:通过工厂模式或在作用域内解析。
-
内存泄漏:
-
错误:在 Scoped 或 Transient 服务中持有大对象引用。
-
解决:及时释放资源,使用
IDisposable。 -
循环依赖:
-
A 依赖 B,B 依赖 A。
-
解决:重构设计,使用中介者模式或事件总线。
十一、总结
.NET Core 内置的依赖注入容器功能强大且易于使用。通过合理使用构造函数注入、理解服务生命周期、避免常见陷阱,可以构建出高内聚、低耦合、易于测试和维护的应用程序。
虽然属性注入在某些场景下有用,但应谨慎使用,优先考虑构造函数注入。对于复杂需求,可以结合 Autofac 等第三方容器。
依赖注入不仅是技术,更是一种设计思想。掌握它,将使你的 .NET Core 应用更加健壮和灵活。