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:
https://github.com/lamondlu/DynamicPlugins

Introduction

In the previous article, I showed you how to use the new introduction in .NET Core 3.0 AssemblyLoadContextto implement runtime upgrades and remove plugins. After completing this article, I got feedback from many garden friends. I am very happy that so many people can participate in it. I will improve this project based on your feedback. In this article, I will mainly solve the problem of loading plug-in references, which is also the most frequently asked question in feedback.

Problem use case

In the plug-ins we did before, we did very, very simple functions, without introducing any third-party libraries. But under normal circumstances, the plug-ins we create will refer to some third-party libraries more or less, so let’s try it out, use our previous project, load a third-party assembly, and see what results will be obtained. .

Here for the simulation, I created a new class library project DemoReferenceLibraryand DemoPlugin1referenced the DemoReferenceLibraryproject in the previous project.

In the DemoReferenceLibrarymiddle, I created a new Demo.cs file with the following code:

Copy    public class Demo
    {
        public string SayHello()
        {
            return "Hello World. Version 1";
        }
    }

Here is a simple pass SayHellomethod that returns a string.

Then in the DemoPlugin1project, we modify the previously created Plugin1Controller, from the Democlass through the SayHellomethod to get the string that needs to be displayed in the page.

Copy    [Area("DemoPlugin1")]
    public class Plugin1Controller : Controller
    {
        public IActionResult HelloWorld()
        {
            var content = new Demo().SayHello();
            ViewBag.Content = content;
            return View();
        }
    }

Finally, we packaged the plugin, reinstalled it into the system, and after accessing the plugin route, we got the following error.


Plug-in_development_of_ASP.NET_Core_MVC(6)_-_How_to_load_plugin_references_1.png

 

This is the problem that most students have encountered and cannot load assemblies DemoReferenceLibrary.

How to load a plugin reference?

The reason for this problem is very simple. When AssemblyLoadContextloading an assembly, we only load the plug-in assembly and not load the assembly it references.

For example, let’s take DemoPlugin1the example of this plugin as follows


Plug-in_development_of_ASP.NET_Core_MVC(6)_-_How_to_load_plugin_references_2.png

 

In this directory, in addition to our well-known DemoPlugin1.dll, DemoPlugin1.Views.dlloutside, there is a DemoReferenceLibrary.dllfile. This file is not loaded into the current plugin when it is enabled AssemblyLoadContext, so the system can’t find the dll file for this component when accessing the plugin route.

Why Mystique.Core.dll, System.Data.SqlClient.dll, Newtonsoft.Json.dllthe DLL will not be a problem it?

There are 2 kinds in .NET Core LoadContext. One is what we introduced before AssemblyLoadContext, it is a kind of customization LoadContext. The other is the system default DefaultLoadContext. When a .NET Core application is launched, it will be created and referenced DefaultLoadContext.

If not specified LoadContext, the system will load the assembly into it DefaultLoadContextby default . Here we can look at our main site project, which we have quoted Mystique.Core.dll, System.Data.SqlClient.dll, Newtonsoft.Json.dll.


Plug-in_development_of_ASP.NET_Core_MVC(6)_-_How_to_load_plugin_references_3.png

 

In the .NET Core design documentation, there is a description of the assembly loading.

If the assembly was already present in A1’s context, either because we had successfully loaded it earlier, or because we failed to load it for some reason, we return the corresponding status (and assembly reference for the success case).

However, if C1 was not found in A1’s context, the Load method override in A1’s context is invoked.

  • For Custom LoadContext , this override is an opportunity to load an assembly before the fallback (see below) to Default LoadContext is attempted to resolve the load.
  • For Default LoadContext , this override always returns null since Default Context cannot override itself.

In simple terms, this means that when LoadContextyou load an assembly in a custom , if you can’t find the assembly, the program will automatically LoadContextlook it up in the default. If LoadContextit is not found by default , it will return null.

As a result, our previous question is solved. This is because the main site has already loaded the required assembly. Although the assembly AssemblyLoadContextis not found in the plugin , the program can still LoadContextload the assembly by default .

So is it true that there is no problem?

In fact, I am not very recommended to use the above method to load third-party assemblies. There are two main reasons

  • Different plugins can reference different versions of third-party assemblies, and different versions of third-party assemblies may be implemented differently. By default, LoadContextonly one version can be loaded, resulting in a function that always has a plugin that references the assembly.
  • LoadContextThird-party assemblies that may be loaded by default are different from other plug-ins, causing other plug-in features to fail to reference the assembly.

So the most correct way here is to abandon the default LoadContextloader assembly and ensure that each plugin AssemblyLoadContextis fully loaded with the required assembly.

So how do you load these third-party assemblies? We will introduce two ways below.

  • Original way
  • Use plugin caching

Original way>

The original way is more violent, we can choose to load all the dll files in the directory where the assembly is located while loading the plug-in assembly.

First we create a plugin reference library loader interface IReferenceLoader.

Copy    public interface IRefenerceLoader
    {
        public void LoadStreamsIntoContext(CollectibleAssemblyLoadContext context, 
            string folderName, 
            string excludeFile);
    }

Then we create a default plugin reference library loader with DefaultReferenceLoaderthe following code:

Copy    public class DefaultReferenceLoader : IRefenerceLoader
    {
        public void LoadStreamsIntoContext(CollectibleAssemblyLoadContext context, 
            string folderName, 
            string excludeFile)
        {
            var streams = new List<Stream>();
            var di = new DirectoryInfo(folderName);
            var allReferences = di.GetFiles("*.dll").Where(p => p.Name != excludeFile);

            foreach (var file in allReferences)
            {
                using (var sr = new StreamReader(file.OpenRead()))
                {
                    context.LoadFromStream(sr.BaseStream);
                }
            }
        }
    }

Code interpretation

  • Here I am in order to exclude the currently loaded plugin assembly, so I added a excludeFileparameter.
  • folderNameThat is, the directory where the current plugin is located, here we get all the dll files in the currently specified directory by DirectoryInfothe GetFilesmethod of the class folderName.
  • Here I still load the third-party assembly required by the plugin through the file stream.

After completing the above code, we also need to modify the two-part code that enables the plugin.

  • [MystiqueStartup.cs] – Inject the IReferenceLoaderservice when the program starts , enable the plugin
  • [MvcModuleSetup.cs] – Trigger to enable plugin operation on the plugin management page

MystiqueStartup.cs

Copy    public static void MystiqueSetup(this IServiceCollection services, IConfiguration configuration)
    {

        ...
            
        services.AddSingleton<IReferenceLoader, DefaultReferenceLoader>();

        var mvcBuilder = services.AddMvc();

        var provider = services.BuildServiceProvider();
        using (var scope = provider.CreateScope())
        {
            ...

            foreach (var plugin in allEnabledPlugins)
            {
                var context = new CollectibleAssemblyLoadContext();
                var moduleName = plugin.Name;
                var filePath = $"{AppDomain.CurrentDomain.BaseDirectory}Modules\\{moduleName}\\{moduleName}.dll";
                var referenceFolderPath = $"{AppDomain.CurrentDomain.BaseDirectory}Modules\\{moduleName}";

                _presets.Add(filePath);
                using (var fs = new FileStream(filePath, FileMode.Open))
                {
                    var assembly = context.LoadFromStream(fs);
                    loader.LoadStreamsIntoContext(context, 
                          referenceFolderPath,
                          $"{moduleName}.dll");

                   ...
                }
            }
        }

        ...
    }

MvcModuleSetup.cs

Copy    public void EnableModule(string moduleName)
    {
        if (!PluginsLoadContexts.Any(moduleName))
        {
            var context = new CollectibleAssemblyLoadContext();

            var filePath = $"{AppDomain.CurrentDomain.BaseDirectory}Modules\\{moduleName}\\{moduleName}.dll";
            var referenceFolderPath = $"{AppDomain.CurrentDomain.BaseDirectory}Modules\\{moduleName}";
            using (var fs = new FileStream(filePath, FileMode.Open))
            {
                var assembly = context.LoadFromStream(fs);
                _referenceLoader.LoadStreamsIntoContext(context, 
                      referenceFolderPath, 
                      $"{moduleName}.dll");

                ...
            }
        }
        else
        {
            var context = PluginsLoadContexts.GetContext(moduleName);
            var controllerAssemblyPart = new MystiqueAssemblyPart(context.Assemblies.First());
            _partManager.ApplicationParts.Add(controllerAssemblyPart);
        }

        ResetControllActions();
    }

Now that we re-run the previous project and access the route to plug-in 1, you will find that the page is displayed properly and the page content is also DemoReferenceLibraryloaded from the assembly.


Plug-in_development_of_ASP.NET_Core_MVC(6)_-_How_to_load_plugin_references_4.png

 

Use plugin caching

The original way can help us successfully load the plugin reference assembly, but it is not efficient. If plugin 1 and plugin 2 refer to the same AssemblyLoadContextassembly, plugin 2 will insert 1 when plugin 1 loads all reference assemblies. Repeat what you have done. This is not what we want, we hope that if multiple plugins use the same assembly at the same time, there is no need to read the dll files repeatedly.

How to avoid repeatedly reading dll files? Here we can use a static dictionary to cache file stream information, thus avoiding repeated reading of dll files.

If you feel that using static dictionaries in ASP.NET Core MVC to cache file stream information is not secure, you can use other caching methods instead, just for a simple demonstration.

Here we first create a reference assembly cache container interface IReferenceContainer, the code is as follows:

Copy    public interface IReferenceContainer
    {
        List<CachedReferenceItemKey> GetAll();

        bool Exist(string name, string version);

        void SaveStream(string name, string version, Stream stream);

        Stream GetStream(string name, string version);
    }

Code interpretation

  • GetAllThe method will be used later to get all the reference assemblies loaded in the system.
  • ExistThe method determines whether the file stream of the specified version of the assembly exists.
  • SaveStreamIs to save the specified version of the assembly file stream to a static dictionary
  • GetStreamIs the file stream of the specified version of the assembly drawn from the static dictionary

Then we can create a default implementation DefaultReferenceContainerclass that references the assembly cache container , the code is as follows:

Copy    public class DefaultReferenceContainer : IReferenceContainer
    {
        private static Dictionary<CachedReferenceItemKey, Stream> _cachedReferences = new Dictionary<CachedReferenceItemKey, Stream>();

        public List<CachedReferenceItemKey> GetAll()
        {
            return _cachedReferences.Keys.ToList();
        }

        public bool Exist(string name, string version)
        {
            return _cachedReferences.Keys.Any(p => p.ReferenceName == name
                && p.Version == version);
        }

        public void SaveStream(string name, string version, Stream stream)
        {
            if (Exist(name, version))
            {
                return;
            }

            _cachedReferences.Add(new CachedReferenceItemKey { ReferenceName = name, Version = version }, stream);
        }

        public Stream GetStream(string name, string version)
        {
            var key = _cachedReferences.Keys.FirstOrDefault(p => p.ReferenceName == name
                && p.Version == version);

            if (key != null)
            {
                _cachedReferences[key].Position = 0;
                return _cachedReferences[key];
            }

            return null;
        }
    }

This class is relatively simple, I will not explain too much.

After completing the reference cache container, I modified the previously created IReferenceLoaderinterface and its default implementation DefaultReferenceLoader.

Copy    public interface IReferenceLoader
    {
        public void LoadStreamsIntoContext(CollectibleAssemblyLoadContext context, string moduleFolder, Assembly assembly);
    }
Copy    public class DefaultReferenceLoader : IReferenceLoader
    {
        private IReferenceContainer _referenceContainer = null;
        private readonly ILogger<DefaultReferenceLoader> _logger = null;

        public DefaultReferenceLoader(IReferenceContainer referenceContainer, ILogger<DefaultReferenceLoader> logger)
        {
            _referenceContainer = referenceContainer;
            _logger = logger;
        }

        public void LoadStreamsIntoContext(CollectibleAssemblyLoadContext context, string moduleFolder, Assembly assembly)
        {
            var references = assembly.GetReferencedAssemblies();

            foreach (var item in references)
            {
                var name = item.Name;

                var version = item.Version.ToString();

                var stream = _referenceContainer.GetStream(name, version);

                if (stream != null)
                {
                    _logger.LogDebug($"Found the cached reference '{name}' v.{version}");
                    context.LoadFromStream(stream);
                }
                else
                {

                    if (IsSharedFreamwork(name))
                    {
                        continue;
                    }

                    var dllName = $"{name}.dll";
                    var filePath = $"{moduleFolder}\\{dllName}";

                    if (!File.Exists(filePath))
                    {
                        _logger.LogWarning($"The package '{dllName}' is missing.");
                        continue;
                    }

                    using (var fs = new FileStream(filePath, FileMode.Open))
                    {
                        var referenceAssembly = context.LoadFromStream(fs);

                        var memoryStream = new MemoryStream();

                        fs.Position = 0;
                        fs.CopyTo(memoryStream);
                        fs.Position = 0;
                        memoryStream.Position = 0;
                        _referenceContainer.SaveStream(name, version, memoryStream);

                        LoadStreamsIntoContext(context, moduleFolder, referenceAssembly);
                    }
                }
            }
        }

        private bool IsSharedFreamwork(string name)
        {
            return SharedFrameworkConst.SharedFrameworkDLLs.Contains($"{name}.dll");
        }
    }

Code explanation:

  • LoadStreamsIntoContextThe assemblyparameters of the method here , the current plugin assembly.
  • Here I GetReferencedAssembliesget all the assemblies referenced by the plugin assembly through methods.
  • If the reference assembly does not exist in the reference container, we load it with the file stream and save it to the reference container. If the reference assembly already exists in the reference container, it is loaded directly into the current plugin AssemblyLoadContext. Here to verify the effect, if the assembly comes from the cache, I use the log component to output a log.
  • Since the assembly referenced by the plugin is likely to come from Shared Framework, this assembly does not need to be loaded, so here I choose to skip the loading of such assemblies. (I have not considered the release of Self-Contained here, which may be changed later)

Finally, we still need to modify MystiqueStartup.csand MvcModuleSetup.csenable the plugin code.

MystiqueStartup.cs

Copy    public static void MystiqueSetup(this IServiceCollection services, IConfiguration configuration)
    {

        ...
        services.AddSingleton<IReferenceContainer, DefaultReferenceContainer>();
        services.AddSingleton<IReferenceLoader, DefaultReferenceLoader>();
        ...

        var mvcBuilder = services.AddMvc();

        var provider = services.BuildServiceProvider();
        using (var scope = provider.CreateScope())
        {
            ...

            foreach (var plugin in allEnabledPlugins)
            {
                ...
               
                using (var fs = new FileStream(filePath, FileMode.Open))
                {
                    var assembly = context.LoadFromStream(fs);
                    loader.LoadStreamsIntoContext(context, referenceFolderPath, assembly);

                    ...
                }
            }
        }

        ...
    }

MvcModuleSetup.cs

Copy    public void EnableModule(string moduleName)
    {
        if (!PluginsLoadContexts.Any(moduleName))
        {
            ...
            using (var fs = new FileStream(filePath, FileMode.Open))
            {
                var assembly = context.LoadFromStream(fs);
                _referenceLoader.LoadStreamsIntoContext(context, referenceFolderPath, assembly);
               ...
            }
        }
        else
        {
            ...
        }

        ResetControllActions();
    }

After completing the code, in order to verify the effect, I created another plugin DemoPlugin2, the code of this project is DemoPlugin1basically the same. When the program starts, you will find DemoPlugin2that the reference assemblies used are loaded from the cache, and DemoPlugin2the routes are also accessible.


Plug-in_development_of_ASP.NET_Core_MVC(6)_-_How_to_load_plugin_references_5.gif

 

Add a page to display the loaded third-party assemblies

Here to show which assemblies are loaded in the system, I added a new page Assembilies, this page is called the method IReferenceContainerdefined in the interface GetAll, showing all loaded assemblies in the static dictionary.

The effect is as follows:


Plug-in_development_of_ASP.NET_Core_MVC(6)_-_How_to_load_plugin_references_6.png

 

Several test scenarios

Finally, after writing the above code, we use the following scenarios to test it and take a look at AssemblyLoadContextthe powerful features that we provide.

scene 1

2 plugins, one for reference DemoReferenceLibraryto version 1.0.0.0 and one for reference DemoReferenceLibraryto version 1.0.1.0. In version 1.0.0.0, SayHellothe string returned by the method is “Hello World. Version 1”, version 1.0.1.0, and SayHellothe string returned by the method is “Hello World. Version 2”.


Plug-in_development_of_ASP.NET_Core_MVC(6)_-_How_to_load_plugin_references_7.gif

 

Start the project, install plugin 1 and plugin 2, run the routes of plugin 1 and plugin 2 respectively, and you will get different results. This shows that AssemblyLoadContextwe have done a good job of isolation. Plugins 1 and 2 have referenced different versions of the same plugin, but they have no effect on each other.

Scene 2

Plugin 1 is disabled when the 2 plugins use the same third-party library and the loading is complete. Although they refer to the same assembly, you will find that plugin 2 still has normal access, which means that AssemblyLoadContextthe release of plugin 1 AssemblyLoadContexthas no effect on plugin 2 .


Plug-in_development_of_ASP.NET_Core_MVC(6)_-_How_to_load_plugin_references_8.gif

 

to sum up

In this article, I introduced you how to solve the loading problem of the plug-in reference assembly. Here we explain two ways, the original mode and the cache mode. The final effect of these two methods is the same, but the efficiency of the cache method is significantly higher. I will continue to add new content based on feedback, so stay tuned.

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