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

作者:Lamond Lu

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

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

前情回顾

在前面两篇中,我为大家演示了如何使用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视图加载的目录规则,同样实现了动态读取视图的功能。

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

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

  1. 从零开始实现ASP.NET Core MVC的插件式开发(九) - 升级.NET 5及启用预编译视图

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

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

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

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

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

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

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

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

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

  6. 从零开始实现ASP.NET Core MVC的插件式开发(八) - Razor视图相关问题及解决方案

    标题:从零开始实现ASP.NET Core MVC的插件式开发(八) - Razor视图相关问题及解决方案 作者:Lamond Lu 地址:https://www.cnblogs.com/lwqlun ...

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

    标题:从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用Application Part动态加载控制器和视图 作者:Lamond Lu 地址:http://www.cnblogs ...

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

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

  9. 使用 ASP.NET Core MVC 创建 Web API(三)

    使用 ASP.NET Core MVC 创建 Web API 使用 ASP.NET Core MVC 创建 Web API(一) 使用 ASP.NET Core MVC 创建 Web API(二) 十 ...

随机推荐

  1. hive -e和hive -f的区别(转)

    大家都知道,hive -f 后面指定的是一个文件,然后文件里面直接写sql,就可以运行hive的sql,hive -e 后面是直接用双引号拼接hivesql,然后就可以执行命令. 但是,有这么一个东西 ...

  2. java多线程之管道流

    java语言中提供了各种各样的流供我们操纵数据,其中管道流(pipeStream)是一种特殊的流,用于在不同线程间直接传送数据. 一个线程发送数据到输出管道,另一个线程从输入管道读取数据,通过使用管道 ...

  3. 源码解读·RT-Thread小内存管理算法分析

    这篇文章最初发布在RT-Thread官方论坛中,最近准备整理放到博客中来让更多人一起探讨学习. 2012年9月28日星期五 前言: 母语能力有限 概述: 这篇文字和大家分享一下今晚对RT-Thread ...

  4. 对shell脚本进行加密

    用shell脚本对系统进行自动化维护,简单,便捷而且可移植性好.但shell脚本是可读写的,很有可能会泄露敏感信息,如用户名,密码,路径,IP等.同样,在shell脚本运行时会也泄露敏感信息.请问如何 ...

  5. Java多线程同步工具类之CyclicBarrier

    一.CyclicBarrier使用 CyclicBarrier从字面上可以直接理解为线程运行的屏障,它可以让一组线程执行到一个共同的屏障点时被阻塞,直到最后一个线程执行到指定位置,你设置的执行线程就会 ...

  6. 【Java源码】集合类-JDK1.8 哈希表-红黑树-HashMap总结

    JDK 1.8 HashMap是数组+链表+红黑树实现的,在阅读HashMap的源码之前先来回顾一下大学课本数据结构中的哈希表和红黑树. 什么是哈希表? 在存储结构中,关键值key通过一种关系f和唯一 ...

  7. JVM 参数类型

    标准参数 -help -server -client -version -showversion -cp -classpath X参数 非标准化参数(在各个JDK版本中可能会变,但是变动比较小) -X ...

  8. Python自学day-14

    一.默认创建的HTML5文件 <!DOCTYPE html> <html lang="en"> <head> <meta charset= ...

  9. git的基本指令

    更多详情请看廖雪峰官方网站 http://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000 1.删 ...

  10. springboot websocket集群(stomp协议)连接时候传递参数

    最近在公司项目中接到个需求.就是后台跟前端浏览器要保持长连接,后台主动往前台推数据. 网上查了下,websocket stomp协议处理这个很简单.尤其是跟springboot 集成. 但是由于开始是 ...