前言:
之前在文章- AppDomain实现【插件式】开发 中介绍了在 .NET Framework 中,通过AppDomain实现动态加载和卸载程序集的效果。
但是.NET Core 仅支持单个默认应用域,那么在.NET Core中如何实现【插件式】开发呢?
一、.NET Core 中 AssemblyLoadContext的使用
1、AssemblyLoadContext简介:
每个 .NET Core 应用程序均隐式使用 AssemblyLoadContext。 它是运行时的提供程序,用于定位和加载依赖项。 只要加载了依赖项,就会调用 AssemblyLoadContext 实例来定位该依赖项。
它提供定位、加载和缓存托管程序集和其他依赖项的服务。
为了支持动态代码加载和卸载,它创建了一个独立上下文,用于在其自己的 AssemblyLoadContext 实例中加载代码及其依赖项。
2、AssemblyLoadContext和AppDomain卸载差异:
使用 AssemblyLoadContext
和使用 AppDomain 进行卸载之间存在一个值得注意的差异。 对于 Appdomain,卸载为强制执行。
卸载时,会中止目标 AppDomain 中运行的所有线程,会销毁目标 AppDomain 中创建的托管 COM 对象,等等。 对于 AssemblyLoadContext
,卸载是“协作式的”。
调用 AssemblyLoadContext.Unload 方法只是为了启动卸载。以下目标达成后,卸载完成:
- 没有线程将程序集中的方法加载到其调用堆栈上的
AssemblyLoadContext
中。 - 程序集中的任何类型都不会加载到
AssemblyLoadContext
,这些类型的实例本身由以下引用: AssemblyLoadContext
外部的引用,弱引用(WeakReference 或 WeakReference<T>)除外。AssemblyLoadContext
内部和外部的强垃圾回收器 (GC) 句柄(GCHandleType.Normal 或 GCHandleType.Pinned)。
二、.NET Core 插件式方式实现
1、创建可卸载的上下文PluginAssemblyLoadContext
class PluginAssemblyLoadContext : AssemblyLoadContext { private AssemblyDependencyResolver _resolver; /// <summary> /// 构造函数 /// isCollectible: true 重点,允许Unload /// </summary> /// <param name="pluginPath"></param> public PluginAssemblyLoadContext(string pluginPath) : base(isCollectible: true) { _resolver = new AssemblyDependencyResolver(pluginPath); } protected override Assembly Load(AssemblyName assemblyName) { string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName); if (assemblyPath != null) { return LoadFromAssemblyPath(assemblyPath); } return null; } protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) { string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName); if (libraryPath != null) { return LoadUnmanagedDllFromPath(libraryPath); } return IntPtr.Zero; } }
2、创建插件接口及实现
整体项目结构为:
a)添加项目PluginInterface,插件接口:
public interface IPlugin { string Name { get; } string Description { get; } string Execute(object inPars); }
b)添加HelloPlugin项目,实现不引用外部dll插件
public class HelloPlugin : PluginInterface.IPlugin { public string Name => "HelloPlugin"; public string Description { get => "Displays hello message."; } public string Execute(object inPars) {return ("Hello !!!" + inPars?.ToString());
}
}
c)添加JsonPlugin项目,实现引用三方dll插件
public class JsonPlugin : PluginInterface.IPlugin { public string Name => "JsonPlugin"; public string Description => "Outputs JSON value."; private struct Info { public string JsonVersion; public string JsonLocation; public string Machine; public DateTime Date; } public string Execute(object inPars) { Assembly jsonAssembly = typeof(JsonConvert).Assembly; Info info = new Info() { JsonVersion = jsonAssembly.FullName, JsonLocation = jsonAssembly.Location, Machine = Environment.MachineName, Date = DateTime.Now }; return JsonConvert.SerializeObject(info, Formatting.Indented); } }
d)添加PluginsApp项目,实现调用插件方法:
修改窗体界面布局:
添加执行方法
/// <summary> /// 将此方法标记为noinline很重要,否则JIT可能会决定将其内联到Main方法中。 /// 这可能会阻止成功卸载插件,因为某些实例的生存期可能会延长到预期卸载插件的时间点之外。 /// </summary> /// <param name="assemblyPath"></param> /// <param name="inPars"></param> /// <param name="alcWeakRef"></param> /// <returns></returns> [MethodImpl(MethodImplOptions.NoInlining)] static string ExecuteAndUnload(string assemblyPath, object inPars, out WeakReference alcWeakRef) { string resultString = string.Empty; // 创建 PluginLoadContext对象 var alc = new PluginAssemblyLoadContext(assemblyPath); //创建一个对AssemblyLoadContext的弱引用,允许我们检测卸载何时完成 alcWeakRef = new WeakReference(alc); // 加载程序到上下文 // 注意:路径必须为绝对路径. Assembly assembly = alc.LoadFromAssemblyPath(assemblyPath); //创建插件对象并调用 foreach (Type type in assembly.GetTypes()) { if (typeof(IPlugin).IsAssignableFrom(type)) { IPlugin result = Activator.CreateInstance(type) as IPlugin; if (result != null) { resultString = result.Execute(inPars); break; } } } //卸载程序集上下文 alc.Unload(); return resultString; }
三、效果验证
1、非引用外部dll的插件执行:执行后对应dll成功卸载,程序集数量未增加。
2、引用外部包的插件:执行后对应dll未卸载,程序集数量增加。
通过监视查看对象状态:该上下文在卸载中。暂未找到原因卸载失败(疑问?)
四、总结:
虽然微软文档说.Net Core中使用AssemblyLoadContext来实现程序集的加载及卸载实现,但通过验证在加载引用外部dll后,加载后不能正常卸载。或者使用方式还不正确。
参考:https://docs.microsoft.com/zh-cn/dotnet/standard/assembly/unloadability