让我们来聊聊.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提供了三种生命周期:

  1. 瞬态(Transient):每次请求都创建新实例,适合轻量级、无状态的服务
  2. 作用域(Scoped):同一请求内共享实例,适合需要保持请求状态的服务
  3. 单例(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();
    }
}

四、高级技巧和常见陷阱

  1. 泛型服务注册:
// 注册开放泛型
builder.Services.AddTransient(typeof(IRepository<>), typeof(Repository<>));

// 使用时可以这样
public class UserService
{
    private readonly IRepository<User> _userRepository;
    
    public UserService(IRepository<User> userRepository)
    {
        _userRepository = userRepository;
    }
}
  1. 多实现处理:
// 注册多个实现
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);
        }
    }
}
  1. 避免的陷阱:
  • 循环依赖:A依赖B,B又依赖A
  • 在单例服务中注入Scoped服务
  • 忘记处理IDisposable服务
  • 在ConfigureServices方法中使用尚未注册的服务

五、实际应用场景分析

  1. Web API开发: 依赖注入让控制器保持干净,所有服务通过构造函数注入,便于单元测试。

  2. 后台服务: 对于长时间运行的后台服务,正确选择生命周期很重要,比如:

  • 使用Scoped生命周期模拟"请求"边界
  • 单例服务要特别注意线程安全
  1. 单元测试: 依赖注入让模拟(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);
}

六、技术优缺点分析

优点:

  1. 代码解耦,便于维护和测试
  2. 对象生命周期由框架管理,减少内存泄漏风险
  3. 配置灵活,可以随时替换实现
  4. 促进面向接口编程

缺点:

  1. 学习曲线较陡
  2. 过度使用会导致代码难以追踪
  3. 性能有轻微开销(但通常可以忽略)
  4. 错误配置可能导致运行时错误

七、注意事项

  1. 生命周期选择:
  • 单例服务中不要依赖瞬态或作用域服务
  • 作用域服务在中间件中要小心使用
  1. 性能考虑:
  • 避免在频繁调用的方法中解析服务
  • 对于高性能场景,考虑使用单例服务
  1. 最佳实践:
  • 尽量使用构造函数注入
  • 为服务定义接口
  • 避免服务定位器模式(Service Locator)

八、总结

.NET 6的依赖注入容器虽然看起来简单,但用好了能让你的代码质量提升一个档次。记住几个关键点:

  1. 根据需求选择合适的生命周期
  2. 优先使用构造函数注入
  3. 注意避免常见的陷阱
  4. 合理组织你的服务注册代码

随着项目规模扩大,你会越来越体会到依赖注入带来的好处——代码更清晰、测试更容易、维护更简单。就像整理好的工具箱,所有工具都放在该放的位置,用起来得心应手。