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

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

65831-20190823140031763-1538282381.jpg

前景回顧:

  • 從零開始實現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視圖的運行時編譯,程序訪問插件視圖的時候,就會報錯,提示視圖找不到。

65831-20190823140048343-974444938.png

使用.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方法,但是調用之后, 你依然會得到一個文件被占用的錯誤

65831-20190823140113583-755646210.png

暫時不知道這是不是.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小時,就得到官方的解決方案。

65831-20190823140114300-744894253.png

這段意思是說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是用來降級組件的,由于篇幅問題,我就不詳細寫了,大家可以自行填補。

最終效果

65831-20190823140223450-1154348183.gif

總結

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

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

轉載于:https://www.cnblogs.com/lwqlun/p/11395828.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/248206.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/248206.shtml
英文地址,請注明出處:http://en.pswp.cn/news/248206.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

JDBC Druid式link

準備工作&#xff1a;導入包------druid-1.0.9.jar src文件夾下放下druid.properties文件 且其中的url和數據庫名要配置完備 import JdbcUtils.JDBCUtils;import java.sql.Connection;import java.sql.PreparedStatement;import java.sql.PseudoColumnUsage;import java.sql…

vs strcore.cpp(156) 內存泄漏

在一個靜態函數的線程回調中&#xff0c;一個cstring的對象沒釋放。 遇到這種問題&#xff1a; 1、查看所有的指針對象&#xff0c;有沒有合適的回收 2、查看代碼的malloc&#xff0c;看看有沒有free 3、一點一點注釋代碼&#xff0c;定位位置 轉載于:https://www.cnblogs.com/…

js(Dom+Bom)第六天(1)

webAPI 01-鍵盤事件 知識點-onkeydown事件[掌握] onkeydown: 當鍵盤上的鍵被按下時候觸發的一個事件知識點-onkeyup事件[掌握] onkeyup: 鍵盤上鍵彈起時候觸發的一個事件知識點-onkeypress事件[了解] onkeypress: 當鍵盤上的鍵被按下時候觸發的事件onkeydown 和 onkeypress的區…

【JavaWeb】石家莊地鐵搭乘系統——第一版(功能尚未完善)

小組成員&#xff1a;高達&#xff0c;程超然 項目目的&#xff1a;能算出地鐵搭乘的最優路線并顯示在地圖上 個人任務&#xff1a;調用隊友寫好的java代碼&#xff0c;將結果顯示在網頁上 新的知識&#xff1a;百度地圖API&#xff0c;使用JQuery的AJAX異步提交 進度&#xff…

掃描槍連接zebra打印機打印條碼標簽無需電腦

在一些流水線生產的現場&#xff0c;需要及時打印條碼標簽&#xff0c;由于現場環境和空間限制&#xff0c;無法擺放電腦或者通過連接電腦來打印條碼標簽的速度太慢&#xff0c; 瑞科條碼特提供了一套掃描槍直接連接條碼打印機&#xff0c;掃描槍掃描條碼之后直接打印輸出條碼標…

簡單的動畫函數封裝(1)

//創建簡單的動畫函數封裝效果(目標對象&#xff0c;目標位置) function animate(obj,target){var id setInterval(function(){if(obj.offsetLeft > target){clearInterval(id);}else{obj.style.left obj.offsetLeft 5 px;}},30) }可以實現如下效果&#xff1a; <div…

NodeMCU學習(三) : 進入網絡世界

閱讀原文可以訪問我的個人博客 把NodeMCU連接到路由器網絡上 NodeMCU可以被配置為Station模式和softAP模式或者Station AP模式&#xff0c;當它被配置為Station模式時&#xff0c;就可以去連接Access Point&#xff08;如路由器&#xff09;。當它被配置為Soft Access Point模…

操作系統原理之進程調度與死鎖(三)

一、進程調度的功能與時機 進程調度&#xff1a;進程調度的功能由操作系統的進程調度程序完成 具體任務&#xff1a;按照某種策略和算法從就緒態進程中為當前空閑的CPU選擇在其上運行的新進程。 進程調度的時機&#xff1a;進程正常或異常結束、進程阻塞、有更高優先級進程到來…

模擬京東側邊欄

思路&#xff1a; // 1. 獲取元素 // 1.1 獲取一組li // 1.2 獲取一組類名為item的div // 1.3 獲取類名為slide的div// 2. 循環遍歷給每一個li注冊onmouseenter&#xff0c;并且每一個li添加一個index表示索引 // 2.1 循環遍歷把所有的li的類名設置為,把所有的item的display設…

ViewPager + TabLayout + Fragment + MediaPlayer的使用

效果圖 在gradle里導包 implementation com.android.support:design:28.0.0 activity_main <?xml version"1.0" encoding"utf-8"?><LinearLayout xmlns:android"http://schemas.android.com/apk/res/android" xmlns:tools"http…

vs code打開文件顯示的中文亂碼

這種情況下&#xff0c;一般是編碼格式導致的&#xff0c;操作辦法&#xff1a; 鼠標點擊之后&#xff0c;上面會彈出這個界面&#xff0c;雙擊選中 然后從UTF-8換到GB2312&#xff0c;或者自己根據情況&#xff0c;更改編碼格式 轉載于:https://www.cnblogs.com/132818Creator…

操作系統原理之內存管理(第四章第一部分)

內存管理的?標&#xff1a;實現內存分配和回收&#xff0c;提高內存空間的利用率和內存的訪問速度 一、存儲器的層次結構 寄存器&#xff1a;在CPU內部有一組CPU寄存器&#xff0c;寄存器是cpu直接訪問和處理的數據&#xff0c;是一個臨時放數據的空間。 高速緩沖區&#xff1…

自寫圖片遮罩層放大功能jquery插件源代碼,photobox.js 1.0版,不兼容IE6

版權聲明&#xff1a;本文為博主原創文章。未經博主同意不得轉載。 https://blog.csdn.net/u010480479/article/details/27362147 阿嚏~~~ 話說本屌絲沒啥開發插件的經驗&#xff0c;可是天公不作美&#xff0c;公司須要讓我自己開發個圖片放大的插件 但公司老大的話&#xff0…

黑白兩客進入頁面(1)

<div><span>歡</span><span>迎</span><span>來</span><span>到</span><span><strong>黑白兩客</strong></span><span>的</span><span>博</span><span>客</sp…

zookeeper學習之原理

一、zookeeper 是什么 Zookeeper是一個分布式協調服務&#xff0c;可用于服務發現&#xff0c;分布式鎖&#xff0c;分布式領導選舉&#xff0c;配置管理等。這一切的基礎&#xff0c;都是Zookeeper提供了一個類似于Linux文件系統的樹形結構&#xff08;可認為是輕量級的內存文…

mysql鎖表查詢和解鎖操作

解除正在死鎖的狀態有兩種方法&#xff1a; 第一種&#xff1a; 1.查詢是否鎖表 show OPEN TABLES where In_use > 0; 2.查詢進程&#xff08;如果您有SUPER權限&#xff0c;您可以看到所有線程。否則&#xff0c;您只能看到您自己的線程&#xff09; show processlist 3.殺…

前端js基礎智能機器人

<script>var flag true;while(flag) {//獲取用戶輸入信息 var code prompt(你好,我是小娜\n請輸入編號或者關鍵詞選擇功能,輸入Q(q)退出聊天\n1.計算\n2.時間\n3.笑話);switch( code ) {case q:case Q:alert(狠心的拋棄了小娜);flag false;break;case 1:case 計算:var…

2018-2019-2 《Java程序設計》第6周學習總結

20175319 2018-2019-2 《Java程序設計》第6周學習總結 教材學習內容總結 本周學習《Java程序設計》第七章和第十章&#xff1a; 內部類&#xff1a; 1.內部類可以使用外嵌類的成員變量和方法。 2.類體中不可以聲明類變量和類方法。 3.內部類僅供外嵌類使用。 4.類聲明可以使用s…

Hbase基本原理

一、hbase是什么 HBase 是一種類似于數據庫的存儲層&#xff0c;也就是說 HBase 適用于結構化的存儲。并且 HBase 是一種列式的分布式數據庫&#xff0c;是由當年的 Google 公布的 BigTable 的論文而生。HBase 底層依舊依賴 HDFS 來作為其物理存儲。 二、hbase的列式存儲結構 行…

最終的動畫函數封裝(2)

<button>點擊觸發1</button><button>點擊觸發2</button><div></div><style>*{margin: 0;padding: 0;}div{width: 100px;height: 100px;background-color: red;position: relative;top: 100px;left: 0;}.div1{display: block;width: …