Title: ASP.NET Core MVC from scratch to achieve the development of plug-in (f) - how to load plug-reference.
Author: Lamond Lu
Address: https://www.cnblogs.com/lwqlun/p/11717254.html
Source: https://github.com/lamondlu/DynamicPlugins
Prospects Review
- ASP.NET Core MVC from scratch to achieve the development of plug-in (a) - Use Application Part dynamic load controllers and views
- ASP.NET Core MVC from scratch to achieve the development of plug-in (two) - How to create a project template
- ASP.NET Core MVC from scratch to achieve plug-in development (c) - how to enable components at runtime
- ASP.NET Core MVC from scratch to achieve the development of plug-in (four) - plug-in installation
- ASP.NET Core MVC from scratch to achieve the development of plug-in (e) - use AssemblyLoadContext upgrade and remove plug-ins
Brief introduction
In the first one, let us demonstrate how to use .NET Core 3.0 introduced a new AssemblyLoadContext
upgrade to achieve operation and remove the plug. After completing this post, I got a lot of feedback park Friends, I am glad that so many people can be involved, I will be based on your feedback to improve the project. Benpian it, I will solve the major problems loading the plug-referenced, this feedback is being asked the most questions.
Problem use case
Before doing plug-in, we do is very, very simple function, does not introduce any third-party libraries. But under normal circumstances, will quote some third-party libraries plug-ins we created more or less, so let's try to use our previous project, the use of third-party program loads a set, and see what the results obtained .
To simulate this, I created a new Class Library project DemoReferenceLibrary
, and before the DemoPlugin1
reference to the project in DemoReferenceLibrary
the project.
In the DemoReferenceLibrary
middle, I created a new class Demo.cs file, its code is as follows:
public class Demo
{
public string SayHello()
{
return "Hello World. Version 1";
}
}
This is simply by SayHello
way of a return string.
Then in the DemoPlugin1
project, we modify previously created Plugin1Controller
, from Demo
class by SayHello
a method to get the string to be displayed on the page.
[Area("DemoPlugin1")]
public class Plugin1Controller : Controller
{
public IActionResult HelloWorld()
{
var content = new Demo().SayHello();
ViewBag.Content = content;
return View();
}
}
Finally, we look at the plug-packaged and re-install it into the system, after a visit to the plug-route, you will get the following error.
Here is the problem encountered by most of the students can not load the assembly DemoReferenceLibrary
.
How to load plugins references?
The reason for this is very simple, that is, when by AssemblyLoadContext
the time the assembly is loaded, we only load the plug-in assembly, the assembly did not load it references.
For example, our DemoPlugin1
example, this plug-in directory is as follows
In this directory, in addition to our well-known DemoPlugin1.dll
, DemoPlugin1.Views.dll
outside, there is a DemoReferenceLibrary.dll
file. This file is loaded when we did not enable plug-ins to the current AssemblyLoadContext
in it when accessing the plug-routing, the system can not find the dll file this component.
Why Mystique.Core.dll
, System.Data.SqlClient.dll
, Newtonsoft.Json.dll
the DLL will not be a problem it?
There are two in the .NET Core LoadContext
. One is before us AssemblyLoadContext
, it is a custom LoadContext
. Another is the system default DefaultLoadContext
. When a .NET Core application startup, and will create a reference DefaultLoadContext
.
If not specified LoadContext
, the system will default assemblies are loaded into DefaultLoadContext
the. Here we can look at our main site project, which we have quoted Mystique.Core.dll
, System.Data.SqlClient.dll
, Newtonsoft.Json.dll
.
In .NET Core design documentation for the assembly loading such a description
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.
Here simple terms, means that when a custom LoadContext
time to load an assembly, if the assembly can not be found, the program will automatically default to LoadContext
find, if the default LoadContext
can not be found, it will return null
.
Thus, the question before us is solved, here precisely because the primary site is already loaded assembly required, although the plugin AssemblyLoadContext
can not find this assembly, the program still can default LoadContext
to load the assembly.
It is not really no issue of it?
In fact, I do not really recommend using more than a way to load third-party assembly. There are two main reasons
- Different plug-ins can reference different versions of third-party assembly, may implement different versions of third-party assembly. The default
LoadContext
can only load one version, resulting in a total reference feature a plug-in that assembly failure. - The default
LoadContext
may be set to load third-party programs and other plug-ins are different, lead to other plug-in function reference function of the assembly failure.
So here is the most correct way, or give up using the default LoadContext
load an assembly, to ensure that each plug AssemblyLoadContext
assembly required are fully loaded.
So how do these third-party assemblies loaded it? Here we introduce two ways
- Original way
- Use plug-in cache
Original way
Compare the original way of violence, we can choose to load the plug assembly, to load an assembly where all the dll files in the directory.
Here we first created a reference library loader plug-in interface IReferenceLoader
.
public interface IRefenerceLoader
{
public void LoadStreamsIntoContext(CollectibleAssemblyLoadContext context,
string folderName,
string excludeFile);
}
Then we create a default reference library loader plug-in DefaultReferenceLoader
, its code is as follows:
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 explanation
- Here I was to exclude the currently loaded plug-in assembly, so add a
excludeFile
parameter. folderName
That is the current directory plug-ins, where we passDirectoryInfo
the classGetFiles
method to get the currently specifiedfolderName
all the dll files in the directory.- I'm still here to load third-party plug-ins required for the assembly by way of the file stream.
After completion of the above code, we also need to modify the code to enable plug-ins in two parts
- [MystiqueStartup.cs] - When the program starts, into the
IReferenceLoader
service, to enable plug-ins - [MvcModuleSetup.cs] - in the plugin admin page to enable plug-trigger operation
MystiqueStartup.cs
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
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 before we re-run the project, and the access routes plug 1, you'll find the page displayed properly, and page content but also from the DemoReferenceLibrary
loader out of focus.
Use plug-in cache
Although the original way can help us successfully loaded plug-reference the assembly, but it is not efficient, if the plug-in 1 and 2 refer to the same plug assembly, when the plug 1 AssemblyLoadContext
after loading all reference assemblies, plug-ins will plug 1 2 've done repeat. This is not what we want, we want if multiple plug-ins at the same time using the same set of programs you do not need to repeat read dll files.
How to avoid duplicate dll file to read it? Here we can use a static dictionary to cache file stream information to avoid duplication reading dll file.
If you feel in using a static dictionary in ASP.NET Core MVC stream information in the file to cache unsafe, you can switch to other caches way here just for a simple demonstration.
Here we first create a reference to an interface assembly cache container IReferenceContainer
, its code is as follows:
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 explanation
GetAll
All methods will follow the use, the system used to obtain the reference assemblies loadedExist
The method of determining the specified version of the assembly if there is a file streamSaveStream
Is the designated version of an assembly file stream stored into the static dictionaryGetStream
Is pulling specified version of the assembly from a static dictionary file stream
Then we can create a reference to the default implementation assembly cache container DefaultReferenceContainer
class, its code is as follows:
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 do not do too much explanation.
After completing the referral cache container, I modified the previously created IReferenceLoader
interface and default implementation DefaultReferenceLoader
.
public interface IReferenceLoader
{
public void LoadStreamsIntoContext(CollectibleAssemblyLoadContext context, string moduleFolder, Assembly assembly);
}
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:
- Here
LoadStreamsIntoContext
processassembly
parameters, i.e., the current plug-in assembly. - Here I pass
GetReferencedAssemblies
method, get all the assembly plug-in referenced assemblies. - If the referenced assemblies do not exist in a reference vessel, we are loading it with a file stream and save it to a reference container, if the referenced assembly already exists in the reference container, it is loaded directly into the current plug-
AssemblyLoadContext
in. Here To test the effect, if the assembly from the cache, I use the log component outputs a log. - Because the plug assembly references, may have come from
Shared Framework
, this assembly is no need to load, so I chose to skip the loading of these assemblies. (Here I have not considered the case of Self-Contained released, there may be subsequent changes)
Finally, we need to modify MystiqueStartup.cs
and MvcModuleSetup.cs
plug-in code is enabled.
MystiqueStartup.cs
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
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 the completion of the code, to test the effect, I created another plugin DemoPlugin2
, the project code and DemoPlugin1
basically the same. When the program starts, you will find DemoPlugin2
reference assemblies used are loaded from the cache, but DemoPlugin2
the routing can be normal access.
Add a page to load third-party display assembly
In order to show what the system here which programs are loading set, I added a new page Assembilies
, the page is to call the IReferenceContainer
interface defined in the GetAll
method, shows the static dictionary, all assemblies loaded.
Results are as follows:
Several test scenarios
Finally, after the completion of the preparation of the code above function, we use the following scenarios to test, take a look at AssemblyLoadContext
the power of our offer.
scene 1
2 plug-ins, a reference DemoReferenceLibrary
version 1.0.0.0, another reference to DemoReferenceLibrary
the 1.0.1.0 version. 1.0.0.0 version of which, SayHello
the string method returns "Hello World. Version 1", 1.0.1.0 version, SayHello
the string method returns "Hello World. Version 2".
Start the project, install the plug 1 and plug 2, respectively, to run plug-ins and plug-ins 1 Route 2, you'll get different results. This means AssemblyLoadContext
for us to do good isolation, plug-ins and plug-ins 2 1 Although references to different versions of the same plug-in, but did not affect each other.
Scene 2
When the two plug-ins use the same third-party libraries, and load is complete, disable the plug 1. Although they referenced assembly the same, but you will find plug-ins can be accessed or 2, which shows the plug 1 of the AssemblyLoadContext
release of the plug-2 AssemblyLoadContext
did not affect.
to sum up
Benpian I showed how to solve the problem plug-loading referenced assemblies for everyone, where we explained in two ways, original mode and cache mode. The net effect of these two methods, although the same, but the way the cache efficiency is significantly higher. I will follow-up based on feedback, continue to add new content, we stay tuned.