ASP.NET Core MVC如何實(shí)現(xiàn)運(yùn)行時(shí)動(dòng)態(tài)定義Controller類(lèi)型
昨天有個(gè)朋友在微信上問(wèn)我一個(gè)問(wèn)題:他希望通過(guò)動(dòng)態(tài)腳本的形式實(shí)現(xiàn)對(duì)ASP.NET Core MVC應(yīng)用的擴(kuò)展,比如在程序運(yùn)行過(guò)程中上傳一段C#腳本將其中定義的Controller類(lèi)型注冊(cè)到應(yīng)用中,問(wèn)我是否有好解決方案。我當(dāng)時(shí)在外邊,回復(fù)不太方便,所以只給他說(shuō)了兩個(gè)接口/類(lèi)型:IActionDescriptorProvider和ApplicationPartManager。這是一個(gè)挺有意思的問(wèn)題,所以回家后通過(guò)兩種方案實(shí)現(xiàn)了這個(gè)需求。源代碼從這里下載。
一、實(shí)現(xiàn)的效果
我們先來(lái)看看實(shí)現(xiàn)的效果。如下所示的是一個(gè)MVC應(yīng)用的主頁(yè),我們可以在文本框中通過(guò)編寫(xiě)C#代碼定義一個(gè)有效的Controller類(lèi)型,然后點(diǎn)擊“Register”按鈕,定義的Controller類(lèi)型將自動(dòng)注冊(cè)到MVC應(yīng)用中
由于我們采用了針對(duì)模板為“{controller}/{action}”的約定路由,所以我們采用路徑“/foo/bar”就可以訪問(wèn)上圖中定義在FooController中的Action方法Bar,下圖證實(shí)了這一點(diǎn)。
二、動(dòng)態(tài)編譯源代碼
要實(shí)現(xiàn)如上所示的“針對(duì)Controller類(lèi)型的動(dòng)態(tài)注冊(cè)”,首先需要解決的是針對(duì)提供源代碼的動(dòng)態(tài)編譯問(wèn)題,我們知道這個(gè)可以利用Roslyn來(lái)解決。具體來(lái)說(shuō),我們定義了如下這個(gè)ICompiler接口,它的Compile方法將會(huì)對(duì)參數(shù)sourceCode提供的源代碼進(jìn)行編譯。該方法返回源代碼動(dòng)態(tài)編譯生成的程序集,它的第二個(gè)參數(shù)代表引用的程序集。
public interface ICompiler { Assembly Compile(string text, params Assembly[] referencedAssemblies); }
如下所示的Compiler類(lèi)型是對(duì)ICompiler接口的默認(rèn)實(shí)現(xiàn)。
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
解決了針對(duì)提供源代碼的動(dòng)態(tài)編譯問(wèn)題之后,我們可以獲得需要注冊(cè)的Controller類(lèi)型,那么如何將它注冊(cè)MVC應(yīng)用上呢?要回答這個(gè)問(wèn)題,我們得對(duì)MVC框架的執(zhí)行原理有一個(gè)大致的了解:ASP.NET Core通過(guò)一個(gè)由服務(wù)器和若干中間件構(gòu)成的管道來(lái)處理請(qǐng)求,MVC框架建立在通過(guò)EndpointRoutingMiddleware和EndpointMiddleare這兩個(gè)中間件構(gòu)成的終結(jié)點(diǎn)路由系統(tǒng)上。此路由系統(tǒng)維護(hù)著一組路由終結(jié)點(diǎn),該終結(jié)點(diǎn)體現(xiàn)為一個(gè)路由模式(Route Pattern)與對(duì)應(yīng)處理器(通過(guò)RequestDelegate委托表示)之間的映射。
由于針對(duì)MVC應(yīng)用的請(qǐng)求總是指向某一個(gè)Action,所以MVC框架提供的路由整合機(jī)制體現(xiàn)在為每一個(gè)Action創(chuàng)建一個(gè)或者多個(gè)終結(jié)點(diǎn)(同一個(gè)Action方法可以注冊(cè)多個(gè)路由)。針對(duì)Action方法的路由終結(jié)點(diǎn)是根據(jù)描述Action方法的ActionDescriptor對(duì)象構(gòu)建而成的。至于ActionDescriptor對(duì)象,則是通過(guò)注冊(cè)的一組IActionDescriptorProvider對(duì)象來(lái)提供的,那么我們的問(wèn)題就迎刃而解:通過(guò)注冊(cè)自定義的IActionDescriptorProvider從動(dòng)態(tài)定義的Controller類(lèi)型中解析出合法的Action方法,并創(chuàng)建對(duì)應(yīng)的ActionDescriptor對(duì)象即可。
那么ActionDescriptor如何創(chuàng)建呢?我們能想到簡(jiǎn)單的方式是調(diào)用如下這個(gè)Build方法。針對(duì)該方法的調(diào)用存在兩個(gè)問(wèn)題:第一,ControllerActionDescriptorBuilder是一個(gè)內(nèi)部(internal)類(lèi)型,我們指定以反射的方式調(diào)用這個(gè)方法,第二,這個(gè)方法接受一個(gè)類(lèi)型為ApplicationModel的參數(shù)。
internal static class ControllerActionDescriptorBuilder { public static IList<ControllerActionDescriptor> Build(ApplicationModel application); }
ApplicationModel類(lèi)型涉及到一個(gè)很大的主題:MVC應(yīng)用模型,目前我們現(xiàn)在只關(guān)注如何創(chuàng)建這個(gè)對(duì)象。表示MVC應(yīng)用模型的ApplicationModel對(duì)象是通過(guò)對(duì)應(yīng)的工廠ApplicationModelFactory創(chuàng)建的。這個(gè)工廠會(huì)自動(dòng)注冊(cè)到MVC應(yīng)用的依賴注入框架中,但是這依然是一個(gè)內(nèi)部(內(nèi)部)類(lèi)型,所以還得反射。
internal class ApplicationModelFactory { public ApplicationModel CreateApplicationModel(IEnumerable<TypeInfo> controllerTypes); }
我們定義了如下這個(gè)DynamicActionProvider類(lèi)型實(shí)現(xiàn)了IActionDescriptorProvider接口。針對(duì)提供的源代碼向ActionDescriptor列表的轉(zhuǎn)換體現(xiàn)在AddControllers方法中:它利用ICompiler對(duì)象編譯源代碼,并在生成的程序集中解析出有效的Controller類(lèi)型,然后利用ApplicationModelFactory創(chuàng)建出代表應(yīng)用模型的ApplicationModel對(duì)象,后者作為參數(shù)調(diào)用ControllerActionDescriptorBuilder的靜態(tài)方法Build創(chuàng)建出描述所有Action方法的ActionDescriptor對(duì)象。
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)); }
四、讓?xiě)?yīng)用感知到變化
DynamicActionProvider 解決了將提供的源代碼向?qū)?yīng)ActionDescriptor列表的轉(zhuǎn)換,但是MVC默認(rèn)情況下對(duì)提供的ActionDescriptor對(duì)象進(jìn)行了緩存。如果框架能夠使用新的ActionDescriptor對(duì)象,需要告訴它當(dāng)前應(yīng)用提供的ActionDescriptor列表發(fā)生了改變,而這可以利用自定義的IActionDescriptorChangeProvider來(lái)實(shí)現(xiàn)。為此我們定義了如下這個(gè)DynamicChangeTokenProvider類(lèi)型,該類(lèi)型實(shí)現(xiàn)了IActionDescriptorChangeProvider接口,并利用GetChangeToken方法返回IChangeToken對(duì)象通知MVC框架當(dāng)前的ActionDescriptor已經(jīng)發(fā)生改變。從實(shí)現(xiàn)實(shí)現(xiàn)代碼可以看出,當(dāng)我們調(diào)用NotifyChanges方法的時(shí)候,狀態(tài)改變通知會(huì)被發(fā)出去。
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(); } }
五、應(yīng)用構(gòu)建
到目前為止,核心的兩個(gè)類(lèi)型DynamicActionProvider和DynamicChangeTokenProvider已經(jīng)定義好了,接下來(lái)我們按照如下的方式將它們注冊(cè)到MVC應(yīng)用的依賴注入框架中。
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(); } }
然后我們定義了如下這個(gè)HomeController。針對(duì)GET請(qǐng)求的Index方法會(huì)將上圖所示的視圖呈現(xiàn)出來(lái)。當(dāng)我們點(diǎn)擊“Register”按鈕之后,提交的源代碼會(huì)通過(guò)針對(duì)POST請(qǐng)求的Index方法進(jìn)行處理。如下面的代碼片段所示,在將將提交的源代碼作為參數(shù)調(diào)用了DynamicActionProvider對(duì)象的 AddControllers方法之后,我們調(diào)用了DynamicChangeTokenProvider對(duì)象的 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>
六、換一種實(shí)現(xiàn)方式
接下來(lái)我們提供一種更加簡(jiǎn)單的解決方案。通過(guò)上面的介紹我們知道,用來(lái)描述Action方法的ActionDescriptor列表是由一組IActionDescriptorProvider對(duì)象提供的,對(duì)于針對(duì)Controller的MVC編程模型(另一種是針對(duì)Razor Page的編程模型)來(lái)說(shuō),對(duì)應(yīng)的實(shí)現(xiàn)類(lèi)型為ControllerActionDescriptorProvider。
當(dāng)ControllerActionDescriptorProvider在提供對(duì)應(yīng)ActionDescriptor對(duì)象之前,會(huì)從作為當(dāng)前應(yīng)用組成部分(ApplicationPart)的程序集中解析出所有Controller類(lèi)型。如果我們能夠讓動(dòng)態(tài)提供給源代碼編程生成的程序集成為其合法的組成部分,那么我們面對(duì)的問(wèn)題自然就能迎刃而解。添加應(yīng)用組成部分其實(shí)很簡(jiǎn)單,我們只需要按照如下的方式調(diào)用ApplicationPartManager對(duì)象的Add方法就可以了。為了讓MVC框架感知到提供的ActionDescriptor列表已經(jīng)發(fā)生改變,我們還是需要調(diào)用DynamicChangeTokenProvider對(duì)象的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,自然也就不需要對(duì)應(yīng)的服務(wù)注冊(cè)了。
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(); } }
七、這其實(shí)不是一個(gè)小問(wèn)題
有人可能覺(jué)得上面我們所做的好像只是一些“奇淫巧計(jì)”,其實(shí)不然,這里涉及到MVC應(yīng)用一個(gè)重大的主題,我個(gè)人將它稱(chēng)為“動(dòng)態(tài)模塊化”。對(duì)于一個(gè)面向Controller的MVC應(yīng)用來(lái)說(shuō),Controller類(lèi)型是應(yīng)用基本的組成單元,所以其應(yīng)用模型(通過(guò)上面提到的ApplicationModel對(duì)象表示)呈現(xiàn)出這樣的結(jié)構(gòu):Application->Controller->Action。如果一個(gè)MVC應(yīng)用需要拆分為多個(gè)獨(dú)立的模塊,意味著需要將Controller類(lèi)型分別定義在不同的程序集中。為了讓這些程序集成為應(yīng)用的一個(gè)有效組成部分,程序集需要封裝成ApplicationPart對(duì)象并利用ApplicationPartManager進(jìn)行注冊(cè)。針對(duì)應(yīng)用組成部分的注冊(cè)不是靜態(tài)的(在應(yīng)用啟動(dòng)的時(shí)候進(jìn)行),而是動(dòng)態(tài)的(在運(yùn)行的任意時(shí)刻都可以進(jìn)行)。
從提供的代碼來(lái)看,兩種解決方案所需的成本都是很少的,但是能否找到解決方案,取決于我們是否對(duì)MVC框架的架構(gòu)設(shè)計(jì)和實(shí)現(xiàn)原理的了解。對(duì)于很大一部分.NET 開(kāi)發(fā)人員來(lái)說(shuō),他們的知識(shí)領(lǐng)域大都僅限于對(duì)基本編程模型的了解,他們可能知道Controller的所有API,也了解各種Razor View的各種定義方式,能夠熟練使用各種過(guò)濾器已經(jīng)算是很不錯(cuò)的了。但是這是不夠的。
到此這篇關(guān)于ASP.NET Core MVC如何實(shí)現(xiàn)運(yùn)行時(shí)動(dòng)態(tài)定義Controller類(lèi)型的文章就介紹到這了,更多相關(guān)ASP.NET Core MVC動(dòng)態(tài)定義Controller內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
作者:蔣金楠
微信公眾賬號(hào):大內(nèi)老A
微博:www.weibo.com/artech
相關(guān)文章
刪除DataTable重復(fù)列,只刪除其中的一列重復(fù)行的解決方法
刪除DataTable重復(fù)列,只刪除其中的一列重復(fù)行,下面的方法就可以,也許有更好的方法,希望大家多多指教2013-02-02.NET性能調(diào)優(yōu)之一:ANTS Performance Profiler的使用介紹
本系列文章主要會(huì)介紹一些.NET性能調(diào)優(yōu)的工具、Web性能優(yōu)化的規(guī)則(如YSlow)及方法等等內(nèi)容。成文前最不希望看到的就是園子里不間斷的“哪個(gè)語(yǔ)言好,哪個(gè)語(yǔ)言性能高”的爭(zhēng)論,不多說(shuō),真正的明白人都應(yīng)該知道這樣的爭(zhēng)論有沒(méi)有意義,希望我們能從實(shí)際性能優(yōu)化的角度去討論問(wèn)題2013-01-01Asp.net Core中實(shí)現(xiàn)自定義身份認(rèn)證的示例代碼
這篇文章主要介紹了Asp.net Core中實(shí)現(xiàn)自定義身份認(rèn)證的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05Asp.Net Core WebAPI使用Swagger時(shí)API隱藏和分組詳解
這篇文章主要給大家介紹了關(guān)于Asp.Net Core WebAPI使用Swagger時(shí)API隱藏和分組的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Asp.Net Core具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04防SQL注入 生成參數(shù)化的通用分頁(yè)查詢語(yǔ)句
前些時(shí)間看了玉開(kāi)兄的“如此高效通用的分頁(yè)存儲(chǔ)過(guò)程是帶有sql注入漏洞的”這篇文章,才突然想起某個(gè)項(xiàng)目也是使用了累似的通用分頁(yè)存儲(chǔ)過(guò)程。2010-07-07Gridview使用CheckBox全選與單選采用js實(shí)現(xiàn)同時(shí)高亮顯示選擇行
Gridview使用CheckBox單選與全選功能再次進(jìn)行簡(jiǎn)單演示,選中的行,使用高亮顯示,讓用戶一目了然看到哪一行被選擇了,在項(xiàng)目中很實(shí)用的,開(kāi)發(fā)中的朋友們可要考慮一下哦2013-01-01Asp.net的GridView控件實(shí)現(xiàn)單元格可編輯方便用戶使用
考慮到用戶使用方便,減少?gòu)棾鲰?yè)面,采用點(diǎn)“編輯”按鈕無(wú)需彈出頁(yè)面直接當(dāng)前行的單元格內(nèi)容就能編輯,思路及代碼如下,有此需求的朋友可以參考下,希望對(duì)大家有所幫助2013-08-08設(shè)置ASP.NET頁(yè)面不被緩存(客戶端/服務(wù)器端取消緩存方法)
設(shè)置頁(yè)面不被緩存:客戶端取消緩存、服務(wù)器具端取消緩存的具體實(shí)現(xiàn)代碼如下感興趣的朋友可以參考下哈,希望對(duì)大家有所幫助2013-06-06ASP.NET操作MySql數(shù)據(jù)庫(kù)的實(shí)例代碼講解
這篇文章主要介紹了ASP.NET操作MySql數(shù)據(jù)庫(kù)的實(shí)例代碼講解,需要的朋友可以參考下2016-12-12