Poznámka
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Tento návod vám ukáže, jak vytvořit vlastní AssemblyLoadContext k načtení pluginů. Jednotka AssemblyDependencyResolver se používá k vyřešení závislostí plug-inu. Tento tutoriál poskytuje samostatný kontext sestavení pro závislosti plug-inu, což umožňuje různé závislosti mezi plug-iny a hostitelskou aplikací. Naučíte se:
- Strukturování projektu pro podporu modulů plug-in
- Vytvořte vlastní AssemblyLoadContext pro načtení každého modulu plug-in.
- Použijte typ System.Runtime.Loader.AssemblyDependencyResolver, aby pluginy mohly mít závislosti.
- Vytvářet pluginy, které lze snadno nasadit, jen zkopírováním artefaktů sestavení.
Poznámka:
Nedůvěryhodný kód nelze bezpečně načíst do důvěryhodného procesu .NET. Pokud chcete poskytnout hranici zabezpečení nebo spolehlivosti, zvažte technologii poskytovanou vaším operačním systémem nebo virtualizační platformou.
Požadavky
- Nejnovější sada .NET SDK
- editor Visual Studio Code editoru
- C# DevKit
Vytvoření aplikace
Prvním krokem je vytvoření aplikace:
Vytvořte novou složku a v této složce spusťte následující příkaz:
dotnet new console -o AppWithPlugin
Pokud chcete usnadnit sestavování projektu, vytvořte soubor řešení sady Visual Studio ve stejné složce. Spusťte následující příkaz:
dotnet new sln
Spuštěním následujícího příkazu přidejte projekt aplikace do řešení:
dotnet sln add AppWithPlugin/AppWithPlugin.csproj
Teď můžeme vyplnit kostru naší aplikace. Nahraďte kód v souboru AppWithPlugin/Program.cs následujícím kódem:
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);
}
}
}
}
Vytvoření rozhraní modulu plug-in
Dalším krokem při vytváření aplikace s moduly plug-in je definování rozhraní, které moduly plug-in potřebují implementovat. Doporučujeme vytvořit knihovnu tříd, která obsahuje všechny typy, které chcete použít pro komunikaci mezi vaší aplikací a moduly plug-in. Tato divize umožňuje publikovat rozhraní modulu plug-in jako balíček, aniž byste museli dodávat celou aplikaci.
V kořenové složce projektu spusťte dotnet new classlib -o PluginBase
. Spuštěním dotnet sln add PluginBase/PluginBase.csproj
přidejte projekt do souboru řešení. Odstraňte soubor PluginBase/Class1.cs
a vytvořte nový soubor ve složce PluginBase
s názvem ICommand.cs
s následující definicí rozhraní:
namespace PluginBase
{
public interface ICommand
{
string Name { get; }
string Description { get; }
int Execute();
}
}
Toto ICommand
rozhraní je to, které budou implementovat všechny pluginy.
Nyní, když je rozhraní ICommand
definováno, je možné projekt aplikace dále rozšířit. Přidejte odkaz z projektu AppWithPlugin
do projektu PluginBase
příkazem dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj
z kořenové složky.
Nahraďte komentář // Load commands from plugins
následujícím úryvkem kódu, aby mohly být pluginy načteny z daných cest k souborům.
string[] pluginPaths = new string[]
{
// Paths to plugins to load.
};
IEnumerable<ICommand> commands = pluginPaths.SelectMany(pluginPath =>
{
Assembly pluginAssembly = LoadPlugin(pluginPath);
return CreateCommands(pluginAssembly);
}).ToList();
Potom nahraďte komentář // Output the loaded commands
následujícím fragmentem kódu:
foreach (ICommand command in commands)
{
Console.WriteLine($"{command.Name}\t - {command.Description}");
}
Nahraďte komentář // Execute the command with the name passed as an argument
následujícím fragmentem kódu:
ICommand command = commands.FirstOrDefault(c => c.Name == commandName);
if (command == null)
{
Console.WriteLine("No such command is known.");
return;
}
command.Execute();
Nakonec přidejte statické metody do třídy Program
s názvem LoadPlugin
a CreateCommands
, jak je znázorněno zde:
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}");
}
}
Načtení modulů plug-in
Aplikace teď dokáže správně načíst a vytvořit instanci příkazů z načtených plugin sestavení, ale stále nemůže načíst plugin sestavení. Ve složce AppWithPlugin vytvořte soubor s názvem PluginLoadContext.cs s následujícím obsahem:
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;
}
}
}
Typ PluginLoadContext
je odvozen z AssemblyLoadContext. Typ AssemblyLoadContext
je speciální typ modulu runtime, který vývojářům umožňuje izolovat načtená sestavení do různých skupin, aby se zajistilo, že verze sestavení nejsou v konfliktu. Kromě toho může vlastní AssemblyLoadContext
zvolit různé cesty pro načtení sestavení a přepsat výchozí chování.
PluginLoadContext
používá instanci typu AssemblyDependencyResolver
zavedeného v .NET Core 3.0 k překladu názvů sestavení do cest. Objekt AssemblyDependencyResolver
je vytvořen s cestou k knihovně tříd .NET. Řeší sestavení a nativní knihovny do jejich relativních cest na základě souboru .deps.json pro knihovnu tříd, jejíž cesta byla předána do AssemblyDependencyResolver
konstruktoru. Vlastní AssemblyLoadContext
umožňuje pluginům mít vlastní závislosti a AssemblyDependencyResolver
usnadňuje správné načtení závislostí.
Teď, když má projekt AppWithPlugin
typ PluginLoadContext
, aktualizujte metodu Program.LoadPlugin
následujícím textem:
static Assembly LoadPlugin(string relativePath)
{
// Navigate up to the solution root
string 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)));
}
Pomocí použití různé instance PluginLoadContext
pro každý plugin mohou mít pluginy různé nebo dokonce konfliktní závislosti bez jakýchkoli problémů.
Jednoduchý modul plug-in bez závislostí
Zpátky v kořenové složce postupujte takto:
Spuštěním následujícího příkazu vytvořte nový projekt knihovny tříd s názvem
HelloPlugin
:dotnet new classlib -o HelloPlugin
Spuštěním následujícího příkazu přidejte projekt do řešení
AppWithPlugin
:dotnet sln add HelloPlugin/HelloPlugin.csproj
Nahraďte soubor HelloPlugin/Class1.cs souborem s názvem HelloCommand.cs následujícím obsahem:
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;
}
}
}
Teď otevřete soubor HelloPlugin.csproj. Měl by vypadat nějak takto:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
</Project>
Mezi značky <PropertyGroup>
přidejte následující prvek:
<EnableDynamicLoading>true</EnableDynamicLoading>
<EnableDynamicLoading>true</EnableDynamicLoading>
připraví projekt tak, aby ho bylo možné použít jako modul plug-in. Mimo jiné se tím zkopírují všechny jeho závislosti na výstupu projektu. Další podrobnosti najdete tady: EnableDynamicLoading
.
Mezi značky <Project>
přidejte následující prvky:
<ItemGroup>
<ProjectReference Include="..\PluginBase\PluginBase.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
</ItemGroup>
Prvek <Private>false</Private>
je důležitý. To říká nástroji MSBuild, aby nekopíroval PluginBase.dll do výstupního adresáře pro HelloPlugin. Pokud je sestavení PluginBase.dll ve výstupním adresáři, PluginLoadContext
tam toto sestavení najde a načte je, když načte sestavení HelloPlugin.dll. V tomto okamžiku typ HelloPlugin.HelloCommand
implementuje rozhraní ICommand
z PluginBase.dll ve výstupním adresáři projektu HelloPlugin
, nikoli rozhraní ICommand
, které je načteno do výchozího kontextu načítání. Vzhledem k tomu, že modul runtime vidí tyto dva typy jako různé typy z různých sestavení, metoda AppWithPlugin.Program.CreateCommands
tyto příkazy nenajde. V důsledku toho se pro odkaz na sestavení obsahující rozhraní modulu plug-in vyžadují metadata <Private>false</Private>
.
Podobně je prvek <ExcludeAssets>runtime</ExcludeAssets>
důležitý také v případě, že PluginBase
odkazuje na jiné balíčky. Toto nastavení má stejný účinek jako <Private>false</Private>
, ale funguje na odkazech na balíčky, které může obsahovat PluginBase
projekt nebo jedna z jejích závislostí.
Po dokončení projektu HelloPlugin
byste měli aktualizovat projekt AppWithPlugin
, abyste věděli, kde je možné najít modul plug-in HelloPlugin
. Po // Paths to plugins to load
komentáři přidejte @"HelloPlugin\bin\Debug\net5.0\HelloPlugin.dll"
(tato cesta se může lišit podle používané verze .NET Core) jako prvek pole pluginPaths
.
Plug-in se závislostmi na knihovnách
Téměř všechny moduly plug-in jsou složitější než jednoduché "Hello World" a mnoho modulů plug-in má závislosti na jiných knihovnách. Projekty JsonPlugin
a OldJsonPlugin
v ukázce ukazují dva příklady modulů plug-in se závislostmi balíčků NuGet na Newtonsoft.Json
. Z tohoto důvodu by všechny projekty modulů plug-in měly přidat <EnableDynamicLoading>true</EnableDynamicLoading>
do vlastností projektu, aby zkopírovaly všechny své závislosti na výstupu dotnet build
. Publikování knihovny tříd s dotnet publish
také zkopíruje všechny její závislosti do výstupního souboru publikování.
Další příklady v ukázce
Kompletní zdrojový kód pro tento kurz najdete v úložišti dotnet/samples. Dokončená ukázka obsahuje několik dalších příkladů chování AssemblyDependencyResolver
. Například objekt AssemblyDependencyResolver
může také řešit nativní knihovny a lokalizovaná satelitní sestavení zahrnutá v balíčcích NuGet. Ukázky UVPlugin
a FrenchPlugin
v úložišti příkladů demonstrují tyto scénáře.
Odkaz na rozhraní modulu plug-in z balíčku NuGet
Řekněme, že existuje aplikace A, která má rozhraní modulu plug-in definované v balíčku NuGet s názvem A.PluginBase
. Jak správně odkazujete na balíček v projektu modulu plug-in? U odkazů na projekt způsobilo použití metadat <Private>false</Private>
v elementu ProjectReference
v souboru projektu, že se DLL nekopíroval do výstupu.
Chcete-li správně odkazovat na balíček A.PluginBase
, chcete změnit prvek <PackageReference>
v souboru projektu na následující:
<PackageReference Include="A.PluginBase" Version="1.0.0">
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
Tím zabráníte zkopírování sestavení A.PluginBase
do výstupního adresáře vašeho plug-inu a zajistíte, že váš plug-in bude používat verzi A.PluginBase
od A.
Doporučení pro cílovou architekturu modulu plug-in
Vzhledem k tomu, že načítání závislostí plug-inu používá soubor .deps.json, existuje úskalí související s cílovým prostředím plug-inu. Konkrétně by vaše plug-iny měly cílit na runtime, například .NET 5, místo na verzi .NET Standard. Soubor .deps.json se generuje na základě rámce, na který cílí projekt, a protože mnoho balíčků kompatibilních s .NET Standard zahrnuje referenční sestavení pro sestavení proti .NET Standard a implementační sestavení pro konkrétní prostředí runtime, .deps.json nemusí správně zobrazit sestavení implementace nebo může namísto očekávané verze .NET Core získat verzi sestavení .NET Standard.
Odkazy na rámec pluginů
Moduly plug-in v současné době nemůžou do procesu zavádět nové architektury. Například nemůžete načíst modul plug-in, který používá rozhraní Microsoft.AspNetCore.App
do aplikace, která používá pouze kořenovou Microsoft.NETCore.App
architekturu. Hostitelská aplikace musí deklarovat odkazy na všechny frameworky potřebné pluginy.