最近在把本身的一个老项目从Framework迁移到.Net Core 3.0,数据访问这块选择的是EFCore+Mysql。使用EF的话不可避免要和DbContext打交道,在Core中的常规用法通常是:建立一个XXXContext类继承自DbContext,实现一个拥有DbContextOptions参数的构造器,在启动类StartUp中的ConfigureServices方法里调用IServiceCollection的扩展方法AddDbContext,把上下文注入到DI容器中,而后在使用的地方经过构造函数的参数获取实例。OK,没任何毛病,官方示例也都是这么来用的。可是,经过构造函数这种方式来获取上下文实例其实很不方便,好比在Attribute或者静态类中,又或者是系统启动时初始化一些数据,更多的是以下一种场景:html
public class BaseController : Controller { public BloggingContext _dbContext; public BaseController(BloggingContext dbContext) { _dbContext = dbContext; } public bool BlogExist(int id) { return _dbContext.Blogs.Any(x => x.BlogId == id); } } public class BlogsController : BaseController { public BlogsController(BloggingContext dbContext) : base(dbContext) { } }
从上面的代码能够看到,任何要继承BaseController的类都要写一个“多余”的构造函数,若是参数再多几个,这将是没法忍受的(就算只有一个参数我也忍受不了)。那么怎样才能更优雅的获取数据库上下文实例呢,我想到如下几种办法。git
一、 直接开溜new github
回归原始,既然要建立实例,没有比直接new一个更好的办法了,在Framework中没有DI的时候也差很少都这么干。但在EFCore中不一样的是,DbContext再也不提供无参构造函数,取而代之的是必须传入一个DbContextOptions类型的参数,这个参数一般是作一些上下文选项配置例如使用什么类型数据库链接字符串是多少。sql
public BloggingContext(DbContextOptions<BloggingContext> options) : base(options) { }
默认状况下,咱们已经在StartUp中注册上下文的时候作了配置,DI容器会自动帮咱们把options传进来。若是要手动new一个上下文,那岂不是每次都要本身传?不行,这太痛苦了。那有没有办法不传这个参数?确定也是有的。咱们能够去掉有参构造函数,而后重写DbContext中的OnConfiguring方法,在这个方法中作数据库配置: 数据库
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlite("Filename=./efcoredemo.db"); }
即便是这样,依然有不够优雅的地方,那就是链接字符串被硬编码在代码中,不能作到从配置文件读取。反正我忍受不了,只能再寻找其余方案。mvc
二、 从DI容器手动获取app
既然前面已经在启动类中注册了上下文,那么从DI容器中获取实例确定是没问题的。因而我写了这样一句测试代码用来验证猜测:框架
var context = app.ApplicationServices.GetService<BloggingContext>();
不过很遗憾抛出了异常:ide
报错信息说的很明确,不能从root provider中获取这个服务。我从G站下载了DI框架的源码(地址是https://github.com/aspnet/Extensions/tree/master/src/DependencyInjection),拿报错信息进行反向追溯,发现异常来自于CallSiteValidator类的ValidateResolution方法:函数
public void ValidateResolution(Type serviceType, IServiceScope scope, IServiceScope rootScope) { if (ReferenceEquals(scope, rootScope) && _scopedServices.TryGetValue(serviceType, out var scopedService)) { if (serviceType == scopedService) { throw new InvalidOperationException( Resources.FormatDirectScopedResolvedFromRootException(serviceType, nameof(ServiceLifetime.Scoped).ToLowerInvariant())); } throw new InvalidOperationException( Resources.FormatScopedResolvedFromRootException( serviceType, scopedService, nameof(ServiceLifetime.Scoped).ToLowerInvariant())); } }
继续往上,看到了GetService方法的实现:
internal object GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope) { if (_disposed) { ThrowHelper.ThrowObjectDisposedException(); } var realizedService = RealizedServices.GetOrAdd(serviceType, _createServiceAccessor); _callback?.OnResolve(serviceType, serviceProviderEngineScope); DependencyInjectionEventSource.Log.ServiceResolved(serviceType); return realizedService.Invoke(serviceProviderEngineScope); }
能够看到,_callback在为空的状况下是不会作验证的,因而猜测有参数能对它进行配置。把追溯对象换成_callback继续往上翻,在DI框架的核心类ServiceProvider中找到以下方法:
internal ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors, ServiceProviderOptions options) { IServiceProviderEngineCallback callback = null; if (options.ValidateScopes) { callback = this; _callSiteValidator = new CallSiteValidator(); } //省略.... }
说明个人猜测没错,验证是受ValidateScopes控制的。这样来看,把ValidateScopes设置成False就能够解决了,这也是网上广泛的解决方案:
.UseDefaultServiceProvider(options => { options.ValidateScopes = false; })
但这样作是极其危险的。
为何危险?到底什么是root provider?那就要从原生DI的生命周期提及。咱们知道,DI容器被封装成一个IServiceProvider对象,服务都是从这里来获取。不过这并非一个单一对象,它是具备层级结构的,最顶层的即前面提到的root provider,能够理解为仅属于系统层面的DI控制中心。在Asp.Net Core中,内置的DI有3种服务模式,分别是Singleton、Transient、Scoped,Singleton服务实例是保存在root provider中的,因此它才能作到全局单例。相对应的Scoped,是保存在某一个provider中的,它能保证在这个provider中是单例的,而Transient服务则是随时须要随时建立,用完就丢弃。由此可知,除非是在root provider中获取一个单例服务,不然必需要指定一个服务范围(Scope),这个验证是经过ServiceProviderOptions的ValidateScopes来控制的。默认状况下,Asp.Net Core框架在建立HostBuilder的时候会断定当前是否开发环境,在开发环境下会开启这个验证:
因此前面那种关闭验证的方式是错误的。这是由于,root provider只有一个,若是刚好有某个singleton服务引用了一个scope服务,这会致使这个scope服务也变成singleton,仔细看一下注册DbContext的扩展方法,它实际上提供的是scope服务:
若是发生这种状况,数据库链接会一直得不到释放,至于有什么后果你们应该都明白。
因此前面的测试代码应该这样写:
using (var serviceScope = app.ApplicationServices.CreateScope()) { var context = serviceScope.ServiceProvider.GetService<BloggingContext>(); }
与之相关的还有一个ValidateOnBuild属性,也就是说在构建IServiceProvider的时候就会作验证,从源码中也能体现出来:
if (options.ValidateOnBuild) { List<Exception> exceptions = null; foreach (var serviceDescriptor in serviceDescriptors) { try { _engine.ValidateService(serviceDescriptor); } catch (Exception e) { exceptions = exceptions ?? new List<Exception>(); exceptions.Add(e); } } if (exceptions != null) { throw new AggregateException("Some services are not able to be constructed", exceptions.ToArray()); } }
正由于如此,Asp.Net Core在设计的时候为每一个请求建立独立的Scope,这个Scope的provider被封装在HttpContext.RequestServices中。
[小插曲]
经过代码提示能够看到,IServiceProvider提供了2种获取service的方式:
这2个有什么区别呢?分别查看各自的方法摘要能够看到,经过GetService获取一个没有注册的服务时会返回null,而GetRequiredService会抛出一个InvalidOperationException,仅此而已。
// 返回结果: // A service object of type T or null if there is no such service. public static T GetService<T>(this IServiceProvider provider); // 返回结果: // A service object of type T. // // 异常: // T:System.InvalidOperationException: // There is no service of type T. public static T GetRequiredService<T>(this IServiceProvider provider);
到如今为止,尽管找到了一种看起来合理的方案,但仍是不够优雅,使用过其余第三方DI框架的朋友应该知道,属性注入的快感无可比拟。那原生DI有没有实现这个功能呢,我满心欢喜上G站搜Issue,看到这样一个回复(https://github.com/aspnet/Extensions/issues/2406):
官方明确表示没有开发属性注入的计划,没办法,只能靠本身了。
个人思路大概是:建立一个自定义标签(Attribute),用来给须要注入的属性打标签,而后写一个服务激活类,用来解析给定实例须要注入的属性并赋值,在某个类型被建立实例的时候也就是构造函数中调用这个激活方法实现属性注入。这里有个核心点要注意的是,从DI容器获取实例的时候必定要保证是和当前请求是同一个Scope,也就是说,必需要从当前的HttpContext中拿到这个IServiceProvider。
先建立一个自定义标签:
[AttributeUsage(AttributeTargets.Property)] public class AutowiredAttribute : Attribute { }
解析属性的方法:
public void PropertyActivate(object service, IServiceProvider provider) { var serviceType = service.GetType(); var properties = serviceType.GetProperties().AsEnumerable().Where(x => x.Name.StartsWith("_")); foreach (PropertyInfo property in properties) { var autowiredAttr = property.GetCustomAttribute<AutowiredAttribute>(); if (autowiredAttr != null) { //从DI容器获取实例 var innerService = provider.GetService(property.PropertyType); if (innerService != null) { //递归解决服务嵌套问题 PropertyActivate(innerService, provider); //属性赋值 property.SetValue(service, innerService); } } } }
而后在控制器中激活属性:
[Autowired] public IAccountService _accountService { get; set; } public LoginController(IHttpContextAccessor httpContextAccessor) { var pro = new AutowiredServiceProvider(); pro.PropertyActivate(this, httpContextAccessor.HttpContext.RequestServices); }
这样子下来,虽然功能实现了,可是里面存着几个问题。第一个是因为控制器的构造函数中不能直接使用ControllerBase的HttpContext属性,因此必需要经过注入IHttpContextAccessor对象来获取,貌似问题又回到原点。第二个是每一个构造函数中都要写这么一堆代码,不能忍。因而想有没有办法在控制器被激活的时候作一些操做?没考虑引入AOP框架,感受为了这一个功能引入AOP有点重。通过网上搜索,发现Asp.Net Core框架激活控制器是经过IControllerActivator接口实现的,它的默认实现是DefaultControllerActivator(https://github.com/aspnet/AspNetCore/blob/master/src/Mvc/Mvc.Core/src/Controllers/DefaultControllerActivator.cs):
/// <inheritdoc /> public object Create(ControllerContext controllerContext) { if (controllerContext == null) { throw new ArgumentNullException(nameof(controllerContext)); } if (controllerContext.ActionDescriptor == null) { throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull( nameof(ControllerContext.ActionDescriptor), nameof(ControllerContext))); } var controllerTypeInfo = controllerContext.ActionDescriptor.ControllerTypeInfo; if (controllerTypeInfo == null) { throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull( nameof(controllerContext.ActionDescriptor.ControllerTypeInfo), nameof(ControllerContext.ActionDescriptor))); } var serviceProvider = controllerContext.HttpContext.RequestServices; return _typeActivatorCache.CreateInstance<object>(serviceProvider, controllerTypeInfo.AsType()); }
这样一来,我本身实现一个Controller激活器不就能够接管控制器激活了,因而有以下这个类:
public class HosControllerActivator : IControllerActivator { public object Create(ControllerContext actionContext) { var controllerType = actionContext.ActionDescriptor.ControllerTypeInfo.AsType(); var instance = actionContext.HttpContext.RequestServices.GetRequiredService(controllerType); PropertyActivate(instance, actionContext.HttpContext.RequestServices); return instance; } public virtual void Release(ControllerContext context, object controller) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (controller == null) { throw new ArgumentNullException(nameof(controller)); } if (controller is IDisposable disposable) { disposable.Dispose(); } } private void PropertyActivate(object service, IServiceProvider provider) { var serviceType = service.GetType(); var properties = serviceType.GetProperties().AsEnumerable().Where(x => x.Name.StartsWith("_")); foreach (PropertyInfo property in properties) { var autowiredAttr = property.GetCustomAttribute<AutowiredAttribute>(); if (autowiredAttr != null) { //从DI容器获取实例 var innerService = provider.GetService(property.PropertyType); if (innerService != null) { //递归解决服务嵌套问题 PropertyActivate(innerService, provider); //属性赋值 property.SetValue(service, innerService); } } } } }
须要注意的是,DefaultControllerActivator中的控制器实例是从TypeActivatorCache获取的,而本身的激活器是从DI获取的,因此必须额外把系统全部控制器注册到DI中,封装成以下的扩展方法:
/// <summary> /// 自定义控制器激活,并手动注册全部控制器 /// </summary> /// <param name="services"></param> /// <param name="obj"></param> public static void AddHosControllers(this IServiceCollection services, object obj) { services.Replace(ServiceDescriptor.Transient<IControllerActivator, HosControllerActivator>()); var assembly = obj.GetType().GetTypeInfo().Assembly; var manager = new ApplicationPartManager(); manager.ApplicationParts.Add(new AssemblyPart(assembly)); manager.FeatureProviders.Add(new ControllerFeatureProvider()); var feature = new ControllerFeature(); manager.PopulateFeature(feature); feature.Controllers.Select(ti => ti.AsType()).ToList().ForEach(t => { services.AddTransient(t); }); }
在ConfigureServices中调用:
services.AddHosControllers(this);
到此,大功告成!能够愉快的继续CRUD了。
市面上好用的DI框架一堆一堆的,集成到Core里面也很简单,为啥还要这么折腾?没办法,这不就是造轮子的乐趣嘛。上面这些东西从头至尾也折腾了很多时间,属性注入那里也还有优化的空间,欢迎探讨。
推荐阅读:
https://www.cnblogs.com/artech/p/inside-asp-net-core-03-05.html
原文出处:https://www.cnblogs.com/hohoa/p/11884719.html