.net core 外掛式開發
阿新 • • 發佈:2020-12-09
外掛式開發
思考一種情況,簡訊傳送,預設實現中只寫了一種實現,因為某些原因該模組的所依賴的第三方無法繼續提供服務,或者對於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