8

ASP.NET Core Controller与IOC的羁绊

 3 years ago
source link: https://www.cnblogs.com/wucy/p/14222973.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

ASP.NET Core Controller与IOC的羁绊

前言#

    看到标题可能大家会有所疑问Controller和IOC能有啥羁绊,但是我还是拒绝当一个标题党的。相信有很大一部分人已经知道了这么一个结论,默认情况下ASP.NET Core的Controller并不会托管到IOC容器中,注意关键字我说的是"默认",首先咱们不先说为什么,如果还有不知道这个结论的同学们可以自己验证一下,验证方式也很简单,大概可以通过以下几种方式。

验证Controller不在IOC中#

首先,我们可以尝试在ServiceProvider中获取某个Controller实例,比如

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    var productController = app.ApplicationServices.GetService<ProductController>();
}

这是最直接的方式,可以在IOC容器中获取注册过的类型实例,很显然结果会为null。另一种方式,也是利用它的另一个特征,那就是通过构造注入的方式,如下所示我们在OrderController中注入ProductController,显然这种方式是不合理的,但是为了求证一个结果,我们这里仅做演示,强烈不建议实际开发中这么写,这是不规范也是不合理的写法

public class OrderController : Controller
{
    private readonly ProductController _productController;
    public OrderController(ProductController productController)
    {
        _productController = productController;
    }

    public IActionResult Index()
    {
        return View();
    }
}

结果显然是会报一个错InvalidOperationException: Unable to resolve service for type 'ProductController' while attempting to activate 'OrderController'。原因就是因为ProductController并不在IOC容器中,所以通过注入的方式会报错。还有一种方式,可能不太常用,这个是利用注入的一个特征,可能有些同学已经了解过了,那就是通过自带的DI,即使一个类中包含多个构造函数,它也会选择最优的一个,也就是说自带的DI允许类包含多个构造函数。利用这个特征,我们可以在Controller中验证一下

public class OrderController : Controller
{
    private readonly IOrderService _orderService;
    private readonly IPersonService _personService;

    public OrderController(IOrderService orderService)
    {
        _orderService = orderService;
    }

    public OrderController(IOrderService orderService, IPersonService personService)
    {
        _orderService = orderService;
        _personService = personService;
    }

    public IActionResult Index()
    {
        return View();
    }
}

我们在Controller中编写了两个构造函数,理论上来说这是符合DI特征的,运行起来测试一下,依然会报错InvalidOperationException: Multiple constructors accepting all given argument types have been found in type 'OrderController'. There should only be one applicable constructor。以上种种都是为了证实一个结论,默认情况下Controller并不会托管到IOC当中。

DefaultControllerFactory源码探究#

    上面虽然我们看到了一些现象,能说明Controller默认情况下并不在IOC中托管,但是还没有足够的说服力,接下来我们就来查看源码,这是最有说服力的。我们找到Controller工厂注册的地方,在MvcCoreServiceCollectionExtensions扩展类中[点击查看源码👈]的AddMvcCoreServices方法里

//给IControllerFactory注册默认的Controller工厂类DefaultControllerFactory
//也是Controller创建的入口
services.TryAddSingleton<IControllerFactory, DefaultControllerFactory>();
//真正创建Controller的工作类DefaultControllerActivator
services.TryAddTransient<IControllerActivator, DefaultControllerActivator>();

由此我们可以得出,默认的Controller创建工厂类为DefaultControllerFactory,那么我们直接找到源码位置[点击查看源码👈],
为了方便阅读,精简一下源码如下所示

internal class DefaultControllerFactory : IControllerFactory
{
    //真正创建Controller的工作者
    private readonly IControllerActivator _controllerActivator;
    private readonly IControllerPropertyActivator[] _propertyActivators;

    public DefaultControllerFactory(
        IControllerActivator controllerActivator,
        IEnumerable<IControllerPropertyActivator> propertyActivators)
    {
        _controllerActivator = controllerActivator;
        _propertyActivators = propertyActivators.ToArray();
    }

    /// <summary>
    /// 创建Controller实例的方法
    /// </summary>
    public object CreateController(ControllerContext context)
    {
        //创建Controller实例的具体方法(这是关键方法)
        var controller = _controllerActivator.Create(context);
        foreach (var propertyActivator in _propertyActivators)
        {
            propertyActivator.Activate(context, controller);
        }
        return controller;
    }

    /// <summary>
    /// 释放Controller实例的方法
    /// </summary>
    public void ReleaseController(ControllerContext context, object controller)
    {
        _controllerActivator.Release(context, controller);
    }
}

用过上面的源码可知,真正创建Controller的地方在_controllerActivator.Create方法中,通过上面的源码可知为IControllerActivator默认注册的是DefaultControllerActivator类,直接找到源码位置[点击查看源码👈],我们继续简化一下源码如下所示

internal class DefaultControllerActivator : IControllerActivator
{
    private readonly ITypeActivatorCache _typeActivatorCache;

    public DefaultControllerActivator(ITypeActivatorCache typeActivatorCache)
    {
        _typeActivatorCache = typeActivatorCache;
    }

    /// <summary>
    /// Controller实例的创建方法
    /// </summary>
    public object Create(ControllerContext controllerContext)
    {
        //获取Controller类型信息
        var controllerTypeInfo = controllerContext.ActionDescriptor.ControllerTypeInfo;
        //获取ServiceProvider
        var serviceProvider = controllerContext.HttpContext.RequestServices;
        //创建controller实例
        return _typeActivatorCache.CreateInstance<object>(serviceProvider, controllerTypeInfo.AsType());
    }

    /// <summary>
    /// 释放Controller实例
    /// </summary>
    public void Release(ControllerContext context, object controller)
    {
        //如果controller实现了IDisposable接口,那么Release的时候会自动调用Controller的Dispose方法
        //如果我们在Controller中存在需要释放或者关闭的操作,可以再Controller的Dispose方法中统一释放
        if (controller is IDisposable disposable)
        {
            disposable.Dispose();
        }
    }
}

通过上面的代码我们依然要继续深入到ITypeActivatorCache实现中去寻找答案,通过查看MvcCoreServiceCollectionExtensions类的AddMvcCoreServices方法源码我们可以找到如下信息

services.TryAddSingleton<ITypeActivatorCache, TypeActivatorCache>();

有了这个信息,我们可以直接找到TypeActivatorCache类的源码[点击查看源码👈]代码并不多,大致如下所示

internal class TypeActivatorCache : ITypeActivatorCache
{
    //创建ObjectFactory的委托
    private readonly Func<Type, ObjectFactory> _createFactory =
        (type) => ActivatorUtilities.CreateFactory(type, Type.EmptyTypes);
    //Controller类型和对应创建Controller实例的ObjectFactory实例的缓存
    private readonly ConcurrentDictionary<Type, ObjectFactory> _typeActivatorCache =
           new ConcurrentDictionary<Type, ObjectFactory>();

    /// <summary>
    /// 真正创建实例的地方
    /// </summary>
    public TInstance CreateInstance<TInstance>(
        IServiceProvider serviceProvider,
        Type implementationType)
    {
        //真正创建的操作是createFactory
        //通过Controller类型在ConcurrentDictionary缓存中获得ObjectFactory
        //而ObjectFactory实例由ActivatorUtilities.CreateFactory方法创建的
        var createFactory = _typeActivatorCache.GetOrAdd(implementationType, _createFactory);
        //返回创建实例
        return (TInstance)createFactory(serviceProvider, arguments: null);
    }
}

通过上面类的代码我们可以清晰的得出一个结论,默认情况下Controller实例是由ObjectFactory创建出来的,而ObjectFactory实例是由ActivatorUtilities的CreateFactory创建出来,所以Controller实例每次都是由ObjectFactory创建而来,并非注册到IOC容器中。并且我们还可以得到一个结论ObjectFactory应该是一个委托,我们找到ObjectFactory定义的地方[点击查看源码👈]

delegate object ObjectFactory(IServiceProvider serviceProvider, object[] arguments);

这个确实如我们猜想的那般,这个委托会通过IServiceProvider实例去构建类型的实例,通过上述源码相关的描述我们会产生一个疑问,既然Controller实例并非由IOC容器托管,它由ObjectFactory创建而来,但是ObjectFactory实例又是由ActivatorUtilities构建的,那么生产对象的核心也就在ActivatorUtilities类中,接下来我们就来探究一下ActivatorUtilities的神秘面纱。

ActivatorUtilities类的探究#

    书接上面,我们知道了ActivatorUtilities类是创建Controller实例最底层的地方,那么ActivatorUtilities到底和容器是啥关系,因为我们看到了ActivatorUtilities创建实例需要依赖ServiceProvider,一切都要从找到ActivatorUtilities类的源码开始。我们最初接触这个类的地方在于它通过CreateFactory方法创建了ObjectFactory实例,那么我们就从这个地方开始,找到源码位置[点击查看源码👈]实现如下

public static ObjectFactory CreateFactory(Type instanceType, Type[] argumentTypes)
{
    //查找instanceType的构造函数
    //找到构造信息ConstructorInfo
    //得到给定类型与查找类型instanceType构造函数的映射关系
    FindApplicableConstructor(instanceType, argumentTypes, out ConstructorInfo constructor, out int?[] parameterMap);
    //构建IServiceProvider类型参数
    var provider = Expression.Parameter(typeof(IServiceProvider), "provider");
    //构建给定类型参数数组参数
    var argumentArray = Expression.Parameter(typeof(object[]), "argumentArray");
    //通过构造信息、构造参数对应关系、容器和给定类型构建表达式树Body
    var factoryExpressionBody = BuildFactoryExpression(constructor, parameterMap, provider, argumentArray);
    //构建lambda
    var factoryLamda = Expression.Lambda<Func<IServiceProvider, object[], object>>(
        factoryExpressionBody, provider, argumentArray);
    var result = factoryLamda.Compile();
    //返回执行结果
    return result.Invoke;
}

ActivatorUtilities类的CreateFactory方法代码虽然比较简单,但是它涉及到调用了其他方法,由于嵌套的比较深代码比较多,而且不是本文讲述的重点,我们就不再这里细说了,我们可以大概的描述一下它的工作流程。

  • 首先在给定的类型里查找到合适的构造函数,这里我们可以理解为查找Controller的构造函数。
  • 然后得到构造信息,并得到构造函数的参数与给定类型参数的对应关系
  • 通过构造信息和构造参数的对应关系,在IServiceProvider得到对应类型的实例为构造函数赋值
  • 最后经过上面的操作通过初始化指定的构造函数来创建给定Controller类型的实例

综上述的相关步骤,我们可以得到一个结论,Controller实例的初始化是通过遍历Controller类型构造函数里的参数,然后根据构造函数每个参数的类型在IServiceProvider查找已经注册到容器中相关的类型实例,最终初始化得到的Controller实例。这就是在IServiceProvider得到需要的依赖关系,然后创建自己的实例,它内部是使用的表达式树来完成的这一切,可以理解为更高效的反射方式。
关于ActivatorUtilities类还包含了其他比较实用的方法,比如CreateInstance方法

public static T CreateInstance<T>(IServiceProvider provider, params object[] parameters)

它可以通过构造注入的方式创建指定类型T的实例,其中构造函数里具体的参数实例是通过在IServiceProvider实例里获取到的,比如我们我们有这么一个类

public class OrderController 
{
    private readonly IOrderService _orderService;
    private readonly IPersonService _personService;

    public OrderController(IOrderService orderService, IPersonService personService)
    {
        _orderService = orderService;
        _personService = personService;
    }
}

其中它所依赖的IOrderService和IPersonService实例是注册到IOC容器中的

IServiceCollection services = new ServiceCollection()
 .AddScoped<IPersonService, PersonService>()
 .AddScoped<IOrderService, OrderService>();

然后你想获取到OrderController的实例,但是它只包含一个有参构造函数,但是构造函数的参数都以注册到IOC容器中。当存在这种场景你便可以通过以下方式得到你想要的类型实例,如下所示

IServiceProvider serviceProvider = services.BuildServiceProvider();
OrderController orderController = ActivatorUtilities.CreateInstance<OrderController>(serviceProvider);

即使你的类型OrderController并没有注册到IOC容器中,但是它的依赖都在容器中,你也可以通过构造注入的方式得到你想要的实例。总的来说ActivatorUtilities里的方法还是比较实用的,有兴趣的同学可以自行尝试一下,也可以通过查看ActivatorUtilities源码的方式了解它的工作原理。

AddControllersAsServices方法#

    上面我们主要是讲解了默认情况下Controller并不是托管到IOC容器中的,它只是表现出来的让你以为它是在IOC容器中,因为它可以通过构造函数注入相关实例,这主要是ActivatorUtilities类的功劳。说了这么多Controller实例到底可不可以注册到IOC容器中,让它成为真正受到IOC容器的托管者。要解决这个,必须要满足两点条件

  • 首先,需要将Controller注册到IOC容器中,但是仅仅这样还不够,因为Controller是由ControllerFactory创建而来
  • 其次,我们要改造ControllerFactory类中创建Controller实例的地方让它从容器中获取Controller实例,这样就解决了所有的问题
    如果我们自己去实现将Controller托管到IOC容器中,就需要满足以上两个操作一个是要将Controller放入容器,然后让创建Controller的地方从IOC容器中直接获取Controller实例。庆幸的是,微软帮我们封装了一个相关的方法,它可以帮我们解决将Controller托管到IOC容器的问题,它的使用方法如下所示
services.AddMvc().AddControllersAsServices();
//或其他方式,这取决于你构建的Web项目的用途可以是WebApi、Mvc、RazorPage等
//services.AddMvcCore().AddControllersAsServices();

相信大家都看到了,玄机就在AddControllersAsServices方法中,但是它存在于MvcCoreMvcBuilderExtensions类和MvcCoreMvcCoreBuilderExtensions类中,不过问题不大,因为它们的代码是完全一样的。只是因为你可以通过多种方式构建Web项目比如AddMvc或者AddMvcCore,废话不多说直接上代码[点击查看源码👈]

public static IMvcBuilder AddControllersAsServices(this IMvcBuilder builder)
{
    if (builder == null)
    {
        throw new ArgumentNullException(nameof(builder));
    }
    var feature = new ControllerFeature();
    builder.PartManager.PopulateFeature(feature);
    //第一将Controller实例添加到IOC容器中
    foreach (var controller in feature.Controllers.Select(c => c.AsType()))
    {
        //注册的生命周期是Transient
        builder.Services.TryAddTransient(controller, controller);
    }
    //第二替换掉原本DefaultControllerActivator的为ServiceBasedControllerActivator
    builder.Services.Replace(ServiceDescriptor.Transient<IControllerActivator, ServiceBasedControllerActivator>());
    return builder;
}

第一点没问题那就是将Controller实例添加到IOC容器中,第二点它替换掉了DefaultControllerActivator为为ServiceBasedControllerActivator。通过上面我们讲述的源码了解到DefaultControllerActivator是默认提供Controller实例的地方是获取Controller实例的核心所在,那么我们看看ServiceBasedControllerActivator与DefaultControllerActivator到底有何不同,直接贴出代码[点击查看源码👈]

public class ServiceBasedControllerActivator : IControllerActivator
{
    public object Create(ControllerContext actionContext)
    {
        if (actionContext == null)
        {
            throw new ArgumentNullException(nameof(actionContext));
        }
        //获取Controller类型
        var controllerType = actionContext.ActionDescriptor.ControllerTypeInfo.AsType();
        //通过Controller类型在容器中获取实例
        return actionContext.HttpContext.RequestServices.GetRequiredService(controllerType);
    }

    public virtual void Release(ControllerContext context, object controller)
    {
    }
}

    相信大家对上面的代码一目了然了,和我们上面描述的一样,将创建Controller实例的地方改造了在容器中获取的方式。不知道大家有没有注意到ServiceBasedControllerActivator的Release的方法居然没有实现,这并不是我没有粘贴出来,确实是没有代码,之前我们看到的DefaultControllerActivator可是有调用Controller的Disposed的方法,这里却啥也没有。相信聪明的你已经想到了,因为Controller已经托管到了IOC容器中,所以他的生命及其相关释放都是由IOC容器完成的,所以这里不需要任何操作。
    我们上面还看到了注册Controller实例的时候使用的是TryAddTransient方法,也就是说每次都会创建Controller实例,至于为什么,我想大概是因为每次请求其实都只会需要一个Controller实例,况且EFCore的注册方式官方建议也是Scope的,而这里的Scope正是对应的一次Controller请求。在加上自带的IOC会提升依赖类型的生命周期,如果将Controller注册为单例的话如果使用了EFCore那么它也会被提升为单例,这样会存在很大的问题。也许正是基于这个原因默认才将Controller注册为Transient类型的,当然这并不代表只能注册为Transient类型的,如果你不使用类似EFCore这种需要作用域为Scope的服务的时候,而且保证使用的主键都可以使用单例的话,完全可以将Controller注册为别的生命周期,当然这种方式个人不是很建议。

Controller结合Autofac#

    有时候大家可能会结合Autofac一起使用,Autofac确实是一款非常优秀的IOC框架,它它支持属性和构造两种方式注入,关于Autofac托管自带IOC的原理咱们在之前的文章浅谈.Net Core DependencyInjection源码探究中曾详细的讲解过,这里咱们就不过多的描述了,咱们今天要说的是Autofac和Controller的结合。如果你想保持和原有的IOC一致的使用习惯,即只使用构造注入的话,你只需要完成两步即可

  • 首先将默认的IOC容器替换为Autofac,具体操作也非常简单,如下所示
public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
              .ConfigureWebHostDefaults(webBuilder =>
              {
                  webBuilder.UseStartup<Startup>();
              })
              //只需要在这里设置ServiceProviderFactory为AutofacServiceProviderFactory即可
              .UseServiceProviderFactory(new AutofacServiceProviderFactory());
  • 然后就是咱们之前说的,要将Controller放入容器中,然后修改生产Controller实例的ControllerFactory的操作为在容器中获取,当然这一步微软已经为我们封装了便捷的方法
services.AddMvc().AddControllersAsServices();

只需要通过上面简单得两步,既可以将Controller托管到Autofac容器中。但是,我们说过了Autofac还支持属性注入,但是默认的方式只支持构造注入的方式,那么怎么让Controller支持属性注入呢?我们还得从最根本的出发,那就是解决Controller实例存和取的问题

  • 首先为了让Controller托管到Autofac中并且支持属性注入,那么就只能使用Autofac的方式去注册Controller实例,具体操作是在Startup类中添加ConfigureContainer方法,然后注册Controller并声明支持属性注入
public void ConfigureContainer(ContainerBuilder builder)
{
    var controllerBaseType = typeof(ControllerBase);
    //扫描Controller类
    builder.RegisterAssemblyTypes(typeof(Program).Assembly)
    .Where(t => controllerBaseType.IsAssignableFrom(t) && t != controllerBaseType)
    //属性注入
    .PropertiesAutowired();
}
  • 其次是解决取的问题,这里我们就不需要AddControllersAsServices方法了,因为AddControllersAsServices解决了Controller实例在IOC中存和取的问题,但是这里我们只需要解决Controller取得问题说只需要使用ServiceBasedControllerActivator即可,具体操作是
services.Replace(ServiceDescriptor.Transient<IControllerActivator, ServiceBasedControllerActivator>());

仅需要在默认的状态下完成这两步,既可以解决Controller托管到Autofac中并支持属性注入的问题,这也是最合理的方式。当然如果你使用AddControllersAsServices可是可以实现相同的效果了,只不过是没必要将容器重复的放入容器中了。

总结#

    本文我们讲述了关于ASP.NET Core Controller与IOC结合的问题,我觉得这是有必要让每个人都有所了解的知识点,因为在日常的Web开发中Controller太常用了,知道这个问题可能会让大家在开发中少走一点弯路,接下来我们来总结一下本文大致讲解的内容

  • 首先说明了一个现象,那就是默认情况下Controller并不在IOC容器中,我们也通过几个示例验证了一下。
  • 其次讲解了默认情况下创造Controller实例真正的类ActivatorUtilities,并大致讲解了ActivatorUtilities的用途。
  • 然后我们找到了将Controller托管到IOC容器中的办法AddControllersAsServices,并探究了它的源码,了解了它的工作方式。
  • 最后我们又演示了如何使用最合理的方式将Controller结合Autofac一起使用,并且支持属性注入。

本次讲解到这里就差不多了,希望本来就知道的同学们能加深一点了解,不知道的同学能够给你们提供一点帮助,能够在日常开发中少走一点弯路。新的一年开始了,本篇文章是我2021年的第一篇文章,新的一年感谢大家的支持。

👇欢迎扫码关注我的公众号👇

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK