標題:從零開始實現ASP.NET Core MVC的插件式開發(五) - 使用AssemblyLoadContext實現插件的升級和刪除
作者:Lamond Lu
地址:https://www.cnblogs.com/lwqlun/p/11395828.html
源代碼:https://github.com/lamondlu/Mystique
前景回顧:
- 從零開始實現ASP.NET Core MVC的插件式開發(一) - 使用Application Part動態加載控制器和視圖
- 從零開始實現ASP.NET Core MVC的插件式開發(二) - 如何創建項目模板
- 從零開始實現ASP.NET Core MVC的插件式開發(三) - 如何在運行時啟用組件
- 從零開始實現ASP.NET Core MVC的插件式開發(四) - 插件安裝
簡介
在上一篇中,我為大家講解了如何實現插件的安裝,在文章的最后,留下了兩個待解決的問題。
- .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
方法。
相關鏈接:
- https://docs.microsoft.com/en-us/dotnet/api/system.runtime.loader.assemblyloadcontext?view=netcore-2.2
- https://docs.microsoft.com/en-us/dotnet/standard/assembly/unloadability-howto?view=netcore-2.2
升級.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