Anteckning
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
Den här handledningen visar hur du skapar en anpassad AssemblyLoadContext för att ladda plugins. En AssemblyDependencyResolver används för att lösa plugin-programmets beroenden. Självstudien innehåller en separat sammansättningskontext för plugin-programmets beroenden, vilket möjliggör olika sammansättningsberoenden mellan plugin-programmen och värdprogrammet. Du får lära dig att:
- Strukturera ett projekt för att stödja plugin-program.
- Skapa en anpassad AssemblyLoadContext för att läsa in varje tillägg.
- Använd den System.Runtime.Loader.AssemblyDependencyResolver typen för att tillåta att plugin-program har beroenden.
- Skapa plugin-program som enkelt kan distribueras genom att bara kopiera byggartefakterna.
Anmärkning
Ej betrodd kod kan inte läsas in på ett säkert sätt i en betrodd .NET-process. För att tillhandahålla en säkerhets- eller tillförlitlighetsgräns bör du överväga en teknik som tillhandahålls av operativsystemet eller virtualiseringsplattformen.
Förutsättningar
- Den senaste versionen av .NET SDK
- Visual Studio Code-redigerare
- C# DevKit
Skapa programmet
Det första steget är att skapa programmet:
- Skapa en ny mapp och kör följande kommando i mappen: - dotnet new console -o AppWithPlugin
- Skapa en Visual Studio-lösningsfil i samma mapp för att göra det enklare att skapa projektet. Kör följande kommando: - dotnet new sln
- Kör följande kommando för att lägga till appprojektet i lösningen: - dotnet sln add AppWithPlugin/AppWithPlugin.csproj
Nu kan vi bygga ut grunden för vårt program. Ersätt koden i filen AppWithPlugin/Program.cs med följande kod:
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);
            }
        }
    }
}
Skapa plugin-gränssnitten
Nästa steg i att skapa en app med plugin-program är att definiera det gränssnitt som plugin-program behöver implementera. Vi föreslår att du skapar ett klassbibliotek som innehåller alla typer som du planerar att använda för kommunikation mellan din app och plugin-program. Med den här divisionen kan du publicera plugin-gränssnittet som ett paket utan att behöva skicka hela programmet.
I rotmappen för projektet kör du dotnet new classlib -o PluginBase. Kör också dotnet sln add PluginBase/PluginBase.csproj för att lägga till projektet i lösningsfilen. Ta bort PluginBase/Class1.cs-filen och skapa en ny fil i mappen PluginBase med namnet ICommand.cs med följande gränssnittsdefinition:
namespace PluginBase
{
    public interface ICommand
    {
        string Name { get; }
        string Description { get; }
        int Execute();
    }
}
Det här ICommand gränssnittet är det gränssnitt som alla plugin-program implementerar.
Nu när ICommand-gränssnittet har definierats kan programprojektet fyllas i lite mer. Lägg till en referens från AppWithPlugin-projektet i PluginBase-projektet med kommandot dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj från rotmappen.
Ersätt // Load commands from plugins-kommentaren med följande kodfragment så att det kan läsa in plugin-program från angivna filsökvägar:
string[] pluginPaths = new string[]
{
    // Paths to plugins to load.
};
IEnumerable<ICommand> commands = pluginPaths.SelectMany(pluginPath =>
{
    Assembly pluginAssembly = LoadPlugin(pluginPath);
    return CreateCommands(pluginAssembly);
}).ToList();
Ersätt sedan // Output the loaded commands-kommentaren med följande kodfragment:
foreach (ICommand command in commands)
{
    Console.WriteLine($"{command.Name}\t - {command.Description}");
}
Ersätt kommentaren // Execute the command with the name passed as an argument med följande kodfragment:
ICommand command = commands.FirstOrDefault(c => c.Name == commandName);
if (command == null)
{
    Console.WriteLine("No such command is known.");
    return;
}
command.Execute();
Och slutligen lägger du till statiska metoder i klassen Program med namnet LoadPlugin och CreateCommands, enligt följande:
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}");
    }
}
Ladda plugins
Nu kan programmet läsa in och instansiera kommandon från inlästa plugin-sammansättningar, men det går fortfarande inte att läsa in plugin-sammansättningarna. Skapa en fil med namnet PluginLoadContext.cs i mappen AppWithPlugin med följande innehåll:
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;
        }
    }
}
Typen PluginLoadContext härleds från AssemblyLoadContext. Den AssemblyLoadContext typen är en särskild typ i körningen som gör att utvecklare kan isolera inlästa sammansättningar i olika grupper för att säkerställa att sammansättningsversionerna inte står i konflikt. Dessutom kan en anpassad AssemblyLoadContext välja olika sökvägar för att läsa in sammansättningar från och åsidosätta standardbeteendet. 
              PluginLoadContext använder en instans av typen AssemblyDependencyResolver som introducerades i .NET Core 3.0 för att översätta assemblies namn till sökvägar. Objektet AssemblyDependencyResolver är konstruerat med sökvägen till ett .NET-klassbibliotek. Den löser sammansättningar och interna bibliotek till deras relativa sökvägar baserat på .deps.json-filen för klassbiblioteket vars sökväg skickades till AssemblyDependencyResolver konstruktorn. Anpassningen AssemblyLoadContext gör det möjligt för plugins att ha egna beroenden, och AssemblyDependencyResolver gör det enkelt att läsa in beroendena korrekt.
Nu när AppWithPlugin-projektet har PluginLoadContext typ uppdaterar du metoden Program.LoadPlugin med följande brödtext:
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)));
}
Genom att använda en annan PluginLoadContext instans för varje plugin-program kan plugin-program ha olika eller till och med motstridiga beroenden utan problem.
Enkelt plugin-program utan beroenden
Gör följande i rotmappen:
- Kör följande kommando för att skapa ett nytt klassbiblioteksprojekt med namnet - HelloPlugin:- dotnet new classlib -o HelloPlugin
- Kör följande kommando för att lägga till projektet i den - AppWithPluginlösningen:- dotnet sln add HelloPlugin/HelloPlugin.csproj
- Ersätt filen HelloPlugin/Class1.cs med en fil med namnet HelloCommand.cs med följande innehåll: 
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;
        }
    }
}
Öppna nu filen HelloPlugin.csproj. Det bör se ut ungefär så här:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>
</Project>
Lägg till följande element mellan de <PropertyGroup> taggarna:
  <EnableDynamicLoading>true</EnableDynamicLoading>
              <EnableDynamicLoading>true</EnableDynamicLoading> förbereder projektet så att det kan användas som ett plugin-program. Detta kopierar bland annat alla dess beroenden till projektets utdata. Mer information finns i: EnableDynamicLoading.
Lägg till följande element mellan <Project>-taggarna:
<ItemGroup>
    <ProjectReference Include="..\PluginBase\PluginBase.csproj">
        <Private>false</Private>
        <ExcludeAssets>runtime</ExcludeAssets>
    </ProjectReference>
</ItemGroup>
Elementet <Private>false</Private> är viktigt. Detta talar om för MSBuild att inte kopiera PluginBase.dll till utdatakatalogen för HelloPlugin. Om den PluginBase.dll sammansättningen finns i utdatakatalogen hittar PluginLoadContext sammansättningen där och läser in den när den läser in HelloPlugin.dll sammansättningen. Nu implementerar HelloPlugin.HelloCommand-typen ICommand-gränssnittet från PluginBase.dll i utdatakatalogen för HelloPlugin-projektet, inte ICommand-gränssnittet som läses in i standardladdningskontexten. Eftersom programkörningen ser dessa två typer som olika typer från olika assemblys kan inte AppWithPlugin.Program.CreateCommands-metoden hitta kommandona. Därför krävs <Private>false</Private> metadata för referensen till sammansättningen som innehåller plugin-gränssnitten.
På samma sätt är <ExcludeAssets>runtime</ExcludeAssets>-elementet också viktigt om PluginBase refererar till andra paket. Den här inställningen har samma effekt som <Private>false</Private> men fungerar på paketreferenser som PluginBase projektet eller något av dess beroenden kan innehålla.
Nu när HelloPlugin projektet är klart bör du uppdatera AppWithPlugin-projektet för att veta var HelloPlugin plugin-programmet finns. Efter den // Paths to plugins to load kommentaren lägger du till @"HelloPlugin\bin\Debug\net5.0\HelloPlugin.dll" (den här sökvägen kan vara annorlunda baserat på den .NET Core-version som du använder) som ett element i pluginPaths matrisen.
Plugin-program med biblioteksberoenden
Nästan alla plugin-program är mer komplexa än en enkel "Hello World" och många plugin-program har beroenden för andra bibliotek. Projekten JsonPlugin och OldJsonPlugin i exemplet visar två exempel på plugin-program med NuGet-paketberoenden på Newtonsoft.Json. Därför bör alla plugin-projekt lägga till <EnableDynamicLoading>true</EnableDynamicLoading> till projektegenskaperna så att de kopierar alla sina beroenden till utdata från dotnet build. Om du publicerar klassbiblioteket med dotnet publish kopieras även alla dess beroenden till publiceringsutdata.
Andra exempel i exemplet
Den fullständiga källkoden för den här handledningen finns i dotnet/samples-lagringsplats. Det färdiga exemplet innehåller några andra exempel på AssemblyDependencyResolver beteende. Till exempel kan AssemblyDependencyResolver-objektet även lösa interna bibliotek samt lokaliserade satellitsammansättningar som ingår i NuGet-paket. De UVPlugin och FrenchPlugin i exempelarkivet visar dessa scenarier.
Referera till ett plugin-gränssnitt från ett NuGet-paket
Anta att det finns en app A som har ett plugin-gränssnitt definierat i NuGet-paketet med namnet A.PluginBase. Hur refererar du till paketet korrekt i plugin-projektet? För projektreferenser förhindrade användningen av <Private>false</Private>-metadata på ProjectReference-elementet i projektfilen att den dll-filen kopierades över till utdata.
Om du vill referera till A.PluginBase-paketet på rätt sätt vill du ändra <PackageReference>-elementet i projektfilen till följande:
<PackageReference Include="A.PluginBase" Version="1.0.0">
    <ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
Detta förhindrar att A.PluginBase sammansättningar kopieras till utdatakatalogen för plugin-programmet och ser till att plugin-programmet använder A:s version av A.PluginBase.
Rekommendationer för plugin-målramverk
Eftersom plugin-beroendeinläsning använder .deps.json-filen finns det en gotcha som är relaterad till plugin-programmets målramverk. Specifikt bör dina insticksprogram anpassa sig för en runtime-miljö, till exempel .NET 5, i stället för en version av .NET Standard. Den .deps.json filen genereras baserat på vilket ramverk projektet har som mål, och eftersom många .NET Standard-kompatibla paket skickar referenssammansättningar för att skapa mot .NET Standard- och implementeringssammansättningar för specifika körningar kanske .deps.json inte korrekt ser implementeringssammansättningar, eller så kan den hämta .NET Standard-versionen av en sammansättning i stället för den .NET Core-version som du förväntar dig.
Referens för plugin-ramverk
För närvarande kan plugin-program inte introducera nya ramverk i processen. Du kan till exempel inte läsa in ett plugin-program som använder Microsoft.AspNetCore.App-ramverket i ett program som bara använder rotramverket Microsoft.NETCore.App. Värdprogrammet måste deklarera referenser till alla ramverk som behövs av plugin-program.