1. 程式人生 > 實用技巧 >.net core 外掛式開發

.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

對於外掛解除安裝,我認為沒有必要去考慮,對於同一型別外掛,只需要將不同版本的放到不同的位置,在一個公共位置維護當前使用的外掛所在位置,如果有更新直接找最新的實現去執行就行,解除安裝很麻煩,需要刪除掉所有的依賴項,還容易出錯,不解決就是最好的解決方案