從零開始實現ASP.NET Core MVC的插件式開發(五) - 插件的刪除和升級

  • 2019 年 10 月 3 日
  • 筆記

標題:從零開始實現ASP.NET Core MVC的插件式開發(五) - 使用AssemblyLoadContext實現插件的升級和刪除
作者:Lamond Lu
地址:https://www.cnblogs.com/lwqlun/p/11395828.html
源程式碼:https://github.com/lamondlu/Mystique

前景回顧:

簡介

在上一篇中,我為大家講解了如何實現插件的安裝,在文章的最後,留下了兩個待解決的問題。

  • .NET Core 2.2中不能實現運行時刪除插件
  • .NET Core 2.2中不能實現運行時升級插件

其實這2個問題歸根結底其實都是一個問題,就是插件程式集被佔用,不能在運行時更換程式集。在本篇中,我將分享一下我是如何一步一步解決這個問題的,其中也繞了不少彎路,查閱過資料,在.NET Core官方提過Bug,幾次差點想放棄了,不過最終是找到一個可行的方案。

.NET Core 2.2的遺留問題

程式集被佔用的原因

回顧一下,我們之前載入插件程式集時所有使用的程式碼。

    var provider = services.BuildServiceProvider();      using (var scope = provider.CreateScope())      {          var unitOfWork = scope.ServiceProvider.GetService<IUnitOfWork>();          var allEnabledPlugins = unitOfWork.PluginRepository              .GetAllEnabledPlugins();            foreach (var plugin in allEnabledPlugins)          {              var moduleName = plugin.Name;              var assembly = Assembly.LoadFile($"{AppDomain.CurrentDomain.BaseDirectory}Modules\{moduleName}\{moduleName}.dll");                var controllerAssemblyPart = new AssemblyPart(assembly);              mvcBuilders.PartManager                      .ApplicationParts                      .Add(controllerAssemblyPart);          }      }

這裡我們使用了Assembly.LoadFile方法載入了插件程式集。 在.NET中使用Assembly.LoadFile方法載入的程式集會被自動鎖定,不能執行任何轉移,刪除等造作,所以這就給我們刪除和升級插件造成了很大困難。

PS: 升級插件需要覆蓋已載入的插件程式集,由於程式集鎖定,所以覆蓋操作不能成功。

使用AssemblyLoadContext

在.NET Framework中,如果遇到這個問題,常用的解決方案是使用AppDomain類來實現插件熱插拔,但是在.NET Core中沒有AppDomain類。不過經過查閱,.NET Core 2.0之後引入了一個AssemblyLoadContext類來替代.NET Freamwork中的AppDomain。本以為使用它就能解決當前程式集佔用的問題,結果沒想到.NET Core 2.x版本提供的AssemblyLoadContext沒有提供Unload方法來釋放載入的程式集,只有在.NET Core 3.0版本中才為AssemblyLoadContext類添加了Unload方法。

相關鏈接:

升級.NET Core 3.0 Preview 8

因此,為了完成插件的刪除和升級功能,我將整個項目升級到了最新的.NET Core 3.0 Preview 8版本。

這裡.NET Core 2.2升級到.NET Core 3.0有一點需要注意的問題。

在.NET Core 2.2中默認啟用了Razor視圖的運行時編譯,簡單點說就是.NET Core 2.2中自動啟用了讀取原始的Razor視圖文件,並編譯視圖的功能。這就是我們在第三章和第四章中的實現方法,每個插件文件最終都放置在了一個Modules目錄中,每個插件既有包含Controller/Action的程式集,又有對應的原始Razor視圖目錄Views,在.NET Core 2.2中當我們在運行時啟用一個組件之後,對應的Views可以自動載入。

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

但是在.NET Core 3.0中,Razor視圖的運行時編譯需要引入程式集Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation。並且在程式啟動時,需要啟動運行時編譯的功能。

public void ConfigureServices(IServiceCollection services)  {      ...      var mvcBuilders = services.AddMvc()          .AddRazorRuntimeCompilation();        ...  }

如果沒有啟用Razor視圖的運行時編譯,程式訪問插件視圖的時候,就會報錯,提示視圖找不到。

使用.NET Core 3.0的AssemblyLoadContext載入程式集

這裡為了創建一個可回收的程式集載入上下文,我們首先基於AssemblyLoadcontext創建一個CollectibleAssemblyLoadContext類。其中我們將IsCollectible屬性通過父類構造函數,將其設置為true。

    public class CollectibleAssemblyLoadContext          : AssemblyLoadContext      {          public CollectibleAssemblyLoadContext()              : base(isCollectible: true)          {          }            protected override Assembly Load(AssemblyName name)          {              return null;          }      }

在整個插件載入上下文的設計上,每個插件都使用一個單獨的CollectibleAssemblyLoadContext來載入,所有插件的CollectibleAssemblyLoadContext都放在一個PluginsLoadContext對象中。

相關程式碼: PluginsLoadContexts.cs

    public static class PluginsLoadContexts      {          private static Dictionary<string, CollectibleAssemblyLoadContext>              _pluginContexts = null;            static PluginsLoadContexts()          {              _pluginContexts = new Dictionary<string, CollectibleAssemblyLoadContext>();          }            public static bool Any(string pluginName)          {              return _pluginContexts.ContainsKey(pluginName);          }            public static void RemovePluginContext(string pluginName)          {              if (_pluginContexts.ContainsKey(pluginName))              {                  _pluginContexts[pluginName].Unload();                  _pluginContexts.Remove(pluginName);              }          }            public static CollectibleAssemblyLoadContext GetContext(string pluginName)          {              return _pluginContexts[pluginName];          }            public static void AddPluginContext(string pluginName,               CollectibleAssemblyLoadContext context)          {              _pluginContexts.Add(pluginName, context);          }      }

程式碼解釋:

  • 當載入插件的時候,我們需要將當前插件的程式集載入上下文放到_pluginContexts字典中。字典的key是插件的名稱,字典的value是插件的程式集載入上下文。
  • 當移除一個插件的時候,我們需要使用Unload方法,來釋放當前的程式集載入上下文。

在完成以上程式碼之後,我們更改程式啟動和啟用組件的程式碼,因為這兩部分都需要將插件程式集載入到CollectibleAssemblyLoadContext中。

Startup.cs

    var provider = services.BuildServiceProvider();      using (var scope = provider.CreateScope())      {          var option = scope.ServiceProvider              .GetService<MvcRazorRuntimeCompilationOptions>();              var unitOfWork = scope.ServiceProvider              .GetService<IUnitOfWork>();          var allEnabledPlugins = unitOfWork.PluginRepository              .GetAllEnabledPlugins();            foreach (var plugin in allEnabledPlugins)          {              var context = new CollectibleAssemblyLoadContext();              var moduleName = plugin.Name;              var filePath = $"{AppDomain.CurrentDomain.BaseDirectory}Modules\{moduleName}\{moduleName}.dll";                var assembly = context.LoadFromAssemblyPath(filePath);                var controllerAssemblyPart = new AssemblyPart(assembly);                mvcBuilders.PartManager.ApplicationParts                      .Add(controllerAssemblyPart);              PluginsLoadContexts.AddPluginContext(plugin.Name, context);          }      }      

PluginsController.cs

    public IActionResult Enable(Guid id)      {          var module = _pluginManager.GetPlugin(id);          if (!PluginsLoadContexts.Any(module.Name))          {              var context = new CollectibleAssemblyLoadContext();                _pluginManager.EnablePlugin(id);              var moduleName = module.Name;                var filePath = $"{AppDomain.CurrentDomain.BaseDirectory}Modules\{moduleName}\{moduleName}.dll";                context.                var assembly = context.LoadFromAssemblyPath(filePath);              var controllerAssemblyPart = new AssemblyPart(assembly);              _partManager.ApplicationParts.Add(controllerAssemblyPart);                MyActionDescriptorChangeProvider.Instance.HasChanged = true;              MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();                PluginsLoadContexts.AddPluginContext(module.Name, context);          }          else          {              var context = PluginsLoadContexts.GetContext(module.Name);              var controllerAssemblyPart = new AssemblyPart(context.Assemblies.First());              _partManager.ApplicationParts.Add(controllerAssemblyPart);              _pluginManager.EnablePlugin(id);                MyActionDescriptorChangeProvider.Instance.HasChanged = true;              MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();          }            return RedirectToAction("Index");      }

意外結果

完成以上程式碼之後,我立刻嘗試了刪除程式集的操作,但是得到的結果卻不是我想要的。

雖然.NET Core 3.0為AssemblyLoadContext提供了Unload方法,但是調用之後, 你依然會得到一個文件被佔用的錯誤

暫時不知道這是不是.NET Core 3.0的bug, 還是功能就是這麼設計的,反正感覺這條路是走不通了,折騰了一天,在網上找了好多方案,但是都不能解決這個問題。

就在快放棄的時候,突然發現AssemblyLoadContext類提供了另外一種載入程式集的方式LoadFromStream。

改用LoadFromStream載入程式集

看到LoadFromStream方法之後,我的第一思路就是可以使用FileStream載入插件程式集,然後將獲得的文件流傳給LoadFromStream方法,並在文件載入完畢之後,釋放掉這個FileStream對象。

根據以上思路,我將載入程式集的方法修改如下

PS: Enable方法的修改方式類似,這裡我就不重複寫了。

    var provider = services.BuildServiceProvider();      using (var scope = provider.CreateScope())      {          var option = scope.ServiceProvider              .GetService<MvcRazorRuntimeCompilationOptions>();              var unitOfWork = scope.ServiceProvider.GetService<IUnitOfWork>();          var allEnabledPlugins = unitOfWork.PluginRepository.GetAllEnabledPlugins();            foreach (var plugin in allEnabledPlugins)          {              var context = new CollectibleAssemblyLoadContext();              var moduleName = plugin.Name;              var filePath = $"{AppDomain.CurrentDomain.BaseDirectory}Modules\{moduleName}\{moduleName}.dll";                _presetReferencePaths.Add(filePath);              using (var fs = new FileStream(filePath, FileMode.Open))              {                  var assembly = context.LoadFromStream(fs);                  var controllerAssemblyPart = new AssemblyPart(assembly);                    mvcBuilders.PartManager.ApplicationParts.Add(controllerAssemblyPart);                  PluginsLoadContexts.AddPluginContext(plugin.Name, context);              }          }      }

修改之後,我又試了一下刪除插件的程式碼,果然成功刪除了。

"Empty path name is not legal. "問題

就在我認為功能已經全部完成之後,我又重新安裝了刪除的插件,嘗試訪問插件中的controller/action, 結果得到了意想不到的錯誤,插件的中包含的頁面打不開了。

fail: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware[1]        An unhandled exception has occurred while executing the request.  System.ArgumentException: Empty path name is not legal. (Parameter 'path')     at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options)     at Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.RazorReferenceManager.CreateMetadataReference(String path)     at System.Linq.Enumerable.SelectListIterator`2.ToList()     at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)     at Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.RazorReferenceManager.GetCompilationReferences()     at System.Threading.LazyInitializer.EnsureInitializedCore[T](T& target, Boolean& initialized, Object& syncLock, Func`1 valueFactory)     at System.Threading.LazyInitializer.EnsureInitialized[T](T& target, Boolean& initialized, Object& syncLock, Func`1 valueFactory)     at Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.RazorReferenceManager.get_CompilationReferences()     at Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.LazyMetadataReferenceFeature.get_References()     at Microsoft.CodeAnalysis.Razor.CompilationTagHelperFeature.GetDescriptors()     at Microsoft.AspNetCore.Razor.Language.DefaultRazorTagHelperBinderPhase.ExecuteCore(RazorCodeDocument codeDocument)     at Microsoft.AspNetCore.Razor.Language.RazorEnginePhaseBase.Execute(RazorCodeDocument codeDocument)     at Microsoft.AspNetCore.Razor.Language.DefaultRazorEngine.Process(RazorCodeDocument document)     at Microsoft.AspNetCore.Razor.Language.DefaultRazorProjectEngine.ProcessCore(RazorCodeDocument codeDocument)     at Microsoft.AspNetCore.Razor.Language.RazorProjectEngine.Process(RazorProjectItem projectItem)     at Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.RuntimeViewCompiler.CompileAndEmit(String relativePath)     at Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.RuntimeViewCompiler.OnCacheMiss(String normalizedPath)  --- End of stack trace from previous location where exception was thrown ---     at Microsoft.AspNetCore.Mvc.Razor.Compilation.DefaultRazorPageFactoryProvider.CreateFactory(String relativePath)     at Microsoft.AspNetCore.Mvc.Razor.RazorViewEngine.CreateCacheResult(HashSet`1 expirationTokens, String relativePath, Boolean isMainPage)     at Microsoft.AspNetCore.Mvc.Razor.RazorViewEngine.OnCacheMiss(ViewLocationExpanderContext expanderContext, ViewLocationCacheKey cacheKey)     at Microsoft.AspNetCore.Mvc.Razor.RazorViewEngine.LocatePageFromViewLocations(ActionContext actionContext, String pageName, Boolean isMainPage)     at Microsoft.AspNetCore.Mvc.Razor.RazorViewEngine.FindView(ActionContext context, String viewName, Boolean isMainPage)     at Microsoft.AspNetCore.Mvc.ViewEngines.CompositeViewEngine.FindView(ActionContext context, String viewName, Boolean isMainPage)     at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewResultExecutor.FindView(ActionContext actionContext, ViewResult viewResult)     at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewResultExecutor.ExecuteAsync(ActionContext context, ViewResult result)     at Microsoft.AspNetCore.Mvc.ViewResult.ExecuteResultAsync(ActionContext context)     at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|29_0[TFilter,TFilterAsync](ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)     at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)     at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)     at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()  --- End of stack trace from previous location where exception was thrown ---     at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|24_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)     at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)     at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)     at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync()  --- End of stack trace from previous location where exception was thrown ---     at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)     at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)     at Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.SetRoutingAndContinue(HttpContext httpContext)     at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware.<Invoke>g__Awaited|6_0(ExceptionHandlerMiddleware middleware, HttpContext context, Task task)

這個文件路徑非法的錯誤讓我感覺很奇怪,為什麼會有這種問題呢?與之前的程式碼的不同之處只有一個地方,就是從LoadFromAssemblyPath改為了LoadFromStream。

為了弄清這個問題,我clone了最新的.NET Core 3.0 Preview 8源程式碼,發現了在 .NET Core運行時編譯視圖的時候,會調用如下方法。

RazorReferenceManager.cs

    internal IEnumerable<string> GetReferencePaths()      {          var referencePaths = new List<string>();            foreach (var part in _partManager.ApplicationParts)          {              if (part is ICompilationReferencesProvider compilationReferenceProvider)              {                  referencePaths.AddRange(compilationReferenceProvider.GetReferencePaths());              }              else if (part is AssemblyPart assemblyPart)              {                  referencePaths.AddRange(assemblyPart.GetReferencePaths());              }          }            referencePaths.AddRange(_options.AdditionalReferencePaths);            return referencePaths;      }

這段程式碼意思是根據當前載入程式集的所在位置,來發現對應視圖。

那麼問題就顯而易見了,我們之前用LoadFromAssemblyPath載入程式集,程式集的文件位置被自動記錄下來,但是我們改用LoadFromStream之後,所需的文件位置資訊丟失了,是一個空字元串,所以.NET Core在嘗試載入視圖的時候,遇到空字元串,拋出了一個非法路徑的錯誤。

其實這裡的方法很好改,只需要將空字元串的路徑排除掉即可。

    internal IEnumerable<string> GetReferencePaths()      {          var referencePaths = new List<string>();            foreach (var part in _partManager.ApplicationParts)          {              if (part is ICompilationReferencesProvider compilationReferenceProvider)              {                  referencePaths.AddRange(compilationReferenceProvider.GetReferencePaths());              }              else if (part is AssemblyPart assemblyPart)              {                  referencePaths.AddRange(assemblyPart.GetReferencePaths().Where(o => !string.IsNullOrEmpty(o));              }          }            referencePaths.AddRange(_options.AdditionalReferencePaths);            return referencePaths;      }

但是由於不清楚會不會導致其他問題,所以我沒有採取這種方法,我將這個問題作為一個Bug提交到了官方。

問題地址: https://github.com/aspnet/AspNetCore/issues/13312

沒想到僅僅8小時,就得到官方的解決方案。

這段意思是說ASP.NET Core暫時不支援動態載入程式集,如果要在當前版本實現功能,需要自己實現一個AssemblyPart類, 在獲取程式集路徑的時候,返回空集合而不是空字元串。

PS: 官方已經將這個問題放到了.NET 5 Preview 1中,相信.NET 5中會得到真正的解決。

根據官方的方案,Startup.cs文件的最終版本

    public class MyAssemblyPart : AssemblyPart, ICompilationReferencesProvider      {          public MyAssemblyPart(Assembly assembly) : base(assembly) { }            public IEnumerable<string> GetReferencePaths() => Array.Empty<string>();      }        public static class AdditionalReferencePathHolder      {          public static IList<string> AdditionalReferencePaths = new List<string>();      }        public class Startup      {          public IList<string> _presets = new List<string>();            public Startup(IConfiguration configuration)          {              Configuration = configuration;          }            public IConfiguration Configuration { get; }            // This method gets called by the runtime. Use this method to add services to the container.          public void ConfigureServices(IServiceCollection services)          {              services.AddOptions();                services.Configure<ConnectionStringSetting>(Configuration.GetSection("ConnectionStringSetting"));                services.AddScoped<IPluginManager, PluginManager>();              services.AddScoped<IUnitOfWork, UnitOfWork>();                var mvcBuilders = services.AddMvc()                  .AddRazorRuntimeCompilation(o =>                  {                      foreach (var item in _presets)                      {                          o.AdditionalReferencePaths.Add(item);                      }                        AdditionalReferencePathHolder.AdditionalReferencePaths = o.AdditionalReferencePaths;                  });                services.Configure<RazorViewEngineOptions>(o =>              {                  o.AreaViewLocationFormats.Add("/Modules/{2}/Views/{1}/{0}" + RazorViewEngine.ViewExtension);                  o.AreaViewLocationFormats.Add("/Views/Shared/{0}.cshtml");              });                services.AddSingleton<IActionDescriptorChangeProvider>(MyActionDescriptorChangeProvider.Instance);              services.AddSingleton(MyActionDescriptorChangeProvider.Instance);                var provider = services.BuildServiceProvider();              using (var scope = provider.CreateScope())              {                  var option = scope.ServiceProvider.GetService<MvcRazorRuntimeCompilationOptions>();                      var unitOfWork = scope.ServiceProvider.GetService<IUnitOfWork>();                  var allEnabledPlugins = unitOfWork.PluginRepository.GetAllEnabledPlugins();                    foreach (var plugin in allEnabledPlugins)                  {                      var context = new CollectibleAssemblyLoadContext();                      var moduleName = plugin.Name;                      var filePath = $"{AppDomain.CurrentDomain.BaseDirectory}Modules\{moduleName}\{moduleName}.dll";                        _presets.Add(filePath);                      using (var fs = new FileStream(filePath, FileMode.Open))                      {                          var assembly = context.LoadFromStream(fs);                            var controllerAssemblyPart = new MyAssemblyPart(assembly);                            mvcBuilders.PartManager.ApplicationParts.Add(controllerAssemblyPart);                          PluginsLoadContexts.AddPluginContext(plugin.Name, context);                      }                  }              }          }            public void Configure(IApplicationBuilder app, IWebHostEnvironment env)          {              if (env.IsDevelopment())              {                  app.UseDeveloperExceptionPage();              }              else              {                  app.UseExceptionHandler("/Home/Error");              }                app.UseStaticFiles();                app.UseRouting();              app.UseEndpoints(routes =>              {                  routes.MapControllerRoute(                      name: "Customer",                      pattern: "{controller=Home}/{action=Index}/{id?}");                    routes.MapControllerRoute(                      name: "Customer",                      pattern: "Modules/{area}/{controller=Home}/{action=Index}/{id?}");              });            }      }

插件刪除和升級的程式碼

解決了程式集佔用問題之後,我們就可以開始編寫刪除/升級插件的程式碼了。

刪除插件

如果要刪除一個插件,我們需要完成以下幾個步驟

  • 刪除組件記錄
  • 刪除組件遷移的表結構
  • 移除載入過的ApplicationPart
  • 刷新Controller/Action
  • 移除組件對應的程式集載入上下文
  • 刪除組件文件

根據這個步驟,我編寫了一個Delete方法,程式碼如下:

        public IActionResult Delete(Guid id)          {              var module = _pluginManager.GetPlugin(id);              _pluginManager.DisablePlugin(id);              _pluginManager.DeletePlugin(id);              var moduleName = module.Name;                var matchedItem = _partManager.ApplicationParts.FirstOrDefault(p =>                                                     p.Name == moduleName);                if (matchedItem != null)              {                  _partManager.ApplicationParts.Remove(matchedItem);                  matchedItem = null;              }                MyActionDescriptorChangeProvider.Instance.HasChanged = true;              MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();                PluginsLoadContexts.RemovePluginContext(module.Name);                var directory = new DirectoryInfo($"{AppDomain.CurrentDomain.BaseDirectory}Modules/{module.Name}");              directory.Delete(true);                return RedirectToAction("Index");          }          

升級插件

對於升級插件的程式碼,我將它和新增插件的程式碼放在了一起

    public void AddPlugins(PluginPackage pluginPackage)      {          var existedPlugin = _unitOfWork.PluginRepository.GetPlugin(pluginPackage.Configuration.Name);            if (existedPlugin == null)          {              InitializePlugin(pluginPackage);          }          else if (new DomainModel.Version(pluginPackage.Configuration.Version) > new DomainModel.Version(existedPlugin.Version))          {              UpgradePlugin(pluginPackage, existedPlugin);          }          else          {              DegradePlugin(pluginPackage);          }      }        private void InitializePlugin(PluginPackage pluginPackage)      {          var plugin = new DTOs.AddPluginDTO          {              Name = pluginPackage.Configuration.Name,              DisplayName = pluginPackage.Configuration.DisplayName,              PluginId = Guid.NewGuid(),              UniqueKey = pluginPackage.Configuration.UniqueKey,              Version = pluginPackage.Configuration.Version          };            _unitOfWork.PluginRepository.AddPlugin(plugin);          _unitOfWork.Commit();            var versions = pluginPackage.GetAllMigrations(_connectionString);            foreach (var version in versions)          {              version.MigrationUp(plugin.PluginId);          }            pluginPackage.SetupFolder();      }        public void UpgradePlugin(PluginPackage pluginPackage, PluginViewModel oldPlugin)      {          _unitOfWork.PluginRepository.UpdatePluginVersion(oldPlugin.PluginId,                      pluginPackage.Configuration.Version);          _unitOfWork.Commit();            var migrations = pluginPackage.GetAllMigrations(_connectionString);            var pendingMigrations = migrations.Where(p => p.Version > oldPlugin.Version);            foreach (var migration in pendingMigrations)          {              migration.MigrationUp(oldPlugin.PluginId);          }            pluginPackage.SetupFolder();      }        public void DegradePlugin(PluginPackage pluginPackage)      {          throw new NotImplementedException();      }

程式碼解釋:

  • 這裡我首先判斷了當前插件包和已安裝版本的版本差異
    • 如果系統沒有安裝過當前插件,就安裝插件
    • 如果當前插件包的版本比已安裝的版本高,就升級插件
    • 如果當前插件包的版本比已安裝的版本低,就降級插件(現實中這種情況不多)
  • InitializePlugin是用來載入新組件的,它的內容就是之前的新增插件方法
  • UpgradePlugin是用來升級組件的,當我們升級一個組件的時候,我們需要做一下幾個事情
    • 升級組件版本
    • 做最新版本組件的腳本遷移
    • 使用最新程式包覆蓋老程式包
  • DegradePlugin是用來降級組件的,由於篇幅問題,我就不詳細寫了,大家可以自行填補。

最終效果

總結

本篇中,我為大家演示如果使用.NET Core 3.0的AssemblyLoadContext來解決已載入程式集佔用的問題,以此實現了插件的升級和降級。本篇的研究時間較長,因為中間出現的問題確實太多了,沒有什麼可以復用的方案,我也不知道是不是第一個在.NET Core中這麼嘗試的。不過結果還算好,想實現的功能最終還是做出來了。後續呢,這個項目會繼續添加新的功能,希望大家多多支援。

項目地址:https://github.com/lamondlu/Mystique

Tags: