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 AppWithPluginPokud 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 slnSpuš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 (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}");
}
}
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(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)));
}
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 HelloPluginSpuštěním následujícího příkazu přidejte projekt do řešení
AppWithPlugin:dotnet sln add HelloPlugin/HelloPlugin.csprojNahraď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.PluginBaseod 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.