从零开始实现ASP.NET Core MVC的插件式开发(六) - 如何加载插件引用

标题:从零开始实现ASP.NET Core MVC的插件式开发(六) - 如何加载插件引用。 

作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/11717254.html

源代码:https://github.com/lamondlu/DynamicPlugins

640?wx_fmt=jpeg

前景回顾

•••••

简介

在前一篇中,我给大家演示了如何使用.NET Core 3.0中新引入的AssemblyLoadContext来实现运行时升级和删除插件。完成此篇之后,我得到了很多园友的反馈,很高兴有这么多人能够参与进来,我会根据大家的反馈,来完善这个项目。本篇呢,我将主要解决加载插件引用的问题,这个也是反馈中被问的最多的问题。

问题用例

在之前做的插件中,我们做的都是非常非常简单的功能,没有引入任何的第三方库。但是正常情况下,我们所创建的插件或多或少的都会引用一些第三方库,那么下面我们来尝试一下,使用我们先前的项目,加载一个使用第三方程序集, 看看会的得到什么结果。

这里为了模拟,我创建了一个新的类库项目DemoReferenceLibrary, 并在之前的DemoPlugin1项目中引用DemoReferenceLibrary项目。

DemoReferenceLibrary中,我新建了一个类Demo.cs文件, 其代码如下:

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

这里就是简单的通过SayHello方法,返回了一个字符串。

然后在DemoPlugin1项目中,我们修改之前创建的Plugin1Controller,从Demo类中通过SayHello方法得到需要在页面中显示的字符串。

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

最后我们打包一下插件,重新将其安装到系统中,访问插件路由之后,就会得到以下错误。

640?wx_fmt=png

这里就是大部分同学遇到的问题,无法加载程序集DemoReferenceLibrary

如何加载插件引用?

这个问题的原因很简单,就是当通过AssemblyLoadContext加载程序集的时候,我们只加载了插件程序集,没有加载它引用的程序集。

例如,我们以DemoPlugin1的为例,在这个插件的目录如下

640?wx_fmt=png

在这个目录中,除了我们熟知的DemoPlugin1.dll,DemoPlugin1.Views.dll之外,还有一个DemoReferenceLibrary.dll文件。这个文件我们并没有在插件启用时加载到当前的AssemblyLoadContext中,所以在访问插件路由时,系统找不到这个组件的dll文件。

为什么Mystique.Core.dllSystem.Data.SqlClient.dllNewtonsoft.Json.dll这些DLL不会出现问题呢?

在.NET Core中有2种LoadContext。一种是我们之前介绍的AssemblyLoadContext, 它是一种自定义LoadContext。另外一种就是系统默认的DefaultLoadContext。当一个.NET Core应用启动的时候,都会创建并引用一个DefaultLoadContext

如果没有指定LoadContext, 系统默认会将程序集都加载到DefaultLoadContext中。这里我们可以查看一下我们的主站点项目,这个项目我们也引用了Mystique.Core.dllSystem.Data.SqlClient.dllNewtonsoft.Json.dll

640?wx_fmt=png

在.NET Core的设计文档中,对于程序集加载有这样一段描述

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.

这里简单来说,意思就是当在一个自定义LoadContext中加载程序集的时候,如果找不到这个程序集,程序会自动去默认LoadContext中查找,如果默认LoadContext中都找不到,就会返回null

由此,我们之前的疑问就解决了,这里正是因为主站点已经加载了所需的程序集,虽然在插件的AssemblyLoadContext中找不到这个程序集,程序依然可以通过默认LoadContext来加载程序集。

那么是不是真的就没有问题了呢?

其实我不是很推荐用以上的方式来加载第三方程序集。主要原因有两点

•不同插件可以引用不同版本的第三方程序集,可能不同版本的第三方程序集实现不同。而默认LoadContext只能加载一个版本,导致总有一个插件引用该程序集的功能失效。•默认LoadContext中可能加载的第三方程序集与其他插件都不同,导致其他插件功能引用该程序集的功能失效。

所以这里最正确的方式,还是放弃使用默认LoadContext加载程序集,保证每个插件的AssemblyLoadContext都完全加载所需的程序集。

那么如何加载这些第三方程序集呢?我们下面就来介绍两种方式

•原始方式•使用插件缓存

原始方式

原始方式比较暴力,我们可以选择加载插件程序集的同时,加载程序集所在目录中所有的dll文件。

这里首先我们创建了一个插件引用库加载器接口IReferenceLoader

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

然后我们创建一个默认的插件引用库加载器DefaultReferenceLoader,其代码如下:

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

代码解释

•这里我是为了排除当前已经加载插件程序集,所以添加了一个excludeFile参数。•folderName即当前插件的所在目录,这里我们通过DirectoryInfo类的GetFiles方法,获取了当前指定folderName目录中的所有dll文件。•这里我依然通过文件流的方式加载了插件所需的第三方程序集。

完成以上代码之后,我们还需要修改启用插件的两部分代码

•[MystiqueStartup.cs] - 程序启动时,注入IReferenceLoader服务,启用插件•[MvcModuleSetup.cs] - 在插件管理页面,触发启用插件操作

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

现在我们重新运行之前的项目,并访问插件1的路由,你会发现页面正常显示了,并且页面内容也是从DemoReferenceLibrary程序集中加载出来了。

640?wx_fmt=png

使用插件缓存

原始方式虽然可以帮助我们成功加载插件引用程序集,但是它并不效率,如果插件1和插件2引用了相同的程序集,当插件1的AssemblyLoadContext加载所有的引用程序集之后,插件2会将插件1所干的事情重复一遍。这并不是我们想要的,我们希望如果多个插件同时使用了相同的程序集,就不需要重复读取dll文件了。

如何避免重复读取dll文件呢?这里我们可以使用一个静态字典来缓存文件流信息,从而避免重复读取dll文件。

如果大家觉着在ASP.NET Core MVC中使用静态字典来缓存文件流信息不安全,可以改用其他缓存方式,这里只是为了简单演示。

这里我们首先创建一个引用程序集缓存容器接口IReferenceContainer, 其代码如下:

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

代码解释

GetAll方法会在后续使用,用来获取系统中加载的所有引用程序集•Exist方法判断了指定版本程序集的文件流是否存在•SaveStream是将指定版本的程序集文件流保存到静态字典中•GetStream是从静态字典中拉取指定版本程序集的文件流

然后我们可以创建一个引用程序集缓存容器的默认实现DefaultReferenceContainer类,其代码如下:

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

这个类比较简单,我就不做太多解释了。

完成了引用缓存容器之后,我修改了之前创建的IReferenceLoader接口,及其默认实现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");	}	}

代码解释:

•这里LoadStreamsIntoContext方法的assembly参数,即当前插件程序集。•这里我通过GetReferencedAssemblies方法,获取了插件程序集引用的所有程序集。•如果引用程序集在引用容器中不存在,我们就是用文件流加载它,并将其保存到引用容器中, 如果引用程序集已存在于引用容器,就直接加载到当前插件的AssemblyLoadContext中。这里为了检验效果,如果程序集来自缓存,我使用日志组件输出了一条日志。•由于插件引用的程序集,有可能是来自Shared Framework, 这种程序集是不需要加载的,所以这里我选择跳过这类程序集的加载。(这里我还没有考虑Self-Contained发布的情况,后续这里可能会更改)

最后我们还是需要修改MystiqueStartup.csMvcModuleSetup.cs中启用插件的代码。

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

完成代码之后,为了检验效果,我创建了另外一个插件DemoPlugin2, 这个项目的代码和DemoPlugin1基本一样。程序启动时,你会发现DemoPlugin2所使用的引用程序集都是从缓存中加载的,而且DemoPlugin2的路由也能正常访问。

添加页面来显示加载的第三方程序集

这里为了显示一下系统中加载了哪些程序集,我添加了一个新页面Assembilies, 这个页面就是调用了IReferenceContainer接口中定义的GetAll方法,显示了静态字典中,所有加载的程序集。

效果如下:

640?wx_fmt=png

几个测试场景

最后,在编写完成以上代码功能之后,我们使用以下几种场景来测试一下,看一看AssemblyLoadContext为我们提供的强大功能。

场景1

2个插件,一个引用DemoReferenceLibrary的1.0.0.0版本,另外一个引用DemoReferenceLibrary的1.0.1.0版本。其中1.0.0.0版本,SayHello方法返回的字符串是"Hello World. Version 1", 1.0.1.0版本, SayHello方法返回的字符串是“Hello World. Version 2”。

启动项目,安装插件1和插件2,分别运行插件1和插件2的路由,你会得到不同的结果。这说明AssemblyLoadContext为我们做了很好的隔离,插件1和插件2虽然引用了相同插件的不同版本,但是互相之间完全没有影响。

场景2

当2个插件使用了相同的第三方库,并加载完成之后,禁用插件1。虽然他们引用的程序集相同,但是你会发现插件2还是能够正常访问,这说明插件1的AssemblyLoadContext的释放,对插件2的AssemblyLoadContext完全没有影响。

总结

本篇我为大家介绍了如何解决插件引用程序集的加载问题,这里我们讲解了两种方式,原始方式和缓存方式。这两种方式的最终效果虽然相同,但是缓存方式的效率明显更高。后续我会根据反馈,继续添加新内容,大家敬请期待。

前景回顾

•••••

640?wx_fmt=jpeg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/313269.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

小波变换基础

http://blog.csdn.net/GarfieldEr007/article/details/50151845 第十二章 小波变换 目录 1 引言 2 连续小波变换 3 二进小波变换 3.1 Haar变换 4 离散小波变换 4.1 多分辨率分析 4.2 快速小波变换算法 4.3 离散小波变换的…

.Net Core3.0使用gRPC

gRPC是什么gRPC是可以在任何环境中运行的现代开源高性能RPC框架。它可以通过可插拔的支持来有效地连接数据中心内和跨数据中心的服务&#xff0c;以实现负载平衡&#xff0c;跟踪&#xff0c;运行状况检查和身份验证。它也适用于分布式计算的最后一英里&#xff0c;以将设备&am…

单链表逆向

转自&#xff1a;http://blog.csdn.net/heyabo/article/details/7610732 对于单链表的逆置有两种方法可以实现&#xff1a; &#xff08;1&#xff09;利用辅助指针 基本思想&#xff1a;在遍历结点过程中&#xff0c;设置辅助指针&#xff0c;用于记录先前遍历的结点。这样依次…

中国.NET开发者峰会特别活动-基于k8s的微服务和CI/CD动手实践报名

2019.11.9 的中国.NET开发者峰会将在上海举办&#xff0c;到目前为止&#xff0c;大会的主题基本确定&#xff0c;这两天就会和大家会面&#xff0c;很多社区的同学基于对社区的信任在我们议题没有确定的情况下已经购票超过了300张&#xff0c;而且分享的主题都来自于社区&…

高斯混合模型学习

转自&#xff1a;http://blog.csdn.net/jojozhangju/article/details/19182013 1.高斯混合模型概述 高斯密度函数估计是一种参数化模型。高斯混合模型&#xff08;Gaussian Mixture Model, GMM&#xff09;是单一高斯概率密度函数的延伸&#xff0c;GMM能够平滑地近似任意形状的…

[工具]OFFICE插件管理工具-帮助更好地管理及使用电脑安装过的OFFICE插件

在OFFICE软件的世界中&#xff0c;除了由微软提供的OFFICE软件功能外&#xff0c;还有大量的功能由第三方开发者完成&#xff0c;市面上也存在大量的OFFICE插件供用户选择。使用场景有些插件仅在某个特定场景下才会使用&#xff0c;日常办公过程中&#xff0c;无需开启&#xf…

随机梯度下降的实现细节

http://www.miaoerduo.com/deep-learning/%E5%9F%BA%E4%BA%8Ecaffe%E7%9A%84deepid2%E5%AE%9E%E7%8E%B0%EF%BC%88%E4%B8%8A%EF%BC%89.html 最近看了一篇文章&#xff0c;详细说明了随机梯度下降中随机是在create_imagenet.sh中shuffle实现的。 相关资源&#xff1a; DeepID&am…

那位标榜技术驱动的开发者去哪了?

作者&#xff1a;邹溪源&#xff0c;长沙资深互联网从业者&#xff0c;架构师社区合伙人&#xff01;一他是一位曾经标榜技术驱动世界的开发者&#xff0c;在他年轻的时候&#xff0c;一段独特的经历&#xff0c;让他对技术充满了兴趣&#xff0c;并在技术这条道路上走了很远很…

图像PCA方法

http://blog.csdn.net/lifeng_math/article/details/50014073 http://blog.csdn.net/lifeng_math/article/details/49993763#旋转不变的-lbp 引言 PCA是Principal Component Analysis的缩写&#xff0c;也就是主成分分析。也是用于降维常用的一中方法。PCA 主要用于数据降维&a…

.NET实时2D渲染入门·动态时钟

前言说来这是个我和我老婆的爱情故事。从小以来“坦克大战”、“魂斗罗”等游戏总令我魂牵梦绕。这些游戏的基础就是 2D实时渲染&#xff0c;以前没意识&#xff0c;直到后来找到了 Direct2D。我的 2D实时渲染入门&#xff0c;是从这个 动态时钟开始的。本文将使用我写的“准游…

ASP.NET Core在 .NET Core 3.1 Preview 1中的更新

.NET Core 3.1 Preview 1现在可用。此版本主要侧重于错误修复&#xff0c;但同时也包含一些新功能。对Razor components的部分类支持将参数传递给顶级组件在HttpSysServer中支持共享队列在SameSite cookies的重大更改除了.NET Core 3.1 Preview版本发布之外&#xff0c;我们还发…

小波变换学习(1)

转自&#xff1a;https://www.zhihu.com/question/22864189/answer/40772083从傅里叶变换到小波变换&#xff0c;并不是一个完全抽象的东西&#xff0c;可以讲得很形象。小波变换有着明确的物理意义&#xff0c;如果我们从它的提出时所面对的问题看起&#xff0c;可以整理出非常…

.NET Core 3.0 新 JSON API - JsonDocument

JsonDocument类 JsonDocument是基于Utf8JsonReader 构建的。JsonDocument 可分析 JSON 数据并生成只读文档对象模型 (DOM)&#xff0c;可对模型进行查询&#xff0c;以支持随机访问和枚举。使用 JsonDocument 分析常规 JSON 有效负载并访问其所有成员比使用 Json.NET 快 2-3 倍…

微软推出 Microsoft.Data.SqlClient,替代 System.Data.SqlClient

背景在 .NET 创建之初&#xff0c;System.Data 框架是一个重要的组件。它为创建 .NET 数据库驱动程序提供了一种方式&#xff0c;类似 Visual Basic 的 ActiveX Data Objects。虽然 API 不一样&#xff0c;但重用了它的名称&#xff0c;所以才有了 ADO .NET 这个绰号。ADO 和 A…

C++ 从文件夹中读取文件

OpenCV从文件夹中读取内含文件方法 参考&#xff1a;http://www.2cto.com/kf/201407/316515.html http://www.it610.com/article/5126146.htm http://blog.csdn.net/adong76/article/details/39432467 windows平台代码&#xff1a; [cpp] view plaincopy #include <io.h&…

你必须知道的容器监控 (1) Docker自带子命令与Weave Scope

本篇已加入《》&#xff0c;可以点击查看更多容器化技术相关系列文章。本篇会介绍几个目前比较常用且流行的容器监控工具&#xff0c;首先我们来看看Docker自带的几个监控子命令&#xff1a;ps、top以及stats&#xff0c;然后是一个功能更强的开源监控工具Weave Scope。# 实验环…

char *与char []

由于指针的灵活性&#xff0c;导致指针能代替数组使用&#xff0c;或者混合使用&#xff0c;这些导致了许多指针和数组的迷惑&#xff0c;因此&#xff0c;刻意再次深入探究了指针和数组这玩意儿&#xff0c;其他类型的数组比较简单&#xff0c;容易混淆的是字符数组和字符指针…

.NET Core ORM 类库Petapoco中对分页Page添加Order By对查询的影响

介绍最近一直在使用PetapocoEntity Framework Core结合开发一套系统。使用EFCore进行Code First编码&#xff0c;使用使用Petapoco进行数据库的常规操作。并且结合PetaPoco.SqlKata的使用&#xff0c;减少了编写SQL语句的工作量&#xff0c;对提升开发效率有很大的帮助。Petapo…

.Net Core 3.0 IdentityServer4 快速入门

一、简介IdentityServer4是用于ASP.NET Core的OpenID Connect和OAuth 2.0框架。将IdentityServer4部署到您的应用中具备如下特点&#xff1a;1&#xff09;、认证服务2&#xff09;、单点登陆3&#xff09;、API访问控制4&#xff09;、联合网关5&#xff09;、专注于定制6&…

.NET Core3.0创建Worker Services

.NET CORE 3.0新增了Worker Services的新项目模板&#xff0c;可以编写长时间运行的后台服务&#xff0c;并且能轻松的部署成windows服务或linux守护程序。如果安装的vs2019是中文版本&#xff0c;Worker Services变成了辅助角色服务。Worker Services 咱也不知道怎么翻译成了这…