从零开始实现ASP.NET Core MVC的插件式开发(三) - 如何在运行时启用组件
标题:从零开始实现ASP.NET Core MVC的插件式开发(三) - 如何在运行时启用组件
作者:Lamond Lu
地址:https://www.cnblogs.com/lwqlun/p/11260750.html
源代码:https://github.com/lamondlu/DynamicPlugins
前情回顾
- 从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用Application Part动态加载控制器和视图
- 从零开始实现ASP.NET Core MVC的插件式开发(二) - 如何创建项目模板
在前面两篇中,我为大家演示了如何使用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, 即控制器和视图没有正确的激活。
这里你可能有疑问,为什么会激活失败呢?
这里的原因是,只有当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.cs的
ConfigureServices方法中,将
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。
如何解决插件的预编译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.cs的
ConfigureServices方法中配置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, 页面正常显示了。
总结
本篇中,我为大家演示了如何在运行时启用一个插件,这里我们借助
IActionDescriptorChangeProvider, 让ASP.NET Core在运行时重新加载了控制器,虽然不支持预编译Razor视图的加载,但是我们通过配置原始Razor视图加载的目录规则,同样实现了动态读取视图的功能。
下一篇我将继续将这个项目重构,编写业务模型,并尝试编写插件的安装以及升降级版本的代码。
- 从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用ApplicationPart动态加载控制器和视图
- ASP.NET 开发必备知识点(1):如何让Asp.net网站运行在自定义的Web服务器上
- 如何在 ASP.NET 应用程序中实现模拟用户身份(在ASP.NET中以管理员身份运行网站)
- 零基础ASP.NET Core MVC插件式开发
- Asp.net是快速开发的代名词, 那么如何用Asp.net来实现 RIA, 高性能, 同时又不过份的失于快速开发呢?
- ASP.NET中启用Windows集成验证,怎样在调用System.DirectoryServices下的组件时传递安全上下文,也就是说当前用户凭据,来实现权限管理
- 庖丁解牛ASP.NET3.5控件和组件开发技术-(3)从零开始开发服务器控件
- FluorineFx:实现Flex与ASP.NET的协同开发之组件库
- 庖丁解牛ASP.NET3.5控件和组件开发技术-(3)从零开始开发服务器控件
- ★★★【庖丁解牛:纵向切入Asp.net 3.5控件和组件开发技术系列—(3)从零开始开发服务器控件】★★★
- 高级别问题:如何在ASP.NET大型应用系统的模块化开发实现多版本程序集并存支持
- 如何启用集成Windows身份验证(asp.net程序运行中遇到)
- 项目开发中的一些注意事项以及技巧总结 基于Repository模式设计项目架构—你可以参考的项目架构设计 Asp.Net Core中使用RSA加密 EF Core中的多对多映射如何实现? asp.net core下的如何给网站做安全设置 获取服务端https证书 Js异常捕获
- 如何在ASP.NET大型应用系统的模块化开发实现多版本程序集并存支持[转载]
- ASP.NET 2.0如何实现数据库应用开发
- Asp.net是快速开发的代名词, 那么如何用Asp.net来实现 RIA, 高性能, 同时又不过份的失于快速开发呢?
- [转]ASP.NET 2.0如何实现数据库应用开发
- ASP.NET 开发必备知识点(1):如何让Asp.net网站运行在自定义的Web服务器上
- 轉載:ASP.NET 2.0实现数据库应用开发
- 如何在ASP.NET实现数据图表