全面理解 ASP.NET Core 依赖注入 (转载)
DI在.NET Core里面被提到了一个非常重要的位置, 这篇文章主要再给大家普及一下关于依赖注入的概念,身边有工作六七年的同事还个东西搞不清楚。另外再介绍一下.NET Core的DI实现以及对实例生命周期的管理(这个是经常面试会问到的问题)。最后再给大家简单介绍一下在控制台以及Mvc下如何使用DI,以及如何把默认的Service Container 替换成Autofac。
一、什么是依赖注入
1.1 依赖
1.2 什么注入
1.3 为什么反转
1.4 何为容器
二、.NET Core DI
2.1 实例的注册
2.2 实例生命周期之单例
2.3 实例生命周期之Tranisent
2.4 实例生命周期之Scoped
三、DI在ASP.NET Core中的应用
3.1 在Startup类中初始化
3.2 Controller中使用
3.3 View中使用
3.4 通过HttpContext来获取
四、如何替换其它的Ioc容器
五、避免循环注入
一、什么是依赖注入(Denpendency Injection)
这也是个老身常谈的问题,到底依赖注入是什么? 为什么要用它? 初学者特别容易对控制反转IOC(Iversion of Control),DI等概念搞晕。
1.1依赖
当一个类需要另一个类协作来完成工作的时候就产生了依赖。比如我们在AccountController这个控制器需要完成和用户相关的注册、登录 等事情。其中的登录我们由EF结合Idnetity来完成,所以我们封装了一个EFLoginService。这里AccountController就有一个ILoginService的依赖。
这里有一个设计原则:依赖于抽象,而不是具体的实现。所以我们给EFLoginService定义了一个接口,抽象了LoginService的行为。
1.2 什么是注入
private ILoginService<ApplicationUser> _loginService; public AccountController() { _loginService = new EFLoginService() }
大师说,这样不好。你不应该自己创建它,而是应该由你的调用者给你。于是你通过构造函数让外界把这两个依赖传给你。
public AccountController(ILoginService<ApplicationUser> loginService) { _loginService = loginService; }
把依赖的创建丢给其它人,自己只负责使用,其它人丢给你依赖的这个过程理解为注入。
1.3 为什么要反转?
public AccountController(ILoginService<ApplicationUser> loginService) { _loginService = loginService; }
// 用Redis来替换原来的EF登录 var controller = new AccountController(new RedisLoginService()); controller.Login(userName, password);
1.4 何为容器
- 绑定服务与实例之间的关系
- 获取实例,并对实例进行管理(创建与销毁)
- IServiceCollection 负责注册
- IServiceProvider 负责提供实例
var serviceCollection = new ServiceCollection() .AddTransient<ILoginService, EFLoginService>() .AddSingleton<ILoginService, EFLoginService>() .AddScoped<ILoginService, EFLoginService>();
public interface IServiceCollection : IList<ServiceDescriptor> { }
我们上面的AddTransient、AddSignletone和Scoped方法是IServiceCollection的扩展方法, 都是往这个List里面添加ServiceDescriptor。
private static IServiceCollection Add( IServiceCollection collection, Type serviceType, Type implementationType, ServiceLifetime lifetime) { var descriptor = new ServiceDescriptor(serviceType, implementationType, lifetime); collection.Add(descriptor); return collection; }
2.2 实例的生命周期之单例
- Transient: 每一次GetService都会创建一个新的实例
- Scoped: 在同一个Scope内只初始化一个实例 ,可以理解为( 每一个request级别只创建一个实例,同一个http request会在一个 scope内。实际上ASP.NET Core会针对每个http request在pipeline中自动创建一个Scope,所以单个http request中执行的ASP.NET Core代码其实都是在一个Scope中的)
- Singleton :整个应用程序生命周期以内只创建一个实例
public enum ServiceLifetime { Singleton, Scoped, Transient }
public interface IOperation { Guid OperationId { get; } } public interface IOperationSingleton : IOperation { } public interface IOperationTransient : IOperation{} public interface IOperationScoped : IOperation{}
我们的 Operation实现很简单,可以在构造函数中传入一个Guid进行赋值,如果没有的话则自已New一个 Guid。
public class Operation : IOperationSingleton, IOperationTransient, IOperationScoped { private Guid _guid; public Operation() { _guid = Guid.NewGuid(); } public Operation(Guid guid) { _guid = guid; } public Guid OperationId => _guid; }
在程序内我们可以多次调用ServiceProvider的GetService方法,获取到的都是同一个实例。
var services = new ServiceCollection(); // 默认构造 services.AddSingleton<IOperationSingleton, Operation>(); // 自定义传入Guid空值 services.AddSingleton<IOperationSingleton>( new Operation(Guid.Empty)); // 自定义传入一个New的Guid services.AddSingleton <IOperationSingleton>( new Operation(Guid.NewGuid())); var provider = services.BuildServiceProvider(); // 输出singletone1的Guid var singletone1 = provider.GetService<IOperationSingleton>(); Console.WriteLine($"signletone1: {singletone1.OperationId}"); // 输出singletone2的Guid var singletone2 = provider.GetService<IOperationSingleton>(); Console.WriteLine($"signletone2: {singletone2.OperationId}"); Console.WriteLine($"singletone1 == singletone2 ? : { singletone1 == singletone2 }");
我们对IOperationSingleton注册了三次,最后获取两次,大家要注意到我们获取到的始终都是我们最后一次注册的那个给了一个Guid的实例,前面的会被覆盖。
var services = new ServiceCollection() .AddScoped<IOperationScoped, Operation>() .AddTransient<IOperationTransient, Operation>() .AddSingleton<IOperationSingleton, Operation>();
接下来我们用ServiceProvider.CreateScope方法创建一个Scope
var provider = services.BuildServiceProvider(); using (var scope1 = provider.CreateScope()) { var p = scope1.ServiceProvider; var scopeobj1 = p.GetService<IOperationScoped>(); var transient1 = p.GetService<IOperationTransient>(); var singleton1 = p.GetService<IOperationSingleton>(); var scopeobj2 = p.GetService<IOperationScoped>(); var transient2 = p.GetService<IOperationTransient>(); var singleton2 = p.GetService<IOperationSingleton>(); Console.WriteLine( $"scope1: { scopeobj1.OperationId }," + $"transient1: {transient1.OperationId}, " + $"singleton1: {singleton1.OperationId}"); Console.WriteLine($"scope2: { scopeobj2.OperationId }, " + $"transient2: {transient2.OperationId}, " + $"singleton2: {singleton2.OperationId}"); }
接下来,看看结果:
scope1: 200d1e63-d024-4cd3-88c9-35fdf5c00956,
transient1: fb35f570-713e-43fc-854c-972eed2fae52,
singleton1: da6cf60f-670a-4a86-8fd6-01b635f74225
scope2: 200d1e63-d024-4cd3-88c9-35fdf5c00956,
transient2: 2766a1ee-766f-4116-8a48-3e569de54259,
singleton2: da6cf60f-670a-4a86-8fd6-01b635f74225
如果再创建一个新的Scope运行:
scope1: 29f127a7-baf5-4ab0-b264-fcced11d0729,
transient1: 035d8bfc-c516-44a7-94a5-3720bd39ce57,
singleton1: da6cf60f-670a-4a86-8fd6-01b635f74225
scope2: 29f127a7-baf5-4ab0-b264-fcced11d0729,
transient2: 74c37151-6497-4223-b558-a4ffc1897d57,
singleton2: da6cf60f-670a-4a86-8fd6-01b635f74225
大家注意到上面我们一共得到了 4个Transient实例,2个Scope实例,1个Singleton实例。
这有什么用?
var services = new ServiceCollection(); services.AddScoped<IOperationScoped, Operation>(); var provider = services.BuildServiceProvider(); IOperationScoped scopeobj0 = null; IOperationScoped scopeobj1 = null; IOperationScoped scopeobj2 = null; using (var scope0 = provider.CreateScope()) { var p0 = scope0.ServiceProvider; scopeobj0 = p0.GetService<IOperationScoped>(); using (var scope1 = provider.CreateScope()) { var p1 = scope1.ServiceProvider; scopeobj1 = p1.GetService<IOperationScoped>(); } using (var scope2 = provider.CreateScope()) { var p2 = scope2.ServiceProvider; scopeobj2 = p2.GetService<IOperationScoped>(); } } bool comparison = scopeobj0 == scopeobj1; Console.WriteLine($"scopeobj0==scopeobj1?{comparison.ToString()}");//scopeobj0==scopeobj1?False comparison = scopeobj0 == scopeobj2; Console.WriteLine($"scopeobj0==scopeobj2?{comparison.ToString()}");//scopeobj0==scopeobj2?False comparison = scopeobj1 == scopeobj2; Console.WriteLine($"scopeobj1==scopeobj2?{comparison.ToString()}");//scopeobj1==scopeobj2?False Console.WriteLine("Press key to end..."); Console.ReadKey();
可以看到尽管scope0与scope1、scope2之间是嵌套关系,但是它们各自创建的实例scopeobj0、scopeobj1、scopeobj2都不相等,比较结果都为false。所以scope0、scope1、scope2之间实际上是彼此独立的。
public void ConfigureServices(IServiceCollection services) { services.AddTransient<ILoginService<ApplicationUser>, EFLoginService>(); services.AddMvc(); )
ASP.NET Core的一些组件已经提供了一些实例的绑定,像AddMvc就是Mvc Middleware在 IServiceCollection上添加的扩展方法。
public static IMvcBuilder AddMvc(this IServiceCollection services) { if (services == null) { throw new ArgumentNullException(nameof(services)); } var builder = services.AddMvcCore(); builder.AddApiExplorer(); builder.AddAuthorization(); AddDefaultFrameworkParts(builder.PartManager); ... }
因为在ASP.NET Core 6.0(.NET 6)中,没有Startup.cs文件了,Startup.cs文件和Program.cs文件合并了,所以可以按照下面的代码,在Program.cs文件中来配置依赖注入关系和获取依赖注入对象:
using UTS.Web.Models; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllersWithViews(); //builder.Services就是ASP.NET Core的全局IServiceCollection对象,可以配置IPerson和Person的依赖注入关系 builder.Services.AddTransient<IPerson, Person>(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); } //可以在Program.cs文件中,通过app.Services来获取到ASP.NET Core的全局IServiceProvider对象,从而可以获取到依赖注入的所有注入对象,例如下面我们就通过依赖注入接口IPerson,获取到了Person对象 IPerson? person = app.Services.GetService<IPerson>(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); app.Run();
3.2 Controller中使用
一般可以通过构造函数或者属性来实现注入(注意这里说的不准确,实际上.NET Core内置的DI包Microsoft.Extensions.DependencyInjection,现在不支持属性注入,只支持构造函数注入,不支持属性注入的原因请点击这里),但是官方推荐是通过构造函数。这也是所谓的显式依赖。
private ILoginService<ApplicationUser> _loginService; public AccountController(ILoginService<ApplicationUser> loginService) { _loginService = loginService; }
我们只要在控制器的构造函数里面写了这个参数,ServiceProvider就会帮我们注入进来。这一步是在Mvc初始化控制器的时候完成的,我们后面再介绍到Mvc的时候会往细里讲。
3.3 View中使用
在View中需要用@inject 再声明一下,起一个别名。
@using MilkStone.Services; @model MilkStone.Models.AccountViewModel.LoginViewModel @inject ILoginService<ApplicationUser> loginService <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head></head> <body> @loginService.GetUserName() </body> </html>
3.4 通过 HttpContext来获取实例
HttpContext下有一个RequestedService同样可以用来获取实例对象,不过这种方法一般不推荐。同时要注意GetService<>这是个范型方法,默认如果没有添加Microsoft.Extension.DependencyInjection的using,是不用调用这个方法的。
HttpContext.RequestServices.GetService<ILoginService<ApplicationUser>>();
四、如何替换其它的Ioc容器
Autofac也是不错的选择,但我们首先要搞清楚为什么要替换掉默认的 DI容器?,替换之后有什么影响?.NET Core默认的实现对于一些小型的项目完全够用,甚至大型项目麻烦点也能用,但是会有些麻烦,原因在于只提供了最基本的AddXXXX方法来绑定实例关系,需要一个一个的添加。如果项目可能要添加好几百行这样的方法。
如果熟悉Autofac的同学可能会这下面这样的代码有映象。
lder.RegisterGeneric(typeof(LoggingBehavior<,>)).As(typeof(IPipelineBehavior<,>)); builder.RegisterGeneric(typeof(ValidatorBehavior<,>)).As(typeof(IPipelineBehavior<,>));
这会给我们的初始化带来一些便利性,我们来看看如何替换Autofac到ASP.NET Core。我们只需要把Startup类里面的 ConfigureService的 返回值从 void改为 IServiceProvider即可。而返回的则是一个AutoServiceProvider。
public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddMvc(); // Add other framework services // Add Autofac var containerBuilder = new ContainerBuilder(); containerBuilder.RegisterModule<DefaultModule>(); containerBuilder.Populate(services); var container = containerBuilder.Build(); return new AutofacServiceProvider(container); }
4.1 有何变化
其中很大的一个变化在于,Autofac 原来的一个生命周期InstancePerRequest,将不再有效。正如我们前面所说的,整个request的生命周期被ASP.NET Core管理了,所以Autofac的这个将不再有效。我们可以使用 InstancePerLifetimeScope ,同样是有用的,对应了我们ASP.NET Core DI 里面的Scoped。
五、避免循环注入
新建一个.NET Core控制台项目,如下代码会造成.NET Core的构造函数循环注入:
using Microsoft.Extensions.DependencyInjection; using System; namespace NetCoreDependencyInjection { public interface IMyInterface { } public class MyClass : IMyInterface { public IMyInterface myInterface; /// <summary> /// MyClass类的构造函数,通过接口IMyInterface,又注入MyClass类,相当于自己要注入自己,造成循环注入 /// </summary> public MyClass(IMyInterface myInterface) { this.myInterface = myInterface; } } class Program { static void Main(string[] args) { var serviceCollection = new ServiceCollection(); serviceCollection.AddTransient<IMyInterface, MyClass>(); IMyInterface myInterface = serviceCollection.BuildServiceProvider().GetService<IMyInterface>();//这里会抛出循环注入异常 Console.WriteLine("Press key to quit..."); Console.ReadKey(); } } }
可以看到由于MyClass类实现了IMyInterface接口,而MyClass类的构造函数又依赖于IMyInterface接口,所以DI每次调用MyClass类的构造函数时又要构造一个MyClass类,造成无限循环构造,循环注入。
运行上面的代码会抛出循环注入异常,如下所示:
所以要避免这种循环依赖注入,造成不必要的异常。
作者:Jesse 出处: http://jesse2013.cnblogs.com/ 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。