音效素材网提供各类素材,打造精品素材网站!

站内导航 站长工具 投稿中心 手机访问

音效素材

ASP.NET Core MVC如何实现运行时动态定义Controller类型
日期:2021-09-07 22:47:16   来源:脚本之家

昨天有个朋友在微信上问我一个问题:他希望通过动态脚本的形式实现对ASP.NET Core MVC应用的扩展,比如在程序运行过程中上传一段C#脚本将其中定义的Controller类型注册到应用中,问我是否有好解决方案。我当时在外边,回复不太方便,所以只给他说了两个接口/类型:IActionDescriptorProvider和ApplicationPartManager。这是一个挺有意思的问题,所以回家后通过两种方案实现了这个需求。源代码从这里下载。

一、实现的效果

我们先来看看实现的效果。如下所示的是一个MVC应用的主页,我们可以在文本框中通过编写C#代码定义一个有效的Controller类型,然后点击“Register”按钮,定义的Controller类型将自动注册到MVC应用中

由于我们采用了针对模板为“{controller}/{action}”的约定路由,所以我们采用路径“/foo/bar”就可以访问上图中定义在FooController中的Action方法Bar,下图证实了这一点。

二、动态编译源代码

要实现如上所示的“针对Controller类型的动态注册”,首先需要解决的是针对提供源代码的动态编译问题,我们知道这个可以利用Roslyn来解决。具体来说,我们定义了如下这个ICompiler接口,它的Compile方法将会对参数sourceCode提供的源代码进行编译。该方法返回源代码动态编译生成的程序集,它的第二个参数代表引用的程序集。

public interface ICompiler
{
  Assembly Compile(string text, params Assembly[] referencedAssemblies);
}

如下所示的Compiler类型是对ICompiler接口的默认实现。

public class Compiler : ICompiler
{
  public Assembly Compile(string text, params Assembly[] referencedAssemblies)
  {
    var references = referencedAssemblies.Select(it => MetadataReference.CreateFromFile(it.Location));
    var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary);
    var assemblyName = "_" + Guid.NewGuid().ToString("D");
    var syntaxTrees = new SyntaxTree[] { CSharpSyntaxTree.ParseText(text) };
    var compilation = CSharpCompilation.Create(assemblyName, syntaxTrees, references, options);
    using var stream = new MemoryStream();
    var compilationResult = compilation.Emit(stream);
    if (compilationResult.Success)
    {
      stream.Seek(0, SeekOrigin.Begin);
      return Assembly.Load(stream.ToArray());
    }
    throw new InvalidOperationException("Compilation error");
  }
}

三、自定义IActionDescriptorProvider

解决了针对提供源代码的动态编译问题之后,我们可以获得需要注册的Controller类型,那么如何将它注册MVC应用上呢?要回答这个问题,我们得对MVC框架的执行原理有一个大致的了解:ASP.NET Core通过一个由服务器和若干中间件构成的管道来处理请求,MVC框架建立在通过EndpointRoutingMiddleware和EndpointMiddleare这两个中间件构成的终结点路由系统上。此路由系统维护着一组路由终结点,该终结点体现为一个路由模式(Route Pattern)与对应处理器(通过RequestDelegate委托表示)之间的映射。

由于针对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));
}

四、让应用感知到变化

DynamicActionProvider 解决了将提供的源代码向对应ActionDescriptor列表的转换,但是MVC默认情况下对提供的ActionDescriptor对象进行了缓存。如果框架能够使用新的ActionDescriptor对象,需要告诉它当前应用提供的ActionDescriptor列表发生了改变,而这可以利用自定义的IActionDescriptorChangeProvider来实现。为此我们定义了如下这个DynamicChangeTokenProvider类型,该类型实现了IActionDescriptorChangeProvider接口,并利用GetChangeToken方法返回IChangeToken对象通知MVC框架当前的ActionDescriptor已经发生改变。从实现实现代码可以看出,当我们调用NotifyChanges方法的时候,状态改变通知会被发出去。

public class DynamicChangeTokenProvider : IActionDescriptorChangeProvider
{
  private CancellationTokenSource _source;
  private CancellationChangeToken _token;
  public DynamicChangeTokenProvider()
  {
    _source = new CancellationTokenSource();
    _token = new CancellationChangeToken(_source.Token);
  }
  public IChangeToken GetChangeToken() => _token;

  public void NotifyChanges()
  {
    var old = Interlocked.Exchange(ref _source, new CancellationTokenSource());
    _token = new CancellationChangeToken(_source.Token);
    old.Cancel();
  }
}

五、应用构建

到目前为止,核心的两个类型DynamicActionProvider和DynamicChangeTokenProvider已经定义好了,接下来我们按照如下的方式将它们注册到MVC应用的依赖注入框架中。

public class Program
{
  public static void Main()
  {

    Host.CreateDefaultBuilder()
      .ConfigureWebHostDefaults(web => web
        .ConfigureServices(svcs => svcs
          .AddSingleton<ICompiler, Compiler>()
          .AddSingleton<DynamicActionProvider>()
          .AddSingleton<DynamicChangeTokenProvider>()
          .AddSingleton<IActionDescriptorProvider>(provider => provider.GetRequiredService<DynamicActionProvider>())
          .AddSingleton<IActionDescriptorChangeProvider>(provider => provider.GetRequiredService<DynamicChangeTokenProvider>())
          .AddRouting().AddControllersWithViews())
        .Configure(app => app
          .UseRouting()
          .UseEndpoints(endpoints => endpoints.MapControllerRoute(
            name: default,
            pattern: "{controller}/{action}"
            ))))
      .Build()
      .Run();
  }
}

然后我们定义了如下这个HomeController。针对GET请求的Index方法会将上图所示的视图呈现出来。当我们点击“Register”按钮之后,提交的源代码会通过针对POST请求的Index方法进行处理。如下面的代码片段所示,在将将提交的源代码作为参数调用了DynamicActionProvider对象的 AddControllers方法之后,我们调用了DynamicChangeTokenProvider对象的 NotifyChanges方法。

public class HomeController : Controller
{

  [HttpGet("/")]
  public IActionResult Index() => View();

  [HttpPost("/")]
  public IActionResult Index(
    string source,
    [FromServices]DynamicActionProvider actionProvider,
    [FromServices] DynamicChangeTokenProvider tokenProvider)
  {
    try
    {
      actionProvider.AddControllers(source);
      tokenProvider.NotifyChanges();
      return Content("OK");
    }
    catch (Exception ex)
    {
      return Content(ex.Message);
    }
  }
}

如下所示的是View的定义。

<html>
<body>
  <form method="post">
    <textarea name="source" cols="50" rows="10">Define your controller here...</textarea>
    <br/>
    <button type="submit">Register</button>
  </form>
</body>
</html>

六、换一种实现方式

接下来我们提供一种更加简单的解决方案。通过上面的介绍我们知道,用来描述Action方法的ActionDescriptor列表是由一组IActionDescriptorProvider对象提供的,对于针对Controller的MVC编程模型(另一种是针对Razor Page的编程模型)来说,对应的实现类型为ControllerActionDescriptorProvider。

当ControllerActionDescriptorProvider在提供对应ActionDescriptor对象之前,会从作为当前应用组成部分(ApplicationPart)的程序集中解析出所有Controller类型。如果我们能够让动态提供给源代码编程生成的程序集成为其合法的组成部分,那么我们面对的问题自然就能迎刃而解。添加应用组成部分其实很简单,我们只需要按照如下的方式调用ApplicationPartManager对象的Add方法就可以了。为了让MVC框架感知到提供的ActionDescriptor列表已经发生改变,我们还是需要调用DynamicChangeTokenProvider对象的NotifyChanges方法。

public class HomeController : Controller
{

  [HttpGet("/")]
  public IActionResult Index() => View();

  [HttpPost("/")]
  public IActionResult Index(string source,
    [FromServices] ApplicationPartManager manager,
    [FromServices] ICompiler compiler,
    [FromServices] DynamicChangeTokenProvider tokenProvider)
  {
    try
    {
      manager.ApplicationParts.Add(new AssemblyPart(compiler.Compile(source, Assembly.Load(new AssemblyName("System.Runtime")),
        typeof(object).Assembly,
        typeof(ControllerBase).Assembly,
        typeof(Controller).Assembly)));
      tokenProvider.NotifyChanges();
      return Content("OK");
    }
    catch (Exception ex)
    {
      return Content(ex.Message);
    }
  }
}

由于我们不在需要自定义的DynamicActionProvider,自然也就不需要对应的服务注册了。

public class Program
{
  public static void Main()
  {

    Host.CreateDefaultBuilder()
      .ConfigureWebHostDefaults(web => web
        .ConfigureServices(svcs => svcs
          .AddSingleton<ICompiler, Compiler>()
          .AddSingleton<DynamicChangeTokenProvider>()
          .AddSingleton<IActionDescriptorChangeProvider>(provider => provider.GetRequiredService<DynamicChangeTokenProvider>())
          .AddRouting().AddControllersWithViews())
        .Configure(app => app
          .UseRouting()
          .UseEndpoints(endpoints => endpoints.MapControllerRoute(
            name: default,
            pattern: "{controller}/{action}"
            ))))
      .Build()
      .Run();
  }
}

七、这其实不是一个小问题

有人可能觉得上面我们所做的好像只是一些“奇淫巧计”,其实不然,这里涉及到MVC应用一个重大的主题,我个人将它称为“动态模块化”。对于一个面向Controller的MVC应用来说,Controller类型是应用基本的组成单元,所以其应用模型(通过上面提到的ApplicationModel对象表示)呈现出这样的结构:Application->Controller->Action。如果一个MVC应用需要拆分为多个独立的模块,意味着需要将Controller类型分别定义在不同的程序集中。为了让这些程序集成为应用的一个有效组成部分,程序集需要封装成ApplicationPart对象并利用ApplicationPartManager进行注册。针对应用组成部分的注册不是静态的(在应用启动的时候进行),而是动态的(在运行的任意时刻都可以进行)。

从提供的代码来看,两种解决方案所需的成本都是很少的,但是能否找到解决方案,取决于我们是否对MVC框架的架构设计和实现原理的了解。对于很大一部分.NET 开发人员来说,他们的知识领域大都仅限于对基本编程模型的了解,他们可能知道Controller的所有API,也了解各种Razor View的各种定义方式,能够熟练使用各种过滤器已经算是很不错的了。但是这是不够的。

到此这篇关于ASP.NET Core MVC如何实现运行时动态定义Controller类型的文章就介绍到这了,更多相关ASP.NET Core MVC动态定义Controller内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!

作者:蒋金楠
微信公众账号:大内老A
微博:www.weibo.com/artech

    您感兴趣的教程

    在docker中安装mysql详解

    本篇文章主要介绍了在docker中安装mysql详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编...

    详解 安装 docker mysql

    win10中文输入法仅在桌面显示怎么办?

    win10中文输入法仅在桌面显示怎么办?

    win10系统使用搜狗,QQ输入法只有在显示桌面的时候才出来,在使用其他程序输入框里面却只能输入字母数字,win10中...

    win10 中文输入法

    一分钟掌握linux系统目录结构

    这篇文章主要介绍了linux系统目录结构,通过结构图和多张表格了解linux系统目录结构,感兴趣的小伙伴们可以参考一...

    结构 目录 系统 linux

    PHP程序员玩转Linux系列 Linux和Windows安装

    这篇文章主要为大家详细介绍了PHP程序员玩转Linux系列文章,Linux和Windows安装nginx教程,具有一定的参考价值,感兴趣...

    玩转 程序员 安装 系列 PHP

    win10怎么安装杜比音效Doby V4.1 win10安装杜

    第四代杜比®家庭影院®技术包含了一整套协同工作的技术,让PC 发出清晰的环绕声同时第四代杜比家庭影院技术...

    win10杜比音效

    纯CSS实现iOS风格打开关闭选择框功能

    这篇文章主要介绍了纯CSS实现iOS风格打开关闭选择框,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作...

    css ios c

    Win7如何给C盘扩容 Win7系统电脑C盘扩容的办法

    Win7如何给C盘扩容 Win7系统电脑C盘扩容的

    Win7给电脑C盘扩容的办法大家知道吗?当系统分区C盘空间不足时,就需要给它扩容了,如果不管,C盘没有足够的空间...

    Win7 C盘 扩容

    百度推广竞品词的投放策略

    SEM是基于关键词搜索的营销活动。作为推广人员,我们所做的工作,就是打理成千上万的关键词,关注它们的质量度...

    百度推广 竞品词

    Visual Studio Code(vscode) git的使用教程

    这篇文章主要介绍了详解Visual Studio Code(vscode) git的使用,小编觉得挺不错的,现在分享给大家,也给大家做个参考。...

    教程 Studio Visual Code git

    七牛云储存创始人分享七牛的创立故事与

    这篇文章主要介绍了七牛云储存创始人分享七牛的创立故事与对Go语言的应用,七牛选用Go语言这门新兴的编程语言进行...

    七牛 Go语言

    Win10预览版Mobile 10547即将发布 9月19日上午

    微软副总裁Gabriel Aul的Twitter透露了 Win10 Mobile预览版10536即将发布,他表示该版本已进入内部慢速版阶段,发布时间目...

    Win10 预览版

    HTML标签meta总结,HTML5 head meta 属性整理

    移动前端开发中添加一些webkit专属的HTML5头部标签,帮助浏览器更好解析HTML代码,更好地将移动web前端页面表现出来...

    移动端html5模拟长按事件的实现方法

    这篇文章主要介绍了移动端html5模拟长按事件的实现方法的相关资料,小编觉得挺不错的,现在分享给大家,也给大家...

    移动端 html5 长按

    HTML常用meta大全(推荐)

    这篇文章主要介绍了HTML常用meta大全(推荐),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参...

    cdr怎么把图片转换成位图? cdr图片转换为位图的教程

    cdr怎么把图片转换成位图? cdr图片转换为

    cdr怎么把图片转换成位图?cdr中插入的图片想要转换成位图,该怎么转换呢?下面我们就来看看cdr图片转换为位图的...

    cdr 图片 位图

    win10系统怎么录屏?win10系统自带录屏详细教程

    win10系统怎么录屏?win10系统自带录屏详细

    当我们是使用win10系统的时候,想要录制电脑上的画面,这时候有人会想到下个第三方软件,其实可以用电脑上的自带...

    win10 系统自带录屏 详细教程

    + 更多教程 +
    ASP编程JSP编程PHP编程.NET编程python编程