Plug-in development of ASP.NET Core MVC(1) – Dynamically load controllers and views using ApplicationPart

Plug-in development of ASP.NET Core MVC(2) How to create a project template

Plug-in development of ASP.NET Core MVC(3) – How to enable components at runtime

Plug-in development of ASP.NET Core MVC(4) – Plugin installation

Plug-in development of ASP.NET Core MVC(5)- Plugin removal and upgrade

Plug-in development of ASP.NET Core MVC(6) – How to load plugin references

Source code:
https://github.com/lamondlu/Mystique

Introduction

In the previous article, I explained how to implement the plugin installation. At the end of the article, I left two questions to be solved.

  • Runtime delete plugins are not implemented in .NET Core 2.2
  • Runtime upgrade plugin is not implemented in .NET Core 2.2

In fact, these two problems are in fact a problem, that is, the plug-in assembly is occupied, and the assembly cannot be replaced at runtime. In this article, I will share how I solved this problem step by step. It also took a lot of detours, consulted the information, and officially mentioned the bug in .NET Core. I almost wanted to give up, but in the end it was Find a viable solution.

Legacy issues with .NET Core 2.2

Reason why the assembly is occupied

Recall that we used all the code we used when we loaded the plugin assembly.

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

Here we used the Assembly.LoadFilemethod to load the plugin assembly. The Assembly.LoadFileassembly loaded by the method in .NET will be automatically locked, and no transfer, deletion, etc. can be performed, so this makes it very difficult for us to delete and upgrade the plugin.

PS: The upgrade plugin needs to overwrite the loaded plugin assembly. Because the assembly is locked, the overlay operation cannot be successful.

Use AssemblyLoadContext

In the .NET Framework, if you encounter this problem, the common solution is to use the AppDomainclass to implement plug-in hot plugging, but there is no AppDomainclass in .NET Core . However, after reviewing, .NET Core 2.0 introduced a AssemblyLoadContextclass to replace the .NET Freamwork AppDomain. I thought it would solve the problem of the current assembly. I didn’t expect that the .NET Core 2.x version provided AssemblyLoadContextno Unloadway to release the loaded assembly. Only the .NET Core 3.0 version AssemblyLoadContextadded Unloadmethods to the class. .

Related Links:

Upgrade .NET Core 3.0 Preview 8

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

Here, upgrading .NET Core 2.2 to .NET Core 3.0 has a few issues to be aware of.

Runtime compilation of Razor views is enabled by default in .NET Core 2.2. Simply put, .NET Core 2.2 automatically enables reading the original Razor view file and compiling the view. This is how we implemented in Chapters 3 and 4. Each plugin file is ultimately placed in a Modules directory. Each plugin has both an assembly containing Controller/Action and a corresponding raw Razor view. Directory Views, in .NET Core 2.2, when we enable a component at runtime, the corresponding Views can be loaded automatically.

CopyThe 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

But in .NET Core 3.0, runtime compilation of Razor views requires the introduction of assemblies Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation. And when the program starts, you need to start the runtime compile function.

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

If the runtime compilation of the Razor view is not enabled, the program will report an error when accessing the plugin view, indicating that the view cannot be found.


Plug-in_development_of_ASP.NET_Core_MVC-_Plugin_removal_and_upgrade_1.png

 

Load assembly using AssemblyLoadContext in .NET Core 3.0

Here to create a recyclable assembly load context, we first AssemblyLoadcontextcreate a CollectibleAssemblyLoadContextclass based on it . Among them we set the IsCollectibleproperty to true by passing the property through the parent class constructor.

Copy    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 plugin is CollectibleAssemblyLoadContextloaded with a separate one , and all plugins CollectibleAssemblyLoadContextare placed in one PluginsLoadContextobject.

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

Code explanation:

  • When loading the plugin, we need to put the assembly load context of the current plugin into the _pluginContextsdictionary. The key of the dictionary is the name of the plugin, and the value of the dictionary is the assembly loading context of the plugin.
  • When removing a plugin, we need to use Unloadmethods to release the current assembly load context.

After completing the above code, we change the code for the program to start and enable the component, because both parts need to load the plugin assembly into CollectibleAssemblyLoadContextit.

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

Unexpected result

After completing the above code, I immediately tried to delete the assembly, but the result is not what I want.

Although .NET Core 3.0 AssemblyLoadContextprovides a Unloadmethod, after calling, you will still get a file that is occupied.


Plug-in_development_of_ASP.NET_Core_MVC-_Plugin_removal_and_upgrade_2.png

 

I don’t know if this is a bug in .NET Core 3.0, or the function is designed like this. Anyway, I feel that this road is not going to work. I have tossed it for a day and found a lot of solutions on the Internet, but I can’t solve this problem.

Just as soon as I gave up, I suddenly found that the AssemblyLoadContextclass provided another way to load the assembly LoadFromStream.

Use LoadFromStream to load assembly

After seeing the LoadFromStreammethod, my first thought is to use the FileStreamload plugin assembly, then pass the obtained file to the LoadFromStreammethod, and release the FileStreamobject after the file is loaded .

According to the above ideas, I will modify the method of loading the assembly as follows

PS: The Enable method is modified in a similar way, so I won’t repeat it here.

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

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

“Empty path name is not legal. ” Issue

Just after I thought the function was all done, I reinstalled the removed plugin and tried to access the controller/action in the plugin. The result was an unexpected error, and the page contained in the plugin could not be opened.

Copyfail: 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)

The illegal error in this file path makes me feel very strange. Why is this problem? Differs from the previous code is only one place, that is, from LoadFromAssemblyPathchanged to LoadFromStream.

In order to clarify this problem, I cloned the latest .NET Core 3.0 Preview 8 source code and found that when compiling the view in the .NET Core runtime, the following methods are called.

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

This code means to find the corresponding view based on the location of the currently loaded assembly.

Then the problem is obvious. We used to LoadFromAssemblyPathload the assembly, the file location of the assembly is automatically recorded, but LoadFromStreamafter we use it, the required file location information is lost, it is an empty string, so .NET Core is trying When loading the view, it encountered an empty string and thrown an illegal path error.

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

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

But since I don’t know if it will cause other problems, I didn’t take this method. I submitted this question to the official as a bug.

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

I didn’t expect to get an official solution in just 8 hours.


Plug-in_development_of_ASP.NET_Core_MVC-_Plugin_removal_and_upgrade_3.png

 

This means that ASP.NET Core does not currently support dynamic loading of assemblies. If you want to implement functions in the current version, you need to implement a AssemblyPartclass yourself . When you get the assembly path, you return an empty collection instead of an empty string.

PS: The official has put this issue in .NET 5 Preview 1, I believe that .NET 5 will get a real solution.

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

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

        }
    }

Plugin removal and upgrade code

After solving the assembly usage problem, we can start writing the code to delete/upgrade the plugin.

Remove plugin

If you want to delete a plugin, we need to complete the following steps

  • Delete component record
  • Delete the table structure of the component migration
  • Remove the loaded ApplicationPart
  • Refresh Controller/Action
  • Remove the assembly loading context for the component
  • Delete component file

Based on this step, I wrote a Deletemethod with the following code:

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

Upgrade Plugin

For the code to upgrade the plugin, I put it together with the code for the new plugin.

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

Code explanation:

  • Here I first judge the difference between the current plugin package and the installed version.
    • Install the plugin if the current plugin is not installed on the system
    • If the current plugin package has a higher version than the installed version, upgrade the plugin
    • If the current plugin package version is lower than the installed version, downgrade the plugin (in reality this is not much)
  • InitializePluginIs used to load new components, its content is the previous new plugin method
  • UpgradePluginIs used to upgrade components, when we upgrade a component, we need to do a few things
    • Upgrade component version
    • Script migration to make the latest version of the component
    • Overwrite old packages with the latest packages
  • DegradePluginIt is used to downgrade components. Due to space issues, I will not write them in detail. You can fill them yourself.

Final effect


Plug-in_development_of_ASP.NET_Core_MVC-_Plugin_removal_and_upgrade_4.gif

 

Summary

In this article, I will demonstrate AssemblyLoadContexthow to upgrade and downgrade plugins if you use .NET Core 3.0 to solve the problem of the loaded assembly. The research in this article takes a long time, because there are so many problems in the middle, there is no solution that can be reused, and I don’t know if it is the first one to try this in .NET Core. However, the results are still good, and the functions that I want to achieve are finally made. In the follow-up, this project will continue to add new features, I hope everyone can support.

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

Orignal link:https://www.cnblogs.com/lwqlun/p/11395828.html