Plug-in development for ASP.NET Core MVC from scratch - deletion and upgrade of plug-ins

Title: Plug-in development for ASP.NET Core MVC from scratch (5) - Upgrade and delete plug-ins using AssemblyLoadContext
Author: Lamond Lu
Address: https://www.cnblogs.com/lwqlun/p/11395828.html
Source code: https://github.com/lamondlu/Mystique


Prospect Review:

brief introduction

In the last article, I explained how to install the plug-in. At the end of the article, there are two issues to be solved.

  • Runtime deletion plug-ins cannot be implemented in.NET Core 2.2
  • Runtime upgrade plug-ins cannot be implemented in.NET Core 2.2

In fact, both of these problems are ultimately a problem: plug-in assemblies are occupied and cannot be replaced at runtime.In this article, I'll share how I've tackled this problem step by step, bypassing many detours, consulting data, mentioning Bug officially at.NET Core, and almost quitting several times, but ultimately finding a workable solution.

Legacy issues from.NET Core 2.2

Reasons why assemblies are occupied

Recall that all the code we used to load the plug-in assembly before.

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

Here we used the Assembly.LoadFile method to load the plug-in assembly.Assemblies loaded in.NET using the Assembly.LoadFile method are automatically locked and cannot perform any transfers, deletions, and so on, which makes it difficult for us to delete and upgrade plug-ins.

PS: Upgrading a plug-in requires overwriting the loaded plug-in assembly. Because the assembly is locked, the overwrite operation cannot succeed.

Use AssemblyLoadContext

In the.NET Framework, if you encounter this problem, a common solution is to use the AppDomain class for plug-in hot-plugging, but there is no AppDomain class in.NET Core.However, after.NET Core 2.0, an AssemblyLoadContext class was introduced to replace AppDomain in.NET Freamwork.I thought using it would solve the current assembly usage problem. As a result, the AssemblyLoadContext provided by the.NET Core 2.x version does not provide an Unload method to release loaded assemblies. Unload methods are only added to the AssemblyLoadContext class in the.NET Core 3.0 version.

Related links:

Upgrade.NET Core 3.0 Preview 8

Therefore, to complete the removal and upgrade of the plug-in, I upgraded the entire project to the latest version of.NET Core 3.0 Preview 8.

There is one thing to note here about upgrading.NET Core 2.2 to.NET Core 3.0.

Runtime compilation of Razor views is enabled by default in.NET Core 2.2, or simply. NET Core 2.2 automatically enables reading the original Razor view files and compiling the views.This is how we implement it in Chapters 3 and 4. Each plug-in file is eventually placed in a Modules directory, where each plug-in has both an assembly containing Controller/Action and a corresponding original Razor View directory Views, which can be automatically loaded when a component is enabled at run time in.NET Core 2.2.

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

However, in.NET Core 3.0, the runtime compilation of the Razor view requires the introduction of the assembly Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.And when the program starts, it needs to start the runtime compilation function.

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

If runtime compilation of Razor view is not enabled, an error will occur when the program accesses the plug-in view, indicating that the view cannot be found.

Loading assemblies using the AssemblyLoadContext of.NET Core 3.0

In order to create a recyclable assembly loading context, we first create a CollectibleAssemblyLoadContext class based on AssemblyLoadcontext.Here we set the IsCollectible property to true through the parent constructor.

	public class CollectibleAssemblyLoadContext 
        : AssemblyLoadContext
    {
        public CollectibleAssemblyLoadContext() 
        	: base(isCollectible: true)
        {
        }

        protected override Assembly Load(AssemblyName name)
        {
            return null;
        }
    }

In the design of the entire plug-in loading context, each plug-in is loaded using a separate CollectibleAssemblyLoadContext, and CollectibleAssemblyLoadContext for all plug-ins is placed in a PluginsLoadContext object.

Related code: 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);
        }
    }

Code explanation:

  • When loading a plug-in, we need to place the assembly loading context of the current plug-in in the _pluginContexts dictionary.The key of the dictionary is the name of the plug-in, and the value of the dictionary is the assembly loading context of the plug-in.
  • When removing a plug-in, we need to use the Unload method to release the current assembly loading context.

After completing the above code, we change the code for program startup and component enablement, as both parts require the plug-in assembly to be loaded into the 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");
    }

Unexpected results

Immediately after completing the above code, I tried to delete the assembly, but the result was not what I wanted.

Although.NET Core 3.0 provides the Unload method for AssemblyLoadContext, you still get a file occupancy error after calling it

I don't know if it's a bug in.NET Core 3.0 or if it's a function designed like this for the moment. I feel I can't walk along this road anymore. I have been working around for a day and I have found a lot of solutions on the Internet, but I can't solve this problem.

Just as we were about to give up, we suddenly discovered that the AssemblyLoadContext class provided another way to load assemblies, LoadFromStream.

LoadFromStream to load assemblies instead

After seeing the LoadFromStream method, my first thought was to load the plug-in assembly using FileStream, then stream the resulting file to the LoadFromStream method and release the FileStream object after the file has been loaded.

Based on the ideas above, I'll modify the way I load assemblies as follows

PS: The Enable method is modified similarly, so I won't repeat it here.

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

After the modification, I tried to delete the plug-in code again, and it was successfully deleted.

"Empty path name is not legal." Question

Just after I thought the functionality was complete, I reinstalled the deleted plug-in and tried to access the controller/action in the plug-in. An unexpected error occurred and the page contained in the plug-in could not be opened.

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)

This file path illegal error makes me wonder why there is such a problem?The only difference from the previous code is that it changed from LoadFromAssemblyPath to LoadFromStream.

To figure this out, I clone updated the latest.NET Core 3.0 Preview 8 source code and found that when you compile a view at the.NET Core runtime, the following methods are called.

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;
    }

This code means to discover the corresponding view based on where the assembly is currently loaded.

The problem is obvious. We loaded the assembly with LoadFromAssemblyPath before, and the file location of the assembly was automatically recorded, but when we changed to LoadFromStream, the required file location information was lost and was an empty string, so.NET Core encountered an empty string and threw an error in an illegal path when trying to load the view.

In fact, the method here is very good, just exclude the path of the empty string.

	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;
    }

However, since it was not clear if other problems would result, I did not take this approach and submitted the issue to the authorities as a Bug.

Question address: https://github.com/aspnet/AspNetCore/issues/13312

Unexpectedly, there was an official solution in just eight hours.

This means that ASP.NET Core does not support dynamic loading of assemblies at this time. If you want to implement functionality in the current version, you need to implement an AssemblyPart class yourself, which returns an empty collection instead of an empty string when you get the assembly path.

PS: The authorities have put this issue in.NET 5 Preview 1 and believe that.NET 5 will actually be solved.

According to the official scheme, the final version of the Startup.cs file

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

        }
    }

Code for plug-in deletion and upgrade

Once we have resolved the assembly usage issue, we can start writing code to delete/upgrade the plug-in.

Delete Plugin

To remove a plug-in, we need to complete the following steps

  • Delete Component Record
  • Delete table structure for component migration
  • Remove Loaded ApplicationPart
  • Refresh Controller/Action
  • Remove assembly load context corresponding to component
  • Delete Component Files

Following this step, I wrote a Delete method with the following code:

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

Upgrade Plugin

For the code to upgrade the plug-in, I put it with the code to add the plug-in

	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();
    }

Code explanation:

  • Here I first judge the version differences between the current plug-in package and the installed version

    • If the current plug-in has not been installed on the system, install the plug-in
    • Upgrade the plug-in if the current version of the plug-in package is higher than the installed version
    • If the current version of the plug-in package is lower than the installed version, downgrade the plug-in (this is not much in reality)
  • Initialize Plugin is used to load new components, and its content is what was previously added as a plug-in method

  • UpgradePlugin is used to upgrade components. When we upgrade a component, there are a few things we need to do

    • Upgrade Component Version
    • Do script migration for the latest version of components
    • Overwrite old packages with the latest
  • DegradePlugin is used to downgrade components. Due to space issues, I won't go into details and you can fill it in.

Final results

summary

In this article, I'll show you how you can upgrade and downgrade plug-ins by using the AssemblyLoadContext of.NET Core 3.0 to solve the problem of assembly occupancy when loaded.This article takes a long time to study because there are really too many problems in it, and there is nothing to reuse, and I don't know if it's the first time I've tried this in.NET Core.But the result is okay, the function you want to achieve is finally made.Subsequently, this project will continue to add new features, I hope you can support more.

Project Address: https://github.com/lamondlu/Mystique

Tags: github JSON

Posted on Tue, 05 May 2020 18:25:01 -0700 by Fender963