[ASP.NET Core MVC] 如何实现运行时动态定义Controller类型?
昨天有個朋友在微信上問我一個問題:他希望通過動態腳本的形式實現對ASP.NET Core MVC應用的擴展,比如在程序運行過程中上傳一段C#腳本將其中定義的Controller類型注冊到應用中,問我是否有好解決方案。這是一個挺有意思的問題,我們可以通過兩種方案實現了這個需求。
01
實現效果
我們先來看看實現的效果。如下所示的是一個MVC應用的主頁,我們可以在文本框中通過編寫C#代碼定義一個有效的Controller類型,然后點擊“Register”按鈕,定義的Controller類型將自動注冊到MVC應用中
由于我們采用了針對模板為“{controller}/{action}”的約定路由,所以我們采用路徑“/foo/bar”就可以訪問上圖中定義在FooController中的Action方法Bar,下圖證實了這一點。
02
動態編譯源代碼
要實現如上所示的“針對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");} }03
DynamicActionProvider?
解決了針對提供源代碼的動態編譯問題之后,我們可以獲得需要注冊的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)); }總結
以上是生活随笔為你收集整理的[ASP.NET Core MVC] 如何实现运行时动态定义Controller类型?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 让我的 .NET Core 博客系统支持
- 下一篇: 【视频回放与课件】Build your