Creare un'applicazione .NET Core con i plug-in
Questa esercitazione illustra come creare un'istanza personalizzata AssemblyLoadContext per caricare i plug-in. Viene AssemblyDependencyResolver usato per risolvere le dipendenze del plug-in. L'esercitazione isola correttamente le dipendenze del plug-in dall'applicazione host. Nello specifico:
- Strutturare un progetto per il supporto dei plug-in.
- Creare una classe AssemblyLoadContext personalizzata per caricare ogni plug-in.
- Usare il tipo System.Runtime.Loader.AssemblyDependencyResolver per consentire l'uso di plug-in con dipendenze.
- Creare plug-in che possono essere distribuiti con facilità copiando semplicemente gli artefatti di compilazione.
Prerequisiti
- Installare .NET 5 SDK o una versione più recente.
Nota
Il codice di esempio è destinato a .NET 5, ma tutte le funzionalità usate sono state introdotte in .NET Core 3.0 e sono disponibili in tutte le versioni .NET da allora.
Creare l'applicazione
Il primo passaggio consiste nel creare l'applicazione:
Creare una nuova cartella e in tale cartella eseguire il comando seguente:
dotnet new console -o AppWithPlugin
Per semplificare la compilazione del progetto, creare un file di soluzione di Visual Studio nella stessa cartella. Esegui questo comando:
dotnet new sln
Eseguire il comando seguente per aggiungere il progetto dell'app alla soluzione:
dotnet sln add AppWithPlugin/AppWithPlugin.csproj
Ora è possibile compilare la struttura dell'applicazione. Sostituire il codice nel file AppWithPlugin/Program.cs con il codice seguente:
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);
}
}
}
}
Creare le interfacce dei plug-in
Il passaggio successivo della compilazione di un'app con plug-in consiste nel definire l'interfaccia che deve essere implementata dai plug-in. È consigliabile creare una libreria di classi che contiene tutti i tipi che si prevede di usare per la comunicazione tra l'app e i plug-in. Questa divisione consente di pubblicare l'interfaccia del plug-in come pacchetto senza dover distribuire l'applicazione completa.
Nella cartella radice del progetto eseguire dotnet new classlib -o PluginBase
. Eseguire anche dotnet sln add PluginBase/PluginBase.csproj
per aggiungere il progetto al file della soluzione. Eliminare il file PluginBase/Class1.cs
e nella cartella PluginBase
creare un nuovo file denominato ICommand.cs
con la definizione di interfaccia seguente:
namespace PluginBase
{
public interface ICommand
{
string Name { get; }
string Description { get; }
int Execute();
}
}
Questa interfaccia ICommand
è l'interfaccia che verrà implementata da tutti i plug-in.
Dopo aver definito l'interfaccia ICommand
, è possibile compilare ulteriormente il progetto dell'applicazione. Aggiungere un riferimento dal AppWithPlugin
progetto al PluginBase
progetto con il dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj
comando dalla cartella radice.
Sostituire il commento // Load commands from plugins
con il frammento di codice seguente per abilitare il caricamento dei plug-in dai percorsi di file specificati:
string[] pluginPaths = new string[]
{
// Paths to plugins to load.
};
IEnumerable<ICommand> commands = pluginPaths.SelectMany(pluginPath =>
{
Assembly pluginAssembly = LoadPlugin(pluginPath);
return CreateCommands(pluginAssembly);
}).ToList();
Quindi sostituire il commento // Output the loaded commands
con il frammento di codice seguente:
foreach (ICommand command in commands)
{
Console.WriteLine($"{command.Name}\t - {command.Description}");
}
Sostituire il commento // Execute the command with the name passed as an argument
con il frammento seguente:
ICommand command = commands.FirstOrDefault(c => c.Name == commandName);
if (command == null)
{
Console.WriteLine("No such command is known.");
return;
}
command.Execute();
Infine, aggiungere i metodi statici denominati LoadPlugin
e CreateCommands
alla classe Program
come illustrato di seguito:
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}");
}
}
Caricare i plug-in
Ora l'applicazione può caricare e creare correttamente istanze dei comandi dagli assembly plug-in caricati, ma non è ancora in grado di caricare gli assembly del plug-in. Creare un file denominato PluginLoadContext.cs nella cartella AppWithPlugin con il contenuto seguente:
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;
}
}
}
Il tipo PluginLoadContext
deriva da AssemblyLoadContext. Il AssemblyLoadContext
tipo è un tipo speciale nel runtime che consente agli sviluppatori di isolare gli assembly caricati in gruppi diversi per garantire che le versioni degli assembly non siano in conflitto. Inoltre, un tipo AssemblyLoadContext
personalizzato può scegliere percorsi diversi da cui caricare gli assembly ed eseguire l'override del comportamento predefinito. PluginLoadContext
usa un'istanza del tipo AssemblyDependencyResolver
introdotta in .NET Core 3.0 per risolvere i nomi di assembly in percorsi. L'oggetto AssemblyDependencyResolver
è costruito con il percorso per una libreria di classi .NET. Risolve gli assembly e le librerie native nei relativi percorsi in base al file deps.json per la libreria di classi il cui percorso è stato passato al costruttore AssemblyDependencyResolver
. Il tipo AssemblyLoadContext
personalizzato consente ai plug-in di avere proprie dipendenze, mentre l'oggetto AssemblyDependencyResolver
rende più semplice caricare correttamente le dipendenze.
Ora che il progetto AppWithPlugin
ha il tipo PluginLoadContext
, aggiornare il metodo Program.LoadPlugin
con il corpo seguente:
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)));
}
Usando un'istanza PluginLoadContext
diversa per ogni plug-in, i plug-in possono avere dipendenze diverse o persino in conflitto senza problemi.
Plug-in semplice senza dipendenze
Nella cartella radice eseguire le operazioni seguenti:
Eseguire il comando seguente per creare un nuovo progetto di libreria di classi denominato
HelloPlugin
:dotnet new classlib -o HelloPlugin
Eseguire il comando seguente per aggiungere il progetto alla
AppWithPlugin
soluzione:dotnet sln add HelloPlugin/HelloPlugin.csproj
Sostituire il file HelloPlugin/Class1.cs con un file denominato HelloCommand.cs con il contenuto seguente:
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;
}
}
}
A questo punto, aprire il file HelloPlugin.csproj. La voce deve essere simile alla seguente:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
</Project>
Tra i <PropertyGroup>
tag aggiungere l'elemento seguente:
<EnableDynamicLoading>true</EnableDynamicLoading>
Prepara <EnableDynamicLoading>true</EnableDynamicLoading>
il progetto in modo che possa essere usato come plug-in. Tra le altre cose, verranno copiate tutte le relative dipendenze nell'output del progetto. Per informazioni dettagliate, vedere EnableDynamicLoading
.
Tra i due tag <Project>
aggiungere gli elementi seguenti:
<ItemGroup>
<ProjectReference Include="..\PluginBase\PluginBase.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
</ItemGroup>
L'elemento <Private>false</Private>
è importante. Indica a MSBuild di non copiare PluginBase.dll nella directory di output per HelloPlugin. Se l'assembly PluginBase.dll è presente nella directory di output, PluginLoadContext
individuerà l'assembly e lo caricherà durante il caricamento dell'assembly HelloPlugin.dll. A questo punto, il tipo HelloPlugin.HelloCommand
implementerà l'interfaccia ICommand
di PluginBase.dll nella directory di output del progetto HelloPlugin
, non l'interfaccia ICommand
caricata nel contesto di caricamento predefinito. Poiché il runtime vede questi due tipi come tipi diversi da assembly diversi, il AppWithPlugin.Program.CreateCommands
metodo non troverà i comandi. Di conseguenza, saranno necessari i metadati <Private>false</Private>
per il riferimento all'assembly che contiene le interfacce dei plug-in.
Analogamente, l'elemento <ExcludeAssets>runtime</ExcludeAssets>
è importante anche se fa PluginBase
riferimento ad altri pacchetti. Questa impostazione ha lo stesso effetto di <Private>false</Private>
ma funziona sui riferimenti al pacchetto che il PluginBase
progetto o una delle relative dipendenze possono includere.
Ora che il HelloPlugin
progetto è completo, è necessario aggiornare il AppWithPlugin
progetto per sapere dove è possibile trovare il HelloPlugin
plug-in. Dopo il // Paths to plugins to load
commento, aggiungere @"HelloPlugin\bin\Debug\net5.0\HelloPlugin.dll"
(questo percorso potrebbe essere diverso in base alla versione di .NET Core usata) come elemento della pluginPaths
matrice.
Plug-in con dipendenze della libreria
Quasi tutti i plug-in sono più complessi rispetto a un semplice "Hello World" e molti plug-in hanno dipendenze in altre librerie. I JsonPlugin
progetti e OldJsonPlugin
nell'esempio mostrano due esempi di plug-in con dipendenze del pacchetto NuGet da Newtonsoft.Json
. Per questo motivo, tutti i progetti di plug-in devono essere aggiunti <EnableDynamicLoading>true</EnableDynamicLoading>
alle proprietà del progetto in modo che copiano tutte le relative dipendenze nell'output di dotnet build
. La pubblicazione della libreria di classi con dotnet publish
copia anche tutte le relative dipendenze nell'output di pubblicazione.
Altri esempi nell'esempio
Il codice sorgente completo per questa esercitazione è reperibile nel repository dotnet/samples. L'esempio completato include alcuni altri esempi del comportamento di AssemblyDependencyResolver
. Ad esempio, l'oggetto AssemblyDependencyResolver
può anche risolvere le librerie native nonché gli assembly satellite localizzati inclusi nei pacchetti NuGet. UVPlugin
e FrenchPlugin
nel repository degli esempi illustrano questi scenari.
Fare riferimento a un'interfaccia del plug-in da un pacchetto NuGet
Si supponga che sia presente un'app A con un'interfaccia di plug-in definita nel pacchetto NuGet denominato A.PluginBase
. Come fare riferimento correttamente al pacchetto nel progetto di plug-in? Per i riferimenti al progetto, l'uso dei metadati <Private>false</Private>
nell'elemento ProjectReference
nel file di progetto ha impedito la copia della dll nell'output.
Per fare riferimento correttamente al pacchetto A.PluginBase
, si modifica l'elemento <PackageReference>
nel file di progetto come segue:
<PackageReference Include="A.PluginBase" Version="1.0.0">
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
In questo modo si impedisce che gli assembly A.PluginBase
vengano copiati nella directory di output del plug-in e si garantisce che il plug-in usi la versione A di A.PluginBase
.
Consigli sul framework di destinazione del plug-in
Poiché il caricamento delle dipendenze del plug-in usa il file deps.json, tenere presente la raccomandazione relativa al framework di destinazione del plug-in. In particolare, i plug-in devono essere destinati a un runtime, ad esempio .NET 5, anziché una versione di .NET Standard. Il file .deps.json viene generato in base al framework di destinazione del progetto e poiché numerosi pacchetti compatibili con .NET Standard offrono assembly di riferimento per la compilazione in .NET Standard e assembly di implementazione per runtime specifici, è possibile che .deps.json non consideri correttamente gli assembly di implementazione oppure ottenga la versione .NET Standard di un assembly anziché la versione .NET Core prevista.
Riferimenti al framework del plug-in
Attualmente, i plug-in non possono introdurre nuovi framework nel processo. Ad esempio, non è possibile caricare un plug-in che usa il Microsoft.AspNetCore.App
framework in un'applicazione che usa solo il framework radice Microsoft.NETCore.App
. L'applicazione host deve dichiarare riferimenti a tutti i framework necessari per i plug-in.