Delen via


Een .NET Core-toepassing maken met invoegtoepassingen

In deze handleiding leert u hoe u een aangepaste AssemblyLoadContext maakt om plug-ins te laden. Een AssemblyDependencyResolver wordt gebruikt om de afhankelijkheden van de plug-in op te lossen. De zelfstudie biedt een aparte assembly-omgeving voor de afhankelijkheden van de invoegtoepassing, waardoor verschillende assembly-afhankelijkheden tussen de invoegtoepassingen en de host-toepassing mogelijk worden gemaakt. U leert het volgende:

  • Structureer een project ter ondersteuning van invoegtoepassingen.
  • Maak een aangepaste AssemblyLoadContext om elke plug-in te laden.
  • Gebruik het type System.Runtime.Loader.AssemblyDependencyResolver om toe te staan dat invoegtoepassingen afhankelijkheden hebben.
  • Ontwerpinvoegtoepassingen die eenvoudig kunnen worden geïmplementeerd door alleen de buildartefacten te kopiëren.

Notitie

Niet-vertrouwde code kan niet veilig worden geladen in een vertrouwd .NET-proces. Als u een beveiligings- of betrouwbaarheidsgrens wilt bieden, kunt u een technologie overwegen die wordt geleverd door uw besturingssysteem of virtualisatieplatform.

Benodigdheden

De toepassing maken

De eerste stap is het maken van de toepassing:

  1. Maak een nieuwe map en voer in die map de volgende opdracht uit:

    dotnet new console -o AppWithPlugin
    
  2. Als u het project eenvoudiger wilt maken, maakt u een Visual Studio-oplossingsbestand in dezelfde map. Voer de volgende opdracht uit:

    dotnet new sln
    
  3. 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 skelet 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. U wordt aangeraden een klassebibliotheek te maken die alle typen bevat die u wilt gebruiken voor de communicatie tussen uw app en invoegtoepassingen. Met deze divisie kunt u uw invoegtoepassingsinterface publiceren als een pakket zonder dat u uw volledige toepassing hoeft te verzenden.

Voer dotnet new classlib -o PluginBaseuit in de hoofdmap van het project. Voer ook dotnet sln add PluginBase/PluginBase.csproj uit om het project toe te voegen aan het oplossingsbestand. Verwijder het PluginBase/Class1.cs bestand en maak een nieuw bestand in de map PluginBase met de 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 door alle invoegtoepassingen wordt geïmplementeerd.

Nu de ICommand-interface is gedefinieerd, kan het toepassingsproject iets meer worden ingevuld. Voeg een verwijzing van het AppWithPlugin-project toe aan het PluginBase-project met de dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj-opdracht vanuit de hoofdmap.

Vervang de opmerking // Load commands from plugins 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 opmerking // Output the loaded commands 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 fragment:

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

command.Execute();

Voeg tot slot statische methoden toe aan de klasse Program met de 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 verschillende paden kiezen om assembly's te laden en het standaardgedrag te overschrijven. De PluginLoadContext maakt gebruik van een exemplaar van het AssemblyDependencyResolver type dat is geïntroduceerd in .NET Core 3.0 om assemblynamen om te zetten in paden. Het AssemblyDependencyResolver-object wordt geconstrueerd met het pad naar een .NET-klassebibliotheek. Hiermee worden assembly's en systeemeigen bibliotheken omgezet in hun relatieve paden op basis van het .deps.json-bestand voor de klassebibliotheek waarvan het pad is doorgegeven aan de AssemblyDependencyResolver constructor. Met de aangepaste AssemblyLoadContext kunnen invoegtoepassingen hun eigen afhankelijkheden hebben en de AssemblyDependencyResolver maakt het eenvoudig om de afhankelijkheden correct te laden.

Nu het AppWithPlugin project het type PluginLoadContext 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 een ander PluginLoadContext exemplaar voor elke invoegtoepassing te gebruiken, kunnen de invoegtoepassingen verschillende of zelfs conflicterende afhankelijkheden hebben zonder probleem.

Eenvoudige invoegtoepassing zonder afhankelijkheden

Terug in de hoofdmap, doe het volgende:

  1. Voer de volgende opdracht uit om een nieuw klassebibliotheekproject met de naam HelloPluginte maken:

    dotnet new classlib -o HelloPlugin
    
  2. Voer de volgende opdracht uit om het project toe te voegen aan de AppWithPlugin-oplossing:

    dotnet sln add HelloPlugin/HelloPlugin.csproj
    
  3. 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 invoegtoepassing. Dit kopieert onder andere alle afhankelijkheden naar de uitvoer van het project. Zie EnableDynamicLoadingvoor 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. Dit vertelt MSBuild dat PluginBase.dll niet naar de uitvoermap voor HelloPlugin moet worden gekopieerd. Als de PluginBase.dll assembly aanwezig is in de uitvoermap, vindt PluginLoadContext de assembly daar en laadt deze wanneer de HelloPlugin.dll assembly wordt geladen. Op dit punt 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 laadsituatie wordt geladen. Omdat de runtime deze twee typen ziet als verschillende typen van verschillende assembly's, worden de opdrachten niet door de AppWithPlugin.Program.CreateCommands methode gevonden. Als gevolg hiervan is de <Private>false</Private> metadata vereist voor de verwijzing naar de assembly met de invoegtoepassingsinterfaces.

Op dezelfde manier is het element <ExcludeAssets>runtime</ExcludeAssets> 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 opmerking // Paths to plugins to load@"HelloPlugin\bin\Debug\net5.0\HelloPlugin.dll" toe (dit pad kan afwijken op basis van de .NET Core-versie die u gebruikt) als element van de pluginPaths matrix.

Invoegtoepassing met bibliotheekafhankelijkheden

Bijna alle invoegtoepassingen zijn complexer dan een eenvoudige 'Hallo wereld', en veel invoegtoepassingen hebben afhankelijkheden van andere bibliotheken. De JsonPlugin- en OldJsonPlugin-projecten in het voorbeeld tonen twee voorbeelden van invoegtoepassingen met NuGet-pakketafhankelijkheden op Newtonsoft.Json. Daarom moeten alle invoegtoepassingsprojecten <EnableDynamicLoading>true</EnableDynamicLoading> toevoegen aan de projecteigenschappen, zodat ze al hun afhankelijkheden naar de uitvoer van dotnet buildkopiëren. 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 tutorial vindt u in de dotnet/samples-repository. Het voltooide voorbeeld bevat enkele andere voorbeelden van AssemblyDependencyResolver gedrag. Het AssemblyDependencyResolver-object kan bijvoorbeeld ook systeemeigen bibliotheken en gelokaliseerde satellietassembly's in NuGet-pakketten oplossen. De UVPlugin en FrenchPlugin in de opslagplaats voor voorbeelden laten deze scenario's zien.

Refereren aan een plug-ininterface 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 naar het pakket in uw invoegtoepassingsproject? Voor projectverwijzingen heeft het gebruik van de <Private>false</Private> metagegevens op het element ProjectReference in het projectbestand verhinderd dat het dll-bestand naar de uitvoer wordt gekopieerd.

Als u naar het A.PluginBase-pakket wilt verwijzen, wilt u het <PackageReference>-element in het projectbestand als volgt wijzigen:

<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 gebruikmaakt van de A-versie van A.PluginBase.

Aanbevelingen voor het doelframework van de invoegtoepassing

Omdat het laden van afhankelijkheden van plug-ins gebruikmaakt van het .deps.json-bestand, is er een probleem met betrekking tot het doel-framework van de plug-in. Uw invoegtoepassingen moeten zich specifiek richten op een runtime, zoals .NET 5, in plaats van op een versie van .NET Standard. Het .deps.json-bestand wordt gegenereerd op basis van het framework waarop het project is gericht, en omdat 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 kan het de .NET Standard-versie van een assembly pakken 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 geen invoegtoepassing laden die gebruikmaakt van het Microsoft.AspNetCore.App-framework in een toepassing die alleen gebruikmaakt van het hoofdframework Microsoft.NETCore.App. De hosttoepassing moet verwijzingen declareren naar alle frameworks die nodig zijn voor invoegtoepassingen.