zoukankan      html  css  js  c++  java
  • 从零开始实现ASP.NET Core MVC的插件式开发(五)

    标题:从零开始实现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的遗留问题#

    程序集被占用的原因#

    回顾一下,我们之前加载插件程序集时所有使用的代码。

    Copy
    	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可以自动加载。

    Copy
    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。并且在程序启动时,需要启动运行时编译的功能。

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

    如果没有启用Razor视图的运行时编译,程序访问插件视图的时候,就会报错,提示视图找不到。

    使用.NET Core 3.0的AssemblyLoadContext加载程序集#

    这里为了创建一个可回收的程序集加载上下文,我们首先基于AssemblyLoadcontext创建一个CollectibleAssemblyLoadContext类。其中我们将IsCollectible属性通过父类构造函数,将其设置为true。

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

    在整个插件加载上下文的设计上,每个插件都使用一个单独的CollectibleAssemblyLoadContext来加载,所有插件的CollectibleAssemblyLoadContext都放在一个PluginsLoadContext对象中。

    相关代码: PluginsLoadContexts.cs

    Copy
    	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

    Copy
    	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

    Copy
    	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方法的修改方式类似,这里我就不重复写了。

    Copy
    	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, 结果得到了意想不到的错误,插件的中包含的页面打不开了。

    Copy
    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

    Copy
        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在尝试加载视图的时候,遇到空字符串,抛出了一个非法路径的错误。

    其实这里的方法很好改,只需要将空字符串的路径排除掉即可。

    Copy
    	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文件的最终版本

    Copy
    	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方法,代码如下:

    Copy
    	    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");
            }
            
    

    升级插件#

    对于升级插件的代码,我将它和新增插件的代码放在了一起

    Copy
    	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中这么尝试的。不过结果还算好,想实现的功能最终还是做出来了。后续呢,这个项目会继续添加新的功能,希望大家多多支持。

  • 相关阅读:
    记一次线上网络问题排查
    记服务器上session设置引起的OutofMemory
    由DateFormat引起的线程安全问题
    十二周总结
    团队开发冲刺第十天
    团队开发冲刺第九天
    团队开发冲刺第八天
    团队开发冲刺第七天
    十二周总结
    团队开发冲刺第六天
  • 原文地址:https://www.cnblogs.com/luomingui/p/12573250.html
Copyright © 2011-2022 走看看