标题:从零开始实现ASP.NET Core MVC的插件式开发(五) - 使用AssemblyLoadContext实现插件的升级和删除
作者:Lamond Lu
地址:https://www.cnblogs.com/lwqlun/p/11395828.html
源代码:https://github.com/lamondlu/DynamicPlugins
前景回顾:
- 从零开始实现ASP.NET Core MVC的插件式开发(一) - 使用Application Part动态加载控制器和视图
- 从零开始实现ASP.NET Core MVC的插件式开发(二) - 如何创建项目模板
- 从零开始实现ASP.NET Core MVC的插件式开发(三) - 如何在运行时启用组件
- 从零开始实现ASP.NET Core MVC的插件式开发(四) - 插件安装
简介
在上一篇中,我为大家讲解了如何实现插件的安装,在文章的最后,留下了两个待解决的问题。
- .NET Core 2.2中不能实现运行时删除插件
- .NET Core 2.2中不能实现运行时升级插件
其实这2个问题归根结底其实都是一个问题,就是插件程序集被占用,不能在运行时更换程序集。在本篇中,我将分享一下我是如何一步一步解决这个问题的,其中也绕了不少弯路,查阅过资料,在.NET Core官方提过Bug,几次差点想放弃了,不过最终是找到一个可行的方案。
.NET Core 2.2的遗留问题
程序集被占用的原因
回顾一下,我们之前加载插件程序集时所有使用的代码。
var provider = services.BuildServiceProvider();
using (var scope = provider.CreateScope())
{
var unitOfWork = scope.ServiceProvider.GetService<IUnitOfWork>();
var allEnabledPlugins = unitOfWork.PluginRepository
.GetAllEnabledPlugins();
foreach (var plugin in allEnabledPlugins)
{
var moduleName = plugin.Name;
var assembly = Assembly.LoadFile($"{AppDomain.CurrentDomain.BaseDirectory}Modules\\{moduleName}\\{moduleName}.dll");
var controllerAssemblyPart = new AssemblyPart(assembly);
mvcBuilders.PartManager
.ApplicationParts
.Add(controllerAssemblyPart);
}
}
这里我们使用了Assembly.LoadFile
方法加载了插件程序集。 在.NET中使用Assembly.LoadFile
方法加载的程序集会被自动锁定,不能执行任何转移,删除等造作,所以这就给我们删除和升级插件造成了很大困难。
PS: 升级插件需要覆盖已加载的插件程序集,由于程序集锁定,所以覆盖操作不能成功。
使用AssemblyLoadContext
在.NET Framework中,如果遇到这个问题,常用的解决方案是使用AppDomain
类来实现插件热插拔,但是在.NET Core中没有AppDomain
类。不过经过查阅,.NET Core 2.0之后引入了一个AssemblyLoadContext
类来替代.NET Freamwork中的AppDomain
。本以为使用它就能解决当前程序集占用的问题,结果没想到.NET Core 2.x版本提供的AssemblyLoadContext
没有提供Unload
方法来释放加载的程序集,只有在.NET Core 3.0版本中才为AssemblyLoadContext
类添加了Unload
方法。
相关链接:
升级.NET Core 3.0 Preview 8
因此,为了完成插件的删除和升级功能,我将整个项目升级到了最新的.NET Core 3.0 Preview 8版本。
这里.NET Core 2.2升级到.NET Core 3.0有一点需要注意的问题。
在.NET Core 2.2中默认启用了Razor视图的运行时编译,简单点说就是.NET Core 2.2中自动启用了读取原始的Razor视图文件,并编译视图的功能。这就是我们在第三章和第四章中的实现方法,每个插件文件最终都放置在了一个Modules目录中,每个插件既有包含Controller/Action的程序集,又有对应的原始Razor视图目录Views,在.NET Core 2.2中当我们在运行时启用一个组件之后,对应的Views可以自动加载。
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
但是在.NET Core 3.0中,Razor视图的运行时编译需要引入程序集Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation
。并且在程序启动时,需要启动运行时编译的功能。
public void ConfigureServices(IServiceCollection services)
{
...
var mvcBuilders = services.AddMvc()
.AddRazorRuntimeCompilation();
...
}
如果没有启用Razor视图的运行时编译,程序访问插件视图的时候,就会报错,提示视图找不到。
使用.NET Core 3.0的AssemblyLoadContext加载程序集
这里为了创建一个可回收的程序集加载上下文,我们首先基于AssemblyLoadcontext
创建一个CollectibleAssemblyLoadContext
类。其中我们将IsCollectible
属性通过父类构造函数,将其设置为true。
public class CollectibleAssemblyLoadContext
: AssemblyLoadContext
{
public CollectibleAssemblyLoadContext()
: base(isCollectible: true)
{
}
protected override Assembly Load(AssemblyName name)
{
return nul