依赖注入概念
ASP.NET Core 支持依赖关系注入 (DI) 软件设计模式,这是一种在类及其依赖关系之间实现控制反转 (IoC) 的技术。
按照官方文档的描述:
依赖关系注入通过以下方式解决了这些问题:
- 使用接口或基类将依赖关系实现抽象化。
- 在服务容器中注册依赖关系。 ASP.NET Core 提供了一个内置的服务容器 IServiceProvider。 服务通常已在应用的 Program.cs 文件中注册。
- 将服务注入到使用它的类的构造函数中。 框架负责创建依赖关系的实例,并在不再需要时将其释放。
探索Asp.net core中的依赖注入
生命周期
在asp.net core中,以来注入有三个生命周期。
分别为Singleton(单例),Scoped(范围),Transient(瞬态)。
Singleton(单例),很好理解,就是一个单例模式,在整个应用的生命周期中只会初始化一次。
Scoped(范围),每一次请求中实例化一次。
Transient(瞬态),每次使用都是一个新的实例化对象。
注入方式分别如下:
services.AddSingleton(); //单例
services.AddScoped(); //范围
services.AddTransient(); //瞬态
来实践一下,用VS新建一个WebApi项目,然后添加三个类,对应三个生命周期。
public class TestTransient
{
public TestTransient()
{
Id = Guid.NewGuid();
}
public Guid Id { get; set; }
}
public class TestSingleton
{
public TestSingleton()
{
Id = Guid.NewGuid();
}
public Guid Id { get; set; }
}
public class TestScoped
{
public TestScoped()
{
Id = Guid.NewGuid();
}
public Guid Id { get; set; }
}
然后在Program中添加注入,这里我没用接口注入,直接注入类,我们也可以使用接口注入的方式。
builder.Services.AddSingleton<TestSingleton>();
builder.Services.AddScoped<TestScoped>();
builder.Services.AddTransient<TestTransient>();
接下来我们在控制器中通过构造函数注入我们的三个类。
private readonly ILogger<WeatherForecastController> _logger;
private readonly TestScoped _testScoped;
private readonly TestSingleton _testSingleton;
private readonly TestTransient _testTransient;
public WeatherForecastController(ILogger<WeatherForecastController> logger, TestScoped testScoped, TestSingleton testSingleton, TestTransient testTransient)
{
_logger = logger;
_testScoped = testScoped;
_testSingleton = testSingleton;
_testTransient = testTransient;
}
在调用Get方法中打印我们的Id
第一次请求
第二第三次请求
可以看到单例的Id每次请求都是一致的,而范围和瞬态的在不同请求中都不一样。
那么如何区别Scoped和Transient呢?很简单,我们直接整一个简单的中间件,分别注入并答应对应Id。
app.Use(async (httpContext, next) =>
{
var scoped = httpContext.RequestServices.GetRequiredService<TestScoped>();
var transient = httpContext.RequestServices.GetRequiredService<TestTransient>();
Console.WriteLine($"Middleware scoped: {scoped.Id}");
Console.WriteLine($"Middleware transient: {transient.Id}");
await next(httpContext);
});
可以看到,在一次请求中Scoped的Id是一致的,Transient的Id每次都不一样。
服务注册方法
在上面中我只是用了其中一种注册方法,就是直接注册类。
除此之外,我们还可以通过接口注入。
比如我们添加一个IScopedDependency的接口,然后新建一个TestAbcScoped继承IScopedDependency,然后在Program中添加注入
builder.Services.AddScoped<IScopedDependency, TestAbcScoped>();
之后我们在构造器中使用IScopedDependency注入的话,则自动会获得TestAbcScoped的实现实例。
通过我们Debug监视,可以发现IScopedDependency注入的实例确实是TestAbcScoped。
当我们注册同一个接口的多个实现时,默认取最后一次注入的实例,当我们需要获取全部接口的实现时,可以通过注入IEnumerable
我们增加一个IScopedDependency的实现
public class TestAbcScoped : IScopedDependency
{
}
public class TestAbcdScoped : IScopedDependency
{
}
注册顺序为:
builder.Services.AddScoped<IScopedDependency, TestAbcScoped>();
builder.Services.AddScoped<IScopedDependency, TestAbcdScoped>();
可以看到,单个注入会取后注入的实例,IEnumerable注入则会获取所有的实例。
注意:
除此之外,还有TryAddXXX的方法,注册服务时,如果还没有添加相同类型的实例,就添加一个实例。
服务注册通常与顺序无关,除了注册同一类型的多个实现时。
服务注入
上面我们实操时所用的注入方法都是构造器注入,这也是官方推荐的注入方式。
除此之外,我们还可以使用IServiceProvider获取服务,上面中间件所用到的HttpContext.RequestService本质是一个IServiceProvider实例。
三方框架加持注入功能,asp.net core的注入方式有限,我们可以使用Autofac来增强。
使用autofac之后我们可以支持属性注入,即无需在构造器中添加,只需要构造对应的属性即可。
属性注入和构造器注入的优缺点对比。
构造器注入可以清晰的看出我们所有注入的实例,对于协作和沟通有比较大的帮助。但是,若是注入的东西太多,会导致一个很庞大的构造器,当然官方的建议是,当存在那么多的注入的时候,就需要考虑拆分业务了。
属性注入则只需要通过构造一个属性,系统自动注入,弱点是没有构造器清晰辨别。毕竟不容易区分哪些属性是通过注入的,哪些是业务赋值的。
在考虑到继承方面时,有时候属性注入会比构造器注入合适,比如在基类中,我们往往可以注入通用的服务,这样在子类的构造器中就无需再次注入该服务。
注意事项
在使用依赖注入的时候,我们最好要明确每个服务的生命周期,在长生命周期的服务中,切勿注入短生命周期的服务。
如在单例中注入范围服务或瞬时服务,在范围服务中注入瞬时服务。否则会出现对象已被释放的情况。
在新版本中,单例里面注入范围服务,程序会自动检测并提示异常。但是在旧版本中是没有提示的,这点需要注意。
如何在单例中使用Scoped范围服务呢,可以使用IServiceScopeFactory,IServiceScopeFactory始终注册为单例实例,通过IServiceScopeFactory创建一个Scope生命周期。
public class TestSingleton
{
private readonly IServiceScopeFactory _serviceScopeFactory;
public TestSingleton(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
Id = Guid.NewGuid();
}
public Guid Id { get; set; }
public void Console()
{
using(var scope = _serviceScopeFactory.CreateScope())
{
var testScoped = scope.ServiceProvider.GetRequiredService<TestScoped>();
System.Console.WriteLine($"TestSingleton - TestScoped: {testScoped.Id}");
}
}
}
再次启动服务正常,并且请求可以看到,我们CreateScope后,生成的Id也是跟请求中的Scoped不一样的,因为他们属于不同的Scoped。
欢迎进群催更。