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


    前言:

     之前在文章- 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 

     

     

  • 相关阅读:
    在SQL使用正则表达式
    What is callback?
    readResolve()的使用
    What is a serialVersionUID and why should I use it?
    How to terminate a thread in Java
    Eclipse导入源文件出现编码的问题
    What is livelock?
    [转]不要迷失在技术的海洋中
    [转]基于.Net的单点登录(SSO)解决方案
    IIS网站真正301重定向的方法(包括首页和内页)
  • 原文地址:https://www.cnblogs.com/cwsheng/p/14828754.html
Copyright © 2020-2023  润新知