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

故事背景

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 浅谈.net core 注入中的三种模式:Singleton、Scoped 和 Transient

    从上篇内容不如题的文章<.net core 并发下的线程安全问题>扩展认识.net core注入中的三种模式:Singleton.Scoped 和 Transient 我们都知道在 Startup 的ConfigureServices 可以注入我们想要的服务,那么在注入的时候有三种模式可以选择,那么我们在什么时候选择什么样的模式呢? 在讲注入模式之前,我觉得很有必要了解服务生存期的概念! 服务生存期:ASP.NET Core 提供了一个内置的服务容器 IServiceProvider负责管理服

  • ASP.NET Core依赖注入系列教程之服务的注册与提供

    前言 在采用了依赖注入的应用中,我们总是直接利用DI容器直接获取所需的服务实例,换句话说,DI容器起到了一个服务提供者的角色,它能够根据我们提供的服务描述信息提供一个可用的服务对象.ASP.NET Core中的DI容器体现为一个实现了IServiceProvider接口的对象. ServiceProvider与ServiceDescriptor 服务的注册与提供     利用ServiceProvider来提供服务     提供一个服务实例的集合     获取ServiceProvider自身对

  • .NET Core中依赖注入AutoMapper的方法示例

    本文主要介绍了关于.NET Core中依赖注入AutoMapper的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍: 最近在 review 代码时发现同事没有像其他项目那样使用 AutoMapper.Mapper.Initialize() 静态方法配置映射,而是使用了依赖注入 IMapper 接口的方式 services.AddSingleton<IMapper>(new Mapper(new MapperConfiguration(cfg => { cfg.Cr

  • 详解ASP.NET Core 中的框架级依赖注入

    1.ASP.NET Core 中的依赖注入 此示例展示了框架级依赖注入如何在 ASP.NET Core 中工作. 其简单但功能强大,足以完成大部分的依赖注入工作.框架级依赖注入支持以下 scope: Singleton - 总是返回相同的实例 Transient - 每次都返回新的实例 Scoped - 在当前(request)范围内返回相同的实例 假设我们有两个要通过依赖注入来进行工作的工件: PageContext - 自定义请求上下文 Settings - 全局应用程序设置 这两个都是非常

  • 在.NET Core控制台程序中如何使用依赖注入详解

    背景介绍 Dependency Injection:又称依赖注入,简称DI.在以前的开发方式中,层与层之间.类与类之间都是通过new一个对方的实例进行相互调用,这样在开发过程中有一个好处,可以清晰的知道在使用哪个具体的实现.随着软件体积越来越庞大,逻辑越来越复杂,当需要更换实现方式,或者依赖第三方系统的某些接口时,这种相互之间持有具体实现的方式不再合适.为了应对这种情况,就要采用契约式编程:相互之间依赖于规定好的契约(接口),不依赖于具体的实现.这样带来的好处是相互之间的依赖变得非常简单,又称松

  • ASP.NET Core 过滤器中使用依赖注入知识点总结

    如何给过滤器ActionFilterAttribute也用上构造函数注入呢? 一般自定义的过滤器直接用特性方式标识就能使用 [ContentFilter] 因为构造函数在使用的时候要求传参,然后我们可以使用这个 ServiceFilter 在ASP.NET Core里,我们可以使用ServiceFilter来完成这个需求. ServiceFilter允许我们解析一个已经添加IoC容器的服务,因此我们需要把ContentFilter注册一下. services.AddScoped<ContentF

  • .Net Core3.0 WEB API中使用FluentValidation验证(批量注入)

    为什么要使用FluentValidation 1.在日常的开发中,需要验证参数的合理性,不紧前端需要验证传毒的参数,后端也需要验证参数 2.在领域模型中也应该验证,做好防御性的编程是一种好的习惯(其实以前重来不写的,被大佬教育了一番) 3.FluentValidation 是.NET 开发的验证框架,开源,主要是简单好用,内置了一些常用的验证器,可以直接使用,扩展也很方便 使用FluentValidation 1.引入FluentValidation.AspNetCore NuGet包 2.建立

  • ASP.NET Core DI手动获取注入对象的方法

    依赖注入简单介绍: 依赖注入(Dependency injection , DI)是一种实现对象及其合作者或依赖项之间松散耦合的技术.将类用来执行其操作的这些对象以某种方式提供给该类,而不是直接实例化合作者或使用静态引用. ASP.NET Core DI 一般使用构造函数注入获取对象,比如在ConfigureServices配置注入后,通过下面方式获取: private IValueService _valueService; public ValueController(IValueServi

  • .NET Core源码解析配置文件及依赖注入

    写在前面 上篇文章我给大家讲解了ASP.NET Core的概念及为什么使用它,接着带着你一步一步的配置了.NET Core的开发环境并创建了一个ASP.NET Core的mvc项目,同时又通过一个实战教你如何在页面显示一个Content的列表.不知道你有没有跟着敲下代码,千万不要做眼高手低的人哦. 这篇文章我们就会设计一些复杂的概念了,因为要对ASP.NET Core的启动及运行原理.配置文件的加载过程进行分析,依赖注入,控制反转等概念的讲解等. 俗话说,授人以鱼不如授人以渔,所以文章旨在带着大

  • .Net Core在程序的任意位置使用和注入服务的方法

    最近有人问我:我该如何在Startup类之外的地方注入我的服务呢,都写在startup里看着好乱:我该如何在程序的其他地方获取我注入的服务呢:下面给大家写篇文章帮助大家学习. 一.如何在Stratup类外注入服务 首先,我们看startup类的ConfigureServices方法,我们会发现我们所有的服务都是使用IServiceCollection注入进去的 所以我们在其他地方进行注入依然是使用这个接口进行注入,话不多说,上源码 我们首先定义一个静态类,然后在静态类内写一个IServiceCo

  • ASP.NET Core依赖注入系列教程之控制反转(IoC)

    前言 ASP.NET Core在启动以及后续针对每个请求的处理过程中的各个环节都需要相应的组件提供相应的服务,为了方便对这些组件进行定制,ASP.NET通过定义接口的方式对它们进行了"标准化",我们将这些标准化的组件称为服务,ASP.NET在内部专门维护了一个DI容器来提供所需的服务.要了解这个DI容器以及现实其中的服务提供机制,我们先得知道什么是DI(Dependence Injection),而一旦我们提到DI,又不得不说IoC(Inverse of Control). 一.流程控

  • 详解ASP.NET Core 在 JSON 文件中配置依赖注入

    前言 在上一篇文章中写了如何在MVC中配置全局路由前缀,今天给大家介绍一下如何在在 json 文件中配置依赖注入. 在以前的 ASP.NET 4+ (MVC,Web Api,Owin,SingalR等)时候,都是提供了专有的接口以供使用第三方的依赖注入组件,比如我们常用的会使用 Autofac.Untiy.String.Net 等,这些第三放依赖注入组件基本上都提供了一套配置注入或者配置生命周期的方式,除了直接配置到类里面之外,还提供了要么使用 xml 文件,要么使用 json 等,那么在新的

随机推荐