标题:从零开始实现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动态加载控制器和视图[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, 即控制器和视图没有正确的激活。

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

这里的原因是,只有当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。

如何解决插件的预编译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, 页面正常显示了。

总结

本篇中,我为大家演示了如何在运行时启用一个插件,这里我们借助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

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

  1. 从零开始实现ASP.NET Core MVC的插件式开发(五) - 插件的删除和升级

    标题:从零开始实现ASP.NET Core MVC的插件式开发(五) - 使用AssemblyLoadContext实现插件的升级和删除 作者:Lamond Lu 地址:https://www.cnb ...

  2. 从零开始实现ASP.NET Core MVC的插件式开发(四) - 插件安装

    标题:从零开始实现ASP.NET Core MVC的插件式开发(四) - 插件安装 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/11343141. ...

  3. 从零开始实现 ASP.NET Core MVC 的插件式开发(九) - 如何启用预编译视图

    标题:从零开始实现 ASP.NET Core MVC 的插件式开发(九) - 升级.NET 5及启用预编译视图 作者:Lamond Lu 地址:https://www.cnblogs.com/lwql ...

  4. 从零开始实现 ASP.NET Core MVC 的插件式开发(七) - 问题汇总及部分问题解决方案...

    标题:从零开始实现 ASP.NET Core MVC 的插件式开发(七) - 问题汇总及部分问题解决方案 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/ ...

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

    标题:从零开始实现ASP.NET Core MVC的插件式开发(六) - 如何加载插件引用. 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/1171 ...

  6. 从零开始实现ASP.NET Core MVC的插件式开发(二) - 如何创建项目模板

    标题:从零开始实现ASP.NET Core MVC的插件式开发(二) - 如何创建项目模板 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun/p/11155 ...

  7. 从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用Application Part动态加载控制器和视图...

    如果你使用过一些开源CMS的话,肯定会用过其中的的插件化功能,用户可以通过启用或者上传插件包的方式动态添加一些功能,那么在ASP.NET Core MVC中如何实现插件化开发呢,下面我们来探究一下. ...

  8. 从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用ApplicationPart动态加载控制器和视图

    目录 前言 什么是ApplicationPart? 创建项目 添加控制器和视图 如何动态加载插件中的控制器?# 如何加载组件的预编译Razor视图? 最终效果 总结 源代码:https://githu ...

  9. ASP.NET Core appsettings.json文件(9)《从零开始学ASP.NET CORE MVC》:

    本文出自<从零开始学ASP.NET CORE MVC> 推荐文章:ASP.NET Core launchsettings.json文件 ASP.NET Core appsettings.j ...

最新文章

  1. 一些技术图书编写、推荐、出版人员需要自重
  2. 【337天】我爱刷题系列096(2018.01.08)
  3. asp.net学习笔记·get与post区别
  4. 计算机网络-信道的极限容量
  5. mysql 报错1045 - Access denied for user “root” @ 192.111.111.11 (using password: YES)
  6. truecrypt加密分区的编译
  7. 链表基本功能:初始化、增、删、查、改
  8. 搭建 springMVC 框架
  9. ClassLoader.getResourceAsStream(name);获取配置文件的方法
  10. 为什么我们不应该使用过多的线程
  11. 8-2:C++继承之父类和子类对象赋值转换(公有继承)也即切片
  12. 利用js代码引入其他js文件到页面中
  13. c++注册表操作(未完成)
  14. 刀塔霸业android安装包,刀塔霸业安卓下载-刀塔霸业安卓官网版(dota2自走棋)下载v1.0...
  15. 如何根据原理图画封装_画了这么多年PCB,你真的了解原理图吗?
  16. (二)匈牙利算法简介
  17. 【算法笔记】求给定序列的第k大(权值线段树/直接离散化)
  18. 想以游戏纸娃娃系统专利主张暴雪的暗黑3侵权? 先过暗黑2这关!
  19. android谷歌打印插件下载地址,谷歌浏览器打印插件:Print Plus
  20. 各种水龙头拆卸图解_各种水龙头拆卸图解 蜜罐蚁小编带您了解水龙头拆卸方法...

热门文章

  1. 知道第一章计算机基础知识作业答案,大学计算机基础作业答案
  2. Teams数据统计 - 通话记录
  3. cmd暂停快捷键_是否有键盘快捷键可以暂停正在运行的CMD窗口的输出?
  4. Python_list部分功能介绍
  5. threeJS 实用
  6. Java基础系列8:Java的序列化与反序列化(修)
  7. JS多个对象添加到一个对象中
  8. js温故而知新11(AJAX)——学习廖雪峰的js教程
  9. C#学习笔记——通用对话框
  10. c++11新特性(4) lambda捕捉块