从零开始实现ASP.NET Core MVC的插件式开发(三) - 如何在运行时启用组件

标题:从零开始实现ASP.NET Core MVC的插件式开发(三) - 如何在运行时启用组件 

作者:Lamond Lu

地址:https://www.cnblogs.com/lwqlun/p/11260750.html

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

640?wx_fmt=jpeg

前情回顾

从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用Application Part动态加载控制器和视图[1]从零开始实现ASP.NET Core MVC的插件式开发(二) - 如何创建项目模板[2]



在前面两篇中,我为大家演示了如何使用Application Part动态加载控制器和视图,以及如何创建插件模板来简化操作。在上一篇写完之后,我突然想到了一个问题,如果像前两篇所设计那个来构建一个插件式系统,会有一个很严重的问题,即

当你添加一个插件之后,整个程序不能立刻启用该插件,只有当重启整个ASP.NET Core应用之后,才能正确的加载插件。因为所有插件的加载都是在程序启动时ConfigureService方法中配置的。

这种方式的插件系统会很难用,我们期望的效果是在运行时动态启用和禁用插件,那么有没有什么解决方案呢?答案是肯定的。下面呢,我将一步一步说明一下自己的思路、编码中遇到的问题,以及这些问题的解决方案。

为了完成这个功能,我走了许多弯路,当前这个方案可能不是最好的,但是确实是一个可行的方案,如果大家有更好的方案,我们可以一起讨论一下。

在Action中激活组件

当遇到这个问题的时候,我的第一思路就是将ApplicationPartManager加载插件库的代码移动到某个Action中。于是我就在主站点中创建了一个PluginsController, 并在启用添加了一个名为Enable的Action方法。

public class PluginsController : Controller	
{	public IActionResult Enable()	{	var assembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "DemoPlugin1\\DemoPlugin1.dll");	var viewAssembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "DemoPlugin1\\DemoPlugin1.Views.dll");	var viewAssemblyPart = new CompiledRazorAssemblyPart(viewAssembly);	var controllerAssemblyPart = new AssemblyPart(assembly);	_partManager.ApplicationParts.Add(controllerAssemblyPart);	_partManager.ApplicationParts.Add(viewAssemblyPart);	return Content("Enabled");	}	
}

修改代码之后,运行程序,这里我们首先调用/Plugins/Enable来尝试激活组件,激活之后,我们再次调用/Plugin1/HelloWorld

这里会发现程序返回了404, 即控制器和视图没有正确的激活。

640?wx_fmt=png

这里你可能有疑问,为什么会激活失败呢?

这里的原因是,只有当ASP.NET Core应用启动时,才会去ApplicationPart管理器中加载控制器与视图的程序集,所以虽然新的控制器程序集在运行时被添加到了ApplicationPart管理器中,但是ASP.NET Core不会自动进行更新操作,所以这里我们需要寻找一种方式能够让ASP.NET Core重新加载控制器的方法。

通过查询各种资料,我最终找到了一个切入点,在ASP.NET Core 2.2中有一个类是ActionDescriptorCollectionProvider,它的子类DefaultActionDescriptorCollectionProvider是用来配置Controller和Action的。

源代码:

    internal class DefaultActionDescriptorCollectionProvider : ActionDescriptorCollectionProvider	{	private readonly IActionDescriptorProvider[] _actionDescriptorProviders;	private readonly IActionDescriptorChangeProvider[] _actionDescriptorChangeProviders;	private readonly object _lock;	private ActionDescriptorCollection _collection;	private IChangeToken _changeToken;	private CancellationTokenSource _cancellationTokenSource;	private int _version = 0;	public DefaultActionDescriptorCollectionProvider(	IEnumerable<IActionDescriptorProvider> actionDescriptorProviders,	IEnumerable<IActionDescriptorChangeProvider> actionDescriptorChangeProviders)	{	...	ChangeToken.OnChange(	GetCompositeChangeToken,	UpdateCollection);	}	public override ActionDescriptorCollection ActionDescriptors	{	get	{	Initialize();	return _collection;	}	}	...	private IChangeToken GetCompositeChangeToken()	{	if (_actionDescriptorChangeProviders.Length == 1)	{	return _actionDescriptorChangeProviders[0].GetChangeToken();	}	var changeTokens = new IChangeToken[_actionDescriptorChangeProviders.Length];	for (var i = 0; i < _actionDescriptorChangeProviders.Length; i++)	{	changeTokens[i] = _actionDescriptorChangeProviders[i].GetChangeToken();	}	return new CompositeChangeToken(changeTokens);	}	...	private void UpdateCollection()	{	lock (_lock)	{	var context = new ActionDescriptorProviderContext();	for (var i = 0; i < _actionDescriptorProviders.Length; i++)	{	_actionDescriptorProviders[i].OnProvidersExecuting(context);	}	for (var i = _actionDescriptorProviders.Length - 1; i >= 0; i--)	{	_actionDescriptorProviders[i].OnProvidersExecuted(context);	}	var oldCancellationTokenSource = _cancellationTokenSource;	_collection = new ActionDescriptorCollection(	new ReadOnlyCollection<ActionDescriptor>(context.Results),	_version++);	_cancellationTokenSource = new CancellationTokenSource();	_changeToken = new CancellationChangeToken(_cancellationTokenSource.Token);	oldCancellationTokenSource?.Cancel();	}	}	}

•这里ActionDescriptors属性中记录了当ASP.NET Core程序启动后,匹配到的所有Controller/Action集合。•UpdateCollection方法使用来更新ActionDescriptors集合的。•在构造函数中设计了一个触发器,ChangeToken.OnChange(GetCompositeChangeToken,UpdateCollection)。这里程序会监听一个Token对象,当这个Token对象发生变化时,就自动触发UpdateCollection方法。•这里Token是由一组IActionDescriptorChangeProvider接口对象组合而成的。

所以这里我们就可以通过自定义一个IActionDescriptorChangeProvider接口对象,并在组件激活方法Enable中修改这个接口Token的方式,使DefaultActionDescriptorCollectionProvider中的CompositeChangeToken发生变化,从而实现控制器的重新装载。

使用IActionDescriptorChangeProvider在运行时激活控制器

这里我们首先创建一个MyActionDescriptorChangeProvider类,并让它实现IActionDescriptorChangeProvider接口

    public class MyActionDescriptorChangeProvider : IActionDescriptorChangeProvider	{	public static MyActionDescriptorChangeProvider Instance { get; } = new MyActionDescriptorChangeProvider();	public CancellationTokenSource TokenSource { get; private set; }	public bool HasChanged { get; set; }	public IChangeToken GetChangeToken()	{	TokenSource = new CancellationTokenSource();	return new CancellationChangeToken(TokenSource.Token);	}	}

然后我们需要在Startup.csConfigureServices方法中,将MyActionDescriptorChangeProvider.Instance属性以单例的方式注册到依赖注入容器中。

    public void ConfigureServices(IServiceCollection services)	{	...	services.AddSingleton<IActionDescriptorChangeProvider>(MyActionDescriptorChangeProvider.Instance);	services.AddSingleton(MyActionDescriptorChangeProvider.Instance);	...	}

最后我们在Enable方法中通过两行代码来修改当前MyActionDescriptorChangeProvider对象的Token。

    public class PluginsController : Controller	{	public IActionResult Enable()	{	var assembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "DemoPlugin1\\DemoPlugin1.dll");	var viewAssembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "DemoPlugin1\\DemoPlugin1.Views.dll");	var viewAssemblyPart = new CompiledRazorAssemblyPart(viewAssembly);	var controllerAssemblyPart = new AssemblyPart(assembly);	_partManager.ApplicationParts.Add(controllerAssemblyPart);	_partManager.ApplicationParts.Add(viewAssemblyPart);	MyActionDescriptorChangeProvider.Instance.HasChanged = true;	MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();	return Content("Enabled");	}	}

修改代码之后重新运行程序,这里我们依然首先调用/Plugins/Enable,然后再次调用/Plugin1/Helloworld, 这时候你会发现Action被触发了,只是没有找到对应的Views。

640?wx_fmt=png

如何解决插件的预编译Razor视图不能重新加载的问题?

通过以上的方式,我们终于获得了在运行时加载插件控制器程序集的能力,但是插件的预编译Razor视图程序集没有被正确加载,这就说明IActionDescriptorChangeProvider只会触发控制器的重新加载,不会触发预编译Razor视图的重新加载。ASP.NET Core只会在整个应用启动时,才会加载插件的预编译Razor程序集,所以我们并没有获得在运行时重新加载预编译Razor视图的能力。

针对这一点,我也查阅了好多资料,最终也没有一个可行的解决方案,也许使用ASP.NET Core 3.0的Razor Runtime Compilation可以实现,但是在ASP.NET Core 2.2版本,我们还没有获得这种能力。

为了越过这个难点,最终我还是选择了放弃预编译Razor视图,改用原始的Razor视图。

因为在ASP.NET Core启动时,我们可以在Startup.csConfigureServices方法中配置Razor视图引擎检索视图的规则。

这里我们可以把每个插件组织成ASP.NET Core MVC中一个Area, Area的名称即插件的名称, 这样我们就可以将为Razor视图引擎的添加一个检索视图的规则,代码如下

    services.Configure<RazorViewEngineOptions>(o =>	{	o.AreaViewLocationFormats.Add("/Modules/{2}/{1}/Views/{0}" + RazorViewEngine.ViewExtension);	});

这里{2}代表Area名称, {1}代表Controller名称, {0}代表Action名称。

这里Modules是我重新创建的一个目录,后续所有的插件都会放置在这个目录中。

同样的,我们还需要在Configure方法中为Area注册路由。

    app.UseMvc(routes =>	{	routes.MapRoute(	name: "default",	template: "{controller=Home}/{action=Index}/{id?}");	routes.MapRoute(	name: "default",	template: "Modules/{area}/{controller=Home}/{action=Index}/{id?}");	});

因为我们已经不需要使用Razor的预编译视图,所以Enable方法我们的最终代码如下

    public IActionResult Enable()	{	var assembly = Assembly.LoadFile(AppDomain.CurrentDomain.BaseDirectory + "Modules\\DemoPlugin1\\DemoPlugin1.dll");	var controllerAssemblyPart = new AssemblyPart(assembly);	_partManager.ApplicationParts.Add(controllerAssemblyPart);	MyActionDescriptorChangeProvider.Instance.HasChanged = true;	MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();	return Content("Enabled");	}

以上就是针对主站点的修改,下面我们再来修改一下插件项目。

首先我们需要将整个项目的Sdk类型改为由之前的Microsoft.Net.Sdk.Razor改为Microsoft.Net.Sdk.Web, 由于之前我们使用了预编译的Razor视图,所以我们使用了Microsoft.Net.Sdk.Razor,它会将视图编译为一个dll文件。但是现在我们需要使用原始的Razor视图,所以我们需要将其改为Microsoft.Net.Sdk.Web, 使用这个Sdk, 最终的Views文件夹中的文件会以原始的形式发布出来。

<Project Sdk="Microsoft.NET.Sdk.Web">	<PropertyGroup>	<TargetFramework>netcoreapp2.2</TargetFramework>	</PropertyGroup>	<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">	<OutputPath></OutputPath>	</PropertyGroup>	<ItemGroup>	<PackageReference Include="Microsoft.AspNetCore.App" Version="2.2.0" />	<PackageReference Include="Microsoft.AspNetCore.Razor" Version="2.2.0" />	<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" />	</ItemGroup>	<ItemGroup>	<ProjectReference Include="..\DynamicPlugins.Core\DynamicPlugins.Core.csproj" />	</ItemGroup>	</Project>	

最后我们需要在Plugin1Controller上添加Area配置, 并将编译之后的程序集以及Views目录放置到主站点项目的Modules目录中

    [Area("DemoPlugin1")]	public class Plugin1Controller : Controller	{	public IActionResult HelloWorld()	{	return View();	}	}

最终主站点项目目录结构

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	

现在我们重新启动项目,重新按照之前的顺序,先激活插件,再访问新的插件路由/Modules/DemoPlugin1/plugin1/helloworld, 页面正常显示了。

640?wx_fmt=png

总结

本篇中,我为大家演示了如何在运行时启用一个插件,这里我们借助IActionDescriptorChangeProvider, 让ASP.NET Core在运行时重新加载了控制器,虽然不支持预编译Razor视图的加载,但是我们通过配置原始Razor视图加载的目录规则,同样实现了动态读取视图的功能。

下一篇我将继续将这个项目重构,编写业务模型,并尝试编写插件的安装以及升降级版本的代码。

References

[1] 从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用Application Part动态加载控制器和视图: https://www.cnblogs.com/lwqlun/p/11137788.html#4310745
[2] 从零开始实现ASP.NET Core MVC的插件式开发(二) - 如何创建项目模板: https://www.cnblogs.com/lwqlun/p/11155666.html


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

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

相关文章

SP1043 GSS1 - Can you answer these queries I 猫树

传送门 文章目录题意&#xff1a;思路&#xff1a;题意&#xff1a; 思路&#xff1a; 猫树是一种可以O(nlogn)O(nlogn)O(nlogn)预处理&#xff0c;O(1)O(1)O(1)查询的数据结构。预处理的信息应该满足可合并的性质&#xff0c;与线段树pushuppushuppushup的原理相同&#xff0…

动手造轮子:基于 Redis 实现 EventBus

动手造轮子&#xff1a;基于 Redis 实现 EventBusIntro上次我们造了一个简单的基于内存的 EventBus&#xff0c;但是如果要跨系统的话就不合适了&#xff0c;所以有了这篇基于 Redis 的 EventBus 探索。本文的实现是基于 StackExchange.Redis 来实现。RedisEventStore 实现既然…

最小生成树KrusKal算法(并查集)

洛谷p1111链接 克鲁斯卡尔算法的思路就是由森林变成树的过程&#xff0c;其中最主要的就是贪心和并查集的应用。 我们知道链接n个点需要n-1条边&#xff0c;这就满足的最后生成的是一颗树&#xff0c;而不是一个环。在这n-1条边的选择上我们又要尽可能的让边的权重小&#xff0…

#6278. 数列分块 2 分块 + 块内二分

传送门 文章目录题意&#xff1a;思路&#xff1a;题意&#xff1a; 思路&#xff1a; 真 调一晚上血压上来了。 考虑第一个操作&#xff0c;块内打个标记&#xff0c;其他的暴力查询即可。 考虑第二个操作&#xff0c;讲块内元素排序之后&#xff0c;直接二分查询。 注意修改…

使用腾讯云提供的针对Nuget包管理器的缓存加速服务

继阿里巴巴开源镜像站&#xff08;https://opsx.alibaba.com/&#xff09;、华为云镜像站点&#xff08;https://mirrors.huaweicloud.com/ &#xff09;之后&#xff0c;腾讯也已于近日上线了类似的服务&#xff0c;官方名称为腾讯云软件源&#xff08;Tencent Open Source Mi…

最小生成树Prime算法

洛谷p1546链接 Prime算法的核心也是贪心&#xff0c;但是不同的就是&#xff0c;它是一直维护一颗树&#xff0c; 直到变成一颗最小生成树&#xff0c; #include<bits/stdc.h> using namespace std; const int maxn 110; const int inf 0x3f3f3f3f; int maze[maxn][m…

#6284. 数列分块 8 分块

传送门 文章目录题意&#xff1a;思路&#xff1a;题意&#xff1a; 思路&#xff1a; 乍一看貌似没有什么东西能维护块内同一个数的个数&#xff0c;但是通过第六感可以发现每次操作后区间都会被推成一个数&#xff0c;那么我们分个块&#xff0c;让后块内打个标记&#xff0…

最短路弗洛伊德(Floyd)算法加保存路径

弗洛伊德算法大致有点像dp的推导 dp[i][j] min(dp[i][k] dp[k][j], dp[i][j]), 其中 i 是起始点&#xff0c;j 是终止点。k是它们经过的中途点。 通过这个公式不断地更新dp[i][j],得到最短路径长。 我们先定义两个矩阵&#xff0c;minpath[i][j],表示的是从 i 到 j 当前得到的…

云考古 | Azure 自建 RDS 让 iPad 跑 Office 97

导语苹果一直在尝试把iPad做成电脑&#xff0c;但效果始终不如真正的PC理想。如果能在iPad上运行PC软件&#xff0c;如完整版的Office&#xff0c;那一定是一种非常理想的方式。我小时候电脑启蒙使用的第一个软件就是Office 97里的Word&#xff0c;这也是第一款引入Office助手&…

P3338 [ZJOI2014]力 FFT + 推式子

传送门 文章目录题意&#xff1a;思路&#xff1a;题意&#xff1a; 思路&#xff1a; 这个式子看起来很FFTFFTFFT&#xff0c;让我们来化简一下。 考虑EEE中直接将qiq_iqi​约掉&#xff0c;所以Ei∑j1i−1qj(i−j)2−∑ji1nqj(i−j)2E_i\sum_{j1}^{i-1}\frac{q_j}{(i-j)^2}-…

DevOps案例研究:庖丁解牛,剖析Google持续交付之道

内容来源&#xff1a;DevOps案例深度研究 –Google持续交付实践战队&#xff08;本文只展示部分PPT及研究成果&#xff0c;更多细节请关注案例分享会&#xff0c;及本公众号。&#xff09;本案例内容贡献者&#xff1a;姚元庆 (Topic Leader) 、任跃兵、王红阳、王晓敏、张彪本…

架构杂谈《八》Docker 架构

Docker 架构 一、Docker 引擎的三大组件1&#xff09;Docker 后台服务&#xff08;Docker Daemon&#xff09;&#xff1a;是长时间运行在后台的守护进程&#xff0c;是Docker的核心服务&#xff0c;可以通过命令dockerd与它进行交互通信。2&#xff09;REST 接口&#xff08;R…

P3723 [AH2017/HNOI2017]礼物 FFT + 式子化简

传送门 文章目录题意&#xff1a;思路&#xff1a;题意&#xff1a; 思路&#xff1a; 首先可以知道&#xff0c;我们对某个数组加上一个正数数的操作可以转换成对一个数组加上一个任意数&#xff0c;所以我们设变化量为xxx。 对于∑i1n(ai−bi)2\sum_{i1}^n(a_i-b_i)^2i1∑n​…

.net core 基于 IHostedService 实现定时任务

.net core 基于 IHostedService 实现定时任务Intro从 .net core 2.0 开始&#xff0c;开始引入 IHostedService&#xff0c;可以通过 IHostedService 来实现后台任务&#xff0c;但是只能在 WebHost 的基础上使用。从 .net core 2.1 开始微软引入通用主机( GenericHost)&#x…

nowcoder 清楚姐姐的翅膀们 F 一般图的最大匹配

传送门 文章目录题意思路&#xff1a;题意 思路&#xff1a; 这个题很容易就会掉到二分图匹配的坑里。。 但实际上这个是一个一般图匹配。 考虑将妹子拆点&#xff0c;一个入点一个出点&#xff0c;入点出点都连蝴蝶结。 我们看看最终会有三种匹配情况&#xff1a; (1)(1)(1)妹…

HDU - 7072 Boring data structure problem 双端队列 + 思维

传送门 文章目录题意&#xff1a;思路&#xff1a;题意&#xff1a; 你需要实现如下四个操作 q≤1e7q\le1e7q≤1e7 思路&#xff1a; 做的时候想了个链表的思路让队友写了&#xff0c;懒。 看了题解感觉题解还是很妙的。 你需要快速插入一个数在前后两端&#xff0c;还需要…

C#中谁最快:结构还是类?

前言在内存当道的日子里&#xff0c;无论什么时候都要考虑这些代码是否会影响程序性能呢&#xff1f;在现在的世界里&#xff0c;几乎不会去考虑用了几百毫秒&#xff0c;可是在特别的场景了&#xff0c;往往这几百毫米确影响了整个项目的快慢。通过了解这两者之间的性能差异&a…

阅读nopcommerce startup源码

创建一个asp.net core项目&#xff0c;可以到到startup类有两个方法// This method gets called by the runtime. Use this method to add services to the container.public void ConfigureServices(IServiceCollection services)public void Configure(IApplicationBuilder a…

HDU - 7073 Integers Have Friends 2.0 随机化 + 质因子

传送门 文章目录题意&#xff1a;思路&#xff1a;题意&#xff1a; 给你一个序列aaa&#xff0c;找一个最大的集合&#xff0c;集合中所有元素模mmm相等。 思路&#xff1a; 之前做过一道连续的&#xff0c;直接尺取就好&#xff0c;这个不连续加大了难度。 考虑最简单的…

一份关于.NET Core云原生采用情况调查

调查背景Kubernetes 越来越多地在生产环境中使用&#xff0c;围绕 Kubernetes 的整个生态系统在不断演进&#xff0c;新的工具和解决方案也在持续发布。云原生计算的发展驱动着各个企业转向遵循云原生原则&#xff08;启动速度快、内存占用低&#xff09;的平台&#xff0c; .N…