上篇介绍了ASP.NET Core内置的依赖注入容器用法,现在对其作一些补充说明。
1.生命周期
1.Transient瞬时服务 是每次从服务容器进行请求时创建的,它的生命周期适合轻量级、 无状态的服务,在请求结束时会释放。是最通用也是最基本的服务类型。
2.Scoped作用域服务 会在每个客户端请求(连接)时创建一次服务实例,在请求结束时会释放有作用域的服务。
3.Singleton单例服务会在首次请求它们时进行创建。来自容器的服务实现的每一个后续请求都使用同一个实例。单例服务的生命周期在程序运行时会一直保留,程序停止时视情况释放,参考以下表格。
代码 | 是否自动释放对象 | 是否可以传参 |
---|---|---|
services.AddSingleton<MyDep>(); services.AddSingleton<IMyDep, MyDep>(); | 是 | 否 |
services.AddSingleton<IMyDep>(sp => new MyDep()); services.AddSingleton<IMyDep>(sp => new MyDep(99)); | 是 | 是 |
services.AddSingleton<IMyDep>(new MyDep()); services.AddSingleton<IMyDep>(new MyDep(99)); services.AddSingleton(new MyDep()); services.AddSingleton(new MyDep(99)); | 否 | 是 |
2.理解服务的运作
1、一个普通的类如果不注册成服务,但需要使用到其他服务的实例时,只能在外部手动将服务实例传入类中。因为服务的实例是由容器创建的,容器无法自动将服务注入到普通的类中。在webapi和mvc项目中已经自动注册了所有控制器,因此能够直接在控制器中获取服务。您可以在Startup内找到这样的代码:
csharp
// 在webapi项目中
services.AddControllers();
// 在mvc项目中
services.AddControllersWithViews();
2、并非所有的类都得注册成服务,哪些类需要注册成服务应该由实际用途决定,比如对StringBuilder和UserModel这种简单的类使用,仍然采用传统的new方式手动创建实例。
3、服务的生命周期由容器管理,所以获取实例后不可通过赋值再去改变实例。幸运的是,编辑器通常会自动将服务设置为只读(readonly),以避免在构造函数外修改它。
3.获取服务的多种途径
通常在构造函数内获取服务,但也有其他情况。
1.在过滤器中获取服务
可通过HttpContext拿到IServiceProvider然后获取服务。注:GetRequiredService获取服务失败会引发异常,方便在开发时规避错误,因此很适合实际开发使用。
csharp
public class MyAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
// using Microsoft.Extensions.DependencyInjection;
var provider = context.HttpContext.RequestServices;
// 从IServiceProvider中获取服务
var service = provider.GetRequiredService<MyService>();
}
}
2.在视图中获取服务
以mvc项目的视图为例,可使用@inject
在视图内获取服务。
csharp
@inject MyService service;
@{
service.DoSomthing();
}
3.在控制器中使用FromServices特性获取服务
FromServicesAttribute可以将服务直接注入到方法内,而无需从构造函数内注入。
csharp
[HttpGet]
public IActionResult Get([FromServices] MyService service)
{
service.DoSomething();
return Ok();
}
4.在中间件内获取服务,可以在构造函数获取,也可在方法上获取,当然也可以像过滤器那样通过context.RequestServices
获取服务。
csharp
// 第1种做法:在构造函数内获取
private readonly RequestDelegate _next;
private readonly MyService service;
public MyMiddleware(RequestDelegate next, MyService service)
{
_next = next;
this.service = service;
}
// =============================
// 第2种做法:在方法上获取
public async Task InvokeAsync(HttpContext context, MyService service)
{
service.DoSomething();
}
// =============================
// 第3种做法:在HttpContext内获取
public async Task InvokeAsync(HttpContext context)
{
var provider = context.RequestServices;
var service = provider.GetRequiredService<NumService>();
}
4.避免过早使用服务
1、建议通常情况下避免直接在构造函数内使用服务,考虑以下代码可能会引发的问题:
csharp
private readonly string name;
public TestController(IMyService myService)
{
name = myService.GetName();
}
在构造函数内过早执行某些初始化操作可能存在的隐患:①部分方法可能不需要执行这段代码,②如果初始化很耗时则会牵连其他无辜的方法,导致整个服务执行效率变差,③如果初始化操作容易引发异常,则访问服务内所有方法都会出现异常,这样的灾难是崩溃性的。
以上只是建议,具体分情况而定。如果所有方法都需要使用这类准备工作也可将其放在构造函数内初始化。
2、某些情况下也可直接接收IServiceProvider
,然后在方法内使用时再获取服务的实例。参考以下用法:
csharp
private readonly IServiceProvider provider;
public MyService(IServiceProvider provider)
{
this.provider = provider;
}
public void Do() {
var testService = provider.GetRequiredService<TestService>();
testService.DoSomething();
}
5.合理封装注册服务
在实际开发中,随着开发进度的变化,应用层需要注册的服务会越来越多。诸如内部服务、EFCore、Http请求、MVC的Session、其他第三方服务等,这些会使得Startup文件变得臃肿并难以维护。
可根据实际情况封装注册服务的代码。比如对EFCore的配置进行封装,使得不同调用者都可直接调用扩展方法完成EFCore的配置;或者将MyService1
和MyService2
的注册封装到一起,确保能使用MyService1
的地方必然会使用到MyService2
。
csharp
public static IServiceCollection AddMyServices(this IServiceCollection services)
{
services.AddTransient<MyService1>();
services.AddTransient<MyService2>();
}
6.谨慎使用单例服务
单例服务必须是线程安全的,并且通常在无状态服务中使用。这意味着单例服务不能依赖作用域服务 ,否则会在运行时出现异常:Cannot consume scoped service 'MyService1' from singleton 'MyService2'
。这种现象不但针对直接使用的情况,也针对间接使用的情况,比如单例服务A->瞬时服务B->作用域服务C,也会引发相同的异常。
如果内容对您有帮助,也可给笔者一点小小的支持。