.Net Core DI 使用注意事项
1.一个接口多个Service实现
builder.Services.AddTransient<Service1>(); builder.Services.AddTransient<Service2>(); builder.Services.AddTransient(serviceProvider => { Func<Type, IService> accesor = key => { if (key == typeof(Service1)) return serviceProvider.GetService<Service1>(); else if (key == typeof(Service2)) return serviceProvider.GetService<Service2>(); else throw new ArgumentException($"不支持的DI Key: {key}"); }; return accesor; });
调用:
public class ValuesController : ControllerBase { readonly IService _Service1; readonly IService _Service2; public ValuesController(Func<Type, IService> func) { _Service1 = func(typeof(Service1)); _Service2 = func(typeof(Service2)); } xxx... }
知识点:
当一个interface被多个Service实现时,在正常调用时内存会load全部的Service实现类(如果Service过多,则会占用大量的内存,常见的以CAP Event通信为主),
如果不指定则默认为最后一个注入的Service(管道:request进入是有顺序的),这种情况往往所以我们通过以上方式来定向获取,还有一种比较普遍的的用法,
Service Locator模式,这个下文介绍,这是一种设计模式,根本不是"依赖注入"。
上面这段话,我们会发现一个问题,为什么在调用时需要加载全部的实现类呢,通常我们进行构造函数注入时往往要求声明的是interface类型,这其实不是一种硬性要求,这是一种设计思想,降低程序的耦合度,在使用处实例化!!! 然后 规则?规则是用来打破滴~
如果在构造函数中直接注入的是实现类,就可以避免加载全部的service导致内存溢出的问题了,当然,这又增加了代码的耦合度,
在实际的开发中,很多时候都是需要做一个取舍,但最好还是拆分Service功能,在根源处降低耦合。
2.一个Service具有多个构造函数
一个Service有多个构造函数,虽然它们的参数均能够由ServiceProvider来提供,但是并没有一个构造函数的参数类型集合能够成为所有有效构造函数参数类型集合的超集,所以ServiceProvider无法选择出一个最佳的构造函数。如果我们运行这个程序,一个System.AggregateException异常会被抛出来,控制台上将呈现出如下所示的错误消息。
InvalidOperationException: Unable to activate type 'DIDemo.Services.Service.MethodA'. The following constructors are ambiguous:
不同的DI容器可能有不同的策略,因为只接触过.net Core,所以不懂的俺不能乱说,在网上查 可以在构造函数上标注一个InjectionAttribute特性,来指定构造函数,
1: public class Foo 2: { 3: public IBar Bar{get; private set;} 4: public IBaz Baz {get; private set;} 5: 6: [Injection] 7: public Foo(IBar bar) 8: { 9: this.Bar = bar; 10: } 11: 12: public Foo(IBar bar, IBaz):this(bar) 13: { 14: this.Baz = baz; 15: } 16: }
类似这种,这个Inject是需要根据业务自己实现的!
.net core默认的则是,需要有一个是所有构造函数参数合集的这么一个构造函数!
3.Service Locator模式
上文我们提到过,这哥们根本就不是"依赖注入",那么怎么来区分呢,以下来自 互联网,,,可以直接跳到后面
从使用者的角度,在一个采用依赖注入的应用中,我们只需要采用标准的注入形式将服务类型定义好,并在应用启动之前完成相应的服务注册就可以了,框架自身的引擎在运行过程中会利用依赖注入容器来提供当前所需的服务实例。换句话说,依赖注入容器的使用者应该是框架而不是应用程序。Service Locator模式显然不是这样,很明显是应用程序在利用它来提供所需的服务实例,所以它的使用者是应用程序。
本着“松耦合、高内聚”的设计原则,我们既然将一组相关的操作定义在一个能够复用的服务中,就应该尽量要求服务自身不但具有独立和自治的特性,也要求服务之间的应该具有明确的界限,服务之间的依赖关系应该是明确的而不是模糊的。不论是采用属性注入或者方法注入,还是使用Service Locator来提供当前依赖的服务,这无疑为当前的服务增添了一个新的依赖,即针对依赖注入容器或者Service Locator的依赖。
当前服务针对另一个服务的依赖与针对依赖注入容器或者Service Locator的依赖具有本质的不同,前者是一种基于类型的依赖,不论是基于服务的接口还是实现类型,这是一种基于“契约”的依赖。这种依赖不仅是明确的,也是有保障的。但是依赖注入容器或者Service Locator本质上是一个黑盒,它能够提供所需服务的前提是相应的服务注册已经预先添加了容器之中,但是这种依赖不仅是模糊的也是不可靠的。
我自己的理解是,DI是将所需Service推送给程序,而Service Locator则是在使用时自己拉取Service,前者可以确定有且明确,不然没东西推啊,后者,不清楚拉的东西是不是存在的,所以在会用到模糊和不可靠来形容,不说废话,直接上代码。
public class ServiceLocator { public static IServiceProvider Instance { get; set; } }
定义一个全局变量,然后在program.cs的注册Service的最后!最后!最后!赋值,如不在最后,可能会有遗漏的Service没有被添加到ServiceProvider,使用的时候获取不到。
ServiceLocator.Instance = builder.Services.BuildServiceProvider();
...
//在使用处拉去Service即可。
ServiceLocator.Instance.GetService<IMethodA>().A();
如果是Controller中,还可以使用HttpContext获取,HttpContext只能在Controller层获取,所以局限还挺大的。
HttpContext.RequestServices.GetService<T>();
细心的小伙伴们,可能会发现一个问题,我定义的全局变量是个静态字段,好处从名字可见,而缺点会是什么呢?问的好~
as you know,每次请求都会经过管道,为该字段赋值,且只有一次,当遭遇多线程时,生命周期为Transient(即用即销)的Service,将会被固定使用一个,线程安全问题显而易见,不定时的会出现实例被Dispose掉了,于是我们要剔除Static,那就只剩下ServiceProvider了。
_serviceProvider.GetService<IMethodA>().A(); var services = _serviceProvider.GetServices<IMethodA>();
这是思考的过程哈,如果使用,请直接使用IServciePovider来拉取,注入方式和其他正常Service一样!
private readonly ILogger<HomeController> _logger; private readonly IMethodA _methodA; private readonly IService _Service1; private readonly IService _Service2; private readonly IServiceProvider _serviceProvider; public HomeController(ILogger<HomeController> logger, IMethodA methodA, Func<Type, IService> func, IServiceProvider serviceProvider) { _logger = logger; _methodA = methodA; _Service1 = func(typeof(Service1)); _Service2 = func(typeof(Service2)); _serviceProvider = serviceProvider; }
最后我们介绍一下ServiceProvider与ServiceDescriptor,解释一下上文出现的各种现象。
ASP.NET Core中的DI容器最终体现为一个IServiceProvider接口,我们将所有实现了该接口的类型及其实例统称为ServiceProvider。如下面的代码片段所示,该接口简单至极,它仅仅提供了唯一个GetService方法,该方法根据提供的服务类型为你提供对应的服务实例。
public interface IServiceProvider { object GetService(Type serviceType); }
ASP.NET Core内部真正使用的是一个实现了IServiceProvider接口的内部类型(该类型的名称为“ServiceProvider”),我们不能直接创建该对象,只能间接地通过调用IServiceCollection接口的扩展方法BuildServiceProvider得到它。IServiceCollection接口定义在“Microsoft.Extensions.DependencyInjection”命名空间下。 如下面的代码片段所示,IServiceCollection接口实际上代表一个元素为ServiceDescriptor对象的集合,它直接继承了另一个接口IList<ServiceDescriptor>,而ServiceCollection类实现了该接口。
public static class ServiceCollectionExtensions { public static IServiceProvider BuildServiceProvider(this IServiceCollection services); } public interface IServiceCollection : IList<ServiceDescriptor> {} Public class ServiceCollection: IServiceCollection { //省略成员 }
体现为DI容器的ServiceProvider之所以能够根据我们给定的服务类型(一般是一个接口类型)提供一个能够开箱即用的服务实例,是因为我们预先注册了相应的服务描述信息,这些指导ServiceProvider正确实施服务提供操作的服务描述体现为如下一个ServiceDescriptor类型。
public class ServiceDescriptor { public ServiceDescriptor(Type serviceType, object instance); public ServiceDescriptor(Type serviceType, Func<IServiceProvider, object> factory, ServiceLifetime lifetime); public ServiceDescriptor(Type serviceType, Type implementationType, ServiceLifetime lifetime); public Type ServiceType { get; } public ServiceLifetime Lifetime { get; } public Type ImplementationType { get; } public object ImplementationInstance { get; } public Func<IServiceProvider, object> ImplementationFactory { get; } }
ServiceDescriptor的ServiceType属性代表提供服务的生命类型,由于标准化的服务一般会定义成接口,所以在绝大部分情况下体现为一个接口类型。类型为ServiceLifetime的属性Lifetime体现了ServiceProvider针对服务实例生命周期的控制方式。如下面的代码片段所示,ServiceLifetime是一个枚举类型,定义其中的三个选项(Singleton、Scoped和Transient)体现三种对服务对象生命周期的控制形式,这个我们就As you know了。
对于ServiceDescriptor的其他三个属性来说,它们实际上是辅助ServiceProvider完成具体的服务实例提供操作。ImplementationType属性代表被提供服务实例的真实类型,属性ImplementationInstance则直接代表被提供的服务实例,ImplementationFactory则提供了一个创建服务实例的委托对象。
由于ASP.NET Core中的ServiceProvider是根据一个代表ServiceDescriptor集合的IServiceCollection对象创建的,当我们调用其GetService方法的时候,它会根据我们提供的服务类型找到对应的ServiceDecriptor对象。如果该ServiceDecriptor对象的ImplementationInstance属性返回一个具体的对象,该对象将直接用作被提供的服务实例。如果ServiceDecriptor对象的ImplementationFactory返回一个具体的委托,该委托对象将直接用作创建服务实例的工厂。
如果这两个属性均为Null,ServiceProvider才会根据ImplementationType属性返回的类型调用相应的构造函数创建被提供的服务实例。至于我们在上面一节中提到的三种依赖注入方式,ServiceProvider仅仅支持构造器注入,属性注入和方法注入的支持并未提供。
代码已同步到GitHub:https://github.com/Seth-Song/DIDemo