ASP.NET Core MVC如何实现运行时动态定义Controller类型

2020-06-11 16:00:18王冬梅

由于针对MVC应用的请求总是指向某一个Action,所以MVC框架提供的路由整合机制体现在为每一个Action创建一个或者多个终结点(同一个Action方法可以注册多个路由)。针对Action方法的路由终结点是根据描述Action方法的ActionDescriptor对象构建而成的。至于ActionDescriptor对象,则是通过注册的一组IActionDescriptorProvider对象来提供的,那么我们的问题就迎刃而解:通过注册自定义的IActionDescriptorProvider从动态定义的Controller类型中解析出合法的Action方法,并创建对应的ActionDescriptor对象即可。

那么ActionDescriptor如何创建呢?我们能想到简单的方式是调用如下这个Build方法。针对该方法的调用存在两个问题:第一,ControllerActionDescriptorBuilder是一个内部(internal)类型,我们指定以反射的方式调用这个方法,第二,这个方法接受一个类型为ApplicationModel的参数。

internal static class ControllerActionDescriptorBuilder
{
  public static IList<ControllerActionDescriptor> Build(ApplicationModel application);
}

ApplicationModel类型涉及到一个很大的主题:MVC应用模型,目前我们现在只关注如何创建这个对象。表示MVC应用模型的ApplicationModel对象是通过对应的工厂ApplicationModelFactory创建的。这个工厂会自动注册到MVC应用的依赖注入框架中,但是这依然是一个内部(内部)类型,所以还得反射。

internal class ApplicationModelFactory
{
  public ApplicationModel CreateApplicationModel(IEnumerable<TypeInfo> controllerTypes);
}

我们定义了如下这个DynamicActionProvider类型实现了IActionDescriptorProvider接口。针对提供的源代码向ActionDescriptor列表的转换体现在AddControllers方法中:它利用ICompiler对象编译源代码,并在生成的程序集中解析出有效的Controller类型,然后利用ApplicationModelFactory创建出代表应用模型的ApplicationModel对象,后者作为参数调用ControllerActionDescriptorBuilder的静态方法Build创建出描述所有Action方法的ActionDescriptor对象。

public class DynamicActionProvider : IActionDescriptorProvider
{
  private readonly List<ControllerActionDescriptor> _actions;
  private readonly Func<string, IEnumerable<ControllerActionDescriptor>> _creator;

  public DynamicActionProvider(IServiceProvider serviceProvider, ICompiler compiler)
  {
    _actions = new List<ControllerActionDescriptor>();
    _creator = CreateActionDescrptors;

    IEnumerable<ControllerActionDescriptor> CreateActionDescrptors(string sourceCode)
    {
      var assembly = compiler.Compile(sourceCode, 
        Assembly.Load(new AssemblyName("System.Runtime")),
        typeof(object).Assembly,
        typeof(ControllerBase).Assembly,
        typeof(Controller).Assembly);
      var controllerTypes = assembly.GetTypes().Where(it => IsController(it));
      var applicationModel = CreateApplicationModel(controllerTypes);

      assembly = Assembly.Load(new AssemblyName("Microsoft.AspNetCore.Mvc.Core"));
      var typeName = "Microsoft.AspNetCore.Mvc.ApplicationModels.ControllerActionDescriptorBuilder";
      var controllerBuilderType = assembly.GetTypes().Single(it => it.FullName == typeName);
      var buildMethod = controllerBuilderType.GetMethod("Build", BindingFlags.Static | BindingFlags.Public);
      return (IEnumerable<ControllerActionDescriptor>)buildMethod.Invoke(null, new object[] { applicationModel });
    }

    ApplicationModel CreateApplicationModel(IEnumerable<Type> controllerTypes)
    {
      var assembly = Assembly.Load(new AssemblyName("Microsoft.AspNetCore.Mvc.Core"));
      var typeName = "Microsoft.AspNetCore.Mvc.ApplicationModels.ApplicationModelFactory";
      var factoryType = assembly.GetTypes().Single(it => it.FullName == typeName);
      var factory = serviceProvider.GetService(factoryType);
      var method = factoryType.GetMethod("CreateApplicationModel");
      var typeInfos = controllerTypes.Select(it => it.GetTypeInfo());
      return (ApplicationModel)method.Invoke(factory, new object[] { typeInfos });
    }

    bool IsController(Type typeInfo)
    {
      if (!typeInfo.IsClass) return false;
      if (typeInfo.IsAbstract) return false;
      if (!typeInfo.IsPublic) return false;
      if (typeInfo.ContainsGenericParameters) return false;
      if (typeInfo.IsDefined(typeof(NonControllerAttribute))) return false;
      if (!typeInfo.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) && !typeInfo.IsDefined(typeof(ControllerAttribute))) return false;
      return true;
    }
  }

  public int Order => -100;
  public void OnProvidersExecuted(ActionDescriptorProviderContext context) { }
  public void OnProvidersExecuting(ActionDescriptorProviderContext context)
  {
    foreach (var action in _actions)
    {
      context.Results.Add(action);
    }
  }
  public void AddControllers(string sourceCode) => _actions.AddRange(_creator(sourceCode));
}