.net core 插件式开发

插件式开发

思考一种情况,短信发送,默认实现中只写了一种实现,因为某些原因该模块的所依赖的第三方无法继续提供服务,或者对于winform程序,某按钮单击,需要在运行时增加额外的操作,或者替换目前使用的功能,对于类似这样的需求,可以考虑使用插件式的方式搭建框架,以实现更灵活的可拆卸动态增加功能。 .net core 中提供了一种热加载外部dll的方式,可以满足该类型的需求 AssemblyLoadContext

流程

1,定义针对系统中所有可插拔点的接口
2,针对接口开发插件/增加默认实现
3,根据需要,在运行时执行相应的逻辑
4,在动态载入dll时谨防内存泄漏

代码

1,定义接口

在单独的类库中定义针对插拔点的接口

    public interface ICommand
    {
        string Name { get; }
        string Description { get; }
        int Execute();
    }

2,开发插件

新建类库,引用接口所在的类库,值得注意的的是 CopyLocalLockFileAssemblies,表示将所有依赖项生成到生成目录,对于插件中有对其他项目或者类库有引用的这个属性是必须的,Private表示引用的类库为公共程序集,该属性默认为true,为使插件可以正确在运行时加载,该属性必须为 ** false **

<Project Sdk="Microsoft.NET.Sdk">
	<PropertyGroup>
		<TargetFramework>net5.0</TargetFramework>
		<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
	</PropertyGroup>
	<ItemGroup>
	  <PackageReference Include="AutoMapper" Version="10.1.1" />
	  <PackageReference Include="System.Text.Json" Version="4.6.0" />
	</ItemGroup>
	<ItemGroup>
	  <ProjectReference Include="..\Plugins\Plugins.csproj">
		  <Private>false</Private>
		  <ExcludeAssets>runtime</ExcludeAssets>
		</ProjectReference>
	</ItemGroup>
</Project>

修改完类库中这两处的值以后添加类,继承自ICommand 将接口定义的方法和属性做相关的实现,如下

    public class Class1 : ICommand
    {
        public string Name => "Classb";
        public string Description => "Classb Description";
        public int Execute()
        {
            var thisv = JsonSerializer.Serialize(this);
            Assembly ass = typeof(AutoMapper.AdvancedConfiguration).Assembly;
            Console.WriteLine(ass.FullName);
            Console.WriteLine(thisv);
            Console.WriteLine("111111111111111111111111111111111111111111");
            return 10000;
        }
    }

3,根据需要在运行时执行相应逻辑

编写用于运行时 插件加载上下文, 该类主要负责将给定路径的dll加载到当前应用程序域,静态方法用户获取实现了插件接口的实例

  public class PluginLoadContext : AssemblyLoadContext
    {
        private AssemblyDependencyResolver _resolver;
        public PluginLoadContext(string pluginPath,bool isCollectible) :base(isCollectible)
        {
            _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;
        }
  
        public static List<ICommand> CreateCommands(string[] pluginPaths)
        {
            List<Assembly> _assemblies = new List<Assembly>();
            foreach (var pluginPath in pluginPaths)
            {
                string pluginLocation = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, pluginPath.Replace('\\', Path.DirectorySeparatorChar)));
                var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(o => o.Location == pluginLocation);
                //根据程序集的物理位置判断当前域中是否存在该类库,如果不存在就读取,如果存在就从当前程序域中读取,由于AssemblyLoadContext已经做了相应的上下文隔离
                //,所以即便是名称一样位置一样也可以重复加载,执行也可以按照预期执行,但由于会重复加载程序集,就会造成内存一直增加导致内存泄漏
                if (assembly == null)
                {
                    PluginLoadContext pluginLoadContext = new PluginLoadContext(pluginLocation, true);
                    assembly = pluginLoadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(pluginLocation)));
                }
                _assemblies.Add(assembly);
            }
            var results = new List<ICommand>();
            foreach (var assembly in _assemblies)
            {
                foreach (Type type in assembly.GetTypes())
                {
                    if (typeof(ICommand).IsAssignableFrom(type))
                    {
                        ICommand result = Activator.CreateInstance(type) as ICommand;
                        if (result != null)
                        {
                            results.Add(result);
                        }
                    }
                }
            }
            return results;
        }
    }

调用

            try
            {
                //插件添加后,相应的位置保存下载
                string[] pluginPaths = new string[]
                {
                    "Plugin/PluginA/PluginA.dll",//将插件所在类库生成后的文件复制到PluginA下边
                };
                var i = 0;
                while (true)
                {
                    List<ICommand> commands = PluginLoadContext.CreateCommands(pluginPaths);
                    foreach (var command in commands)
                    {
                        Console.WriteLine(command.Name);
                        Console.WriteLine(command.Description);
                        Console.WriteLine(command.Execute());
                    }
                }
                
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
            Console.ReadKey();

图2中去掉了当前程序集中根据地址确定是否重新加载插件,可以看到内存的使用量在一直增加,最终一定会导致溢出。

对比图 1

对比图 2

对于插件卸载,我认为没有必要去考虑,对于同一类型插件,只需要将不同版本的放到不同的位置,在一个公共位置维护当前使用的插件所在位置,如果有更新直接找最新的实现去执行就行,卸载很麻烦,需要删除掉所有的依赖项,还容易出错,不解决就是最好的解决方案

posted @ 2020-12-09 19:02  FreeTimeWorker  阅读(1278)  评论(0编辑  收藏  举报