Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Ce tutoriel vous montre comment créer un AssemblyLoadContext personnalisé pour charger des plug-ins. Un AssemblyDependencyResolver est utilisé pour résoudre les dépendances du plugin. Le tutoriel fournit un contexte d’assembly distinct pour les dépendances du plug-in, ce qui permet de distinguer les dépendances d’assembly entre les plug-ins et l’application d’hébergement. Vous allez apprendre à :
- Structurez un projet pour prendre en charge les plug-ins.
- Créez un module personnalisé AssemblyLoadContext pour charger chaque plug-in.
- Utilisez le System.Runtime.Loader.AssemblyDependencyResolver type pour autoriser les plug-ins à avoir des dépendances.
- Créez des plug-ins qui peuvent être facilement déployés en copiant simplement les artefacts de build.
Note
Le code non approuvé ne peut pas être chargé en toute sécurité dans un processus .NET approuvé. Pour fournir une limite de sécurité ou de fiabilité, envisagez une technologie fournie par votre système d’exploitation ou votre plateforme de virtualisation.
Prerequisites
- La dernière version du SDK .NET
- Éditeur de code Visual Studio
- Le DevKit C#
Créer l’application
La première étape consiste à créer l’application :
Créez un dossier et, dans ce dossier, exécutez la commande suivante :
dotnet new console -o AppWithPluginPour faciliter la création du projet, créez un fichier de solution Visual Studio dans le même dossier. Exécutez la commande suivante:
dotnet new slnExécutez la commande suivante pour ajouter le projet d’application à la solution :
dotnet sln add AppWithPlugin/AppWithPlugin.csproj
Maintenant, nous pouvons remplir le squelette de notre application. Remplacez le code dans le fichier AppWithPlugin/Program.cs par le code suivant :
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);
}
}
}
}
Créer les interfaces de plug-in
L’étape suivante de la création d’une application avec des plug-ins consiste à définir l’interface que les plug-ins doivent implémenter. Nous vous suggérons de créer une bibliothèque de classes qui contient tous les types que vous envisagez d’utiliser pour communiquer entre votre application et les plug-ins. Cette division vous permet de publier votre interface de plug-in en tant que package sans avoir à expédier votre application complète.
Dans le dossier racine du projet, exécutez dotnet new classlib -o PluginBase. Exécutez dotnet sln add PluginBase/PluginBase.csproj également pour ajouter le projet au fichier solution. Supprimez le PluginBase/Class1.cs fichier et créez un fichier dans le PluginBase dossier nommé ICommand.cs avec la définition d’interface suivante :
namespace PluginBase
{
public interface ICommand
{
string Name { get; }
string Description { get; }
int Execute();
}
}
Cette ICommand interface est l’interface que tous les plug-ins implémentent.
Maintenant que l’interface ICommand est définie, le projet d’application peut être complété un peu plus. Ajoutez une référence du projet AppWithPlugin au projet PluginBase avec la commande dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj à partir du dossier racine.
Remplacez le commentaire par l’extrait // Load commands from plugins de code suivant pour lui permettre de charger des plug-ins à partir de chemins de fichier donnés :
string[] pluginPaths = new string[]
{
// Paths to plugins to load.
};
IEnumerable<ICommand> commands = pluginPaths.SelectMany(pluginPath =>
{
Assembly pluginAssembly = LoadPlugin(pluginPath);
return CreateCommands(pluginAssembly);
}).ToList();
Remplacez ensuite le // Output the loaded commands commentaire par l’extrait de code suivant :
foreach (ICommand command in commands)
{
Console.WriteLine($"{command.Name}\t - {command.Description}");
}
Remplacez le commentaire // Execute the command with the name passed as an argument par l’extrait de code suivant :
ICommand command = commands.FirstOrDefault(c => c.Name == commandName);
if (command == null)
{
Console.WriteLine($"No such command is known: {commandName}");
continue;
}
command.Execute();
Enfin, ajoutez des méthodes statiques à la Program classe nommée LoadPlugin et CreateCommands, comme illustré ici :
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}");
}
}
Charger des plug-ins
À présent, l’application peut charger et instancier correctement des commandes à partir d’assemblys de plug-in chargés, mais elle n’est toujours pas en mesure de charger les assemblys de plug-in. Créez un fichier nommé PluginLoadContext.cs dans le dossier AppWithPlugin avec le contenu suivant :
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;
}
}
}
Le PluginLoadContext type dérive de AssemblyLoadContext. Le AssemblyLoadContext type est un type spécial dans le runtime qui permet aux développeurs d’isoler les assemblys chargés dans différents groupes pour s’assurer que les versions d’assembly ne sont pas en conflit. En outre, un AssemblyLoadContext personnalisé peut choisir différents chemins d'accès pour charger des assemblages et remplacer le comportement par défaut. Le PluginLoadContext utilise une instance du type AssemblyDependencyResolver introduit par .NET Core 3.0 pour résoudre les noms d’assembly vers des chemins d’accès. L’objet AssemblyDependencyResolver est construit avec le chemin d’accès à une bibliothèque de classes .NET. Il résout les assemblages et les bibliothèques système à leurs chemins relatifs sur la base du fichier .deps.json de la bibliothèque de classes dont le chemin a été passé au AssemblyDependencyResolver constructeur. La fonctionnalité personnalisée AssemblyLoadContext permet aux plug-ins d’avoir leurs propres dépendances et facilite AssemblyDependencyResolver le chargement correct des dépendances.
Maintenant que le AppWithPlugin projet a le PluginLoadContext type, mettez à jour la Program.LoadPlugin méthode avec le corps suivant :
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)));
}
En utilisant une instance différente PluginLoadContext pour chaque plug-in, les plug-ins peuvent avoir des dépendances différentes ou même en conflit sans problème.
Plug-in simple sans dépendances
De retour dans le dossier racine, procédez comme suit :
Exécutez la commande suivante pour créer un projet de bibliothèque de classes nommé
HelloPlugin:dotnet new classlib -o HelloPluginExécutez la commande suivante pour ajouter le projet à la
AppWithPluginsolution :dotnet sln add HelloPlugin/HelloPlugin.csprojRemplacez le fichier HelloPlugin/Class1.cs par un fichier nommé HelloCommand.cs par le contenu suivant :
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;
}
}
}
Ouvrez maintenant le fichier HelloPlugin.csproj . Il doit ressembler à ce qui suit :
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
Entre les <PropertyGroup> balises, ajoutez l’élément suivant :
<EnableDynamicLoading>true</EnableDynamicLoading>
Le <EnableDynamicLoading>true</EnableDynamicLoading> prépare le projet afin qu’il puisse être utilisé comme plugin. Entre autres choses, cela copie toutes ses dépendances dans la sortie du projet. Pour plus d’informations, consultez EnableDynamicLoading.
Entre les <Project> balises, ajoutez les éléments suivants :
<ItemGroup>
<ProjectReference Include="..\PluginBase\PluginBase.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
</ItemGroup>
L’élément <Private>false</Private> est important. Cela indique à MSBuild de ne pas copier PluginBase.dll dans le répertoire de sortie pour HelloPlugin. Si l’assembly PluginBase.dll est présent dans le répertoire de sortie, PluginLoadContext recherche l’assembly là-bas et le charge lorsqu’il charge l’assembly HelloPlugin.dll . À ce stade, le HelloPlugin.HelloCommand type implémente l’interface ICommand à partir du PluginBase.dll dans le répertoire de sortie du HelloPlugin projet, et non l’interface ICommand chargée dans le contexte de chargement par défaut. Étant donné que le runtime voit ces deux types comme des types différents à partir d’assemblys différents, la AppWithPlugin.Program.CreateCommands méthode ne trouve pas les commandes. Par conséquent, les <Private>false</Private> métadonnées sont requises pour la référence à l’assembly contenant les interfaces de plug-in.
De même, l’élément <ExcludeAssets>runtime</ExcludeAssets> est également important si des paquets font référence à PluginBase. Ce paramètre a le même effet que <Private>false</Private>, mais fonctionne sur les références de package que le projet PluginBase ou l’une de ses dépendances peuvent inclure.
Maintenant que le HelloPlugin projet est terminé, vous devez mettre à jour le AppWithPlugin projet pour savoir où se trouve le HelloPlugin plug-in. Après le // Paths to plugins to load commentaire, ajoutez @"HelloPlugin\bin\Debug\net10.0\HelloPlugin.dll" (ce chemin d’accès peut être différent en fonction de la version de .NET Core que vous utilisez) comme élément du pluginPaths tableau.
Plug-in avec dépendances de bibliothèque
Presque tous les plug-ins sont plus complexes qu’un simple « Hello World », et de nombreux plug-ins ont des dépendances sur d’autres bibliothèques. Les projets JsonPlugin et OldJsonPlugin de l’exemple montrent deux exemples de plug-ins avec des dépendances de packages NuGet envers Newtonsoft.Json. En raison de cela, tous les projets de plug-in doivent ajouter <EnableDynamicLoading>true</EnableDynamicLoading> aux propriétés du projet afin qu’ils copient toutes leurs dépendances dans la sortie de dotnet build. La publication de la bibliothèque de classes avec dotnet publish copiera également toutes ses dépendances dans la sortie de publication.
Autres exemples dans l’exemple
Le code source complet de ce didacticiel se trouve dans le référentiel dotnet/samples. L’exemple terminé inclut quelques autres exemples de AssemblyDependencyResolver comportement. Par exemple, l’objet AssemblyDependencyResolver peut également résoudre les bibliothèques natives ainsi que les assemblies satellites localisés inclus dans les packages NuGet. Le UVPlugin et le FrenchPlugin dans le référentiel d’exemples démontrent ces scénarios.
Référencer une interface de plug-in à partir d’un package NuGet
Supposons qu’il existe une application A qui a une interface de plug-in définie dans le package NuGet nommé A.PluginBase. Comment référencez-vous correctement le package dans votre projet de plug-in ? Pour les références de projet, l’utilisation des <Private>false</Private> métadonnées sur l’élément ProjectReference du fichier projet a empêché la copie de la dll dans la sortie.
Pour référencer correctement le A.PluginBase package, vous souhaitez modifier l’élément <PackageReference> du fichier projet en procédant comme suit :
<PackageReference Include="A.PluginBase" Version="1.0.0">
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
Cela empêche les A.PluginBase assemblages d'être copiés vers le répertoire de destination de votre plug-in et garantit que votre plug-in utilisera la version d’A de A.PluginBase.
Recommandations relatives au framework cible du plug-in
Étant donné que le chargement des dépendances de plug-in utilise le fichier .deps.json, il existe un problème lié au framework cible du plug-in. Plus précisément, vos plug-ins doivent cibler un runtime, tel que .NET 10, au lieu d’une version de .NET Standard. Le fichier .deps.json est généré en fonction de l’infrastructure cible du projet et, étant donné que de nombreux packages compatibles avec .NET Standard permettent de créer des assemblys de référence sur .NET Standard et d’implémenter des assemblys pour des runtimes spécifiques, les .deps.json peuvent ne pas voir correctement les assemblys d’implémentation, ou il peut saisir la version .NET Standard d’un assembly au lieu de la version .NET Core attendue.
Références du framework de plug-in
Actuellement, les plug-ins ne peuvent pas introduire de nouvelles infrastructures dans le processus. Par exemple, vous ne pouvez pas charger un plug-in qui utilise l’infrastructure Microsoft.AspNetCore.App dans une application qui utilise uniquement l’infrastructure racine Microsoft.NETCore.App . L’application hôte doit déclarer des références à toutes les infrastructures nécessaires par les plug-ins.