从EFCore上下文的使用到深入剖析DI的生命周期最后实现自动属性注入_实用技巧

来源:脚本之家  责任编辑:小易  

深度难度高颜色重情意重www.zgxue.com防采集请勿采集本网。

故事背景

唐代边塞(也就是北方边疆有敌人入侵)战事多发,诗人当时就是去边疆慰问将士的,然后写下自己的所见所感,有很多大诗人都写过这种诗,统称边塞诗,问边,居延,胡天,大漠,萧关,燕然这些都是

最近在把自己的一个老项目从Framework迁移到.Net Core 3.0,数据访问这块选择的是EFCore+Mysql。使用EF的话不可避免要和DbContext打交道,在Core中的常规用法一般是:创建一个XXXContext类继承自DbContext,实现一个拥有DbContextOptions参数的构造器,在启动类StartUp中的ConfigureServices方法里调用IServiceCollection的扩展方法AddDbContext,把上下文注入到DI容器中,然后在使用的地方通过构造函数的参数获取实例。OK,没任何毛病,官方示例也都是这么来用的。但是,通过构造函数这种方式来获取上下文实例其实很不方便,比如在Attribute或者静态类中,又或者是系统启动时初始化一些数据,更多的是如下一种场景:

要翻译全文,要对照原文,所以首先介绍原文如下: 浣溪沙 宋代 李清照 莫许杯深琥珀浓,未成沉醉意先融。疏钟已应晚来风。瑞脑香消魂梦断,辟寒金小髻鬟松。醒时空对烛花红。

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) { } }

下文“此先汉所以兴隆也”、“此后汉所以倾颓也”、“此臣所以报先帝”中“所”均为特殊的指示代词,具体的指代对象可以从上下文来体会) 优劣得所(地方,处所,名词) 13、当 咨臣以当世之事(正在…的

从上面的代码可以看到,任何要继承BaseController的类都要写一个“多余”的构造函数,如果参数再多几个,这将是无法忍受的(就算只有一个参数我也忍受不了)。那么怎样才能更优雅的获取数据库上下文实例呢,我想到以下几种办法。

答题技巧:(1)在熟读文章的基础上,对上下文的内,容理解到位,使所补写的句子与上下文连贯,符合文章的原意;(2)对文段的概括要做到全面、准确,所补写的句子能与文段相对应;(3)注意语言的表达形式,

DbContext从哪来

⑶词序(从生活逻辑和上下文的照应上判断);⑷句序(关联词语的使用,特别要注意递进关系). 6、驳论文的阅读 ⑴作者要批驳的错误观点是什么?⑵作者是怎样进行批驳的,用了那些道理和论据;⑶由此,作者树立

1、 直接开溜new

回归原始,既然要创建实例,没有比直接new一个更好的办法了,在Framework中没有DI的时候也差不多都这么干。但在EFCore中不同的是,DbContext不再提供无参构造函数,取而代之的是必须传入一个DbContextOptions类型的参数,这个参数通常是做一些上下文选项配置例如使用什么类型数据库连接字符串是多少。

public BloggingContext(DbContextOptions<BloggingContext> options) : base(options) { }

默认情况下,我们已经在StartUp中注册上下文的时候做了配置,DI容器会自动帮我们把options传进来。如果要手动new一个上下文,那岂不是每次都要自己传?不行,这太痛苦了。那有没有办法不传这个参数?肯定也是有的。我们可以去掉有参构造函数,然后重写DbContext中的OnConfiguring方法,在这个方法中做数据库配置:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlite("Filename=./efcoredemo.db"); }

即使是这样,依然有不够优雅的地方,那就是连接字符串被硬编码在代码中,不能做到从配置文件读取。反正我忍受不了,只能再寻找其他方案。

2、 从DI容器手动获取

既然前面已经在启动类中注册了上下文,那么从DI容器中获取实例肯定是没问题的。于是我写了这样一句测试代码用来验证猜想:

 var context = app.ApplicationServices.GetService<BloggingContext>();

不过很遗憾抛出了异常:

报错信息说的很明确,不能从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里面也很简单,为啥还要这么折腾?没办法,这不就是造轮子的乐趣嘛。上面这些东西从头到尾也折腾了不少时间,属性注入那里也还有优化的空间,欢迎探讨。

1:深度;2:(道理,含义)高深不易了解;3:颜色浓;4:感情厚,关系密切内容来自www.zgxue.com请勿采集。


  • 本文相关:
  • .net core3.0 web api中使用fluentvalidation验证(批量注入)
  • asp.net core 过滤器中使用依赖注入知识点总结
  • 浅谈.net core 注入中的三种模式:singleton、scoped 和 transient
  • .net core源码解析配置文件及依赖注入
  • asp.net core依赖注入系列教程之控制反转(ioc)
  • asp.net core依赖注入系列教程之服务的注册与提供
  • asp.net core di手动获取注入对象的方法
  • .net core在程序的任意位置使用和注入服务的方法
  • 在.net core控制台程序中如何使用依赖注入详解
  • .net core中依赖注入automapper的方法示例
  • 详解asp.net core 中的框架级依赖注入
  • 详解asp.net core 在 json 文件中配置依赖注入
  • 在.net使用json作为数据交换格式实例演示
  • asp.net简单生成xml文件的方法
  • asp.net(c#)函数对象参数传递的问题
  • ajaxtoolkit:calendarextender演示与实现代码
  • datagrid和repeader控件中替换标识值的方法
  • .net下调用sqlserver存储过程的小例子
  • asp.net用户控件技术
  • asp.net mvc 身份验证、异常处理、权限验证(拦截器)实现代码
  • mvc4制作网站教程第四章 添加栏目4.1
  • asp.net在后端动态添加样式表调用的方法
  • 根据上下文写出下列句子中的“深”的含义
  • 初中学历以上的进(根据上下文写出下面句子中的深的含义)
  • 根据上下文写出句子中加点字深的含义
  • 一段据说是村上春树的话,求出处和上下文
  • 为什么说《使至塞上》是一首边塞诗?结合诗句具体说明
  • 李清照《浣溪沙》“莫许杯深琥珀浓……”译文。
  • 那个《出师表》的古今异义、词类活用、一词多义、通假字有木有啊
  • 怎样分析文章句段的作用
  • 从说明文语言的角度来赏析句子
  • 1.根据上下文在文中横线上把句子补充完整。2.用自己的话概括主要内容。 答得好加分!!
  • 网站首页网页制作脚本下载服务器操作系统网站运营平面设计媒体动画电脑基础硬件教程网络安全基础应用实用技巧自学过程首页asp.net实用技巧.net core3.0 web api中使用fluentvalidation验证(批量注入)asp.net core 过滤器中使用依赖注入知识点总结浅谈.net core 注入中的三种模式:singleton、scoped 和 transient.net core源码解析配置文件及依赖注入asp.net core依赖注入系列教程之控制反转(ioc)asp.net core依赖注入系列教程之服务的注册与提供asp.net core di手动获取注入对象的方法.net core在程序的任意位置使用和注入服务的方法在.net core控制台程序中如何使用依赖注入详解.net core中依赖注入automapper的方法示例详解asp.net core 中的框架级依赖注入详解asp.net core 在 json 文件中配置依赖注入在.net使用json作为数据交换格式实例演示asp.net简单生成xml文件的方法asp.net(c#)函数对象参数传递的问题ajaxtoolkit:calendarextender演示与实现代码datagrid和repeader控件中替换标识值的方法.net下调用sqlserver存储过程的小例子asp.net用户控件技术asp.net mvc 身份验证、异常处理、权限验证(拦截器)实现代码mvc4制作网站教程第四章 添加栏目4.1asp.net在后端动态添加样式表调用的方法java正则表达式 pattern和matche未将对象引用设置到对象的实例 (asp.net(c#)网页跳转七种方法小结未能加载文件或程序集“xxx”或它asp.net“服务器应用程序不可用”asp.net中的几种弹出框提示基本实asp.net gridview 72般绝技asp.net生成excel并导出下载五种asp.net汉字转拼音和获取汉字首字asp.net对路径"xxxxx"引用全局程序集缓存内的程序集的方法asp.net使用datatable构造json字符串的方asp.net静态方法弹出对话框实现思路visual studio.net 内幕(7)asp.net ckeditor编辑器的使用方法asp.net 面试 笔试题目[附答案]asp.net用signalr建立浏览器和服务器的持asp.net实现上传文件显示本地绝对路径的实调试asp.net应用程序的方法和技巧自己常用到的自定义公共类(已测试通过)
    免责声明 - 关于我们 - 联系我们 - 广告联系 - 友情链接 - 帮助中心 - 频道导航
    Copyright © 2017 www.zgxue.com All Rights Reserved