详细解析 .NET 依赖注入的三种生命周期模式

发布于:2025-07-06 ⋅ 阅读:(17) ⋅ 点赞:(0)

在这里插入图片描述

一、Transient(瞬时生命周期)

原理
客户端 DI容器 请求IMyService 创建新的MyService实例 返回新实例 再次请求IMyService 再次创建新的MyService实例 返回另一个新实例 客户端 DI容器
使用方式
// 注册服务
builder.Services.AddTransient<IMyService, MyService>();

// 使用示例
public class ClientService
{
    private readonly IMyService _service1;
    private readonly IMyService _service2;

    public ClientService(IMyService service1, IMyService service2)
    {
        // 两个参数会收到不同的实例
        _service1 = service1;
        _service2 = service2;
    }
}
核心特性
  1. 每次请求创建新实例
  2. 不共享状态
  3. 自动释放(当请求处理完成时)
适用场景
  • 轻量级无状态服务(如计算器、验证器)
  • 需要线程隔离的服务
  • 每次操作需要全新状态的场景
// 典型应用:数据转换服务
public interface IDataTransformer
{
    string Transform(string input);
}

public class ReverseTransformer : IDataTransformer
{
    public string Transform(string input) 
        => new string(input.Reverse().ToArray());
}

// 注册
services.AddTransient<IDataTransformer, ReverseTransformer>();
优势
  1. 内存安全:不会意外共享状态
  2. 线程安全:每个线程使用独立实例
  3. 简单可靠:无需考虑状态管理
劣势
  1. 性能开销:频繁创建/销毁对象
  2. 内存碎片:大量短期对象增加GC压力
  3. 资源浪费:不适合初始化成本高的服务

二、Scoped(作用域生命周期)

原理
请求1 DI容器 作用域 请求2 S2 开始请求 创建作用域 请求IUserRepository 创建新实例 返回实例A 再次请求IUserRepository 返回相同的实例A 结束请求 销毁作用域 释放所有Scoped实例 新请求 创建新作用域 请求IUserRepository 返回新实例B 请求1 DI容器 作用域 请求2 S2
使用方式
// 注册服务
builder.Services.AddScoped<IUserRepository, UserRepository>();

// ASP.NET Core 中间件中
app.Use(async (context, next) =>
{
    // 手动创建作用域
    using var scope = context.RequestServices.CreateScope();
    var repo = scope.ServiceProvider.GetService<IUserRepository>();
    await repo.LogRequestAsync(context.Request);
    await next();
});
核心特性
  1. 作用域内单例(同一作用域内实例共享)
  2. 跨请求隔离(不同请求不同实例)
  3. 自动释放(作用域结束时)
适用场景
  • 数据库上下文(如EF Core DbContext)
  • 请求级状态管理
  • 事务处理单元
// 典型应用:EF Core DbContext
public class AppDbContext : DbContext
{
    public DbSet<User> Users { get; set; }
}

// 注册
services.AddScoped<AppDbContext>();

// 在控制器中使用
public class UserController : Controller
{
    private readonly AppDbContext _context;

    public UserController(AppDbContext context)
    {
        _context = context; // 同一请求内共享实例
    }
}
优势
  1. 状态隔离:不同请求互不影响
  2. 资源优化:重用初始化成本高的对象
  3. 事务一致性:天然支持事务边界(整个请求)
劣势
  1. 作用域泄漏:意外在单例中引用会导致内存泄漏
// 错误示例:单例中引用Scoped服务
public class SingletonService
{
    private readonly IUserRepository _repo; // 危险!
    
    public SingletonService(IUserRepository repo)
    {
        _repo = repo; // 这会导致Scoped服务变成"伪单例"
    }
}
  1. 异步风险:在async/await中可能跨越不同作用域
  2. 测试复杂性:需模拟作用域环境

三、Singleton(单例生命周期)

原理
持有引用
1
DI容器
+SingletonCache
Singleton实例
+首次请求时创建
+全局唯一
使用方式
// 注册服务
builder.Services.AddSingleton<ICacheService, CacheService>();

// 预创建实例(立即初始化)
var cache = new CacheService();
builder.Services.AddSingleton<ICacheService>(cache);

// 延迟初始化
builder.Services.AddSingleton<IBackgroundService>(provider => 
    new BackgroundService(provider.GetRequiredService<ILogger>()));
核心特性
  1. 全局唯一实例(整个应用生命周期)
  2. 首次请求时创建(除非预注册实例)
  3. 应用关闭时释放
适用场景
  • 配置服务(如IOptions)
  • 内存缓存
  • 共享资源连接(如Redis连接池)
// 典型应用:内存缓存
public class MemoryCacheService : ICacheService, IDisposable
{
    private readonly ConcurrentDictionary<string, object> _cache = new();
    private Timer _cleanupTimer;

    public MemoryCacheService()
    {
        _cleanupTimer = new Timer(_ => Cleanup(), null, 0, 60_000);
    }

    public object Get(string key) => _cache.TryGetValue(key, out var value) ? value : null;

    public void Dispose() => _cleanupTimer?.Dispose();
}

// 注册
services.AddSingleton<ICacheService, MemoryCacheService>();
优势
  1. 性能最佳:单次初始化,零实例化开销
  2. 全局状态共享:跨请求共享数据
  3. 资源集中管理:如连接池、线程池
劣势
  1. 线程安全风险:需手动实现同步机制
public class CounterService
{
    private int _count = 0;
    
    // 危险:非线程安全
    public void Increment() => _count++;

    // 正确:线程安全版本
    public void SafeIncrement() => Interlocked.Increment(ref _count);
}
  1. 内存泄漏:意外持有引用导致GC无法回收
  2. 启动延迟:复杂单例初始化影响应用启动时间

三、生命周期对比分析

功能对比表
特性 Transient Scoped Singleton
实例创建时机 每次请求 作用域首次请求 全局首次请求
实例数量 多个 每作用域一个 全局一个
状态共享范围 无共享 作用域内共享 全局共享
线程安全要求 中等
适用场景 无状态服务 请求级状态 全局共享资源
内存管理 自动回收 作用域结束时回收 应用结束时回收
性能开销 高(频繁创建) 中等 低(单次创建)
性能基准测试
BenchmarkDotNet=v0.13.1, OS=Windows 10
Intel Core i7-11800H 2.30GHz, 1 CPU, 16 cores

| 方法                | 调用次数 | 平均耗时 | 内存分配 |
|---------------------|---------|----------|----------|
| TransientResolve    | 10000   | 158 ns   | 32 B     |
| ScopedResolve       | 10000   | 76 ns    | 0 B      |
| SingletonResolve    | 10000   | 38 ns    | 0 B      |
典型错误案例

案例1:作用域泄漏

// 错误:单例中注入Scoped服务
builder.Services.AddSingleton<ReportService>();
builder.Services.AddScoped<DatabaseContext>();

// 解决方案1:使用工厂方法
builder.Services.AddSingleton<ReportService>(provider => 
    new ReportService(provider.GetRequiredService<DatabaseContext>));

// 解决方案2:改为作用域服务
builder.Services.AddScoped<ReportService>();

案例2:线程竞争

public class CacheService
{
    private Dictionary<string, object> _cache = new();
    
    // 错误:非线程安全
    public void Add(string key, object value)
    {
        _cache[key] = value;
    }

    // 正确:使用并发集合
    private ConcurrentDictionary<string, object> _safeCache = new();
    public void SafeAdd(string key, object value)
    {
        _safeCache[key] = value;
    }
}

案例3:资源未释放

public class FileService : IDisposable
{
    private FileStream _fileStream;

    public FileService()
    {
        _fileStream = File.Open("data.bin", FileMode.Open);
    }

    // 必须实现Dispose
    public void Dispose()
    {
        _fileStream?.Dispose();
    }
}

// 注册(Singleton需显式释放)
builder.Services.AddSingleton<FileService>();

四、生命周期决策树

graph TD
    A[新服务注册] --> B{是否有状态?}
    B -->|无状态| C[优先Transient]
    B -->|有状态| D{状态共享范围?}
    D -->|请求级| E[选择Scoped]
    D -->|应用级| F{是否线程安全?}
    F -->|是| G[选择Singleton]
    F -->|否| H[重构为线程安全或选Scoped]
    
    C --> I{创建成本高?}
    I -->|是| J[考虑Scoped]
    I -->|否| K[保持Transient]
    
    G --> L{需要立即初始化?}
    L -->|是| M[预注册实例]
    L -->|否| N[延迟初始化]
    
    E --> O[确保作用域边界]
    G --> P[实现IDisposable]

五、最佳实践指南

  1. 默认选择Transient

    • 除非有明确需求,否则优先无状态服务
    // 好:无状态服务使用Transient
    services.AddTransient<IValidator, EmailValidator>();
    
  2. Scoped生命周期黄金法则

    • 一个请求对应一个工作单元
    services.AddScoped<OrderProcessingService>();
    
  3. Singleton安全准则

    • 实现线程安全
    • 实现IDisposable
    • 避免依赖非Singleton服务
    public class SafeCache : ICache, IDisposable
    {
        private readonly ConcurrentDictionary<string, object> _store;
        private readonly Timer _timer;
        private readonly ReaderWriterLockSlim _lock = new();
    
        public void Dispose()
        {
            _timer?.Dispose();
            _lock?.Dispose();
        }
    }
    
  4. 生命周期验证

    // 启用容器验证
    var provider = services.BuildServiceProvider(validateScopes: true);
    
  5. 混合生命周期策略

    public class HybridService
    {
        // 长周期依赖Singleton
        private readonly ICache _cache;
        
        // 短周期依赖Transient工厂
        private readonly Func<ITransientService> _factory;
        
        public HybridService(
            ICache cache,
            Func<ITransientService> factory)
        {
            _cache = cache;
            _factory = factory;
        }
        
        public void Process()
        {
            // 按需创建Transient实例
            using var service = _factory();
            service.DoWork(_cache.GetData());
        }
    }
    

在这里插入图片描述

六、总结

  1. Transient:轻量级无状态服务的首选,但需警惕高频创建的性能开销
  2. Scoped:请求敏感资源(如数据库连接)的黄金标准,注意作用域边界
  3. Singleton:全局共享资源的最佳载体,但必须确保线程安全和资源释放

架构师建议:在大型系统中采用分层生命周期策略:

  • 基础设施层(缓存、配置):Singleton
  • 领域服务层:Scoped
  • 工具类/辅助服务:Transient

定期使用 .BuildServiceProvider(validateScopes: true) 检测生命周期错误,
这对预防生产环境的内存泄漏和状态污染至关重要。

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到