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. 构造函数注入的底层解密
依赖注入框架就像一个智能装配工,通过反射扫描构造函数参数,自动寻找合适的部件进行组装。这个过程大致分为三步:
- 类型扫描:发现需要注入的构造函数
- 参数解析:遍历构造函数参数列表
- 实例组合:递归构建依赖树
复杂注入示例
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 性能优化点
大型项目中的服务解析可能成为性能瓶颈。我们可以:
- 尽量使用Singleton生命周期
- 避免在构造函数中执行耗时操作
- 使用
BuildServiceProvider(validateScopes: true)
进行作用域验证
6. 最佳实践路线图
- 接口先行:基于接口而非实现编程
- 生命周期标注:为每个服务明确生命周期
- 构造函数纯净:避免业务逻辑和异常抛出
- 分层注册:将服务注册按模块分类管理
- 健康检查:使用
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. 应用场景分析
- 微服务架构:通过DI实现松耦合的模块化设计
- 单元测试:轻松替换依赖实现进行测试
- 插件系统:动态加载和注册服务
- 多租户系统:根据不同租户切换服务实现
8. 技术优缺点
优点:
- 提升代码可维护性和可测试性
- 实现关注点分离
- 便于实现面向接口编程
- 支持灵活的策略切换
缺点:
- 学习曲线陡峭
- 过度使用可能造成性能损耗
- 调试难度增加
- 可能隐藏复杂的依赖关系
9. 注意事项备忘录
- 避免在Singleton服务中引用Scoped服务
- 谨慎使用属性注入([FromServices])
- 及时释放实现了IDisposable接口的服务
- 注意线程安全问题(特别是Singleton服务)
- 定期进行服务注册的合理性审查
评论