• .NET Core 中插件式开发实现


    在 .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 方法只是为了启动卸载。以下目标达成后,卸载完成:

    1、没有线程将程序集中的方法加载到其调用堆栈上的 AssemblyLoadContext 中。

    2、程序集中的任何类型都不会加载到 AssemblyLoadContext,这些类型的实例本身由以下引用:

      • AssemblyLoadContext 外部的引用,弱引用(WeakReference 或 WeakReference)除外。

      • AssemblyLoadContext 内部和外部的强垃圾回收器 (GC) 句柄(GCHandleType.Normal 或 GCHandleType.Pinned)。

    二、.NET Core 插件式方式实现

    1、创建可卸载的上下文PluginAssemblyLoadContext

    1. class PluginAssemblyLoadContext : AssemblyLoadContext
    2. {
    3.     private AssemblyDependencyResolver _resolver;
    4.     /// <summary>
    5.     /// 构造函数
    6.     /// isCollectible: true 重点,允许Unload
    7.     /// </summary>
    8.     /// <param name=pluginPath></param>
    9.     public PluginAssemblyLoadContext(string pluginPath) : base(isCollectible: true)
    10.     {
    11.         _resolver = new AssemblyDependencyResolver(pluginPath);
    12.     }
    13.     protected override Assembly Load(AssemblyName assemblyName)
    14.     {
    15.         string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
    16.         if (assemblyPath != null)
    17.         {
    18.             return LoadFromAssemblyPath(assemblyPath);
    19.         }
    20.         return null;
    21.     }
    22.     protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
    23.     {
    24.         string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
    25.         if (libraryPath != null)
    26.         {
    27.             return LoadUnmanagedDllFromPath(libraryPath);
    28.         }
    29.         return IntPtr.Zero;
    30.     }
    31. }

    2、创建插件接口及实现

    整体项目结构为:

    图片

    a)添加项目PluginInterface,插件接口:

    1. public interface IPlugin
    2. {
    3.     string Name { get; }
    4.     string Description { get; }
    5.     string Execute(object inPars);
    6. }

    b)添加HelloPlugin项目,实现不引用外部dll插件

    1. public class HelloPlugin : PluginInterface.IPlugin
    2. {
    3.     public string Name => "HelloPlugin";
    4.     public string Description { get => "Displays hello message."; }
    5.     public string Execute(object inPars)
    6.     {return ("Hello !!!" + inPars?.ToString());    } }

    c)添加JsonPlugin项目,实现引用三方dll插件

    1. public class JsonPlugin : PluginInterface.IPlugin
    2. {
    3.     public string Name => "JsonPlugin";
    4.     public string Description => "Outputs JSON value.";
    5.     private struct Info
    6.     {
    7.         public string JsonVersion;
    8.         public string JsonLocation;
    9.         public string Machine;
    10.         public DateTime Date;
    11.     }
    12.     public string Execute(object inPars)
    13.     {
    14.         Assembly jsonAssembly = typeof(JsonConvert).Assembly;
    15.         Info info = new Info()
    16.         {
    17.             JsonVersion = jsonAssembly.FullName,
    18.             JsonLocation = jsonAssembly.Location,
    19.             Machine = Environment.MachineName,
    20.             Date = DateTime.Now
    21.         };
    22.         return JsonConvert.SerializeObject(info, Formatting.Indented);
    23.     }
    24. }

    d)添加PluginsApp项目,实现调用插件方法:

    修改窗体界面布局:

    图片

    添加执行方法

    1. /// <summary>
    2. /// 将此方法标记为noinline很重要,否则JIT可能会决定将其内联到Main方法中。
    3. /// 这可能会阻止成功卸载插件,因为某些实例的生存期可能会延长到预期卸载插件的时间点之外。
    4. /// </summary>
    5. /// <param name="assemblyPath"></param>
    6. /// <param name="inPars"></param>
    7. /// <param name="alcWeakRef"></param>
    8. /// <returns></returns>
    9. [MethodImpl(MethodImplOptions.NoInlining)]
    10. static string ExecuteAndUnload(string assemblyPath, object inPars, out WeakReference alcWeakRef)
    11. {
    12.     string resultString = string.Empty;
    13.     // 创建 PluginLoadContext对象
    14.     var alc = new PluginAssemblyLoadContext(assemblyPath);
    15.     //创建一个对AssemblyLoadContext的弱引用,允许我们检测卸载何时完成
    16.     alcWeakRef = new WeakReference(alc);
    17.     // 加载程序到上下文
    18.     // 注意:路径必须为绝对路径.
    19.     Assembly assembly = alc.LoadFromAssemblyPath(assemblyPath);
    20.     //创建插件对象并调用
    21.     foreach (Type type in assembly.GetTypes())
    22.     {
    23.         if (typeof(IPlugin).IsAssignableFrom(type))
    24.         {
    25.             IPlugin result = Activator.CreateInstance(typeas IPlugin;
    26.             if (result != null)
    27.             {
    28.                 resultString = result.Execute(inPars);
    29.                 break;
    30.             }
    31.         }
    32.     }
    33.     //卸载程序集上下文
    34.     alc.Unload();
    35.     return resultString;
    36. }

    三、效果验证

    1、非引用外部dll的插件执行:执行后对应dll成功卸载,程序集数量未增加。

    图片


    2、引用外部包的插件:执行后对应dll未卸载,程序集数量增加。

    图片


    通过监视查看对象状态:该上下文在卸载中。暂未找到原因卸载失败(疑问?)

    图片

    四、总结

    虽然微软文档说.NET Core中使用AssemblyLoadContext来实现程序集的加载及卸载实现,但通过验证在加载引用外部dll后,加载后不能正常卸载。或者使用方式还不正确。

    源码地址:https://github.com/cwsheng/PluginsApp

  • 相关阅读:
    数据库实验二
    在KubeSphere中部署微服务(阡陌)+ DevOps
    算法进修Day-32
    jQuery
    实时即未来,大数据项目车联网之原始数据实时ELT流式任务流程总结【七】
    Vue——video标签实现不静音自动播放
    【每天学习一点新知识】跟咩咩一起学“宽字节注入”
    Qt+ECharts开发笔记(一):ECharts介绍、下载和Qt调用ECharts基础柱状图Demo
    和数集团浅谈区块链技术如何赋能数字政务?
    docker camunda 8.5 部署步骤
  • 原文地址:https://blog.csdn.net/u011966339/article/details/134197375