標題:從零開始實現ASP.NET Core MVC的插件式開發(三) - 如何在運行時啓用組件
做者:Lamond Lu
地址:http://www.javashuo.com/article/p-fmzvvncn-eq.html
源代碼:https://github.com/lamondlu/DynamicPluginshtml
在前面兩篇中,我爲你們演示瞭如何使用Application Part動態加載控制器和視圖,以及如何建立插件模板來簡化操做。
在上一篇寫完以後,我忽然想到了一個問題,若是像前兩篇所設計那個來構建一個插件式系統,會有一個很嚴重的問題,即git
當你添加一個插件以後,整個程序不能馬上啓用該插件,只有當重啓整個ASP.NET Core應用以後,才能正確的加載插件。由於全部插件的加載都是在程序啓動時
ConfigureService
方法中配置的。github
這種方式的插件系統會很難用,咱們指望的效果是在運行時動態啓用和禁用插件,那麼有沒有什麼解決方案呢?答案是確定的。下面呢,我將一步一步說明一下本身的思路、編碼中遇到的問題,以及這些問題的解決方案。json
爲了完成這個功能,我走了許多彎路,當前這個方案可能不是最好的,可是確實是一個可行的方案,若是你們有更好的方案,咱們能夠一塊兒討論一下。c#
當遇到這個問題的時候,個人第一思路就是將ApplicationPartManager
加載插件庫的代碼移動到某個Action中。因而我就在主站點中建立了一個PluginsController
, 並在啓用添加了一個名爲Enable
的Action方法。app
public class PluginsController : Controller { public IActionResult Enable() { var assembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "DemoPlugin1\\DemoPlugin1.dll"); var viewAssembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "DemoPlugin1\\DemoPlugin1.Views.dll"); var viewAssemblyPart = new CompiledRazorAssemblyPart(viewAssembly); var controllerAssemblyPart = new AssemblyPart(assembly); _partManager.ApplicationParts.Add(controllerAssemblyPart); _partManager.ApplicationParts.Add(viewAssemblyPart); return Content("Enabled"); } }
修改代碼以後,運行程序,這裏咱們首先調用/Plugins/Enable
來嘗試激活組件,激活以後,咱們再次調用/Plugin1/HelloWorld
ide
這裏會發現程序返回了404, 即控制器和視圖沒有正確的激活。函數
這裏你可能有疑問,爲何會激活失敗呢?編碼
這裏的緣由是,只有當ASP.NET Core應用啓動時,纔會去ApplicationPart管理器中加載控制器與視圖的程序集,因此雖然新的控制器程序集在運行時被添加到了ApplicationPart
管理器中,可是ASP.NET Core不會自動進行更新操做,因此這裏咱們須要尋找一種方式可以讓ASP.NET Core從新加載控制器的方法。插件
經過查詢各類資料,我最終找到了一個切入點,在ASP.NET Core 2.2中有一個類是ActionDescriptorCollectionProvider
,它的子類DefaultActionDescriptorCollectionProvider
是用來配置Controller和Action的。
源代碼:
internal class DefaultActionDescriptorCollectionProvider : ActionDescriptorCollectionProvider { private readonly IActionDescriptorProvider[] _actionDescriptorProviders; private readonly IActionDescriptorChangeProvider[] _actionDescriptorChangeProviders; private readonly object _lock; private ActionDescriptorCollection _collection; private IChangeToken _changeToken; private CancellationTokenSource _cancellationTokenSource; private int _version = 0; public DefaultActionDescriptorCollectionProvider( IEnumerable<IActionDescriptorProvider> actionDescriptorProviders, IEnumerable<IActionDescriptorChangeProvider> actionDescriptorChangeProviders) { ... ChangeToken.OnChange( GetCompositeChangeToken, UpdateCollection); } public override ActionDescriptorCollection ActionDescriptors { get { Initialize(); return _collection; } } ... private IChangeToken GetCompositeChangeToken() { if (_actionDescriptorChangeProviders.Length == 1) { return _actionDescriptorChangeProviders[0].GetChangeToken(); } var changeTokens = new IChangeToken[_actionDescriptorChangeProviders.Length]; for (var i = 0; i < _actionDescriptorChangeProviders.Length; i++) { changeTokens[i] = _actionDescriptorChangeProviders[i].GetChangeToken(); } return new CompositeChangeToken(changeTokens); } ... private void UpdateCollection() { lock (_lock) { var context = new ActionDescriptorProviderContext(); for (var i = 0; i < _actionDescriptorProviders.Length; i++) { _actionDescriptorProviders[i].OnProvidersExecuting(context); } for (var i = _actionDescriptorProviders.Length - 1; i >= 0; i--) { _actionDescriptorProviders[i].OnProvidersExecuted(context); } var oldCancellationTokenSource = _cancellationTokenSource; _collection = new ActionDescriptorCollection( new ReadOnlyCollection<ActionDescriptor>(context.Results), _version++); _cancellationTokenSource = new CancellationTokenSource(); _changeToken = new CancellationChangeToken(_cancellationTokenSource.Token); oldCancellationTokenSource?.Cancel(); } } }
ActionDescriptors
屬性中記錄了當ASP.NET Core程序啓動後,匹配到的全部Controller/Action集合。ActionDescriptors
集合的。ChangeToken.OnChange(GetCompositeChangeToken,UpdateCollection)
。這裏程序會監聽一個Token對象,當這個Token對象發生變化時,就自動觸發UpdateCollection
方法。IActionDescriptorChangeProvider
接口對象組合而成的。因此這裏咱們就能夠經過自定義一個IActionDescriptorChangeProvider
接口對象,並在組件激活方法Enable中修改這個接口Token的方式,使DefaultActionDescriptorCollectionProvider
中的CompositeChangeToken
發生變化,從而實現控制器的從新裝載。
IActionDescriptorChangeProvider
在運行時激活控制器這裏咱們首先建立一個MyActionDescriptorChangeProvider
類,並讓它實現IActionDescriptorChangeProvider
接口
public class MyActionDescriptorChangeProvider : IActionDescriptorChangeProvider { public static MyActionDescriptorChangeProvider Instance { get; } = new MyActionDescriptorChangeProvider(); public CancellationTokenSource TokenSource { get; private set; } public bool HasChanged { get; set; } public IChangeToken GetChangeToken() { TokenSource = new CancellationTokenSource(); return new CancellationChangeToken(TokenSource.Token); } }
而後咱們須要在Startup.cs
的ConfigureServices
方法中,將MyActionDescriptorChangeProvider.Instance
屬性以單例的方式註冊到依賴注入容器中。
public void ConfigureServices(IServiceCollection services) { ... services.AddSingleton<IActionDescriptorChangeProvider>(MyActionDescriptorChangeProvider.Instance); services.AddSingleton(MyActionDescriptorChangeProvider.Instance); ... }
最後咱們在Enable
方法中經過兩行代碼來修改當前MyActionDescriptorChangeProvider
對象的Token。
public class PluginsController : Controller { public IActionResult Enable() { var assembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "DemoPlugin1\\DemoPlugin1.dll"); var viewAssembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "DemoPlugin1\\DemoPlugin1.Views.dll"); var viewAssemblyPart = new CompiledRazorAssemblyPart(viewAssembly); var controllerAssemblyPart = new AssemblyPart(assembly); _partManager.ApplicationParts.Add(controllerAssemblyPart); _partManager.ApplicationParts.Add(viewAssemblyPart); MyActionDescriptorChangeProvider.Instance.HasChanged = true; MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel(); return Content("Enabled"); } }
修改代碼以後從新運行程序,這裏咱們依然首先調用/Plugins/Enable
,而後再次調用/Plugin1/Helloworld
, 這時候你會發現Action被觸發了,只是沒有找到對應的Views。
經過以上的方式,咱們終於得到了在運行時加載插件控制器程序集的能力,可是插件的預編譯Razor視圖程序集沒有被正確加載,這就說明IActionDescriptorChangeProvider
只會觸發控制器的從新加載,不會觸發預編譯Razor視圖的從新加載。ASP.NET Core只會在整個應用啓動時,纔會加載插件的預編譯Razor程序集,因此咱們並無得到在運行時從新加載預編譯Razor視圖的能力。
針對這一點,我也查閱了好多資料,最終也沒有一個可行的解決方案,也許使用ASP.NET Core 3.0的Razor Runtime Compilation能夠實現,可是在ASP.NET Core 2.2版本,咱們尚未得到這種能力。
爲了越過這個難點,最終我仍是選擇了放棄預編譯Razor視圖,改用原始的Razor視圖。
由於在ASP.NET Core啓動時,咱們能夠在Startup.cs
的ConfigureServices
方法中配置Razor視圖引擎檢索視圖的規則。
這裏咱們能夠把每一個插件組織成ASP.NET Core MVC中一個Area, Area的名稱即插件的名稱, 這樣咱們就能夠將爲Razor視圖引擎的添加一個檢索視圖的規則,代碼以下
services.Configure<RazorViewEngineOptions>(o => { o.AreaViewLocationFormats.Add("/Modules/{2}/{1}/Views/{0}" + RazorViewEngine.ViewExtension); });
這裏{2}
表明Area名稱, {1}
表明Controller名稱, {0}
表明Action名稱。
這裏Modules是我從新建立的一個目錄,後續全部的插件都會放置在這個目錄中。
一樣的,咱們還須要在Configure
方法中爲Area註冊路由。
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( name: "default", template: "Modules/{area}/{controller=Home}/{action=Index}/{id?}"); });
由於咱們已經不須要使用Razor的預編譯視圖,因此Enable
方法咱們的最終代碼以下
public IActionResult Enable() { var assembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "Modules\\DemoPlugin1\\DemoPlugin1.dll"); var controllerAssemblyPart = new AssemblyPart(assembly); _partManager.ApplicationParts.Add(controllerAssemblyPart); MyActionDescriptorChangeProvider.Instance.HasChanged = true; MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel(); return Content("Enabled"); }
以上就是針對主站點的修改,下面咱們再來修改一下插件項目。
首先咱們須要將整個項目的Sdk類型改成由以前的Microsoft.Net.Sdk.Razor改成Microsoft.Net.Sdk.Web, 因爲以前咱們使用了預編譯的Razor視圖,因此咱們使用了Microsoft.Net.Sdk.Razor,它會將視圖編譯爲一個dll文件。可是如今咱們須要使用原始的Razor視圖,因此咱們須要將其改成Microsoft.Net.Sdk.Web, 使用這個Sdk, 最終的Views文件夾中的文件會以原始的形式發佈出來。
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>netcoreapp2.2</TargetFramework> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <OutputPath></OutputPath> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.App" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Razor" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\DynamicPlugins.Core\DynamicPlugins.Core.csproj" /> </ItemGroup> </Project>
最後咱們須要在Plugin1Controller上添加Area配置, 並將編譯以後的程序集以及Views目錄放置到主站點項目的Modules目錄中
[Area("DemoPlugin1")] public class Plugin1Controller : Controller { public IActionResult HelloWorld() { return View(); } }
最終主站點項目目錄結構
The files tree is: ================= |__ DynamicPlugins.Core.dll |__ DynamicPlugins.Core.pdb |__ DynamicPluginsDemoSite.deps.json |__ DynamicPluginsDemoSite.dll |__ DynamicPluginsDemoSite.pdb |__ DynamicPluginsDemoSite.runtimeconfig.dev.json |__ DynamicPluginsDemoSite.runtimeconfig.json |__ DynamicPluginsDemoSite.Views.dll |__ DynamicPluginsDemoSite.Views.pdb |__ Modules |__ DemoPlugin1 |__ DemoPlugin1.dll |__ Views |__ Plugin1 |__ HelloWorld.cshtml |__ _ViewStart.cshtml
如今咱們從新啓動項目,從新按照以前的順序,先激活插件,再訪問新的插件路由/Modules/DemoPlugin1/plugin1/helloworld
, 頁面正常顯示了。
本篇中,我爲你們演示瞭如何在運行時啓用一個插件,這裏咱們藉助IActionDescriptorChangeProvider
, 讓ASP.NET Core在運行時從新加載了控制器,雖然不支持預編譯Razor視圖的加載,可是咱們經過配置原始Razor視圖加載的目錄規則,一樣實現了動態讀取視圖的功能。
下一篇我將繼續將這個項目重構,編寫業務模型,並嘗試編寫插件的安裝以及升降級版本的代碼。