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

標題:從零開始實現ASP.NET Core MVC的插件式開發(五) - 使用AssemblyLoadContext實現插件的升級和刪除
做者:Lamond Lu
地址:http://www.javashuo.com/article/p-ujnxnwyv-z.html
源代碼:https://github.com/lamondlu/Mystiquehtml

前景回顧:git

簡介

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

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

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

.NET Core 2.2的遺留問題

程序集被佔用的緣由

回顧一下,咱們以前加載插件程序集時全部使用的代碼。c#

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方法加載的程序集會被自動鎖定,不能執行任何轉移,刪除等造做,因此這就給咱們刪除和升級插件形成了很大困難。api

PS: 升級插件須要覆蓋已加載的插件程序集,因爲程序集鎖定,因此覆蓋操做不能成功。mvc

使用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方法。app

相關連接:ide

升級.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

相關文章
相關標籤/搜索