本教學課程說明如何建立自定義 AssemblyLoadContext 以載入外掛程式。 AssemblyDependencyResolver 可用來解析外掛程式的相依性。 本教學課程提供外掛程式相依性的個別組件上下文,允許外掛程式與主控應用程式之間的不同的組件相依性。 您將瞭解如何:
- 建構專案以支援外掛程式。
- 建立一個自訂的 AssemblyLoadContext 以載入每個外掛程式。
- 使用 System.Runtime.Loader.AssemblyDependencyResolver 類型來允許外掛程式具有相依性。
- 撰寫只需複製構建成品即可輕鬆部署的外掛程式。
備註
無法安全地將不受信任的程式代碼載入信任的 .NET 進程。 若要提供安全性或可靠性界限,請考慮作系統或虛擬化平臺所提供的技術。
先決條件
- 最新 .NET SDK
- Visual Studio Code 編輯器
- C# 開發套件
建立應用程式
第一個步驟是建立應用程式:
建立新的資料夾,然後在該資料夾中執行下列命令:
dotnet new console -o AppWithPlugin若要讓建置專案變得更容易,請在相同的資料夾中建立Visual Studio方案檔。 執行下列命令:
dotnet new sln執行下列命令,將應用程式專案新增至方案:
dotnet sln add AppWithPlugin/AppWithPlugin.csproj
現在我們可以填充應用程式的框架。 使用下列程式代碼取代 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 (var type in assembly.GetTypes().Where(t => typeof(ICommand).IsAssignableFrom(t)))
{
if (Activator.CreateInstance(type) is ICommand result)
{
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 root
string root = Path.GetFullPath(
Path.Combine(typeof(Program).Assembly.Location, "..", "..", "..", "..", ".."));
string pluginLocation = Path.GetFullPath(Path.Combine(root, relativePath.Replace('\\', Path.DirectorySeparatorChar)));
Console.WriteLine($"Loading commands from: {pluginLocation}");
PluginLoadContext loadContext = new(pluginLocation);
return loadContext.LoadFromAssemblyName(new(Path.GetFileNameWithoutExtension(pluginLocation)));
}
針對每個外掛程式使用不同的 PluginLoadContext 實例,外掛程式可以有不同的或甚至衝突的相依性,而不會發生問題。
沒有相依性的簡單外掛程式
回到根資料夾中,執行下列動作:
執行下列命令以建立名為
HelloPlugin的新類別庫專案:dotnet new classlib -o HelloPlugin執行下列命令,將專案新增至
AppWithPlugin方案: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.0</TargetFramework>
</PropertyGroup>
</Project>
在 <PropertyGroup> 標記之間,新增下列元素:
<EnableDynamicLoading>true</EnableDynamicLoading>
<EnableDynamicLoading>true</EnableDynamicLoading> 會準備專案,使其可作為外掛程式使用。 除此之外,這會將它的所有相依性複製到項目的輸出。 如需詳細資訊,請參閱 EnableDynamicLoading。
在 <Project> 標籤之間,新增下列元素:
<ItemGroup>
<ProjectReference Include="..\PluginBase\PluginBase.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
</ItemGroup>
<Private>false</Private> 元素很重要。 這會告訴 MSBuild 不要將 PluginBase.dll 複製到 HelloPlugin 的輸出目錄。 如果輸出目錄中有 PluginBase.dll 元件,PluginLoadContext 會在載入 HelloPlugin.dll 元件時找到該元件並載入該元件。 此時,HelloPlugin.HelloCommand 類型會在 HelloPlugin 專案的輸出目錄中,透過 PluginBase.dll 實作 ICommand 介面,而不是載入到預設載入內容的 ICommand 介面。 由於運行時間會將這兩種類型視為不同元件的不同類型,因此 AppWithPlugin.Program.CreateCommands 方法找不到命令。 因此,需要 <Private>false</Private> 元數據來參考包含插件接口的組件。
同樣地,如果 PluginBase 參考其他套件,則 <ExcludeAssets>runtime</ExcludeAssets> 元素也很重要。 此設定的效果與 <Private>false</Private> 相同,但適用於 PluginBase 專案或其相依性之一可能包含的套件參考。
既然 HelloPlugin 專案已完成,您應該更新 AppWithPlugin 專案,以瞭解 HelloPlugin 外掛程式的所在位置。 在 // Paths to plugins to load 批注之後,新增 @"HelloPlugin\bin\Debug\net5.0\HelloPlugin.dll"(此路徑可能會根據您使用的 .NET Core 版本而有所不同),做為 pluginPaths 陣列的元素。
具有連結庫相依性的外掛程式
幾乎所有外掛程式都比簡單的 「Hello World」 更複雜,而且許多外掛程式與其他連結庫都有相依性。 範例中的 JsonPlugin 和 OldJsonPlugin 項目會顯示兩個在 Newtonsoft.Json上具有 NuGet 套件相依性的外掛程式範例。 因此,所有外掛程式專案都應該將 <EnableDynamicLoading>true</EnableDynamicLoading> 新增至項目屬性,以便將其所有相依性複製到 dotnet build的輸出。 發行具有 dotnet publish 的類別庫也會將其所有相依性複製到發行輸出。
範例中的其他範例
本教學課程的完整原始程式碼可在 dotnet/samples 存放庫 找到,。 已完成的範例包含一些其他 AssemblyDependencyResolver 行為範例。 例如,AssemblyDependencyResolver 物件也可以解析原生程式庫,以及 NuGet 套件中包含的本地化衛星組件。 範例存放庫中的 UVPlugin 和 FrenchPlugin 會示範這些案例。
從 NuGet 套件參考外掛程式介面
假設有應用程式 A 擁有一個在名為 A.PluginBase的 NuGet 套件中定義的外掛程式介面。 如何在外掛程式項目中正確引用套件? 針對專案參考,在專案檔案中的 ProjectReference 元素上使用 <Private>false</Private> 中繼資料,可防止 dll 被複製到輸出。
若要正確參考 A.PluginBase 套件,您想要將專案檔案中的 <PackageReference> 元素變更為以下內容:
<PackageReference Include="A.PluginBase" Version="1.0.0">
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
這可防止 A.PluginBase 元件複製到外掛程式的輸出目錄,並確保外掛程式將使用 A 版本的 A.PluginBase。
外掛程式目標架構建議
因為外掛程式相依性載入使用 .deps.json 檔案,因此有一個與外掛程式目標架構相關的 gotcha。 具體而言,您的外掛程式應該以運行時間為目標,例如 .NET 5,而不是 .NET Standard 的版本。 .deps.json 檔案會根據專案的目標架構產生,而且由於許多 .NET Standard 相容套件會隨附參考元件,以針對特定運行時間建置 .NET Standard 和實作元件,因此 .deps.json 可能無法正確查看實作元件,或是擷取元件的 .NET Standard 版本,而不是您預期的 .NET Core 版本。
外掛程式架構參考
目前,外掛程式無法將新的架構引入程式中。 例如,您無法將使用 Microsoft.AspNetCore.App 架構的外掛程式載入至只使用根 Microsoft.NETCore.App 架構的應用程式。 主應用程式必須宣告外掛程式所需之所有架構的參考。