故事背景

最近在把自己的一个老项目从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) { }
}

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

DbContext从哪来

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

推荐阅读:

https://www.cnblogs.com/artech/p/inside-asp-net-core-03-05.html

https://www.cnblogs.com/tdfblog/p/controller-activation-and-dependency-injection-in-asp-net-core-mvc.html

从EFCore上下文的使用到深入剖析DI的生命周期最后实现自动属性注入的更多相关文章

  1. React源码剖析系列 - 生命周期的管理艺术

    目前,前端领域中 React 势头正盛,很少能够深入剖析内部实现机制和原理.本系列文章希望通过剖析 React 源码,理解其内部的实现原理,知其然更要知其所以然. 对于 React,其组件生命周期(C ...

  2. React 源码剖析系列 - 生命周期的管理艺术

    目前,前端领域中 React 势头正盛,很少能够深入剖析内部实现机制和原理. 本系列文章 希望通过剖析 React 源码,理解其内部的实现原理,知其然更要知其所以然. 对于 React,其组件生命周期 ...

  3. 一步步剖析spring bean生命周期

    关于spring bean的生命周期,是深入学习spring的基础,也是难点,本篇文章将采用代码+图文结论的方式来阐述spring bean的生命周期,方便大家学习交流.  一  项目结构及源码 1. ...

  4. How tomcat works(深入剖析tomcat)生命周期Lifecycle

    How Tomcat Works (6)生命周期Lifecycle 总体概述 这一章讲的是tomcat的组件之一,LifeCycle组件,通过这个组件可以统一管理其他组件,可以达到统一启动/关闭组件的 ...

  5. WCF技术剖析之二十三:服务实例(Service Instance)生命周期如何控制[下篇]

    原文:WCF技术剖析之二十三:服务实例(Service Instance)生命周期如何控制[下篇] 在[第2篇]中,我们深入剖析了单调(PerCall)模式下WCF对服务实例生命周期的控制,现在我们来 ...

  6. 第三节:EF Core上下文DbContext相关配置和生命周期

    一. 配置相关 1. 数据库连接字符串的写法 (1).账号密码:Server=localhost;Database=EFDB01;User ID=sa;Password=123456; (2).win ...

  7. [译] ASP.NET 生命周期 – ASP.NET 上下文对象(五)

    ASP.NET 上下文对象 ASP.NET 提供了一系列对象用来给当前请求,将要返回到客户端的响应,以及 Web 应用本身提供上下文信息.间接的,这些上下文对象也可以用来回去核心 ASP.NET 框架 ...

  8. abp模块生命周期设计思路剖析

    abp中将生命周期事件抽象为4个接口: //预初始化 public interface IOnPreApplicationInitialization { void OnPreApplicationI ...

  9. java线程基础巩固---线程生命周期以及start方法源码剖析

    上篇中介绍了如何启动一个线程,通过调用start()方法才能创建并使用新线程,并且这个start()是非阻塞的,调用之后立马就返回的,实际上它是线程生命周期环节中的一种,所以这里阐述一下线程的一个完整 ...

随机推荐

  1. Spring Boot WebFlux 集成 Mongodb 数据源操作

    WebFlux 整合 Mongodb 前言 上一讲用 Map 数据结构内存式存储了数据.这样数据就不会持久化,本文我们用 MongoDB 来实现 WebFlux 对数据源的操作. 什么是 MongoD ...

  2. Oracle基于布尔的盲注总结

    0x01 decode 函数布尔盲注 decode(字段或字段的运算,值1,值2,值3) 这个函数运行的结果是,当字段或字段的运算的值等于值1时,该函数返回值2,否则返回3 当然值1,值2,值3也可以 ...

  3. 机器学习:数据清洗及工具OpenRefine

    数据分析中,首先要进行数据清洗,才可以继续训练模型,预测等操作. 首先介绍一下什么是数据清洗(定义来自 百度百科,有删减) 数据清洗从名字上也看的出就是把“脏”的“洗掉”,指发现并纠正数据文件中可识别 ...

  4. PHP reset

    1.函数的作用:重置数组内部指针,并返回第一个元素 2.函数的参数: @param array  $array 3. 例子一: <?php $arr1 = []; $arr2 = [false, ...

  5. spring源码系列7:Spring中的InstantiationAwareBeanPostProcessor和BeanPostProcessor的区别

    概念 Bean创建过程中的"实例化"与"初始化"名词 实例化(Instantiation): 要生成对象, 对象还未生成. 初始化(Initialization ...

  6. Cocos2d-x 学习笔记(11.1) MoveBy MoveTo

    1. MoveBy MoveTo 两方法都是对node的平移,MoveBy是相对当前位置的移动.MoveTo是By的子类,是移动到世界坐标位置. 1.1 成员变量和create方法 MoveBy的主要 ...

  7. Java获取文件中视频的时长

    public void ReadVideoTime(String path) { long sum = 0; long num = 0; File source = new File(path[i]) ...

  8. 玩转ArduinoJson库 V6版本

    1.前言     前面,博主已经讲解了ArduinoJson库的V5版本.为了节省时间以及不讨论重复内容,博主建议读者先去阅读一下 玩转ArduinoJson库 V5版本 .重点了解几个东西: JSO ...

  9. mp-vue拖拽组件的实现

    作为一个效率还不错的小前端,自己的任务做完之后真的好闲啊,千盼万盼终于盼来了业务的新需求,他要我多加一个排序题,然后用户通过拖拽来排序,项目经理看我是个实习生,说有点复杂做不出来就算了,我这么闲的一个 ...

  10. 解决tortoiseSvn 访问版本库的时候一直初始化,或者无响应的问题

    现象 svn访问版本库时一直提示: please wait while the repository browser is initializing 没有反应,甚至3-4分钟才会出来,即便出来也会很卡 ...