让我们来聊聊.NET 6中依赖注入这个既基础又强大的特性。依赖注入就像是你家里的水电工,你不需要知道水管怎么铺、电线怎么走,只要打开水龙头就有水,按下开关灯就亮。在编程世界里,它帮我们管理各种服务对象,让代码更干净、更灵活。
一、服务注册:把工具放进工具箱
想象你有一个工具箱,在使用前需要把螺丝刀、锤子等工具放进去。服务注册就是这样的过程。在.NET 6中,我们通常在Program.cs里做这件事:
// 示例:基础服务注册
var builder = WebApplication.CreateBuilder(args);
// 添加瞬态服务(每次请求都新建实例)
builder.Services.AddTransient<IMyTransientService, MyTransientService>();
// 添加作用域服务(同一请求内共享实例)
builder.Services.AddScoped<IMyScopedService, MyScopedService>();
// 添加单例服务(整个应用生命周期只有一个实例)
builder.Services.AddSingleton<IMySingletonService, MySingletonService>();
// 添加已经存在的实例作为单例
var myServiceInstance = new MyService();
builder.Services.AddSingleton<IMyService>(myServiceInstance);
这里有个小技巧:如果你觉得写太多builder.Services,可以创建一个扩展方法:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddMyServices(this IServiceCollection services)
{
services.AddTransient<IMyTransientService, MyTransientService>();
services.AddScoped<IMyScopedService, MyScopedService>();
return services;
}
}
然后在Program.cs里这样用:
builder.Services.AddMyServices();
二、生命周期管理:选择合适的"保质期"
服务生命周期就像食品的保质期,用错了可能会"食物中毒"。.NET 6提供了三种生命周期:
- 瞬态(Transient):每次请求都创建新实例,适合轻量级、无状态的服务
- 作用域(Scoped):同一请求内共享实例,适合需要保持请求状态的服务
- 单例(Singleton):整个应用生命周期一个实例,适合重量级、线程安全的服务
看个实际例子:
// 示例:生命周期演示
public class OperationLogger
{
private readonly IMyTransientService _transientService;
private readonly IMyScopedService _scopedService;
private readonly IMySingletonService _singletonService;
public OperationLogger(
IMyTransientService transientService,
IMyScopedService scopedService,
IMySingletonService singletonService)
{
_transientService = transientService;
_scopedService = scopedService;
_singletonService = singletonService;
}
public void LogOperations(string message)
{
// 这里可以看到不同生命周期的服务实例ID
Console.WriteLine($"Transient: {_transientService.InstanceId}");
Console.WriteLine($"Scoped: {_scopedService.InstanceId}");
Console.WriteLine($"Singleton: {_singletonService.InstanceId}");
}
}
在控制器中使用时,你会发现:
- 每次刷新页面,Transient服务的InstanceId都会变
- 同一次请求中,Scoped服务的InstanceId保持不变
- 整个应用运行期间,Singleton服务的InstanceId始终不变
三、构造函数注入:让依赖关系一目了然
构造函数注入就像点菜时明确告诉服务员你要什么,而不是自己去厨房找。这是最推荐的注入方式:
// 示例:控制器中的依赖注入
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly IWeatherService _weatherService;
private readonly ILogger<WeatherForecastController> _logger;
// 依赖通过构造函数明确声明
public WeatherForecastController(
IWeatherService weatherService,
ILogger<WeatherForecastController> logger)
{
_weatherService = weatherService;
_logger = logger;
}
[HttpGet]
public async Task<IActionResult> Get()
{
try
{
var forecast = await _weatherService.GetForecastAsync();
return Ok(forecast);
}
catch (Exception ex)
{
_logger.LogError(ex, "获取天气预报失败");
return StatusCode(500);
}
}
}
有时候你会遇到需要从容器中手动解析服务的情况,这时可以使用IServiceProvider:
// 示例:手动解析服务
public class MyManualResolver
{
private readonly IServiceProvider _serviceProvider;
public MyManualResolver(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void DoWork()
{
// 创建作用域(重要!特别是要解析Scoped服务时)
using var scope = _serviceProvider.CreateScope();
// 从作用域中解析服务
var service = scope.ServiceProvider.GetRequiredService<IMyScopedService>();
service.DoSomething();
}
}
四、高级技巧和常见陷阱
- 泛型服务注册:
// 注册开放泛型
builder.Services.AddTransient(typeof(IRepository<>), typeof(Repository<>));
// 使用时可以这样
public class UserService
{
private readonly IRepository<User> _userRepository;
public UserService(IRepository<User> userRepository)
{
_userRepository = userRepository;
}
}
- 多实现处理:
// 注册多个实现
builder.Services.AddTransient<IMessageService, EmailService>();
builder.Services.AddTransient<IMessageService, SmsService>();
// 通过IEnumerable获取所有实现
public class NotificationSender
{
private readonly IEnumerable<IMessageService> _messageServices;
public NotificationSender(IEnumerable<IMessageService> messageServices)
{
_messageServices = messageServices;
}
public void SendAll(string message)
{
foreach (var service in _messageServices)
{
service.Send(message);
}
}
}
- 避免的陷阱:
- 循环依赖:A依赖B,B又依赖A
- 在单例服务中注入Scoped服务
- 忘记处理IDisposable服务
- 在ConfigureServices方法中使用尚未注册的服务
五、实际应用场景分析
Web API开发: 依赖注入让控制器保持干净,所有服务通过构造函数注入,便于单元测试。
后台服务: 对于长时间运行的后台服务,正确选择生命周期很重要,比如:
- 使用Scoped生命周期模拟"请求"边界
- 单例服务要特别注意线程安全
- 单元测试: 依赖注入让模拟(Mock)服务变得容易:
// 测试示例
[Test]
public void TestWeatherController()
{
// 创建模拟服务
var mockWeatherService = new Mock<IWeatherService>();
mockWeatherService.Setup(x => x.GetForecastAsync())
.ReturnsAsync(new WeatherForecast { Temperature = 25 });
// 注入模拟服务
var controller = new WeatherForecastController(
mockWeatherService.Object,
new NullLogger<WeatherForecastController>());
// 执行测试
var result = controller.Get().Result as OkObjectResult;
// 验证
Assert.AreEqual(25, ((WeatherForecast)result.Value).Temperature);
}
六、技术优缺点分析
优点:
- 代码解耦,便于维护和测试
- 对象生命周期由框架管理,减少内存泄漏风险
- 配置灵活,可以随时替换实现
- 促进面向接口编程
缺点:
- 学习曲线较陡
- 过度使用会导致代码难以追踪
- 性能有轻微开销(但通常可以忽略)
- 错误配置可能导致运行时错误
七、注意事项
- 生命周期选择:
- 单例服务中不要依赖瞬态或作用域服务
- 作用域服务在中间件中要小心使用
- 性能考虑:
- 避免在频繁调用的方法中解析服务
- 对于高性能场景,考虑使用单例服务
- 最佳实践:
- 尽量使用构造函数注入
- 为服务定义接口
- 避免服务定位器模式(Service Locator)
八、总结
.NET 6的依赖注入容器虽然看起来简单,但用好了能让你的代码质量提升一个档次。记住几个关键点:
- 根据需求选择合适的生命周期
- 优先使用构造函数注入
- 注意避免常见的陷阱
- 合理组织你的服务注册代码
随着项目规模扩大,你会越来越体会到依赖注入带来的好处——代码更清晰、测试更容易、维护更简单。就像整理好的工具箱,所有工具都放在该放的位置,用起来得心应手。
评论