使用插件创建 .NET Core 应用程序
使用插件創建 .NET Core 應用程序
本教程展示了如何創建自定義的 ?AssemblyLoadContext ?來加載插件。AssemblyDependencyResolver ?用于解析插件的依賴項。該教程正確地將插件依賴項與主機應用程序隔離開來。將了解如何執行以下操作:
構建支持插件的項目。
創建自定義 ?AssemblyLoadContext ?加載每個插件。
使用 ?System.Runtime.Loader.AssemblyDependencyResolver ?類型允許插件具有依賴項。
只需復制生成項目就可以輕松部署的作者插件。
系統必備
安裝 ?.NET 5 SDK ?或更高版本。? 備注
示例代碼針對 .NET 5,但它使用的所有功能都已在 .NET Core 3.0 中推出,并且在此后所有 .NET 版本中都可用。
創建應用程序
第一步是創建應用程序:
創建新文件夾,并在該文件夾中運行以下命令:
.NET CLI dotnet new console -o AppWithPlugin為了更容易生成項目,請在同一文件夾中創建一個 Visual Studio 解決方案文件。運行以下命令:
運行以下命令,向解決方案添加應用項目:
現在,我們可以填寫應用程序的主干。使用下面的代碼替換 AppWithPlugin/Program.cs 文件中的代碼:
using?PluginBase; using?System; using?System.Collections.Generic; using?System.IO; using?System.Linq; using?System.Reflection;namespace?AppWithPlugin {class?Program{static?void?Main(string[]?args){try{if?(args.Length?==?1?&&?args[0]?==?"/d"){Console.WriteLine("Waiting?for?any?key...");Console.ReadLine();}//?Load?commands?from?plugins.if?(args.Length?==?0){Console.WriteLine("Commands:?");//?Output?the?loaded?commands.}else{foreach?(string?commandName?in?args){Console.WriteLine($"--?{commandName}?--");//?Execute?the?command?with?the?name?passed?as?an?argument.Console.WriteLine();}}}catch?(Exception?ex){Console.WriteLine(ex);}}} }創建插件接口
使用插件生成應用的下一步是定義插件需要實現的接口。我們建議創建類庫,其中包含計劃用于在應用和插件之間通信的任何類型。此部分允許將插件接口作為包發布,而無需發布完整的應用程序。
在項目的根文件夾中,運行 ?dotnet new classlib -o PluginBase。并運行 ?dotnet sln add PluginBase/PluginBase.csproj ?向解決方案文件添加項目。刪除 ?PluginBase/Class1.cs ?文件,并使用以下接口定義在名為 ?ICommand.cs ?的 ?PluginBase ?文件夾中創建新的文件:
namespace?PluginBase {public?interface?ICommand{string?Name?{?get;?}string?Description?{?get;?}int?Execute();} }此 ?ICommand ?接口是所有插件將實現的接口。
由于已定義 ?ICommand ?接口,所以應用程序項目可以填寫更多內容。使用根文件夾中的 ?dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj ?命令將引用從 ?AppWithPlugin ?項目添加到 ?PluginBase ?項目。
使用以下代碼片段替換 ?// Load commands from plugins ?注釋,使其能夠從給定文件路徑加載插件:
string[]?pluginPaths?=?new?string[] {//?Paths?to?plugins?to?load. };IEnumerable<ICommand>?commands?=?pluginPaths.SelectMany(pluginPath?=> {Assembly?pluginAssembly?=?LoadPlugin(pluginPath);return?CreateCommands(pluginAssembly); }).ToList();然后用以下代碼片段替換?// Output the loaded commands 注釋:
foreach?(ICommand?command?in?commands) {Console.WriteLine($"{command.Name}\t?-?{command.Description}"); }使用以下代碼片段替換?// Execute the command with the name passed as an argument 注釋:
ICommand?command?=?commands.FirstOrDefault(c?=>?c.Name?==?commandName); if?(command?==?null) {Console.WriteLine("No?such?command?is?known.");return; }command.Execute(); 最后,將靜態方法添加到名為 ?LoadPlugin ?和 ?CreateCommands ?的 ?Program ?類,如下所示:
static?Assembly?LoadPlugin(string?relativePath) {throw?new?NotImplementedException(); }static?IEnumerable<ICommand>?CreateCommands(Assembly?assembly) {int?count?=?0;foreach?(Type?type?in?assembly.GetTypes()){if?(typeof(ICommand).IsAssignableFrom(type)){ICommand?result?=?Activator.CreateInstance(type)?as?ICommand;if?(result?!=?null){count++;yield?return?result;}}}if?(count?==?0){string?availableTypes?=?string.Join(",",?assembly.GetTypes().Select(t?=>?t.FullName));throw?new?ApplicationException($"Can't?find?any?type?which?implements?ICommand?in?{assembly}?from?{assembly.Location}.\n"?+$"Available?types:?{availableTypes}");} }加載插件
現在,應用程序可以正確加載和實例化來自已加載的插件程序集的命令,但仍然無法加載插件程序集。使用以下內容在 AppWithPlugin 文件夾中創建名為 PluginLoadContext.cs 的文件:
using?System; using?System.Reflection; using?System.Runtime.Loader;namespace?AppWithPlugin {class?PluginLoadContext?:?AssemblyLoadContext{private?AssemblyDependencyResolver?_resolver;public?PluginLoadContext(string?pluginPath){_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;}} }PluginLoadContext ?類型派生自 ?AssemblyLoadContext。AssemblyLoadContext ?類型是運行時中的特殊類型,該類型允許開發人員將已加載的程序集隔離到不同的組中,以確保程序集版本不沖突。此外,自定義 ?AssemblyLoadContext ?可以選擇不同路徑來加載程序集格式并重寫默認行為。PluginLoadContext ?使用 .NET Core 3.0 中引入的 ?AssemblyDependencyResolver ?類型的實例將程序集名稱解析為路徑。AssemblyDependencyResolver ?對象是使用 .NET 類庫的路徑構造的。它根據類庫的 .deps.json 文件(其路徑傳遞給 ?AssemblyDependencyResolver ?構造函數)將程序集和本機庫解析為它們的相對路徑。自定義 ?AssemblyLoadContext ?使插件能夠擁有自己的依賴項,AssemblyDependencyResolver ?使正確加載依賴項變得容易。
由于 ?AppWithPlugin ?項目具有 ?PluginLoadContext ?類型,所以請使用以下正文更新 ?Program.LoadPlugin ?方法:
static?Assembly?LoadPlugin(string?relativePath) {//?Navigate?up?to?the?solution?rootstring?root?=?Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(typeof(Program).Assembly.Location)))))));string?pluginLocation?=?Path.GetFullPath(Path.Combine(root,?relativePath.Replace('\\',?Path.DirectorySeparatorChar)));Console.WriteLine($"Loading?commands?from:?{pluginLocation}");PluginLoadContext?loadContext?=?new?PluginLoadContext(pluginLocation);return?loadContext.LoadFromAssemblyName(new?AssemblyName(Path.GetFileNameWithoutExtension(pluginLocation))); }通過為每個插件使用不同的 ?PluginLoadContext ?實例,插件可以具有不同的甚至沖突的依賴項,而不會出現問題。
不具有依賴項的簡單插件
返回到根文件夾,執行以下步驟:
運行以下命令,新建一個名為 ?HelloPlugin ?的類庫項目:
.NET CLI dotnet new classlib -o HelloPlugin運行以下命令,將項目添加到 ?AppWithPlugin ?解決方案中:
.NET CLI dotnet sln add HelloPlugin/HelloPlugin.csproj使用以下內容將 HelloPlugin/Class1.cs 文件替換為名為 HelloCommand.cs 的文件:
using?PluginBase; using?System;namespace?HelloPlugin {public?class?HelloCommand?:?ICommand{public?string?Name?{?get?=>?"hello";?}public?string?Description?{?get?=>?"Displays?hello?message.";?}public?int?Execute(){Console.WriteLine("Hello?!!!");return?0;}} }現在,打開 HelloPlugin.csproj 文件 。它應類似于以下內容:
<Project?Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net5</TargetFramework></PropertyGroup></Project>在 ?? 標記之間添加以下元素:
<EnableDynamicLoading>true</EnableDynamicLoading> <EnableDynamicLoading>true</EnableDynamicLoading>準備項目,使其可用作插件。此外,這會將其所有依賴項復制到項目的輸出中。有關更多詳細信息,請參閱 ?EnableDynamicLoading。
在 ?? 標記之間添加以下元素:
<ItemGroup><ProjectReference?Include="..\PluginBase\PluginBase.csproj"><Private>false</Private><ExcludeAssets>runtime</ExcludeAssets></ProjectReference> </ItemGroup>false? 元素很重要。它告知 MSBuild 不要將 PluginBase.dll 復制到 HelloPlugin 的輸出目錄 。如果 PluginBase.dll 程序集出現在輸出目錄中,PluginLoadContext ?將在那里查找到該程序集并在加載 HelloPlugin.dll 程序集時加載它。此時,HelloPlugin.HelloCommand ?類型將從 ?HelloPlugin ?項目的輸出目錄中的 PluginBase.dll 實現 ?ICommand ?接口,而不是加載到默認加載上下文中的 ?ICommand ?接口。因為運行時將這兩種類型視為不同程序集的不同類型,所以 ?AppWithPlugin.Program.CreateCommands ?方法找不到命令。因此,對包含插件接口的程序集的引用需要 ?false? 元數據。
同樣,如果 ?PluginBase? 引用其他包,則 ?runtime? 元素也很重要。此設置與 ?false? 的效果相同,但適用于 ?PluginBase ?項目或它的某個依賴項可能包括的包引用。
因為 ?HelloPlugin ?項目已完成,所以應該更新 ?AppWithPlugin ?項目,以確認可以找到 ?HelloPlugin ?插件的位置。在 ?// Paths to plugins to load ?注釋后,添加 ?@"HelloPlugin\bin\Debug\netcoreapp3.0\HelloPlugin.dll"(根據所使用的 .NET Core 版本,此路徑可能有所不同)作為 ?pluginPaths ?數組的元素。
具有庫依賴項的插件
幾乎所有插件都比簡單的“Hello World”更復雜,而且許多插件都具有其他庫上的依賴項。示例中的 ?JsonPlugin ?和 ?OldJsonPlugin ?項目顯示了具有 ?Newtonsoft.Json ?上的 NuGet 包依賴項的兩個插件示例。因此,所有插件項目都應將 ?true? 添加到項目屬性,以便它們將其所有依賴項復制到 ?dotnet build ?的輸出中。使用 ?dotnet publish ?發布類庫也會將其所有依賴項復制到發布輸出。
從 NuGet 包引用插件接口
假設存在應用 A,它具有 NuGet 包(名為 ?A.PluginBase)中定義的插件接口。如何在插件項目中正確引用包?對于項目引用,使用項目文件的 ?ProjectReference ?元素上的 ?false? 元數據會阻止將 dll 復制到輸出。
若要正確引用 ?A.PluginBase? 包,應將項目文件中的 ?? 元素更改為以下內容:
<PackageReference?Include="A.PluginBase"?Version="1.0.0"><ExcludeAssets>runtime</ExcludeAssets> </PackageReference>此操作會阻止將 ?A.PluginBase ?程序集復制到插件的輸出目錄,并確保插件將使用 A 版本的 ?A.PluginBase。
插件目標框架建議
因為插件依賴項加載使用 .deps.json 文件,所以存在一個與插件的目標框架相關的問題 。具體來說,插件應該以運行時為目標,比如 .NET 5,而不是某一版本的 .NET Standard。.deps.json 文件基于項目所針對的框架生成,而且由于許多與 .NET Standard 兼容的包提供了用于針對 .NET Standard 進行生成的引用程序集和用于特定運行時的實現程序集,因此 .deps.json 可能無法正確查看實現程序集,或者它可能會獲取 .NET Standard 版本的程序集,而不是期望的 .NET Core 版本的程序集。
插件框架引用
插件當前無法向該過程引入新的框架。例如,無法將使用 ?Microsoft.AspNetCore.App ?框架的插件加載到只使用根 ?Microsoft.NETCore.App ?框架的應用程序中。主機應用程序必須聲明對插件所需的全部框架的引用。
總結
以上是生活随笔為你收集整理的使用插件创建 .NET Core 应用程序的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: DateOnly和TimeOnly类型居
- 下一篇: 龙芯发布.NET 6.0.100开发者内