乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 贯穿ASP.NET Core整个架构的依赖注入框架(Dependency Injection)
为什么需要依赖注入框架
- 借助依赖注入框架,可以轻松管理类之间的依赖,帮助我们在构建应用时遵循设计原则,确保代码的可维护性和可扩展性。
- ASP.NET Core的整个架构中,依赖注入框架提供了对象创建和生命周期管理的核心能力,各个组件相互协作,也是依靠依赖注入框架的能力来实现的。
组件包
它采用接口实现分离模式,其中Microsoft.Extensions.DependencyInjection.Abstractions
是抽象包,Microsoft.Extensions.DependencyInjection
是具体实现。
意味着后续我们可以用第三方包来替代默认实现。
核心类型
IServiceCollection
,负责服务的注册。ServiceDescriptor
,每一个服务注册时的信息。IServiceProvider
,具体的容器,也是由ServiceCollection
Build出来的。IServiceScope
,表示一个容器的子容器的生命周期。
生命周期(Sevice Lifetime)
单例(Singleton)
,指在整个根容器的生命周期内,都是单例,不管你是根容器还是子容器。作用域(Scoped)
,在容器的生存周期内,或者子容器的生存周期内,如果我的容器释放掉,意味着我的对象也会释放掉,在这个范围内我们得到的是个单例模式。瞬时(暂时)Transient
,指我们每一次从容器中获取对象时,都可以得到一个全新的对象。
单例和作用域的区别是,单例是全局的单例,作用域是范围内的单例。
实践理解
创建项目
dotnet new sln -o HelloDependencyInjection
cd .\HelloDependencyInjection\
dotnet new webapi -o demoForDI60 -f net6.0
dotnet sln add .\demoForDI60\demoForDI60.csproj
dotnet new webapi -o demoForDI31 -f netcoreapp3.1
dotnet sln add .\demoForDI31\demoForDI31.csproj
code .
explorer.exe .
准备一些代码
public interface IMyScopedService { }
public class MyScopedService : IMyScopedService
{
}
public interface IMySingletonService { }
public class MySingletonService : IMySingletonService
{
}
public interface IMyTransientService { }
public class MyTransientService : IMyTransientService
{
}
注册不同服务
// demoForDI31\Startup.cs
public void ConfigureServices(IServiceCollection services)
{
#region 注册服务不同生命周期的服务
services.AddSingleton<IMySingletonService, MySingletonService>();
services.AddScoped<IMyScopedService, MyScopedService>();
services.AddTransient<IMyTransientService, MyTransientService>();
#endregion
services.AddControllers();
}
这里将IMySingletonService
组注册成单例模式,将IMyScopedService
组注册成作用域模式,将IMyTransientService
组注册成瞬时模式。
然后在WeatherForecastController.cs
中添加一个检验的方法,新增一个GetServices
接口来打印从容器中读取的六个实例的HashCode值。
[HttpGet("GetServices")]
public int GetServices
(
[FromServices] IMySingletonService singleton1,
[FromServices] IMySingletonService singleton2,
[FromServices] IMyScopedService scoped1,
[FromServices] IMyScopedService scoped2,
[FromServices] IMyTransientService transient1,
[FromServices] IMyTransientService transient2
)
{
Console.WriteLine($"请求开始");
Console.WriteLine($"singleton1:{singleton1.GetHashCode()}");
Console.WriteLine($"singleton2:{singleton2.GetHashCode()}");
Console.WriteLine($"scoped1:{scoped1.GetHashCode()}");
Console.WriteLine($"scoped2:{scoped2.GetHashCode()}");
Console.WriteLine($"transient1:{transient1.GetHashCode()}");
Console.WriteLine($"transient2:{transient2.GetHashCode()}");
Console.WriteLine($"请求结束");
return 1;
}
这里使用了[FromServices]
代表从容器中获取实例。
分析结果
将demoForDI31\Properties\launchSettings.json
中demoForDI31
节点下的launchUrl
改成:weatherforecast/GetServices
。
运行后,我们看下结果。
请求开始
singleton1:687191
singleton2:687191
scoped1:49385318
scoped2:49385318
transient1:13062350
transient2:10366524
请求结束
在运行一次,看下结果
请求开始
singleton1:687191
singleton2:687191
scoped1:26847985
scoped2:26847985
transient1:199777
transient2:8990007
请求结束
从这两组数据可以看出,注册为单例的实例在两次运行中HashCode是相同的,两个瞬时实例得到的HashCode是完全不同的。对作用域实例来说,单次请求得到的HashCode是相同的,但是两次请求的值是不同的。
花式注册
1. 直接注入实例
public static IServiceCollection AddSingleton<TService>(this IServiceCollection services, TService implementationInstance) where TService : class;
services.AddSingleton<IOrderService>(new OrderService());
2. 工厂方式注册
public static IServiceCollection AddSingleton<TService>(this IServiceCollection services, Func<IServiceProvider, TService> implementationFactory) where TService : class;
services.AddSingleton<IOrderService>(serviceProvider =>
{
return new OrderService();
});
services.AddScoped<IOrderService>(serviceProvider =>
{
return new OrderService();
});
services.AddTransient<IOrderService>(serviceProvider =>
{
return new OrderService();
});
3. 尝试注册(接口维度)
尝试注册意思是,注册的接口对应的实现已经注册过了,就不重复注册了。
services.AddSingleton<IOrderService>(new OrderService());
services.TryAddSingleton<IOrderService, OrderServiceEx>();
然后我们新增一个GetServiceList
路由节点来打印从容器中获取到的IOrderService
到底有多少个。
[HttpGet("GetServiceList")]
public int GetServiceList([FromServices] IEnumerable<IOrderService> orderServices)
{
foreach (var orderService in orderServices)
{
Console.WriteLine($"{orderService}:{orderService.GetHashCode()}");
}
return 1;
}
执行结果来看。
demoForDI31.Services.OrderService:38196344
只有一个,说明第二次试图再次注册IOrderService
失败了。
如果注册两个实例会怎么样?
services.AddSingleton<IOrderService>(new OrderService());
services.AddSingleton<IOrderService, OrderServiceEx>();
这时候输出是
demoForDI31.Services.OrderService:31523018
demoForDI31.Services.OrderServiceEx:17375337
那么当IOrderService
存在两个注册实例的时候,默认会取哪一个呢?
[HttpGet("GetService")]
public int GetService([FromServices]IOrderService orderService)
{
Console.WriteLine($"{orderService}:{orderService.GetHashCode()}");
return 1;
}
运行结果是:
demoForDI31.Services.OrderServiceEx:50833863
我们会看到,它默认会取最后注册的那一个,也就是说,同一个服务注册多次,如果取服务实例的时候,默认取的是最后一个。
4. 尝试注册(实现维度)
这里有个特殊的用法,就是通过TryAddEnumerable
方法可以实现,实现不同就可以注册进去,如果实现相同就不注册进去。
services.AddSingleton<IOrderService>(new OrderService());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IOrderService, OrderService>());
得到的结果只有一个,因为后面注册的接口实现类和之前一样,所以注册不进去。
demoForDI31.Services.OrderService:24250448
services.AddSingleton<IOrderService>(new OrderService());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IOrderService, OrderServiceEx>());
得到的结果是
demoForDI31.Services.OrderService:24250448
demoForDI31.Services.OrderServiceEx:40098280
这时候发现,OrderService
和OrderServiceEx
实现不一样,所以两个成功注册进去了。
这样能解决的那种,同一个接口,不希望被注册多次,但是不同实现可以被注册的场景。
替换注册
通过Replace
可以替换掉之前注册的服务为新注册的服务。
services.AddSingleton<IOrderService>(new OrderService());
services.Replace(ServiceDescriptor.Singleton<IOrderService, OrderServiceEx>());
运行结果
demoForDI31.Services.OrderServiceEx:24250448
可以看到前面注册的OrderService
被替换成OrderServiceEx
实例了。
移除注册
services.RemoveAll<IOrderService>();
运行结果
实际为空,也就是没有注册实例可打印了。
注册泛型模板
准备泛型模板代码
public interface IGenericService<T> { }
public class GenericService<T> : IGenericService<T>
{
public T Data { get; private set; }
public GenericService(T data)
{
this.Data = data;
}
}
注册泛型模板
services.AddSingleton(typeof(IGenericService<>), typeof(GenericService<>));
需要用typeof
来注册。
使用泛型模板
public WeatherForecastController(ILogger<WeatherForecastController> logger, IGenericService<IOrderService> genericService)
{
_logger = logger;
}
运行之后,我们可以看到genericService
的Data是demoForDI31.Services.OrderService
。
依赖注入实例的两种方法
1. 通过构造函数方式
public WeatherForecastController(ILogger<WeatherForecastController> logger, IOrderService orderService)
{
_logger = logger;
}
适合场景:当我们定义一个Controller的时候,它的服务是大部分接口都需要使用的情况下,推荐从构造函数方式来注入。
2. 使用FromServices方式
[HttpGet("GetService")]
public int GetService([FromServices]IOrderService orderService)
{
Console.WriteLine($"{orderService}:{orderService.GetHashCode()}");
return 1;
}
适合场景:当我们一个服务仅仅是在某一个接口场景下使用,推荐使用FromServices的方式来注入。
实现IDisposable接口类型的释放
基本原则
- DependencyInjection只负责释放由其创建的对象实例。
- DependencyInjection在容器或者子容器释放时,才会去释放由其创建的对象实例。
建议
- 避免在根容器创建实现了IDisposable接口的瞬时服务。
- 避免手动创建对象然后塞到容器里面,而应该尽量使用容器来管理对象的创建和释放。
实践理解
准备代码
public interface IOrderService { }
public class DisposableOrderService : IOrderService, IDisposable
{
public void Dispose()
{
Console.WriteLine($"DisposableOrderService Disposed:{this.GetHashCode()}");
}
}
验证释放(瞬时模式)
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IOrderService, DisposableOrderService>();
services.AddControllers();
}
[HttpGet]
public int Get([FromServices] IOrderService orderService1, [FromServices] IOrderService orderService2)
{
Console.WriteLine($"接口请求处理结束");
return 1;
}
执行结果是
接口请求处理结束
DisposableOrderService Disposed:15401461
DisposableOrderService Disposed:25712189
可见这两个实例对象是在整个请求结束之后,才会去触发释放。
但是仍然有一种情况很危险,那就是实现了IDisposable
的瞬时服务,如果是在根容器去获取和创建,那么它会一直保持到整个程序生命周期结束才会被释放。
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IOrderService, DisposableOrderService>();
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.ApplicationServices.GetService<IOrderService>();
}
这里定义了瞬时的IOrderService
服务,但是接下来在根容器中获取并创建了它。
[HttpGet]
public int Get
(
[FromServices] IHostApplicationLifetime hostApplicationLifetime,
[FromQuery] bool stop = false
)
{
Console.WriteLine($"接口请求处理结束");
if (stop)
{
hostApplicationLifetime.StopApplication();
}
return 1;
}
正常运行结果:
接口请求处理结束
没有之前的释放信息。
传入关闭动作:
接口请求处理结束
info: Microsoft.Hosting.Lifetime[0]
Application is shutting down...
DisposableOrderService Disposed:20074041
这时候这个瞬时服务实例才被真正释放掉。
验证释放(作用域模式)
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IOrderService, DisposableOrderService>();
services.AddControllers();
}
[HttpGet]
public int Get([FromServices] IOrderService orderService1, [FromServices] IOrderService orderService2)
{
Console.WriteLine($"子容器开始");
using (IServiceScope scope = HttpContext.RequestServices.CreateScope())
{
var service1 = scope.ServiceProvider.GetService<IOrderService>();
var service2 = scope.ServiceProvider.GetService<IOrderService>();
}
Console.WriteLine($"子容器结束");
Console.WriteLine($"接口请求处理结束");
return 1;
}
我们来看看结果
子容器开始
DisposableOrderService Disposed:20281500
子容器结束
接口请求处理结束
DisposableOrderService Disposed:54516368
我们发现,子容器中那个作用域对象是在子容器生命周期结束时就释放了,而外面那个作用域是在整个请求结束后释放的。
验证释放(单例模式)
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IOrderService, DisposableOrderService>();
services.AddControllers();
}
[HttpGet]
public int Get([FromServices] IOrderService orderService1, [FromServices] IOrderService orderService2)
{
Console.WriteLine($"接口请求处理结束");
return 1;
}
接口请求处理结束
可以看到整个接口请求结束之后,这个单例的实例还没有被释放,那么单例实例会在什么时候释放呢?
[HttpGet]
public int Get
(
[FromServices] IOrderService orderService1,
[FromServices] IOrderService orderService2,
[FromServices] IHostApplicationLifetime hostApplicationLifetime,
[FromQuery] bool stop = false
)
{
Console.WriteLine($"接口请求处理结束");
if (stop)
{
hostApplicationLifetime.StopApplication();
}
return 1;
}
接口请求处理结束
info: Microsoft.Hosting.Lifetime[0]
Application is shutting down...
DisposableOrderService Disposed:48950176
这里我们看到,当使用IHostApplicationLifetime
接口中StopApplication
方法,强行停止整个程序的时候,这时候这个单例实例终于被释放了。
但是单例模式下,如果我们手动创建实例对象
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IOrderService>(new DisposableOrderService());
services.AddControllers();
}
即使是手动关闭程序,你会发现结果是
接口请求处理结束
info: Microsoft.Hosting.Lifetime[0]
Application is shutting down...
这种手动创建的单例服务对象,并不会在根容器生命周期结束的时候被释放。