1. 依赖注入的前世今生

在现代化的软件开发中,依赖注入(DI)就像编程世界里的"快递小哥"。想象一下,以前我们需要自己跑去工厂拿零件(new对象),现在只需在家坐等快递(依赖注入)。在ASP.NET Core这个大家庭里,IServiceCollection就是我们的物流中心,负责管理所有快递的收发路线。

示例1:基础服务注册

// 使用ASP.NET Core 6.0技术栈
public interface IMessageService {
    string Send(string message);
}

public class EmailService : IMessageService {
    public string Send(string message) => $"Email发送:{message}";
}

// 在Program.cs中配置
builder.Services.AddTransient<IMessageService, EmailService>();

这个简单的注册像在物流系统里备案:"当有人需要IMessageService包裹时,请派发EmailService包裹,每次都要新的包装盒"。

2. 生命周期管理的三重境界

2.1 瞬态服务(Transient)

每次请求都创建新实例,就像外卖的一次性餐具:

builder.Services.AddTransient<IDatabaseConnection, SqlConnection>();

适合场景:需要保持无状态的数据库连接、轻量级工具类等。

2.2 作用域服务(Scoped)

每个HTTP请求共享一个实例,好比网购的同一批商品用同一个快递箱:

// 用户请求上下文服务
public class UserContext {
    public Guid SessionId { get; } = Guid.NewGuid();
}

builder.Services.AddScoped<UserContext>();

在Web应用中用于处理请求级别的数据共享,如用户会话、数据库上下文等。

2.3 单例服务(Singleton)

全程唯一实例,类似公司的公用打印机:

// 缓存服务示例
public class MemoryCache {
    private readonly ConcurrentDictionary<string, object> _cache = new();
    
    public void Set(string key, object value) => _cache[key] = value;
    public object Get(string key) => _cache.TryGetValue(key, out var val) ? val : null;
}

builder.Services.AddSingleton<MemoryCache>();

适用于需要长期驻留内存的服务,如配置管理、内存缓存等。

生命周期对照实验

// 在中间件中验证生命周期差异
app.Use(async (context, next) => {
    var transient = context.RequestServices.GetService<IDatabaseConnection>();
    var scoped = context.RequestServices.GetService<UserContext>();
    var singleton = context.RequestServices.GetService<MemoryCache>();
    
    await next();
});

连续刷新页面观察各实例的HashCode变化,会发现:

  • Transient:每次获取值都不同
  • Scoped:同一请求内值相同
  • Singleton:全局始终相同

3. 构造函数注入的底层解密

依赖注入框架就像一个智能装配工,通过反射扫描构造函数参数,自动寻找合适的部件进行组装。这个过程大致分为三步:

  1. 类型扫描:发现需要注入的构造函数
  2. 参数解析:遍历构造函数参数列表
  3. 实例组合:递归构建依赖树

复杂注入示例

public interface ILogger {
    void Log(string message);
}

public class ConsoleLogger : ILogger {
    public void Log(string message) => Console.WriteLine($"[LOG] {DateTime.Now:HH:mm:ss} {message}");
}

public class OrderService {
    private readonly ILogger _logger;
    private readonly IDatabaseConnection _connection;

    // 自动解析ILogger和IDatabaseConnection的实现
    public OrderService(ILogger logger, IDatabaseConnection connection) {
        _logger = logger;
        _connection = connection;
    }

    public void CreateOrder() {
        _logger.Log("开始创建订单");
        // 使用_connection执行数据库操作
    }
}

// 注册关系链
builder.Services.AddSingleton<ILogger, ConsoleLogger>();
builder.Services.AddScoped<IDatabaseConnection, SqlConnection>();
builder.Services.AddTransient<OrderService>();

通过这个链式依赖,当请求OrderService时,容器会先解析ILogger和IDatabaseConnection的实例。这个过程就像搭积木,每一层依赖都是稳固的基础。

4. 服务解析的十八般武艺

4.1 构造函数注入(推荐首选)

这是官方推荐的标准姿势,就像使用原装充电器:

public class HomeController : Controller {
    private readonly IMessageService _service;
    
    public HomeController(IMessageService service) {
        _service = service; // 自动注入
    }
}

4.2 服务定位(应急方案)

某些特殊情况下的"破窗锤",慎用但必要:

public class BackupProcessor {
    private readonly IServiceProvider _provider;
    
    public BackupProcessor(IServiceProvider provider) {
        _provider = provider;
    }

    public void Process() {
        using var scope = _provider.CreateScope();
        var service = scope.ServiceProvider.GetService<ISpecialService>();
        // 临时使用特定服务
    }
}

4.3 工厂模式(灵活控制)

当需要动态创建对象时的"万能钥匙":

builder.Services.AddTransient<IServiceFactory>(provider => {
    return new ServiceFactory(() => {
        return new CustomService(provider.GetService<ILogger>());
    });
});

5. 实战中的血泪经验

5.1 生命周期陷阱

把Scoped服务注册为Singleton,就像把新鲜食材放进冰箱冷冻层——虽然不会坏,但口感全失。特别是EF Core的DbContext,错误注册会导致并发问题。

5.2 循环依赖迷宫

当ClassA依赖ClassB,ClassB又依赖ClassA时,系统会抛出异常。解法方案包括:

  • 引入接口抽象
  • 使用属性注入(虽然不推荐)
  • 重构代码结构

5.3 性能优化点

大型项目中的服务解析可能成为性能瓶颈。我们可以:

  1. 尽量使用Singleton生命周期
  2. 避免在构造函数中执行耗时操作
  3. 使用BuildServiceProvider(validateScopes: true)进行作用域验证

6. 最佳实践路线图

  1. 接口先行:基于接口而非实现编程
  2. 生命周期标注:为每个服务明确生命周期
  3. 构造函数纯净:避免业务逻辑和异常抛出
  4. 分层注册:将服务注册按模块分类管理
  5. 健康检查:使用dotnet counters监控服务实例数量

推荐注册结构示例

// 扩展方法组织注册逻辑
public static class ServiceCollectionExtensions {
    public static IServiceCollection AddApplicationServices(this IServiceCollection services) {
        services.AddSingleton<ICacheService, RedisCache>();
        services.AddScoped<IUserRepository, UserRepository>();
        services.AddTransient<ILogger, CompositeLogger>();
        return services;
    }
}

// 在Program.cs中调用
builder.Services.AddApplicationServices();

7. 应用场景分析

  1. 微服务架构:通过DI实现松耦合的模块化设计
  2. 单元测试:轻松替换依赖实现进行测试
  3. 插件系统:动态加载和注册服务
  4. 多租户系统:根据不同租户切换服务实现

8. 技术优缺点

优点:

  • 提升代码可维护性和可测试性
  • 实现关注点分离
  • 便于实现面向接口编程
  • 支持灵活的策略切换

缺点:

  • 学习曲线陡峭
  • 过度使用可能造成性能损耗
  • 调试难度增加
  • 可能隐藏复杂的依赖关系

9. 注意事项备忘录

  1. 避免在Singleton服务中引用Scoped服务
  2. 谨慎使用属性注入([FromServices])
  3. 及时释放实现了IDisposable接口的服务
  4. 注意线程安全问题(特别是Singleton服务)
  5. 定期进行服务注册的合理性审查