Vytvoření aplikace .NET Core pomocí modulů plug-in

V tomto kurzu se dozvíte, jak vytvořit vlastní AssemblyLoadContext moduly plug-in pro načítání. K AssemblyDependencyResolver vyřešení závislostí modulu plug-in se používá modul plug-in. Tento kurz správně izoluje závislosti modulu plug-in od hostitelské aplikace. Dozvíte se, jak:

  • Strukturování projektu tak, aby podporoval moduly plug-in.
  • Vytvořte vlastní AssemblyLoadContext modul plug-in, který načte jednotlivé moduly plug-in.
  • Pomocí typu System.Runtime.Loader.AssemblyDependencyResolver povolte, aby moduly plug-in měly závislosti.
  • Vytvářejte moduly plug-in, které je možné snadno nasadit zkopírováním artefaktů sestavení.

Požadavky

  • Nainstalujte sadu .NET 5 SDK nebo novější verzi.

Poznámka

Ukázkový kód cílí na .NET 5, ale všechny funkce, které používá, byly zavedeny v .NET Core 3.0 a od té doby jsou dostupné ve všech verzích .NET.

Vytvoření aplikace

Prvním krokem je vytvoření aplikace:

  1. Vytvořte novou složku a v této složce spusťte následující příkaz:

    dotnet new console -o AppWithPlugin
    
  2. Pokud chcete usnadnit vytváření projektu, vytvořte soubor řešení sady Visual Studio ve stejné složce. Spusťte následující příkaz:

    dotnet new sln
    
  3. Spuštěním následujícího příkazu přidejte projekt aplikace do řešení:

    dotnet sln add AppWithPlugin/AppWithPlugin.csproj
    

Teď můžeme vyplnit kostru naší aplikace. Nahraďte kód v souboru AppWithPlugin/Program.cs následujícím kódem:

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

Vytvoření rozhraní modulu plug-in

Dalším krokem při vytváření aplikace s moduly plug-in je definování rozhraní, které musí moduly plug-in implementovat. Doporučujeme vytvořit knihovnu tříd, která bude obsahovat všechny typy, které plánujete používat pro komunikaci mezi vaší aplikací a moduly plug-in. Tato divize umožňuje publikovat rozhraní modulu plug-in jako balíček, aniž byste museli dodávat celou aplikaci.

V kořenové složce projektu spusťte příkaz dotnet new classlib -o PluginBase. Spuštěním příkazu dotnet sln add PluginBase/PluginBase.csproj také přidejte projekt do souboru řešení. PluginBase/Class1.cs Odstraňte soubor a vytvořte nový soubor ve PluginBase složce s názvem ICommand.cs s následující definicí rozhraní:

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

        int Execute();
    }
}

Toto ICommand rozhraní je rozhraní, které budou implementovat všechny moduly plug-in.

Teď, když ICommand je rozhraní definováno, je možné projekt aplikace vyplnit o něco více. Přidejte do projektu odkaz z AppWithPlugin projektu PluginBase pomocí dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj příkazu z kořenové složky.

// Load commands from plugins Nahraďte komentář následujícím fragmentem kódu, který mu umožní načíst moduly plug-in z daných cest k souborům:

string[] pluginPaths = new string[]
{
    // Paths to plugins to load.
};

IEnumerable<ICommand> commands = pluginPaths.SelectMany(pluginPath =>
{
    Assembly pluginAssembly = LoadPlugin(pluginPath);
    return CreateCommands(pluginAssembly);
}).ToList();

Pak nahraďte // Output the loaded commands komentář následujícím fragmentem kódu:

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

// Execute the command with the name passed as an argument Nahraďte komentář následujícím fragmentem kódu:

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

command.Execute();

A nakonec přidejte statické metody do Program třídy s názvem LoadPlugin a , CreateCommandsjak je znázorněno tady:

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

Načtení modulů plug-in

Aplikace teď může správně načíst a vytvořit instance příkazů z načtených sestavení modulů plug-in, ale stále nemůže načíst sestavení modulů plug-in. Ve složce AppWithPlugin vytvořte soubor s názvem PluginLoadContext.cs s následujícím obsahem:

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

Typ je PluginLoadContext odvozen od AssemblyLoadContext. Typ AssemblyLoadContext je speciální typ v modulu runtime, který vývojářům umožňuje izolovat načtená sestavení do různých skupin, aby se zajistilo, že verze sestavení nebudou kolidovat. Kromě toho může vlastní AssemblyLoadContext zvolit různé cesty k načtení sestavení a přepsat výchozí chování. Používá PluginLoadContext instanci typu zavedenou AssemblyDependencyResolver v .NET Core 3.0 k překladu názvů sestavení na cesty. Objekt AssemblyDependencyResolver je vytvořen s cestou ke knihovně tříd .NET. Překládá sestavení a nativní knihovny na jejich relativní cesty založené na souboru .deps.json pro knihovnu tříd, jejíž cesta byla předána konstruktoru AssemblyDependencyResolver . Vlastní AssemblyLoadContext umožňuje modulům plug-in mít vlastní závislosti a AssemblyDependencyResolver usnadňuje správné načtení závislostí.

Teď, když AppWithPluginPluginLoadContext projekt typ, aktualizujte metodu Program.LoadPlugin následujícím textem:

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

Při použití jiné PluginLoadContext instance pro každý modul plug-in můžou mít moduly plug-in bez problémů různé nebo dokonce konfliktní závislosti.

Jednoduchý modul plug-in bez závislostí

Zpátky v kořenové složce postupujte takto:

  1. Spuštěním následujícího příkazu vytvořte nový projekt knihovny tříd s názvem HelloPlugin:

    dotnet new classlib -o HelloPlugin
    
  2. Spuštěním následujícího příkazu přidejte projekt do AppWithPlugin řešení:

    dotnet sln add HelloPlugin/HelloPlugin.csproj
    
  3. Nahraďte soubor HelloPlugin/Class1.cs souborem s názvem HelloCommand.cs následujícím obsahem:

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

Teď otevřete soubor HelloPlugin.csproj . Mělo by to vypadat nějak takto:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

</Project>

Mezi značky <PropertyGroup> přidejte následující prvek:

  <EnableDynamicLoading>true</EnableDynamicLoading>

Připraví <EnableDynamicLoading>true</EnableDynamicLoading> projekt tak, aby ho bylo možné použít jako modul plug-in. Tím se mimo jiné zkopírují všechny jeho závislosti do výstupu projektu. Další podrobnosti najdete tady: EnableDynamicLoading.

Mezi značky <Project> přidejte následující prvky:

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

Prvek <Private>false</Private> je důležitý. To říká nástroji MSBuild, aby nekopírovat PluginBase.dll do výstupního adresáře pro HelloPlugin. Pokud sePluginBase.dll sestavení nachází ve výstupním adresáři, PluginLoadContext najde tam sestavení a načte ho při načtení sestaveníHelloPlugin.dll . V tomto okamžiku HelloPlugin.HelloCommand bude typ implementovat ICommand rozhraní z PluginBase.dll ve výstupním HelloPlugin adresáři projektu, nikoli ICommand rozhraní, které je načteno do výchozího kontextu načítání. Vzhledem k tomu, že modul runtime vidí tyto dva typy jako různé typy z různých sestavení, AppWithPlugin.Program.CreateCommands metoda nenajde příkazy. V důsledku toho <Private>false</Private> se metadata vyžadují pro odkaz na sestavení obsahující rozhraní modulu plug-in.

Podobně je prvek také důležitý, <ExcludeAssets>runtime</ExcludeAssets> pokud odkazuje na PluginBase jiné balíčky. Toto nastavení má stejný účinek jako <Private>false</Private> u odkazů na balíčky, které PluginBase může projekt nebo jedna z jeho závislostí obsahovat.

Teď, když HelloPlugin je projekt hotový, měli byste ho AppWithPlugin aktualizovat, abyste věděli, kde se dá modul plug-in HelloPlugin najít. // Paths to plugins to load Za komentář přidejte @"HelloPlugin\bin\Debug\net5.0\HelloPlugin.dll" (tato cesta se může lišit v závislosti na používané verzi .NET Core) jako prvek pluginPaths pole.

Modul plug-in se závislostmi knihovny

Téměř všechny moduly plug-in jsou složitější než jednoduché "Hello World" a mnoho modulů plug-in má závislosti na jiných knihovnách. Projekty JsonPlugin a OldJsonPlugin v ukázce ukazují dva příklady modulů plug-in se závislostmi balíčků NuGet na Newtonsoft.Json. Z tohoto důvodu by se všechny projekty modulů plug-in měly přidat <EnableDynamicLoading>true</EnableDynamicLoading> do vlastností projektu, aby zkopírovaly všechny své závislosti do výstupu .dotnet build Publikováním knihovny tříd pomocí dotnet publish se také zkopírují všechny její závislosti do výstupu publikování.

Další příklady v ukázce

Kompletní zdrojový kód pro tento kurz najdete v úložišti dotnet/samples. Dokončená ukázka obsahuje několik dalších příkladů AssemblyDependencyResolver chování. Objekt může například AssemblyDependencyResolver také přeložit nativní knihovny a lokalizovaná satelitní sestavení zahrnutá v balíčcích NuGet. Tyto UVPlugin scénáře demonstrují v úložišti ukázek a FrenchPlugin .

Odkaz na rozhraní modulu plug-in z balíčku NuGet

Řekněme, že existuje aplikace A, která má rozhraní modulu plug-in definované v balíčku NuGet s názvem A.PluginBase. Jak správně odkazujete na balíček v projektu modulu plug-in? U odkazů na projekt použití <Private>false</Private> metadat v elementu ProjectReference v souboru projektu zabránilo kopírování knihovny DLL do výstupu.

Chcete-li správně odkazovat na A.PluginBase balíček, chcete změnit <PackageReference> prvek v souboru projektu na následující:

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

Tím zabráníte A.PluginBase kopírování sestavení do výstupního adresáře modulu plug-in a zajistíte, že modul plug-in bude používat verzi A .A.PluginBase

Doporučení cílové architektury modulu plug-in

Vzhledem k tomu, že načítání závislostí modulu plug-in používá soubor .deps.json , existuje něco, co souvisí s cílovou architekturou modulu plug-in. Konkrétně by vaše moduly plug-in měly cílit na modul runtime, jako je .NET 5, a ne na verzi .NET Standard. Soubor .deps.json se generuje na základě toho, na jakou architekturu projekt cílí, a protože mnoho balíčků kompatibilních se standardem .NET Standard dodává referenční sestavení pro sestavení pro .NET Standard a implementační sestavení pro konkrétní moduly runtime, nemusí soubor .deps.json správně zobrazit sestavení implementace nebo může místo očekávané verze .NET Core získat verzi sestavení .NET Standard.

Odkazy na architekturu modulů plug-in

Moduly plug-in v současné době nemůžou do procesu zavádět nové architektury. Nemůžete například načíst modul plug-in, který používá architekturu Microsoft.AspNetCore.App , do aplikace, která používá pouze kořenovou Microsoft.NETCore.App architekturu. Hostitelská aplikace musí deklarovat odkazy na všechna rozhraní, která moduly plug-in potřebují.