<ul id="qxxfc"><fieldset id="qxxfc"><tr id="qxxfc"></tr></fieldset></ul>


      標(biāo)題:從零開始實現(xiàn)ASP.NET Core MVC的插件式開發(fā)(三) - 如何在運行時啟用組件
      作者:Lamond Lu
      地址:https://www.cnblogs.com/lwqlun/p/11260750.html
      <https://www.cnblogs.com/lwqlun/p/11260750.html>
      源代碼:https://github.com/lamondlu/DynamicPlugins
      <https://github.com/lamondlu/DynamicPlugins>



      前情回顧

      * 從零開始實現(xiàn)ASP.NET Core MVC的插件式開發(fā)(一) - 使用Application Part動態(tài)加載控制器和視圖
      <https://www.cnblogs.com/lwqlun/p/11137788.html#4310745>
      * 從零開始實現(xiàn)ASP.NET Core MVC的插件式開發(fā)(二) - 如何創(chuàng)建項目模板
      <https://www.cnblogs.com/lwqlun/p/11155666.html>
      在前面兩篇中,我為大家演示了如何使用Application Part動態(tài)加載控制器和視圖,以及如何創(chuàng)建插件模板來簡化操作。
      在上一篇寫完之后,我突然想到了一個問題,如果像前兩篇所設(shè)計那個來構(gòu)建一個插件式系統(tǒng),會有一個很嚴(yán)重的問題,即

      當(dāng)你添加一個插件之后,整個程序不能立刻啟用該插件,只有當(dāng)重啟整個ASP.NET Core應(yīng)用之后,才能正確的加載插件。因為所有插件的加載都是在程序啟動時
      ConfigureService方法中配置的。


      這種方式的插件系統(tǒng)會很難用,我們期望的效果是在運行時動態(tài)啟用和禁用插件,那么有沒有什么解決方案呢?答案是肯定的。下面呢,我將一步一步說明一下自己的思路、編碼中遇到的問題,以及這些問題的解決方案。

      為了完成這個功能,我走了許多彎路,當(dāng)前這個方案可能不是最好的,但是確實是一個可行的方案,如果大家有更好的方案,我們可以一起討論一下。

      在Action中激活組件

      當(dāng)遇到這個問題的時候,我的第一思路就是將ApplicationPartManager加載插件庫的代碼移動到某個Action中。于是我就在主站點中創(chuàng)建了一個
      PluginsController, 并在啟用添加了一個名為Enable的Action方法。
      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");
      } }
      修改代碼之后,運行程序,這里我們首先調(diào)用/Plugins/Enable來嘗試激活組件,激活之后,我們再次調(diào)用/Plugin1/HelloWorld

      這里會發(fā)現(xiàn)程序返回了404, 即控制器和視圖沒有正確的激活。



      這里你可能有疑問,為什么會激活失敗呢?

      這里的原因是,只有當(dāng)ASP.NET
      Core應(yīng)用啟動時,才會去ApplicationPart管理器中加載控制器與視圖的程序集,所以雖然新的控制器程序集在運行時被添加到了
      ApplicationPart管理器中,但是ASP.NET Core不會自動進(jìn)行更新操作,所以這里我們需要尋找一種方式能夠讓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屬性中記錄了當(dāng)ASP.NET Core程序啟動后,匹配到的所有Controller/Action集合。
      * UpdateCollection方法使用來更新ActionDescriptors集合的。
      * 在構(gòu)造函數(shù)中設(shè)計了一個觸發(fā)器,
      ChangeToken.OnChange(GetCompositeChangeToken,UpdateCollection)
      。這里程序會監(jiān)聽一個Token對象,當(dāng)這個Token對象發(fā)生變化時,就自動觸發(fā)UpdateCollection方法。
      * 這里Token是由一組IActionDescriptorChangeProvider接口對象組合而成的。
      所以這里我們就可以通過自定義一個IActionDescriptorChangeProvider
      接口對象,并在組件激活方法Enable中修改這個接口Token的方式,使DefaultActionDescriptorCollectionProvider中的
      CompositeChangeToken發(fā)生變化,從而實現(xiàn)控制器的重新裝載。

      使用IActionDescriptorChangeProvider在運行時激活控制器

      這里我們首先創(chuàng)建一個MyActionDescriptorChangeProvider類,并讓它實現(xiàn)
      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方法中通過兩行代碼來修改當(dāng)前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"); } }
      修改代碼之后重新運行程序,這里我們依然首先調(diào)用/Plugins/Enable,然后再次調(diào)用/Plugin1/Helloworld,
      這時候你會發(fā)現(xiàn)Action被觸發(fā)了,只是沒有找到對應(yīng)的Views。



      如何解決插件的預(yù)編譯Razor視圖不能重新加載的問題?

      通過以上的方式,我們終于獲得了在運行時加載插件控制器程序集的能力,但是插件的預(yù)編譯Razor視圖程序集沒有被正確加載,這就說明
      IActionDescriptorChangeProvider只會觸發(fā)控制器的重新加載,不會觸發(fā)預(yù)編譯Razor視圖的重新加載。ASP.NET
      Core只會在整個應(yīng)用啟動時,才會加載插件的預(yù)編譯Razor程序集,所以我們并沒有獲得在運行時重新加載預(yù)編譯Razor視圖的能力。

      針對這一點,我也查閱了好多資料,最終也沒有一個可行的解決方案,也許使用ASP.NET Core 3.0的Razor Runtime
      Compilation可以實現(xiàn),但是在ASP.NET Core 2.2版本,我們還沒有獲得這種能力。

      為了越過這個難點,最終我還是選擇了放棄預(yù)編譯Razor視圖,改用原始的Razor視圖。

      因為在ASP.NET Core啟動時,我們可以在Startup.cs的ConfigureServices方法中配置Razor視圖引擎檢索視圖的規(guī)則。

      這里我們可以把每個插件組織成ASP.NET Core MVC中一個Area, Area的名稱即插件的名稱,
      這樣我們就可以將為Razor視圖引擎的添加一個檢索視圖的規(guī)則,代碼如下
      services.Configure<RazorViewEngineOptions>(o => {
      o.AreaViewLocationFormats.Add("/Modules/{2}/{1}/Views/{0}" +
      RazorViewEngine.ViewExtension); });
      這里{2}代表Area名稱, {1}代表Controller名稱, {0}代表Action名稱。

      這里Modules是我重新創(chuàng)建的一個目錄,后續(xù)所有的插件都會放置在這個目錄中。

      同樣的,我們還需要在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?}"); });
      因為我們已經(jīng)不需要使用Razor的預(yù)編譯視圖,所以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,
      由于之前我們使用了預(yù)編譯的Razor視圖,所以我們使用了Microsoft.Net.Sdk.Razor,它會將視圖編譯為一個dll文件。但是現(xiàn)在我們需要使用原始的Razor視圖,所以我們需要將其改為Microsoft.Net.Sdk.Web,
      使用這個Sdk, 最終的Views文件夾中的文件會以原始的形式發(fā)布出來。
      <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(); } }
      最終主站點項目目錄結(jié)構(gòu)
      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
      現(xiàn)在我們重新啟動項目,重新按照之前的順序,先激活插件,再訪問新的插件路由/Modules/DemoPlugin1/plugin1/helloworld,
      頁面正常顯示了。



      總結(jié)

      本篇中,我為大家演示了如何在運行時啟用一個插件,這里我們借助IActionDescriptorChangeProvider, 讓ASP.NET
      Core在運行時重新加載了控制器,雖然不支持預(yù)編譯Razor視圖的加載,但是我們通過配置原始Razor視圖加載的目錄規(guī)則,同樣實現(xiàn)了動態(tài)讀取視圖的功能。

      下一篇我將繼續(xù)將這個項目重構(gòu),編寫業(yè)務(wù)模型,并嘗試編寫插件的安裝以及升降級版本的代碼。

      友情鏈接
      ioDraw流程圖
      API參考文檔
      OK工具箱
      云服務(wù)器優(yōu)惠
      阿里云優(yōu)惠券
      騰訊云優(yōu)惠券
      京東云優(yōu)惠券
      站點信息
      問題反饋
      郵箱:[email protected]
      QQ群:637538335
      關(guān)注微信

        <ul id="qxxfc"><fieldset id="qxxfc"><tr id="qxxfc"></tr></fieldset></ul>
          天天综合天天做 | 美女无套 | 爆操内射 | 插插插插综合 | 日韩一级性爱视频 | 两个男的操一个女的 | 色婷婷18 | 久久这里只有 | 人人操骚逼 | 国产在线a |