第 12 章:依赖注入的原理与实现

jerry北京市2026年4月21日C# 9 次阅读 约 24 分钟
第 12 章:依赖注入的原理与实现

深入理解 IoC 和 DI 的概念、服务生命周期(Transient/Scoped/Singleton)、Microsoft.Extensions.DependencyInjection 的内部实现、常见陷阱,以及 .NET 8 的 Keyed Services。


12.1 IoC 与 DI 概念

控制反转(Inversion of Control)

传统方式:类自己创建依赖(控制权在类内部):

// 紧耦合:OrderService 直接依赖具体实现
class OrderService
{
    private readonly SqlOrderRepository _repo = new SqlOrderRepository(); // 硬编码依赖
    private readonly EmailNotifier _notifier = new EmailNotifier();

    public void PlaceOrder(Order order)
    {
        _repo.Save(order);
        _notifier.Notify(order);
    }
}

IoC 方式:依赖从外部注入(控制权反转到外部):

// 松耦合:依赖接口,由外部注入
class OrderService
{
    private readonly IOrderRepository _repo;
    private readonly INotifier _notifier;

    public OrderService(IOrderRepository repo, INotifier notifier) // 构造函数注入
    {
        _repo = repo;
        _notifier = notifier;
    }

    public void PlaceOrder(Order order)
    {
        _repo.Save(order);
        _notifier.Notify(order);
    }
}

注入方式

// 1. 构造函数注入(推荐)
class MyService(ILogger<MyService> logger, IRepository repo)
{
    // 使用 C# 12 主构造函数
}

// 2. 方法注入(特定场景)
class MyService
{
    public void Process([FromServices] IValidator validator) { }
}

// 3. 属性注入(不推荐,.NET DI 不原生支持)
class MyService
{
    [Inject]
    public ILogger Logger { get; set; } // 需要第三方容器如 Autofac
}

12.2 服务生命周期

三种生命周期

var builder = WebApplication.CreateBuilder(args);

// Transient:每次请求都创建新实例
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();

// Scoped:每个作用域(HTTP 请求)一个实例
builder.Services.AddScoped<IDbContext, AppDbContext>();

// Singleton:整个应用生命周期一个实例
builder.Services.AddSingleton<ICacheService, MemoryCacheService>();
请求 1                          请求 2
┌─────────────────────┐        ┌─────────────────────┐
│ Scoped 实例 A       │        │ Scoped 实例 B       │
│                     │        │                     │
│ Transient 实例 1    │        │ Transient 实例 3    │
│ Transient 实例 2    │        │ Transient 实例 4    │
└─────────────────────┘        └─────────────────────┘

Singleton 实例(全局唯一)
┌─────────────────────────────────────────────────┐
│ 整个应用共享同一个实例                             │
└─────────────────────────────────────────────────┘

验证生命周期

class LifetimeDemo(ILogger<LifetimeDemo> logger)
{
    private readonly Guid _id = Guid.NewGuid();

    public void ShowId() => logger.LogInformation("实例 ID: {Id}", _id);
}

// 注册为不同生命周期,观察 ID 变化
builder.Services.AddTransient<LifetimeDemo>();  // 每次不同
builder.Services.AddScoped<LifetimeDemo>();     // 同一请求内相同
builder.Services.AddSingleton<LifetimeDemo>();  // 永远相同

12.3 Microsoft.Extensions.DI 内部实现

ServiceDescriptor

每个注册的服务都用 ServiceDescriptor 描述:

// 这三种注册方式等价
builder.Services.AddScoped<IUserService, UserService>();

builder.Services.Add(new ServiceDescriptor(
    serviceType: typeof(IUserService),
    implementationType: typeof(UserService),
    lifetime: ServiceLifetime.Scoped));

builder.Services.Add(ServiceDescriptor.Scoped<IUserService, UserService>());

ServiceProvider 的工作流程

解析服务的过程:
1. 查找 ServiceDescriptor(按服务类型)
2. 检查生命周期:
   - Singleton → 从根容器的缓存中获取或创建
   - Scoped → 从当前 Scope 的缓存中获取或创建
   - Transient → 每次创建新实例
3. 递归解析构造函数参数(依赖)
4. 调用构造函数创建实例
// ServiceProvider 内部简化模型
class SimpleServiceProvider
{
    private readonly Dictionary<Type, ServiceDescriptor> _descriptors;
    private readonly Dictionary<Type, object> _singletons = new();
    private readonly Dictionary<Type, object> _scopedInstances = new();

    public object GetService(Type serviceType)
    {
        var descriptor = _descriptors[serviceType];

        return descriptor.Lifetime switch
        {
            ServiceLifetime.Singleton =>
                _singletons.GetOrAdd(serviceType, _ => CreateInstance(descriptor)),
            ServiceLifetime.Scoped =>
                _scopedInstances.GetOrAdd(serviceType, _ => CreateInstance(descriptor)),
            ServiceLifetime.Transient =>
                CreateInstance(descriptor),
            _ => throw new InvalidOperationException()
        };
    }

    private object CreateInstance(ServiceDescriptor descriptor)
    {
        // 找到构造函数,递归解析参数,创建实例
        var ctor = descriptor.ImplementationType!.GetConstructors().First();
        var parameters = ctor.GetParameters()
            .Select(p => GetService(p.ParameterType))
            .ToArray();
        return ctor.Invoke(parameters);
    }
}

注册的高级用法

// 工厂注册
builder.Services.AddScoped<IDbContext>(sp =>
{
    var config = sp.GetRequiredService<IConfiguration>();
    return new AppDbContext(config.GetConnectionString("Default")!);
});

// 注册多个实现
builder.Services.AddTransient<INotifier, EmailNotifier>();
builder.Services.AddTransient<INotifier, SmsNotifier>();
builder.Services.AddTransient<INotifier, PushNotifier>();

// 获取所有实现
class NotificationService(IEnumerable<INotifier> notifiers)
{
    public async Task NotifyAll(string message)
    {
        foreach (var notifier in notifiers)
            await notifier.SendAsync(message);
    }
}

// TryAdd:如果已注册则跳过
builder.Services.TryAddScoped<IUserService, UserService>();

// Replace:替换已有注册
builder.Services.Replace(ServiceDescriptor.Scoped<IUserService, MockUserService>());

12.4 常见陷阱

陷阱 1:Captive Dependency(俘获依赖)

Singleton 服务依赖 Scoped 服务,导致 Scoped 服务的生命周期被"提升"为 Singleton:

// ❌ 错误:Singleton 持有 Scoped 的引用
builder.Services.AddSingleton<ICacheService, CacheService>();
builder.Services.AddScoped<IDbContext, AppDbContext>();

class CacheService : ICacheService
{
    private readonly IDbContext _db; // Scoped 服务被 Singleton 俘获!

    public CacheService(IDbContext db)
    {
        _db = db; // 这个 DbContext 永远不会被释放,整个应用共享同一个
    }
}
// ✅ 正确:使用 IServiceScopeFactory 手动创建 Scope
class CacheService(IServiceScopeFactory scopeFactory) : ICacheService
{
    public async Task<User?> GetUserAsync(int id)
    {
        using var scope = scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<IDbContext>();
        return await db.Users.FindAsync(id);
    }
}

陷阱 2:在 Scope 外解析 Scoped 服务

// ❌ 错误:从根容器解析 Scoped 服务
var app = builder.Build();
var db = app.Services.GetRequiredService<IDbContext>(); // 异常或内存泄漏

// ✅ 正确:创建 Scope
using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<IDbContext>();

陷阱 3:Dispose 问题

// DI 容器会自动 Dispose 它创建的 IDisposable 对象
// 但如果你用工厂方法注册,需要注意

// ❌ 可能的问题:外部创建的对象不会被容器 Dispose
var connection = new SqlConnection(connectionString);
builder.Services.AddSingleton(connection); // 容器不会 Dispose 这个对象

// ✅ 使用工厂方法,让容器管理生命周期
builder.Services.AddSingleton<SqlConnection>(_ =>
    new SqlConnection(connectionString));

开启验证

var builder = WebApplication.CreateBuilder(args);

// 开发环境开启 DI 验证
if (builder.Environment.IsDevelopment())
{
    builder.Host.UseDefaultServiceProvider(options =>
    {
        options.ValidateScopes = true;    // 验证 Scope 问题
        options.ValidateOnBuild = true;   // 构建时验证所有服务可解析
    });
}

12.5 Keyed Services(.NET 8)

.NET 8 新增按名称/键注册和解析服务:

// 注册
builder.Services.AddKeyedSingleton<ICache, RedisCache>("redis");
builder.Services.AddKeyedSingleton<ICache, MemoryCache>("memory");

// 注入(使用 [FromKeyedServices] 特性)
class ProductService(
    [FromKeyedServices("redis")] ICache distributedCache,
    [FromKeyedServices("memory")] ICache localCache)
{
    public async Task<Product?> GetAsync(int id)
    {
        // 先查本地缓存
        var product = localCache.Get<Product>($"product:{id}");
        if (product is not null) return product;

        // 再查分布式缓存
        product = await distributedCache.GetAsync<Product>($"product:{id}");
        if (product is not null)
        {
            localCache.Set($"product:{id}", product, TimeSpan.FromMinutes(1));
        }
        return product;
    }
}

// 手动解析
var cache = app.Services.GetRequiredKeyedService<ICache>("redis");

Keyed Services 之前的变通方案

// .NET 8 之前需要用工厂模式
builder.Services.AddSingleton<RedisCache>();
builder.Services.AddSingleton<MemoryCache>();
builder.Services.AddSingleton<Func<string, ICache>>(sp => key => key switch
{
    "redis" => sp.GetRequiredService<RedisCache>(),
    "memory" => sp.GetRequiredService<MemoryCache>(),
    _ => throw new ArgumentException($"未知的缓存类型: {key}")
});

12.6 面试要点

  1. 三种生命周期的区别?

    • Transient:每次解析创建新实例
    • Scoped:每个作用域(HTTP 请求)一个实例
    • Singleton:整个应用一个实例
  2. 什么是 Captive Dependency?

    • Singleton 依赖 Scoped/Transient 服务,导致短生命周期服务被长生命周期服务俘获
    • 解决方案:使用 IServiceScopeFactory 手动创建 Scope
  3. DI 容器如何解析服务?

    • 查找 ServiceDescriptor → 检查缓存 → 递归解析构造函数参数 → 创建实例
  4. 如何注册同一接口的多个实现?

    • 多次 Add 注册,通过 IEnumerable<T> 获取所有实现
    • .NET 8 使用 Keyed Services 按名称区分
  5. ValidateScopes 和 ValidateOnBuild 的作用?

    • ValidateScopes:检测 Scope 违规(如从根容器解析 Scoped 服务)
    • ValidateOnBuild:构建时验证所有注册的服务都可以被解析

练习

  1. 实现一个简化版的 DI 容器,支持 Transient 和 Singleton 生命周期
  2. 复现 Captive Dependency 问题,并用 ValidateScopes 检测
  3. 使用 Keyed Services 实现策略模式(根据配置选择不同的实现)
  4. 使用 IServiceScopeFactory 在后台任务中安全地使用 Scoped 服务

← 上一章:泛型的底层实现 | 下一章:高性能 .NET →

评论

登录 后发表评论

暂无评论