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

时间:2022-09-07 11:36:29

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

一、实现的效果

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

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

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

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

二、动态编译源代码

要实现如上所示的“针对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进行注册。针对应用组成部分的注册不是静态的(在应用启动的时候进行),而是动态的(在运行的任意时刻都可以进行)。

八、再扯几句

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

正如我在《ASP.NET Core 3框架揭秘》中所说,“不论我们从事何种层次的工作,最根本的目的只有一个,那就是解决问题。解决方案分两种,一种叫做“扬汤止沸”,另一种被称为“釜底抽薪”。如果之关注于编程模型,我们只能看到锅里的滚水,只有对框架具有了深层次的了解,我们才能看到锅下面的薪火。

顺便做一下广告:《ASP.NET Core 3框架揭秘》京东100-50的活动(这应该是本书历史上的最低价格)还有最后一天(4月7日),如果有需要,请抓住最后的机会。如果觉得本书对你确实有帮助,希望能够为本书投票(京东V阅读时代投票截止到4月20日,每人每天有三次投票机会,参与投票有机会获得满100-10,200-20,400-50优惠券,以及面值100/500/1000元京东全品类电子E卡)。

扩展阅读

通过极简模拟框架让你了解ASP.NET Core MVC框架的设计与实现[上篇]:路由整合
通过极简模拟框架让你了解ASP.NET Core MVC框架的设计与实现[中篇]: 请求响应
通过极简模拟框架让你了解ASP.NET Core MVC框架的设计与实现[下篇]:参数绑定

[ASP.NET Core MVC] 如何实现运行时动态定义Controller类型?的更多相关文章

  1. ASP&period;NET MVC和ASP&period;NET Core MVC中获取当前URL&sol;Controller&sol;Action (转载)

    ASP.NET MVC 一.获取URL(ASP.NET通用): [1]获取完整url(协议名+域名+虚拟目录名+文件名+参数) string url=Request.Url.ToString(); [ ...

  2. ASP&period;NET Core MVC中Controller的Action如何直接使用Response&period;Body的Stream流输出数据

    在ASP.NET Core MVC中,我们有时候需要在Controller的Action中直接输出数据到Response.Body这个Stream流中,例如如果我们要输出一个很大的文件到客户端浏览器让 ...

  3. ASP&period;NET Core MVC的路由参数中&colon;exists后缀有什么作用,顺便谈谈路由匹配机制

    我们在ASP.NET Core MVC中如果要启用Area功能,那么会看到在Startup类的Configure方法中是这么定义Area的路由的: app.UseMvc(routes => { ...

  4. C&num; 在运行时动态创建类型

    C# 在运行时动态的创建类型,这里是通过动态生成C#源代码,然后通过编译器编译成程序集的方式实现动态创建类型 public static Assembly NewAssembly() { //创建编译 ...

  5. 从零开始实现ASP&period;NET Core MVC的插件式开发&lpar;三&rpar; - 如何在运行时启用组件

    标题:从零开始实现ASP.NET Core MVC的插件式开发(三) - 如何在运行时启用组件 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/112 ...

  6. ASP&period;NET Core 中文文档 第二章 指南(2)用 Visual Studio 和 ASP&period;NET Core MVC 创建首个 Web API

    原文:Building Your First Web API with ASP.NET Core MVC and Visual Studio 作者:Mike Wasson 和 Rick Anderso ...

  7. ASP&period;NET Core 中文文档 第四章 MVC(01)ASP&period;NET Core MVC 概览

    原文:Overview of ASP.NET Core MVC 作者:Steve Smith 翻译:张海龙(jiechen) 校对:高嵩 ASP.NET Core MVC 是使用模型-视图-控制器(M ...

  8. ASP&period;NET Core - ASP&period;NET Core MVC 的功能划分

    概述 大型 Web 应用比小型 Web 应用需要更好的组织.在大型应用中,ASP.NET MVC(和 Core MVC)所用的默认组织结构开始成为你的负累.你可以使用两种简单的技术来更新组织方法并及时 ...

  9. 创建ASP&period;NET Core MVC应用程序&lpar;5&rpar;-添加查询功能 &amp&semi; 新字段

    创建ASP.NET Core MVC应用程序(5)-添加查询功能 & 新字段 添加查询功能 本文将实现通过Name查询用户信息. 首先更新GetAll方法以启用查询: public async ...

随机推荐

  1. APKTool 提取APK文件的资源

    APK文件本身是一个压缩包,直接用解压工具即可打开,但里面的文件都已被编码为二进制文件格式,不能直接看,比如程序描述文件AndroidManifest.xml. 使用apktool工具可以将这些文件解 ...

  2. &lbrack;转载&rsqb;VS2012程序打包部署详解

    上篇博客把收费系统的总体设计进行了一遍讲解,讲解的同时掺杂了些有关.NET编译机制的总结.程序编写测试完成后接下来我们要做的是打包部署程序,但VS2012让人心痛的是没有了打包工具.不知道出于什么原因 ...

  3. 转 批处理 &percnt;~dp0的意义

    http://nealcai.iteye.com/blog/1685192 http://blog.csdn.net/caz28/article/details/7448677 http://stac ...

  4. 根据文字计算出label的高度

    ios7.0之前用: [strtestsizeWithFont:ContentFontconstrainedToSize:CGSizeMake(ScreenWeight -20, 1000) line ...

  5. 蓝桥杯-算法训练--ALGO-6 安慰奶牛

    问题描述Farmer John变得非常懒,他不想再继续维护供奶牛之间供通行的道路.道路被用来连接N个牧场,牧场被连续地编号为1到N.每一个牧场都是一个奶牛的家.FJ计划除去P条道路中尽可能多的道路,但 ...

  6. css那些事(一)

    一.内边框padding和外边框margin属性缩写 内外边框有四个属性:padding-top,padding-right,padding-bottom,padding-left;margin-to ...

  7. 2018-2019-2 20165313 《网络对抗技术》 Exp6 信息搜集与漏洞扫描

    一.实践目标 掌握信息搜集的最基础技能与常用工具的使用方法. 二.实践内容. (1)各种搜索技巧的应用 (2)DNS IP注册信息的查询 (3)基本的扫描技术:主机发现.端口扫描.OS及服务版本探测. ...

  8. lua 复制table

    cocos2d-lua提供了复制方法clone(),源码如下: function clone(object) local lookup_table = {} local function _copy( ...

  9. hdu 3183 A Magic Lamp 【RMQ】

    <题目链接> <转载于 >>>  > 题目大意: 给出一个长度不超过1000位的数,求删去m位数字以后形成的最小的数字是多少. 解题分析: 分析:我们可以把题 ...

  10. PythonStudy——Python 注释规范

    注释规范:   什么是注释?  注释:不会被python解释器解释执行,是提供给开发者阅读代码的提示 单行注释: # 开头的语句 多行注释:出现在文件最上方,用''' '''包裹的语句   Pycha ...