Een .NET Core-toepassing maken met invoegtoepassingen
In deze zelfstudie leert u hoe u een aangepaste AssemblyLoadContext om invoegtoepassingen te laden maakt. Een AssemblyDependencyResolver wordt gebruikt om de afhankelijkheden van de invoegtoepassing op te lossen. In de zelfstudie worden de afhankelijkheden van de invoegtoepassing correct geïsoleerd van de hostingtoepassing. U leert het volgende:
- Structureer een project om invoegtoepassingen te ondersteunen.
- Maak een aangepaste AssemblyLoadContext om elke invoegtoepassing te laden.
- Gebruik het System.Runtime.Loader.AssemblyDependencyResolver type om toe te staan dat invoegtoepassingen afhankelijkheden hebben.
- Maak invoegtoepassingen die eenvoudig kunnen worden geïmplementeerd door alleen de buildartefacten te kopiëren.
Vereisten
- Installeer de .NET 5 SDK of een nieuwere versie.
Notitie
De voorbeeldcode is gericht op .NET 5, maar alle functies die worden gebruikt, zijn geïntroduceerd in .NET Core 3.0 en zijn sindsdien beschikbaar in alle .NET-releases.
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 bouwen van 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 geraamte 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. We raden u aan een klassenbibliotheek te maken die alle typen bevat die u wilt gebruiken voor de communicatie tussen uw app en invoegtoepassingen. Met deze afdeling kunt u uw invoegtoepassingsinterface als een pakket publiceren zonder dat u uw volledige toepassing hoeft te verzenden.
Voer uit dotnet new classlib -o PluginBase
in de hoofdmap van het project. Voer ook uit dotnet sln add PluginBase/PluginBase.csproj
om het project toe te voegen aan het oplossingsbestand. Verwijder het PluginBase/Class1.cs
bestand en maak een nieuw bestand in de map met de PluginBase
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 alle invoegtoepassingen implementeren.
Nu de ICommand
interface is gedefinieerd, kan het toepassingsproject iets meer worden ingevuld. Voeg een verwijzing uit het AppWithPlugin
project toe aan het PluginBase
project met de dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj
opdracht uit de hoofdmap.
Vervang de // Load commands from plugins
opmerking 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 // Output the loaded commands
opmerking 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 codefragment:
ICommand command = commands.FirstOrDefault(c => c.Name == commandName);
if (command == null)
{
Console.WriteLine("No such command is known.");
return;
}
command.Execute();
Voeg ten slotte statische methoden toe aan de klasse met de Program
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
optie verschillende paden kiezen om assembly's van te laden en het standaardgedrag te overschrijven. De PluginLoadContext
gebruikt een exemplaar van het AssemblyDependencyResolver
type dat is geïntroduceerd in .NET Core 3.0 om assemblynamen om te lossen naar paden. Het AssemblyDependencyResolver
object is samengesteld met het pad naar een .NET-klassebibliotheek. Assembly's en systeemeigen bibliotheken worden omgezet in hun relatieve paden op basis van het bestand .deps.json voor de klassebibliotheek waarvan het pad is doorgegeven aan de AssemblyDependencyResolver
constructor. Met de aangepaste AssemblyLoadContext
kunnen invoegtoepassingen hun eigen afhankelijkheden hebben en kunnen AssemblyDependencyResolver
de afhankelijkheden eenvoudig correct worden geladen.
Nu het AppWithPlugin
project het PluginLoadContext
type 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 voor elke invoegtoepassing een ander PluginLoadContext
exemplaar te gebruiken, kunnen de invoegtoepassingen zonder problemen verschillende of zelfs conflicterende afhankelijkheden hebben.
Eenvoudige invoegtoepassing zonder afhankelijkheden
Ga als volgt te werk in de hoofdmap:
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 een invoegtoepassing. Hiermee worden onder andere alle afhankelijkheden naar de uitvoer van het project gekopieerd. 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. Hiermee wordt aangegeven dat MSBuild PluginBase.dll niet naar de uitvoermap voor HelloPlugin moet kopiëren. Als de PluginBase.dll assembly aanwezig is in de uitvoermap, PluginLoadContext
wordt de assembly daar gevonden en geladen wanneer de HelloPlugin.dll assembly wordt geladen. Op dit moment 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 laadcontext wordt geladen. Omdat de runtime deze twee typen als verschillende typen van verschillende assembly's ziet, vindt de AppWithPlugin.Program.CreateCommands
methode de opdrachten niet. Als gevolg hiervan zijn de <Private>false</Private>
metagegevens vereist voor de verwijzing naar de assembly met de invoegtoepassingsinterfaces.
Op dezelfde manier is het <ExcludeAssets>runtime</ExcludeAssets>
element 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 // Paths to plugins to load
opmerking @"HelloPlugin\bin\Debug\net5.0\HelloPlugin.dll"
(dit pad kan afwijken op basis van de .NET Core-versie die u gebruikt) toe als een element van de pluginPaths
matrix.
Invoegtoepassing met bibliotheekafhankelijkheden
Bijna alle invoegtoepassingen zijn complexer dan een eenvoudige 'Hallo wereld', en veel invoegtoepassingen zijn afhankelijk van andere bibliotheken. De JsonPlugin
projecten en OldJsonPlugin
in het voorbeeld tonen twee voorbeelden van invoegtoepassingen met NuGet-pakketafhankelijkheden op Newtonsoft.Json
. Daarom moeten alle invoegtoepassingsprojecten worden toegevoegd <EnableDynamicLoading>true</EnableDynamicLoading>
aan de projecteigenschappen, zodat ze alle afhankelijkheden kopiëren naar de uitvoer van dotnet build
. 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 zelfstudie vindt u in de opslagplaats dotnet/samples. Het voltooide voorbeeld bevat enkele andere voorbeelden van AssemblyDependencyResolver
gedrag. Het object kan bijvoorbeeld AssemblyDependencyResolver
ook systeemeigen bibliotheken en gelokaliseerde satellietassembly's in NuGet-pakketten oplossen. De UVPlugin
en FrenchPlugin
in de opslagplaats met voorbeelden laten deze scenario's zien.
Verwijzen naar een invoegtoepassingsinterface 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 correct naar het pakket in uw invoegtoepassingsproject? Voor projectverwijzingen voorkwam het gebruik van de <Private>false</Private>
metagegevens voor het ProjectReference
element in het projectbestand dat de DLL naar de uitvoer werd gekopieerd.
Als u correct naar het A.PluginBase
pakket wilt verwijzen, wijzigt u het <PackageReference>
element in het projectbestand in het volgende:
<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 de A-versie van A.PluginBase
gebruikt.
Aanbevelingen voor het doelframework voor invoegtoepassingen
Omdat het laden van invoegtoepassingsafhankelijkheid gebruikmaakt van het bestand .deps.json , is er een gotcha gerelateerd aan het doelframework van de invoegtoepassing. Uw invoegtoepassingen moeten zich richten op een runtime, zoals .NET 5, in plaats van een versie van .NET Standard. Het bestand .deps.json wordt gegenereerd op basis van het framework waarop het project is gericht, en aangezien 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 wordt de .NET Standard-versie van een assembly opgehaald 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 een invoegtoepassing die gebruikmaakt van het Microsoft.AspNetCore.App
framework niet laden in een toepassing die alleen het hoofdframework Microsoft.NETCore.App
gebruikt. De hosttoepassing moet verwijzingen declareren naar alle frameworks die nodig zijn voor invoegtoepassingen.