Skapa ett .NET Core-program med plugin-program
Den här självstudien visar hur du skapar en anpassad AssemblyLoadContext för att läsa in plugin-program. En AssemblyDependencyResolver används för att lösa beroenden för plugin-programmet. Självstudien isolerar korrekt plugin-programmets beroenden från värdprogrammet. Du lär dig följande:
- Strukturera ett projekt för att stödja plugin-program.
- Skapa en anpassad AssemblyLoadContext för att läsa in varje plugin-program.
- Använd typen System.Runtime.Loader.AssemblyDependencyResolver för att tillåta att plugin-program har beroenden.
- Skapa plugin-program som enkelt kan distribueras genom att bara kopiera byggartefakterna.
Krav
- Installera .NET 5 SDK eller en nyare version.
Anteckning
Exempelkoden riktar sig till .NET 5, men alla funktioner som används introducerades i .NET Core 3.0 och är tillgängliga i alla .NET-versioner sedan dess.
Skapa programmet
Det första steget är att skapa programmet:
Skapa en ny mapp och kör följande kommando i mappen:
dotnet new console -o AppWithPlugin
Skapa en Visual Studio-lösningsfil i samma mapp för att göra det enklare att skapa projektet. Kör följande kommando:
dotnet new sln
Kör följande kommando för att lägga till appprojektet i lösningen:
dotnet sln add AppWithPlugin/AppWithPlugin.csproj
Nu kan vi fylla i stommen i vårt program. Ersätt koden i filen AppWithPlugin/Program.cs med följande kod:
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);
}
}
}
}
Skapa gränssnitten för plugin-programmet
Nästa steg i att skapa en app med plugin-program är att definiera det gränssnitt som plugin-program behöver implementera. Vi rekommenderar att du skapar ett klassbibliotek som innehåller alla typer som du planerar att använda för kommunikation mellan din app och plugin-program. Med den här divisionen kan du publicera plugin-gränssnittet som ett paket utan att behöva skicka hela programmet.
I rotmappen för projektet kör dotnet new classlib -o PluginBase
du . dotnet sln add PluginBase/PluginBase.csproj
Kör också för att lägga till projektet i lösningsfilen. PluginBase/Class1.cs
Ta bort filen och skapa en ny fil i PluginBase
mappen med namnet ICommand.cs
med följande gränssnittsdefinition:
namespace PluginBase
{
public interface ICommand
{
string Name { get; }
string Description { get; }
int Execute();
}
}
Det här ICommand
gränssnittet är det gränssnitt som alla plugin-program implementerar.
Nu när ICommand
gränssnittet har definierats kan programprojektet fyllas i lite mer. Lägg till en referens från AppWithPlugin
projektet till PluginBase
projektet med dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj
kommandot från rotmappen.
Ersätt kommentaren // Load commands from plugins
med följande kodfragment så att den kan läsa in plugin-program från angivna filsökvägar:
string[] pluginPaths = new string[]
{
// Paths to plugins to load.
};
IEnumerable<ICommand> commands = pluginPaths.SelectMany(pluginPath =>
{
Assembly pluginAssembly = LoadPlugin(pluginPath);
return CreateCommands(pluginAssembly);
}).ToList();
Ersätt sedan kommentaren // Output the loaded commands
med följande kodfragment:
foreach (ICommand command in commands)
{
Console.WriteLine($"{command.Name}\t - {command.Description}");
}
Ersätt kommentaren // Execute the command with the name passed as an argument
med följande kodfragment:
ICommand command = commands.FirstOrDefault(c => c.Name == commandName);
if (command == null)
{
Console.WriteLine("No such command is known.");
return;
}
command.Execute();
Slutligen lägger du till statiska metoder i Program
klassen med namnet LoadPlugin
och CreateCommands
, som du ser här:
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}");
}
}
Läsa in plugin-program
Nu kan programmet läsa in och instansiera kommandon från inlästa plugin-sammansättningar, men det går fortfarande inte att läsa in plugin-sammansättningarna. Skapa en fil med namnet PluginLoadContext.cs i mappen AppWithPlugin med följande innehåll:
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;
}
}
}
Typen PluginLoadContext
härleds från AssemblyLoadContext. Typen AssemblyLoadContext
är en särskild typ i körningen som gör att utvecklare kan isolera inlästa sammansättningar i olika grupper för att säkerställa att sammansättningsversionerna inte står i konflikt. Dessutom kan en anpassad AssemblyLoadContext
välja olika sökvägar för att läsa in sammansättningar från och åsidosätta standardbeteendet. PluginLoadContext
använder en instans av typen AssemblyDependencyResolver
som introducerades i .NET Core 3.0 för att matcha sammansättningsnamn till sökvägar. Objektet AssemblyDependencyResolver
konstrueras med sökvägen till ett .NET-klassbibliotek. Den löser sammansättningar och interna bibliotek till deras relativa sökvägar baserat på filen .deps.json för klassbiblioteket vars sökväg skickades till AssemblyDependencyResolver
konstruktorn. Den anpassade AssemblyLoadContext
gör det möjligt för plugin-program att ha sina egna beroenden och AssemblyDependencyResolver
gör det enkelt att läsa in beroendena korrekt.
Nu när AppWithPlugin
projektet har PluginLoadContext
typen uppdaterar Program.LoadPlugin
du metoden med följande brödtext:
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)));
}
Genom att använda en annan PluginLoadContext
instans för varje plugin-program kan plugin-program ha olika eller till och med motstridiga beroenden utan problem.
Enkelt plugin-program utan beroenden
Gör följande i rotmappen:
Kör följande kommando för att skapa ett nytt klassbiblioteksprojekt med namnet
HelloPlugin
:dotnet new classlib -o HelloPlugin
Kör följande kommando för att lägga till projektet i
AppWithPlugin
lösningen:dotnet sln add HelloPlugin/HelloPlugin.csproj
Ersätt filen HelloPlugin/Class1.cs med en fil med namnet HelloCommand.cs med följande innehåll:
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;
}
}
}
Öppna nu filen HelloPlugin.csproj . Det bör se ut ungefär så här:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
</Project>
Lägg till följande element mellan taggarna <PropertyGroup>
:
<EnableDynamicLoading>true</EnableDynamicLoading>
<EnableDynamicLoading>true</EnableDynamicLoading>
förbereder projektet så att det kan användas som ett plugin-program. Detta kopierar bland annat alla dess beroenden till projektets utdata. Mer information finns i: EnableDynamicLoading
.
Lägg till följande element mellan taggarna <Project>
:
<ItemGroup>
<ProjectReference Include="..\PluginBase\PluginBase.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
</ItemGroup>
Elementet <Private>false</Private>
är viktigt. Detta instruerar MSBuild att inte kopiera PluginBase.dll till utdatakatalogen för HelloPlugin. Om denPluginBase.dll sammansättningen finns i utdatakatalogen PluginLoadContext
hittar du sammansättningen där och läser in den när den läser inHelloPlugin.dll-sammansättningen . I det här läget HelloPlugin.HelloCommand
implementerar ICommand
typen gränssnittet från PluginBase.dll i projektets HelloPlugin
utdatakatalog, inte ICommand
det gränssnitt som läses in i standardinläsningskontexten. Eftersom körningen ser dessa två typer som olika typer från olika sammansättningar hittar AppWithPlugin.Program.CreateCommands
metoden inte kommandona. Därför <Private>false</Private>
krävs metadata för referensen till sammansättningen som innehåller plugin-gränssnitten.
På samma sätt är elementet <ExcludeAssets>runtime</ExcludeAssets>
också viktigt om refererar PluginBase
till andra paket. Den här inställningen har samma effekt som <Private>false</Private>
men fungerar på paketreferenser som PluginBase
projektet eller något av dess beroenden kan innehålla.
Nu när HelloPlugin
projektet är klart bör du uppdatera AppWithPlugin
projektet för att veta var plugin-programmet HelloPlugin
finns. Efter kommentaren // Paths to plugins to load
lägger du till @"HelloPlugin\bin\Debug\net5.0\HelloPlugin.dll"
(den här sökvägen kan skilja sig från den .NET Core-version som du använder) som ett element i matrisen pluginPaths
.
Plugin-program med biblioteksberoenden
Nästan alla plugin-program är mer komplexa än en enkel "Hello World", och många plugin-program har beroenden på andra bibliotek. Projekten JsonPlugin
och OldJsonPlugin
i exemplet visar två exempel på plugin-program med NuGet-paketberoenden på Newtonsoft.Json
. Därför bör alla plugin-projekt lägga <EnableDynamicLoading>true</EnableDynamicLoading>
till i projektegenskaperna så att de kopierar alla sina beroenden till utdata dotnet build
från . Om du publicerar klassbiblioteket med dotnet publish
kopieras även alla dess beroenden till publiceringsutdata.
Andra exempel i exemplet
Den fullständiga källkoden för den här självstudien finns på lagringsplatsen dotnet/samples. Det färdiga exemplet innehåller några andra exempel på AssemblyDependencyResolver
beteende. Objektet kan till exempel AssemblyDependencyResolver
även matcha interna bibliotek samt lokaliserade satellitsammansättningar som ingår i NuGet-paket. Lagringsplatsen UVPlugin
och FrenchPlugin
i exempellagringsplatsen visar dessa scenarier.
Referera till ett plugin-gränssnitt från ett NuGet-paket
Anta att det finns en app A som har ett plugin-gränssnitt som definierats i NuGet-paketet med namnet A.PluginBase
. Hur refererar du till paketet korrekt i plugin-projektet? För projektreferenser förhindrade användning av <Private>false</Private>
metadata för elementet ProjectReference
i projektfilen dll-filen från att kopieras till utdata.
Om du vill referera A.PluginBase
till paketet korrekt vill du ändra elementet <PackageReference>
i projektfilen till följande:
<PackageReference Include="A.PluginBase" Version="1.0.0">
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
Detta förhindrar A.PluginBase
att sammansättningarna kopieras till utdatakatalogen för plugin-programmet och säkerställer att plugin-programmet använder A:s version av A.PluginBase
.
Rekommendationer för målramverk för plugin-program
Eftersom plugin-beroendeinläsning använder .deps.json-filen finns det en gotcha som är relaterad till plugin-programmets målramverk. Mer specifikt bör dina plugin-program riktas mot en körning, till exempel .NET 5, i stället för en version av .NET Standard. .deps.json-filen genereras baserat på vilket ramverk projektet riktar sig mot, och eftersom många .NET Standard-kompatibla paket skickar referenssammansättningar för att skapa mot .NET Standard och implementeringssammansättningar för specifika körningsmiljöer, kanske .deps.json inte korrekt ser implementeringssammansättningar, eller så kan den hämta .NET Standard-versionen av en sammansättning i stället för den .NET Core-version du förväntar dig.
Referenser till plugin-ramverk
För närvarande kan plugin-program inte introducera nya ramverk i processen. Du kan till exempel inte läsa in ett plugin-program som använder ramverket Microsoft.AspNetCore.App
i ett program som bara använder rotramverket Microsoft.NETCore.App
. Värdprogrammet måste deklarera referenser till alla ramverk som behövs av plugin-program.