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

文章目录

一、Transient(瞬时生命周期)

原理

客户端 DI容器 请求IMyService 创建新的MyService实例 返回新实例 再次请求IMyService 再次创建新的MyService实例 返回另一个新实例 客户端 DI容器

使用方式
csharp 复制代码
// 注册服务
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. 自动释放(当请求处理完成时)
适用场景
  • 轻量级无状态服务(如计算器、验证器)
  • 需要线程隔离的服务
  • 每次操作需要全新状态的场景
csharp 复制代码
// 典型应用:数据转换服务
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

使用方式
csharp 复制代码
// 注册服务
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)
  • 请求级状态管理
  • 事务处理单元
csharp 复制代码
// 典型应用: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. 作用域泄漏:意外在单例中引用会导致内存泄漏
csharp 复制代码
// 错误示例:单例中引用Scoped服务
public class SingletonService
{
    private readonly IUserRepository _repo; // 危险!
    
    public SingletonService(IUserRepository repo)
    {
        _repo = repo; // 这会导致Scoped服务变成"伪单例"
    }
}
  1. 异步风险:在async/await中可能跨越不同作用域
  2. 测试复杂性:需模拟作用域环境

三、Singleton(单例生命周期)

原理

持有引用 1 DI容器 +SingletonCache Singleton实例 +首次请求时创建 +全局唯一

使用方式
csharp 复制代码
// 注册服务
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连接池)
csharp 复制代码
// 典型应用:内存缓存
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. 线程安全风险:需手动实现同步机制
csharp 复制代码
public class CounterService
{
    private int _count = 0;
    
    // 危险:非线程安全
    public void Increment() => _count++;

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

三、生命周期对比分析

功能对比表
特性 Transient Scoped Singleton
实例创建时机 每次请求 作用域首次请求 全局首次请求
实例数量 多个 每作用域一个 全局一个
状态共享范围 无共享 作用域内共享 全局共享
线程安全要求 低 中等 高
适用场景 无状态服务 请求级状态 全局共享资源
内存管理 自动回收 作用域结束时回收 应用结束时回收
性能开销 高(频繁创建) 中等 低(单次创建)
性能基准测试
ini 复制代码
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:作用域泄漏

csharp 复制代码
// 错误:单例中注入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:线程竞争

csharp 复制代码
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:资源未释放

csharp 复制代码
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

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

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

    • 实现线程安全
    • 实现IDisposable
    • 避免依赖非Singleton服务
    csharp 复制代码
    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. 生命周期验证

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

    csharp 复制代码
    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) 检测生命周期错误,

这对预防生产环境的内存泄漏和状态污染至关重要。

相关推荐
小羊没烦恼!4 天前
微服务化的基石——持续集成
java·大数据·word·powerpoint·.net
俊昭喜喜里4 天前
java中的继承和多态的区别
java
小羊没烦恼!4 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
譕痕4 天前
JSONObject与JSONArray封装数据格式区别
java·json
胡写代码4 天前
别再前后端各写一套表单校验了
java·后端
小鱼能吃糖4 天前
缺陷修复总览 · mall电商项目:5类缺陷,1个病根,4个业务域
java·电商
此时不提桶,更待何时4 天前
01-06-A-JVM排查实战详解
java·jvm
伞伞悦读4 天前
【第38期】Python 模块与包详解:import、from、模块搜索路径、包结构和 __init__
开发语言·python
vipxieliang4 天前
ValidX 在 DDD 领域驱动设计中的实践
java·spring boot
C语言小火车4 天前
C/C++ 为什么需要编译器?
开发语言·c++