Share via


Een .NET Core-toepassing maken met invoegtoepassingen

In deze zelfstudie leert u hoe u een aangepaste AssemblyLoadContext om invoegtoepassingen te laden maakt. Een AssemblyDependencyResolver wordt gebruikt om de afhankelijkheden van de invoegtoepassing op te lossen. In de zelfstudie worden de afhankelijkheden van de invoegtoepassing correct geïsoleerd van de hostingtoepassing. U leert het volgende:

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

Vereisten

  • Installeer de .NET 5 SDK of een nieuwere versie.

Notitie

De voorbeeldcode is gericht op .NET 5, maar alle functies die worden gebruikt, zijn geïntroduceerd in .NET Core 3.0 en zijn sindsdien beschikbaar in alle .NET-releases.

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 bouwen van 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 geraamte 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. We raden u aan een klassenbibliotheek te maken die alle typen bevat die u wilt gebruiken voor de communicatie tussen uw app en invoegtoepassingen. Met deze afdeling kunt u uw invoegtoepassingsinterface als een pakket publiceren zonder dat u uw volledige toepassing hoeft te verzenden.

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

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

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

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

command.Execute();

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

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

Eenvoudige invoegtoepassing zonder afhankelijkheden

Ga als volgt te werk in de hoofdmap:

  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 een invoegtoepassing. Hiermee worden onder andere alle afhankelijkheden naar de uitvoer van het project gekopieerd. Zie EnableDynamicLoading voor 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. Hiermee wordt aangegeven dat MSBuild PluginBase.dll niet naar de uitvoermap voor HelloPlugin moet kopiëren. Als de PluginBase.dll assembly aanwezig is in de uitvoermap, PluginLoadContext wordt de assembly daar gevonden en geladen wanneer de HelloPlugin.dll assembly wordt geladen. Op dit moment 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 laadcontext wordt geladen. Omdat de runtime deze twee typen als verschillende typen van verschillende assembly's ziet, vindt de AppWithPlugin.Program.CreateCommands methode de opdrachten niet. Als gevolg hiervan zijn de <Private>false</Private> metagegevens vereist voor de verwijzing naar de assembly met de invoegtoepassingsinterfaces.

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

Invoegtoepassing met bibliotheekafhankelijkheden

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

Verwijzen naar een invoegtoepassingsinterface 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 correct naar het pakket in uw invoegtoepassingsproject? Voor projectverwijzingen voorkwam het gebruik van de <Private>false</Private> metagegevens voor het ProjectReference element in het projectbestand dat de DLL naar de uitvoer werd gekopieerd.

Als u correct naar het A.PluginBase pakket wilt verwijzen, wijzigt u het <PackageReference> element in het projectbestand in het volgende:

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

Aanbevelingen voor het doelframework voor invoegtoepassingen

Omdat het laden van invoegtoepassingsafhankelijkheid gebruikmaakt van het bestand .deps.json , is er een gotcha gerelateerd aan het doelframework van de invoegtoepassing. Uw invoegtoepassingen moeten zich richten op een runtime, zoals .NET 5, in plaats van een versie van .NET Standard. Het bestand .deps.json wordt gegenereerd op basis van het framework waarop het project is gericht, en aangezien 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 wordt de .NET Standard-versie van een assembly opgehaald 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 een invoegtoepassing die gebruikmaakt van het Microsoft.AspNetCore.App framework niet laden in een toepassing die alleen het hoofdframework Microsoft.NETCore.App gebruikt. De hosttoepassing moet verwijzingen declareren naar alle frameworks die nodig zijn voor invoegtoepassingen.