Vytvoření aplikace .NET Core pomocí modulů plug-in
V tomto kurzu se dozvíte, jak vytvořit vlastní AssemblyLoadContext moduly plug-in pro načítání. K AssemblyDependencyResolver vyřešení závislostí modulu plug-in se používá modul plug-in. Tento kurz správně izoluje závislosti modulu plug-in od hostitelské aplikace. Dozvíte se, jak:
- Strukturování projektu tak, aby podporoval moduly plug-in.
- Vytvořte vlastní AssemblyLoadContext modul plug-in, který načte jednotlivé moduly plug-in.
- Pomocí typu System.Runtime.Loader.AssemblyDependencyResolver povolte, aby moduly plug-in měly závislosti.
- Vytvářejte moduly plug-in, které je možné snadno nasadit zkopírováním artefaktů sestavení.
Požadavky
- Nainstalujte sadu .NET 5 SDK nebo novější verzi.
Poznámka
Ukázkový kód cílí na .NET 5, ale všechny funkce, které používá, byly zavedeny v .NET Core 3.0 a od té doby jsou dostupné ve všech verzích .NET.
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 vytváření 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é musí moduly plug-in implementovat. Doporučujeme vytvořit knihovnu tříd, která bude obsahovat všechny typy, které plánujete používat 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 příkaz dotnet new classlib -o PluginBase
. Spuštěním příkazu dotnet sln add PluginBase/PluginBase.csproj
také přidejte projekt do souboru řešení. PluginBase/Class1.cs
Odstraňte soubor a vytvořte nový soubor ve PluginBase
složce 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 rozhraní, které budou implementovat všechny moduly plug-in.
Teď, když ICommand
je rozhraní definováno, je možné projekt aplikace vyplnit o něco více. Přidejte do projektu odkaz z AppWithPlugin
projektu PluginBase
pomocí dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj
příkazu z kořenové složky.
// Load commands from plugins
Nahraďte komentář následujícím fragmentem kódu, který mu umožní načíst moduly plug-in 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();
Pak nahraďte // Output the loaded commands
komentář následujícím fragmentem kódu:
foreach (ICommand command in commands)
{
Console.WriteLine($"{command.Name}\t - {command.Description}");
}
// Execute the command with the name passed as an argument
Nahraďte komentář 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();
A nakonec přidejte statické metody do Program
třídy s názvem LoadPlugin
a , CreateCommands
jak je znázorněno tady:
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ď může správně načíst a vytvořit instance příkazů z načtených sestavení modulů plug-in, ale stále nemůže načíst sestavení modulů plug-in. 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 je PluginLoadContext
odvozen od AssemblyLoadContext. Typ AssemblyLoadContext
je speciální typ v modulu runtime, který vývojářům umožňuje izolovat načtená sestavení do různých skupin, aby se zajistilo, že verze sestavení nebudou kolidovat. Kromě toho může vlastní AssemblyLoadContext
zvolit různé cesty k načtení sestavení a přepsat výchozí chování. Používá PluginLoadContext
instanci typu zavedenou AssemblyDependencyResolver
v .NET Core 3.0 k překladu názvů sestavení na cesty. Objekt AssemblyDependencyResolver
je vytvořen s cestou ke knihovně tříd .NET. Překládá sestavení a nativní knihovny na jejich relativní cesty založené na souboru .deps.json pro knihovnu tříd, jejíž cesta byla předána konstruktoru AssemblyDependencyResolver
. Vlastní AssemblyLoadContext
umožňuje modulům plug-in mít vlastní závislosti a AssemblyDependencyResolver
usnadňuje správné načtení závislostí.
Teď, když AppWithPlugin
má PluginLoadContext
projekt typ, 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)));
}
Při použití jiné PluginLoadContext
instance pro každý modul plug-in můžou mít moduly plug-in bez problémů různé nebo dokonce konfliktní závislosti.
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
AppWithPlugin
řešení: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ělo by to 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>
Připraví <EnableDynamicLoading>true</EnableDynamicLoading>
projekt tak, aby ho bylo možné použít jako modul plug-in. Tím se mimo jiné zkopírují všechny jeho závislosti do 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írovat PluginBase.dll do výstupního adresáře pro HelloPlugin. Pokud sePluginBase.dll sestavení nachází ve výstupním adresáři, PluginLoadContext
najde tam sestavení a načte ho při načtení sestaveníHelloPlugin.dll . V tomto okamžiku HelloPlugin.HelloCommand
bude typ implementovat ICommand
rozhraní z PluginBase.dll ve výstupním HelloPlugin
adresáři projektu, nikoli ICommand
rozhraní, 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í, AppWithPlugin.Program.CreateCommands
metoda nenajde příkazy. V důsledku toho <Private>false</Private>
se metadata vyžadují pro odkaz na sestavení obsahující rozhraní modulu plug-in.
Podobně je prvek také důležitý, <ExcludeAssets>runtime</ExcludeAssets>
pokud odkazuje na PluginBase
jiné balíčky. Toto nastavení má stejný účinek jako <Private>false</Private>
u odkazů na balíčky, které PluginBase
může projekt nebo jedna z jeho závislostí obsahovat.
Teď, když HelloPlugin
je projekt hotový, měli byste ho AppWithPlugin
aktualizovat, abyste věděli, kde se dá modul plug-in HelloPlugin
najít. // Paths to plugins to load
Za komentář přidejte @"HelloPlugin\bin\Debug\net5.0\HelloPlugin.dll"
(tato cesta se může lišit v závislosti na používané verzi .NET Core) jako prvek pluginPaths
pole.
Modul plug-in se závislostmi knihovny
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 se všechny projekty modulů plug-in měly přidat <EnableDynamicLoading>true</EnableDynamicLoading>
do vlastností projektu, aby zkopírovaly všechny své závislosti do výstupu .dotnet build
Publikováním knihovny tříd pomocí dotnet publish
se také zkopírují všechny její závislosti do výstupu 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ů AssemblyDependencyResolver
chování. Objekt může například AssemblyDependencyResolver
také přeložit nativní knihovny a lokalizovaná satelitní sestavení zahrnutá v balíčcích NuGet. Tyto UVPlugin
scénáře demonstrují v úložišti ukázek a FrenchPlugin
.
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 použití <Private>false</Private>
metadat v elementu ProjectReference
v souboru projektu zabránilo kopírování knihovny DLL do výstupu.
Chcete-li správně odkazovat na A.PluginBase
balíček, chcete změnit <PackageReference>
prvek v souboru projektu na následující:
<PackageReference Include="A.PluginBase" Version="1.0.0">
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
Tím zabráníte A.PluginBase
kopírování sestavení do výstupního adresáře modulu plug-in a zajistíte, že modul plug-in bude používat verzi A .A.PluginBase
Doporučení cílové architektury modulu plug-in
Vzhledem k tomu, že načítání závislostí modulu plug-in používá soubor .deps.json , existuje něco, co souvisí s cílovou architekturou modulu plug-in. Konkrétně by vaše moduly plug-in měly cílit na modul runtime, jako je .NET 5, a ne na verzi .NET Standard. Soubor .deps.json se generuje na základě toho, na jakou architekturu projekt cílí, a protože mnoho balíčků kompatibilních se standardem .NET Standard dodává referenční sestavení pro sestavení pro .NET Standard a implementační sestavení pro konkrétní moduly runtime, nemusí soubor .deps.json správně zobrazit sestavení implementace nebo může místo očekávané verze .NET Core získat verzi sestavení .NET Standard.
Odkazy na architekturu modulů plug-in
Moduly plug-in v současné době nemůžou do procesu zavádět nové architektury. Nemůžete například načíst modul plug-in, který používá architekturu Microsoft.AspNetCore.App
, do aplikace, která používá pouze kořenovou Microsoft.NETCore.App
architekturu. Hostitelská aplikace musí deklarovat odkazy na všechna rozhraní, která moduly plug-in potřebují.