日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > asp.net >内容正文

asp.net

从零开始实现ASP.NET Core MVC的插件式开发(五) - 插件的删除和升级

發布時間:2023/12/13 asp.net 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 从零开始实现ASP.NET Core MVC的插件式开发(五) - 插件的删除和升级 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

標題:從零開始實現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

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

創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎

總結

以上是生活随笔為你收集整理的从零开始实现ASP.NET Core MVC的插件式开发(五) - 插件的删除和升级的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。