Condividi tramite


Creare un'applicazione .NET Core con plug-in

Questa esercitazione illustra come creare una AssemblyLoadContext personalizzata per caricare i plugin. Si usa AssemblyDependencyResolver per risolvere le dipendenze del plug-in. L'esercitazione fornisce un contesto di assembly separato per le dipendenze del plug-in, consentendo dipendenze di assembly diverse tra i plug-in e l'applicazione host. Si apprenderà come:

  • Strutturare un progetto per supportare i plug-in.
  • Creare un personalizzato AssemblyLoadContext per caricare ogni plug-in.
  • Usare il System.Runtime.Loader.AssemblyDependencyResolver tipo per consentire ai plug-in di avere dipendenze.
  • Creare plugin che possono essere facilmente distribuiti semplicemente copiando gli artefatti di compilazione.

Nota

Il codice non attendibile non può essere caricato in modo sicuro in un processo .NET attendibile. Per fornire un limite di sicurezza o affidabilità, prendere in considerazione una tecnologia fornita dal sistema operativo o dalla piattaforma di virtualizzazione.

Prerequisiti

  • La versione più recente .NET SDK
  • editor di Visual Studio Code
  • Il DevKit C#

Creare l'applicazione

Il primo passaggio consiste nel creare l'applicazione:

  1. Creare una nuova cartella e in tale cartella eseguire il comando seguente:

    dotnet new console -o AppWithPlugin
    
  2. Per semplificare la compilazione del progetto, creare un file di soluzione di Visual Studio nella stessa cartella. Eseguire il comando seguente:

    dotnet new sln
    
  3. Eseguire il comando seguente per aggiungere il progetto dell'app alla soluzione:

    dotnet sln add AppWithPlugin/AppWithPlugin.csproj
    

A questo punto è possibile compilare lo scheletro 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 del plug-in

Il passaggio successivo nella creazione di un'app con plug-in consiste nel definire l'interfaccia che i plug-in devono implementare. È consigliabile creare una libreria di classi che contenga 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 spedire l'applicazione completa.

Nella cartella radice del progetto eseguire dotnet new classlib -o PluginBase. Esegui anche dotnet sln add PluginBase/PluginBase.csproj per aggiungere il progetto al file di soluzione. Eliminare il PluginBase/Class1.cs file e creare un nuovo file nella PluginBase cartella denominata ICommand.cs con la definizione di interfaccia seguente:

namespace PluginBase
{
    public interface ICommand
    {
        string Name { get; }
        string Description { get; }

        int Execute();
    }
}

Questa ICommand interfaccia è l'interfaccia che tutti i plug-in implementeranno.

Ora che l'interfaccia ICommand è definita, il progetto dell'applicazione può essere compilato un po'di più. Aggiungere un riferimento dal progetto AppWithPlugin al progetto PluginBase utilizzando il comando dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj dalla cartella radice.

Sostituire il // Load commands from plugins commento con il frammento di codice seguente per abilitarlo per caricare i plug-in da 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();

Sostituire quindi il // Output the loaded commands commento con il frammento di codice seguente:

foreach (ICommand command in commands)
{
    Console.WriteLine($"{command.Name}\t - {command.Description}");
}

Sostituire il // Execute the command with the name passed as an argument commento con il frammento di codice seguente:

ICommand command = commands.FirstOrDefault(c => c.Name == commandName);
if (command == null)
{
    Console.WriteLine("No such command is known.");
    return;
}

command.Execute();

Infine, aggiungere metodi statici alla Program classe denominata LoadPlugin e CreateCommands, come illustrato di seguito:

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}");
    }
}

Caricare i plug-in

Ora l'applicazione può correttamente instanziare i comandi dagli assembly plugin già caricati, ma non è ancora in grado di caricare gli assembly del plugin. 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 PluginLoadContext tipo 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 oggetto personalizzato AssemblyLoadContext può scegliere percorsi diversi da cui caricare gli assembly e eseguire l'override del comportamento predefinito. PluginLoadContext usa un'istanza del tipo AssemblyDependencyResolver introdotta in .NET Core 3.0 per tradurre i nomi degli assembly in percorsi. L'oggetto AssemblyDependencyResolver viene costruito con il percorso di 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 AssemblyDependencyResolver costruttore. Il AssemblyLoadContext personalizzato consente ai plug-in di avere le proprie dipendenze e il AssemblyDependencyResolver semplifica il caricamento corretto delle dipendenze.

Ora che il AppWithPlugin progetto ha il PluginLoadContext tipo, aggiornare il Program.LoadPlugin metodo con il corpo seguente:

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)));
}

Usando un'istanza diversa PluginLoadContext 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:

  1. Eseguire il comando seguente per creare un nuovo progetto di libreria di classi denominato HelloPlugin:

    dotnet new classlib -o HelloPlugin
    
  2. Eseguire il comando seguente per aggiungere il progetto alla AppWithPlugin soluzione:

    dotnet sln add HelloPlugin/HelloPlugin.csproj
    
  3. 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;
        }
    }
}

Aprire ora il file HelloPlugin.csproj . Dovrebbe essere simile al 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 un plug-in. Tra le altre cose, verranno copiate tutte le relative dipendenze nell'output del progetto. Per altri dettagli, vedere EnableDynamicLoading.

Tra i <Project> tag aggiungere gli elementi seguenti:

<ItemGroup>
    <ProjectReference Include="..\PluginBase\PluginBase.csproj">
        <Private>false</Private>
        <ExcludeAssets>runtime</ExcludeAssets>
    </ProjectReference>
</ItemGroup>

L'elemento <Private>false</Private> è importante. Ciò 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 troverà l'assembly e lo caricherà quando carica l'assembly HelloPlugin.dll . A questo punto, il HelloPlugin.HelloCommand tipo implementerà l'interfaccia ICommand dal 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, i <Private>false</Private> metadati sono necessari per il riferimento all'assembly contenente le interfacce del 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 di un semplice "Hello World" e molti plug-in hanno dipendenze da altre librerie. I progetti JsonPlugin e OldJsonPlugin nell'esempio mostrano due esempi di plugin con dipendenze da pacchetti NuGet su Newtonsoft.Json. Per questo motivo, tutti i progetti di plug-in devono aggiungere <EnableDynamicLoading>true</EnableDynamicLoading> alle proprietà del progetto in modo tale da copiare tutte le loro dipendenze nell'output di dotnet build. La pubblicazione della libreria di classi con dotnet publish copierà anche tutte le relative dipendenze nell'output di pubblicazione.

Altri esempi nell'esempio

Il codice sorgente completo per questa esercitazione è disponibile nel repository dotnet/samples. L'esempio completato include alcuni altri esempi di AssemblyDependencyResolver comportamento. Ad esempio, l'oggetto AssemblyDependencyResolver può anche risolvere librerie native e assembly satellite localizzati inclusi nei pacchetti NuGet. Il UVPlugin e FrenchPlugin nel repository degli esempi dimostrano questi scenari.

Fare riferimento a un'interfaccia plugin da un pacchetto NuGet

Si supponga che sia presente un'app A con un'interfaccia plug-in definita nel pacchetto NuGet denominato A.PluginBase. Come si fa a fare riferimento correttamente al pacchetto nel progetto di plug-in? Per i riferimenti al progetto, l'uso dei <Private>false</Private> metadati nell'elemento ProjectReference nel file di progetto ha impedito la copia della DLL nell'output.

Per fare riferimento correttamente al A.PluginBase pacchetto, si vuole modificare l'elemento <PackageReference> nel file di progetto nel modo seguente:

<PackageReference Include="A.PluginBase" Version="1.0.0">
    <ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>

Ciò impedisce che gli A.PluginBase assembly vengano copiati nella directory di output del plug-in e garantisce che il plug-in utilizzi la versione A.PluginBase di A.

Raccomandazioni sul framework di destinazione del plug-in

Poiché il caricamento delle dipendenze del plug-in usa il file .deps.json , esiste un gotcha correlato 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é molti pacchetti compatibili con .NET Standard includono assembly di riferimento per la compilazione con .NET Standard e assembly di implementazione per runtime specifici, l' .deps.json potrebbe non visualizzare correttamente gli assembly di implementazione oppure potrebbe acquisire la versione .NET Standard di un assembly anziché la versione di .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.