Een .NET Core-toepassing maken met invoegtoepassingen
In deze handleiding leert u hoe u een aangepaste AssemblyLoadContext maakt om plug-ins te laden. Een AssemblyDependencyResolver wordt gebruikt om de afhankelijkheden van de plug-in op te lossen. De zelfstudie biedt een aparte assembly-omgeving voor de afhankelijkheden van de invoegtoepassing, waardoor verschillende assembly-afhankelijkheden tussen de invoegtoepassingen en de host-toepassing mogelijk worden gemaakt. U leert het volgende:
- Structureer een project ter ondersteuning van invoegtoepassingen.
- Maak een aangepaste AssemblyLoadContext om elke plug-in te laden.
- Gebruik het type System.Runtime.Loader.AssemblyDependencyResolver om toe te staan dat invoegtoepassingen afhankelijkheden hebben.
- Ontwerpinvoegtoepassingen die eenvoudig kunnen worden geïmplementeerd door alleen de buildartefacten te kopiëren.
Notitie
Niet-vertrouwde code kan niet veilig worden geladen in een vertrouwd .NET-proces. Als u een beveiligings- of betrouwbaarheidsgrens wilt bieden, kunt u een technologie overwegen die wordt geleverd door uw besturingssysteem of virtualisatieplatform.
Benodigdheden
- De nieuwste .NET SDK-
- Visual Studio Code-editor
- De C# DevKit
De toepassing maken
De eerste stap is het maken van de toepassing:
Maak een nieuwe map en voer in die map de volgende opdracht uit:
dotnet new console -o AppWithPlugin
Als u het project eenvoudiger wilt maken, maakt u een Visual Studio-oplossingsbestand in dezelfde map. Voer de volgende opdracht uit:
dotnet new sln
Voer de volgende opdracht uit om het app-project toe te voegen aan de oplossing:
dotnet sln add AppWithPlugin/AppWithPlugin.csproj
Nu kunnen we het skelet van onze toepassing invullen. Vervang de code in het bestand AppWithPlugin/Program.cs door de volgende code:
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);
}
}
}
}
De invoegtoepassingsinterfaces maken
De volgende stap bij het bouwen van een app met invoegtoepassingen is het definiëren van de interface die de invoegtoepassingen moeten implementeren. U wordt aangeraden een klassebibliotheek te maken die alle typen bevat die u wilt gebruiken voor de communicatie tussen uw app en invoegtoepassingen. Met deze divisie kunt u uw invoegtoepassingsinterface publiceren als een pakket zonder dat u uw volledige toepassing hoeft te verzenden.
Voer dotnet new classlib -o PluginBase
uit in de hoofdmap van het project. Voer ook dotnet sln add PluginBase/PluginBase.csproj
uit om het project toe te voegen aan het oplossingsbestand. Verwijder het PluginBase/Class1.cs
bestand en maak een nieuw bestand in de map PluginBase
met de naam ICommand.cs
met de volgende interfacedefinitie:
namespace PluginBase
{
public interface ICommand
{
string Name { get; }
string Description { get; }
int Execute();
}
}
Deze ICommand
interface is de interface die door alle invoegtoepassingen wordt geïmplementeerd.
Nu de ICommand
-interface is gedefinieerd, kan het toepassingsproject iets meer worden ingevuld. Voeg een verwijzing van het AppWithPlugin
-project toe aan het PluginBase
-project met de dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj
-opdracht vanuit de hoofdmap.
Vervang de opmerking // Load commands from plugins
door het volgende codefragment om invoegtoepassingen uit bepaalde bestandspaden te laden:
string[] pluginPaths = new string[]
{
// Paths to plugins to load.
};
IEnumerable<ICommand> commands = pluginPaths.SelectMany(pluginPath =>
{
Assembly pluginAssembly = LoadPlugin(pluginPath);
return CreateCommands(pluginAssembly);
}).ToList();
Vervang vervolgens de opmerking // Output the loaded commands
door het volgende codefragment:
foreach (ICommand command in commands)
{
Console.WriteLine($"{command.Name}\t - {command.Description}");
}
Vervang de // Execute the command with the name passed as an argument
opmerking door het volgende fragment:
ICommand command = commands.FirstOrDefault(c => c.Name == commandName);
if (command == null)
{
Console.WriteLine("No such command is known.");
return;
}
command.Execute();
Voeg tot slot statische methoden toe aan de klasse Program
met de naam LoadPlugin
en CreateCommands
, zoals hier wordt weergegeven:
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}");
}
}
Invoegtoepassingen laden
De toepassing kan nu opdrachten van geladen invoegtoepassingsassembly's correct laden en instantiëren, maar kan de invoegtoepassingsassembly's nog steeds niet laden. Maak een bestand met de naam PluginLoadContext.cs in de map AppWithPlugin met de volgende inhoud:
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;
}
}
}
Het PluginLoadContext
type is afgeleid van AssemblyLoadContext. Het AssemblyLoadContext
type is een speciaal type in de runtime waarmee ontwikkelaars geladen assembly's in verschillende groepen kunnen isoleren om ervoor te zorgen dat assemblyversies niet conflicteren. Daarnaast kan een aangepaste AssemblyLoadContext
verschillende paden kiezen om assembly's te laden en het standaardgedrag te overschrijven. De PluginLoadContext
maakt gebruik van een exemplaar van het AssemblyDependencyResolver
type dat is geïntroduceerd in .NET Core 3.0 om assemblynamen om te zetten in paden. Het AssemblyDependencyResolver
-object wordt geconstrueerd met het pad naar een .NET-klassebibliotheek. Hiermee worden assembly's en systeemeigen bibliotheken omgezet in hun relatieve paden op basis van het .deps.json-bestand voor de klassebibliotheek waarvan het pad is doorgegeven aan de AssemblyDependencyResolver
constructor. Met de aangepaste AssemblyLoadContext
kunnen invoegtoepassingen hun eigen afhankelijkheden hebben en de AssemblyDependencyResolver
maakt het eenvoudig om de afhankelijkheden correct te laden.
Nu het AppWithPlugin
project het type PluginLoadContext
heeft, werkt u de Program.LoadPlugin
methode bij met de volgende hoofdtekst:
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)));
}
Door een ander PluginLoadContext
exemplaar voor elke invoegtoepassing te gebruiken, kunnen de invoegtoepassingen verschillende of zelfs conflicterende afhankelijkheden hebben zonder probleem.
Eenvoudige invoegtoepassing zonder afhankelijkheden
Terug in de hoofdmap, doe het volgende:
Voer de volgende opdracht uit om een nieuw klassebibliotheekproject met de naam
HelloPlugin
te maken:dotnet new classlib -o HelloPlugin
Voer de volgende opdracht uit om het project toe te voegen aan de
AppWithPlugin
-oplossing:dotnet sln add HelloPlugin/HelloPlugin.csproj
Vervang het bestand HelloPlugin/Class1.cs door een bestand met de naam HelloCommand.cs door de volgende inhoud:
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;
}
}
}
Open nu het bestand HelloPlugin.csproj. Het moet er ongeveer als volgt uitzien:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
</Project>
Voeg tussen de <PropertyGroup>
tags het volgende element toe:
<EnableDynamicLoading>true</EnableDynamicLoading>
De <EnableDynamicLoading>true</EnableDynamicLoading>
bereidt het project voor, zodat het kan worden gebruikt als invoegtoepassing. Dit kopieert onder andere alle afhankelijkheden naar de uitvoer van het project. Zie EnableDynamicLoading
voor meer informatie.
Voeg tussen de <Project>
tags de volgende elementen toe:
<ItemGroup>
<ProjectReference Include="..\PluginBase\PluginBase.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
</ItemGroup>
Het <Private>false</Private>
element is belangrijk. Dit vertelt MSBuild dat PluginBase.dll niet naar de uitvoermap voor HelloPlugin moet worden gekopieerd. Als de PluginBase.dll assembly aanwezig is in de uitvoermap, vindt PluginLoadContext
de assembly daar en laadt deze wanneer de HelloPlugin.dll assembly wordt geladen. Op dit punt implementeert het HelloPlugin.HelloCommand
-type de ICommand
-interface van de PluginBase.dll in de uitvoermap van het HelloPlugin
-project, niet de ICommand
-interface die in de standaard laadsituatie wordt geladen. Omdat de runtime deze twee typen ziet als verschillende typen van verschillende assembly's, worden de opdrachten niet door de AppWithPlugin.Program.CreateCommands
methode gevonden. Als gevolg hiervan is de <Private>false</Private>
metadata vereist voor de verwijzing naar de assembly met de invoegtoepassingsinterfaces.
Op dezelfde manier is het element <ExcludeAssets>runtime</ExcludeAssets>
ook belangrijk als de PluginBase
verwijst naar andere pakketten. Deze instelling heeft hetzelfde effect als <Private>false</Private>
, maar werkt op pakketverwijzingen die het PluginBase
project of een van de afhankelijkheden ervan kan bevatten.
Nu het HelloPlugin
project is voltooid, moet u het AppWithPlugin
project bijwerken om te weten waar de HelloPlugin
-invoegtoepassing kan worden gevonden. Voeg na de opmerking // Paths to plugins to load
@"HelloPlugin\bin\Debug\net5.0\HelloPlugin.dll"
toe (dit pad kan afwijken op basis van de .NET Core-versie die u gebruikt) als element van de pluginPaths
matrix.
Invoegtoepassing met bibliotheekafhankelijkheden
Bijna alle invoegtoepassingen zijn complexer dan een eenvoudige 'Hallo wereld', en veel invoegtoepassingen hebben afhankelijkheden van andere bibliotheken. De JsonPlugin
- en OldJsonPlugin
-projecten in het voorbeeld tonen twee voorbeelden van invoegtoepassingen met NuGet-pakketafhankelijkheden op Newtonsoft.Json
. Daarom moeten alle invoegtoepassingsprojecten <EnableDynamicLoading>true</EnableDynamicLoading>
toevoegen aan de projecteigenschappen, zodat ze al hun afhankelijkheden naar de uitvoer van dotnet build
kopiëren. Als u de klassebibliotheek met dotnet publish
publiceert, worden ook alle afhankelijkheden naar de publicatie-uitvoer gekopieerd.
Andere voorbeelden in het voorbeeld
De volledige broncode voor deze tutorial vindt u in de dotnet/samples-repository. Het voltooide voorbeeld bevat enkele andere voorbeelden van AssemblyDependencyResolver
gedrag. Het AssemblyDependencyResolver
-object kan bijvoorbeeld ook systeemeigen bibliotheken en gelokaliseerde satellietassembly's in NuGet-pakketten oplossen. De UVPlugin
en FrenchPlugin
in de opslagplaats voor voorbeelden laten deze scenario's zien.
Refereren aan een plug-ininterface vanuit een NuGet-pakket
Stel dat er een app A is met een invoegtoepassingsinterface die is gedefinieerd in het NuGet-pakket met de naam A.PluginBase
. Hoe verwijst u naar het pakket in uw invoegtoepassingsproject? Voor projectverwijzingen heeft het gebruik van de <Private>false</Private>
metagegevens op het element ProjectReference
in het projectbestand verhinderd dat het dll-bestand naar de uitvoer wordt gekopieerd.
Als u naar het A.PluginBase
-pakket wilt verwijzen, wilt u het <PackageReference>
-element in het projectbestand als volgt wijzigen:
<PackageReference Include="A.PluginBase" Version="1.0.0">
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
Dit voorkomt dat de A.PluginBase
assembly's worden gekopieerd naar de uitvoermap van uw invoegtoepassing en zorgt ervoor dat uw invoegtoepassing gebruikmaakt van de A-versie van A.PluginBase
.
Aanbevelingen voor het doelframework van de invoegtoepassing
Omdat het laden van afhankelijkheden van plug-ins gebruikmaakt van het .deps.json-bestand, is er een probleem met betrekking tot het doel-framework van de plug-in. Uw invoegtoepassingen moeten zich specifiek richten op een runtime, zoals .NET 5, in plaats van op een versie van .NET Standard. Het .deps.json-bestand wordt gegenereerd op basis van het framework waarop het project is gericht, en omdat veel .NET Standard-compatibele pakketten referentieassembly's verzenden voor het bouwen op basis van .NET Standard en implementatieassembly's voor specifieke runtimes, ziet de .deps.json mogelijk niet correct implementatieassembly's of kan het de .NET Standard-versie van een assembly pakken in plaats van de .NET Core-versie die u verwacht.
Verwijzingen naar het invoegtoepassingsframework
Op dit moment kunnen invoegtoepassingen geen nieuwe frameworks in het proces introduceren. U kunt bijvoorbeeld geen invoegtoepassing laden die gebruikmaakt van het Microsoft.AspNetCore.App
-framework in een toepassing die alleen gebruikmaakt van het hoofdframework Microsoft.NETCore.App
. De hosttoepassing moet verwijzingen declareren naar alle frameworks die nodig zijn voor invoegtoepassingen.