Anmerkung
Der Zugriff auf diese Seite erfordert eine Genehmigung. Du kannst versuchen, dich anzumelden oder die Verzeichnisse zu wechseln.
Der Zugriff auf diese Seite erfordert eine Genehmigung. Du kannst versuchen , die Verzeichnisse zu wechseln.
In diesem Tutorial erfahren Sie, wie Sie eine benutzerdefinierte AssemblyLoadContext zum Laden von Plug-Ins erstellen. Ein AssemblyDependencyResolver wird verwendet, um die Abhängigkeiten des Plug-Ins aufzulösen. Das Lernprogramm bietet einen separaten Assemblykontext für die Abhängigkeiten des Plug-Ins, sodass unterschiedliche Assemblyabhängigkeiten zwischen den Plug-Ins und der Hostinganwendung möglich sind. Hier erfahren Sie, wie Sie:
- Strukturieren Sie ein Projekt, um Plug-Ins zu unterstützen.
- Erstellen Sie eine benutzerdefinierte AssemblyLoadContext, um jedes Plugin zu laden.
- Verwenden Sie den System.Runtime.Loader.AssemblyDependencyResolver Typ, um Plug-Ins abhängigkeiten zu ermöglichen.
- Erstellen Sie Plug-Ins, die sich problemlos bereitstellen lassen, indem Sie die Build-Artefakte kopieren.
Hinweis
Nicht vertrauenswürdiger Code kann nicht sicher in einen vertrauenswürdigen .NET-Prozess geladen werden. Um eine Sicherheits- oder Zuverlässigkeitsgrenze bereitzustellen, sollten Sie eine Technologie in Betracht ziehen, die von Ihrem Betriebssystem oder Ihrer Virtualisierungsplattform bereitgestellt wird.
Voraussetzungen
- Das neueste .NET SDK
- Visual Studio Code-Editor
- Das C# DevKit
Erstellen der Anwendung
Der erste Schritt besteht darin, die Anwendung zu erstellen:
Erstellen Sie einen neuen Ordner, und führen Sie in diesem Ordner den folgenden Befehl aus:
dotnet new console -o AppWithPluginUm das Erstellen des Projekts zu vereinfachen, erstellen Sie eine Visual Studio-Projektmappendatei im selben Ordner. Führen Sie den folgenden Befehl aus:
dotnet new slnFühren Sie den folgenden Befehl aus, um das App-Projekt der Projektmappe hinzuzufügen:
dotnet sln add AppWithPlugin/AppWithPlugin.csproj
Jetzt können wir das Skelett unserer Anwendung ausfüllen. Ersetzen Sie den Code in der Datei "AppWithPlugin/Program.cs " durch den folgenden 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} --");
// Skip command-line switches/flags (arguments starting with '/' or '-')
if (commandName.StartsWith("/") || commandName.StartsWith("-"))
{
Console.WriteLine($"Skipping command-line flag: {commandName}");
continue;
}
// Execute the command with the name passed as an argument.
Console.WriteLine();
}
}
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}
}
Erstellen der Plug-In-Schnittstellen
Der nächste Schritt beim Erstellen einer App mit Plug-Ins ist die Definition der Schnittstelle, die die Plug-Ins implementieren müssen. Es wird empfohlen, eine Klassenbibliothek zu erstellen, die alle Typen enthält, die Sie für die Kommunikation zwischen Ihrer App und Plug-Ins verwenden möchten. Mit dieser Division können Sie Ihre Plug-In-Schnittstelle als Paket veröffentlichen, ohne Ihre vollständige Anwendung versenden zu müssen.
Führen Sie im Stammordner des Projekts den Befehl dotnet new classlib -o PluginBase aus. Führen Sie außerdem den Befehl aus dotnet sln add PluginBase/PluginBase.csproj , um das Projekt der Projektmappendatei hinzuzufügen. Löschen Sie die PluginBase/Class1.cs Datei, und erstellen Sie eine neue Datei im PluginBase Ordner mit ICommand.cs der folgenden Schnittstellendefinition:
namespace PluginBase
{
public interface ICommand
{
string Name { get; }
string Description { get; }
int Execute();
}
}
Diese ICommand Schnittstelle ist die Schnittstelle, die alle Plug-Ins implementieren.
Nachdem die ICommand Schnittstelle definiert ist, kann das Anwendungsprojekt weiter ausgearbeitet werden. Fügen Sie mithilfe des dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj Befehls aus dem Stammordner einen Verweis von dem AppWithPlugin Projekt zu dem PluginBase Projekt hinzu.
Ersetzen Sie den // Load commands from plugins Kommentar durch den folgenden Codeausschnitt, um es zu ermöglichen, Plug-Ins aus bestimmten Dateipfaden zu laden:
string[] pluginPaths = new string[]
{
// Paths to plugins to load.
};
IEnumerable<ICommand> commands = pluginPaths.SelectMany(pluginPath =>
{
Assembly pluginAssembly = LoadPlugin(pluginPath);
return CreateCommands(pluginAssembly);
}).ToList();
Ersetzen Sie dann den // Output the loaded commands Kommentar durch den folgenden Codeausschnitt:
foreach (ICommand command in commands)
{
Console.WriteLine($"{command.Name}\t - {command.Description}");
}
Ersetzen Sie den // Execute the command with the name passed as an argument Kommentar durch den folgenden Codeausschnitt:
ICommand command = commands.FirstOrDefault(c => c.Name == commandName);
if (command == null)
{
Console.WriteLine($"No such command is known: {commandName}");
continue;
}
command.Execute();
Und schließlich fügen Sie statische Methoden zur Program Klasse namens LoadPlugin und CreateCommands, wie hier gezeigt:
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}");
}
}
Laden von Plug-Ins
Jetzt kann die Anwendung Befehle aus geladenen Plug-In-Assemblys ordnungsgemäß laden und instanziieren, aber die Plug-In-Assemblys können weiterhin nicht geladen werden. Erstellen Sie eine Datei namens PluginLoadContext.cs im Ordner "AppWithPlugin " mit dem folgenden Inhalt:
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;
}
}
}
Der PluginLoadContext-Typ leitet sich von AssemblyLoadContext ab. Der AssemblyLoadContext Typ ist ein spezieller Typ in der Laufzeit, mit dem Entwickler geladene Assemblys in verschiedene Gruppen isolieren können, um sicherzustellen, dass Assemblyversionen keinen Konflikt verursachen. Darüber hinaus kann eine benutzerdefinierte Komponente AssemblyLoadContext verschiedene Pfade zum Laden von Assemblies wählen und das Standardverhalten überschreiben. Die PluginLoadContext verwendet eine Instanz des Typs AssemblyDependencyResolver, der in .NET Core 3.0 eingeführt wurde, um Assemblynamen in Pfade aufzulösen. Das AssemblyDependencyResolver Objekt wird mit dem Pfad zu einer .NET-Klassenbibliothek erstellt. Es löst Assemblys und native Bibliotheken zu ihren relativen Pfaden basierend auf der .deps.json Datei für die Klassenbibliothek auf, deren Pfad an den AssemblyDependencyResolver Konstruktor übergeben wurde. Die angepassten AssemblyLoadContext ermöglichen Plugins, ihre eigenen Abhängigkeiten zu haben, und die AssemblyDependencyResolver erleichtert das korrekte Laden der Abhängigkeiten.
Nachdem das AppWithPlugin Projekt den PluginLoadContext Typ hat, aktualisieren Sie die Program.LoadPlugin Methode mit dem folgenden Text:
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)));
}
Durch die Verwendung einer unterschiedlichen PluginLoadContext Instanz für jedes Plug-In können die Plug-Ins unterschiedliche oder sogar widersprüchliche Abhängigkeiten haben, ohne dass Probleme auftreten.
Einfaches Plug-In ohne Abhängigkeiten
Gehen Sie im Stammordner wie folgt vor:
Führen Sie den folgenden Befehl aus, um ein neues Klassenbibliotheksprojekt mit dem Namen zu
HelloPluginerstellen:dotnet new classlib -o HelloPluginFühren Sie den folgenden Befehl aus, um das Projekt der
AppWithPluginProjektmappe hinzuzufügen:dotnet sln add HelloPlugin/HelloPlugin.csprojErsetzen Sie die Datei "HelloPlugin/Class1.cs " durch eine Datei mit dem Namen HelloCommand.cs durch den folgenden Inhalt:
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;
}
}
}
Öffnen Sie nun die Datei "HelloPlugin.csproj ". Es sollte etwa wie folgt aussehen:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
Fügen Sie zwischen den <PropertyGroup> Tags das folgende Element hinzu:
<EnableDynamicLoading>true</EnableDynamicLoading>
Das <EnableDynamicLoading>true</EnableDynamicLoading> Projekt wird vorbereitet, damit es als Plug-In verwendet werden kann. Dabei werden unter anderem alle Abhängigkeiten in das Ausgabeergebnis des Projekts kopiert. Weitere Informationen finden Sie unter EnableDynamicLoading.
Fügen Sie zwischen den <Project> Tags die folgenden Elemente hinzu:
<ItemGroup>
<ProjectReference Include="..\PluginBase\PluginBase.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
</ItemGroup>
Das <Private>false</Private> Element ist wichtig. Dadurch wird MSBuild aufgefordert, PluginBase.dll nicht in das Ausgabeverzeichnis für HelloPlugin zu kopieren. Wenn die PluginBase.dll Assembly im Ausgabeverzeichnis vorhanden ist, wird die Assembly dort gefunden und geladen, PluginLoadContext wenn sie die HelloPlugin.dll Assembly lädt. Zu diesem Zeitpunkt implementiert der HelloPlugin.HelloCommand Typ die ICommand Schnittstelle aus der PluginBase.dll, die sich im Ausgabeverzeichnis des HelloPlugin Projekts befindet, und nicht die ICommand Schnittstelle, die in den Standardladekontext geladen wird. Da die Laufzeit diese beiden Typen als unterschiedliche Typen von unterschiedlichen Assemblys sieht, findet die AppWithPlugin.Program.CreateCommands Methode die Befehle nicht. Daher sind die <Private>false</Private> Metadaten für den Verweis auf die Assembly erforderlich, die die Plug-In-Schnittstellen enthält.
Ebenso ist das <ExcludeAssets>runtime</ExcludeAssets> Element wichtig, wenn PluginBase auf andere Pakete verweist. Diese Einstellung hat die gleiche Auswirkung wie <Private>false</Private>, wirkt aber auf Paketverweise, die das PluginBase Projekt oder eine seiner Abhängigkeiten möglicherweise enthalten könnte.
Nachdem das HelloPlugin Projekt abgeschlossen ist, sollten Sie das AppWithPlugin Projekt aktualisieren, um zu wissen, wo das HelloPlugin Plug-In gefunden werden kann. Fügen Sie // Paths to plugins to load nach dem @"HelloPlugin\bin\Debug\net10.0\HelloPlugin.dll" Kommentar (dieser Pfad kann je nach verwendeter .NET Core-Version anders sein) als Element des pluginPaths Arrays.
Plug-In mit Bibliotheksabhängigkeiten
Fast alle Plug-Ins sind komplexer als eine einfache "Hello World", und viele Plug-Ins haben Abhängigkeiten von anderen Bibliotheken. Die Projekte JsonPlugin und OldJsonPlugin im Beispiel zeigen zwei Beispiele für Plugins mit NuGet-Paketabhängigkeiten bei Newtonsoft.Json. Aus diesem Grund sollten alle Plug-In-Projekte <EnableDynamicLoading>true</EnableDynamicLoading> zu den Projekteigenschaften hinzufügen, sodass sie alle ihre Abhängigkeiten in die Ausgabe von dotnet build kopieren. Durch das Veröffentlichen der Klassenbibliothek dotnet publish werden auch alle Abhängigkeiten in die Veröffentlichungsausgabe kopiert.
Weitere Beispiele im Beispiel
Der vollständige Quellcode für dieses Lernprogramm finden Sie im Repository dotnet/samples. Das fertige Beispiel enthält einige weitere Beispiele für AssemblyDependencyResolver Verhalten. Das AssemblyDependencyResolver Objekt kann z. B. auch systemeigene Bibliotheken sowie lokalisierte Satellitenassemblys auflösen, die in NuGet-Paketen enthalten sind. Diese Szenarien werden im UVPlugin Beispiel-Repository FrenchPlugin veranschaulicht.
Verweisen auf eine Plug-In-Schnittstelle aus einem NuGet-Paket
Angenommen, es gibt eine App A mit einer Plug-In-Schnittstelle, die im NuGet-Paket mit dem Namen A.PluginBasedefiniert ist. Wie verweisen Sie im Plug-In-Projekt richtig auf das Paket? Bei Projektverweisen verhinderte die Verwendung der <Private>false</Private> Metadaten für das ProjectReference Element in der Projektdatei, dass die DLL in die Ausgabe kopiert wird.
Um korrekt auf das A.PluginBase Paket zu verweisen, möchten Sie das <PackageReference> Element in der Projektdatei wie folgt ändern:
<PackageReference Include="A.PluginBase" Version="1.0.0">
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
Dadurch wird verhindert, dass die A.PluginBase Assemblies in das Ausgabeverzeichnis Ihres Plugins kopiert werden, und es wird sichergestellt, dass Ihr Plugin Version A von A.PluginBase verwendet.
Empfehlungen für Plugin-Zielframeworks
Da das Laden von Plug-In-Abhängigkeiten die .deps.json Datei verwendet, gibt es eine Gotcha im Zusammenhang mit dem Zielframework des Plug-Ins. Insbesondere sollten Ihre Plug-Ins auf eine Laufzeit abzielen, z. B. .NET 10, anstelle einer Version von .NET Standard. Die .deps.json Datei wird basierend auf dem Framework generiert, auf dem das Projekt ausgerichtet ist, und da viele .NET Standard-kompatible Pakete Referenzassemblys für das Erstellen von .NET Standard- und Implementierungsassemblys für bestimmte Laufzeiten liefern, werden vom .deps.json möglicherweise keine Implementierungsassemblys richtig angezeigt, oder es kann die .NET Standard-Version einer Assembly anstelle der erwarteten .NET Core-Version abrufen.
Plug-In-Framework-Referenzen
Derzeit können Plug-Ins keine neuen Frameworks in den Prozess einführen. Sie können z. B. kein Plug-In laden, das das Microsoft.AspNetCore.App Framework verwendet, in eine Anwendung, die nur das Stammframework Microsoft.NETCore.App verwendet. Die Hostanwendung muss Verweise auf alle Frameworks deklarieren, die von Plug-Ins benötigt werden.