标题:从零开始实现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. hadoop之hive集合数据类型

    除了string,boolean,date等基本数据类型之外,hive还支持三种高级数据类型: 1.ARRAY ARRAY类型是由一系列相同数据类型的元素组成,这些元素可以通过下标来访问.比如有一个A ...

  2. NSCache缓存怎么来的

    什么是NSCache NSCache主要用来存储临时数据(键值对),当内存资源不够时,系统会自动释放部分数据.它有三个特点: • NSCache为了保持不占用过多的系统内存,它有多种自动回收内存策略: ...

  3. C++模板的理解与使用

    最近发现原来学的东西根本都不理解,所以本人正在恶补C++,把自己对C++中概念的最简单粗暴的理解写下来. 有问题的地方还请指出~随时更正 模板:顾名思义,就是为了方便以后使用而出现的东西,生活中的模板 ...

  4. mini木马c源码

    #pragma comment(lib, "ws2_32.lib") #pragma comment(linker,"/subsystem:\"windows\ ...

  5. js api 之 fetch、querySelector、form、atob及btoa

    js api 之 fetch.querySelector.form.atob及btoa 转载请注明出处: https://www.cnblogs.com/funnyzpc/p/11095862.htm ...

  6. 交叉编译多平台 FFmpeg 库并提取视频帧

    原文地址: 交叉编译多平台 FFmpeg 库并提取视频帧 交叉编译多平台 FFmpeg 库并提取视频帧 本文档适用于 x86 平台编译 armeabi.armeabi-v7a.arm64-v8a.x8 ...

  7. Java上机题(封装)(编写student类)

    今天帮大一的童鞋写Java上机题 题目虽然很简单,但是刚拿到题目的时候愣了一下,然后就疯狂get set QuQ 其实这是一个特别基本的封装的题目(之前实验室面试大二的时候竟然还有蛮多人不知道封装的概 ...

  8. PyCharm2018 汉化&激活

    一.汉化 将下载好的resources_cn_PyCharm_2018.1_r2.jar 放入pycharm 的lib 目录中,启动app即可 下载链接: https://pan.baidu.com/ ...

  9. Gym 101257G:24(尺取)

    http://codeforces.com/gym/101257/problem/GGym 101257G 题意:给出n个人,和一个数s,接下来给出每个人当前的分数和输掉的概率.当一个人输了之后就会掉 ...

  10. scrapy实战2分布式爬取lagou招聘(加入了免费的User-Agent随机动态获取库 fake-useragent 使用方法查看:https://github.com/hellysmile/fake-useragent)

    items.py # -*- coding: utf-8 -*- # Define here the models for your scraped items # # See documentati ...