Sdílet prostřednictvím


Vytvoření aplikace .NET Core s moduly plug-in

Tento návod vám ukáže, jak vytvořit vlastní AssemblyLoadContext k načtení pluginů. Jednotka AssemblyDependencyResolver se používá k vyřešení závislostí plug-inu. Tento tutoriál poskytuje samostatný kontext sestavení pro závislosti plug-inu, což umožňuje různé závislosti mezi plug-iny a hostitelskou aplikací. Naučíte se:

Poznámka:

Nedůvěryhodný kód nelze bezpečně načíst do důvěryhodného procesu .NET. Pokud chcete poskytnout hranici zabezpečení nebo spolehlivosti, zvažte technologii poskytovanou vaším operačním systémem nebo virtualizační platformou.

Požadavky

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 sestavování 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é moduly plug-in potřebují implementovat. Doporučujeme vytvořit knihovnu tříd, která obsahuje všechny typy, které chcete použít 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 dotnet new classlib -o PluginBase. Spuštěním dotnet sln add PluginBase/PluginBase.csproj přidejte projekt do souboru řešení. Odstraňte soubor PluginBase/Class1.cs a vytvořte nový soubor ve složce PluginBase 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 to, které budou implementovat všechny pluginy.

Nyní, když je rozhraní ICommand definováno, je možné projekt aplikace dále rozšířit. Přidejte odkaz z projektu AppWithPlugin do projektu PluginBase příkazem dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj z kořenové složky.

Nahraďte komentář // Load commands from plugins následujícím úryvkem kódu, aby mohly být pluginy načteny 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();

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

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

Nahraďte komentář // Execute the command with the name passed as an argument 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();

Nakonec přidejte statické metody do třídy Program s názvem LoadPlugin a CreateCommands, jak je znázorněno zde:

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ď dokáže správně načíst a vytvořit instanci příkazů z načtených plugin sestavení, ale stále nemůže načíst plugin sestavení. 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 PluginLoadContext je odvozen z AssemblyLoadContext. Typ AssemblyLoadContext je speciální typ modulu runtime, který vývojářům umožňuje izolovat načtená sestavení do různých skupin, aby se zajistilo, že verze sestavení nejsou v konfliktu. Kromě toho může vlastní AssemblyLoadContext zvolit různé cesty pro načtení sestavení a přepsat výchozí chování. PluginLoadContext používá instanci typu AssemblyDependencyResolver zavedeného v .NET Core 3.0 k překladu názvů sestavení do cest. Objekt AssemblyDependencyResolver je vytvořen s cestou k knihovně tříd .NET. Řeší sestavení a nativní knihovny do jejich relativních cest na základě souboru .deps.json pro knihovnu tříd, jejíž cesta byla předána do AssemblyDependencyResolver konstruktoru. Vlastní AssemblyLoadContext umožňuje pluginům mít vlastní závislosti a AssemblyDependencyResolver usnadňuje správné načtení závislostí.

Teď, když má projekt AppWithPlugin typ PluginLoadContext, 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)));
}

Pomocí použití různé instance PluginLoadContext pro každý plugin mohou mít pluginy různé nebo dokonce konfliktní závislosti bez jakýchkoli problémů.

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 řešení AppWithPlugin:

    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ěl by 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>

<EnableDynamicLoading>true</EnableDynamicLoading> připraví projekt tak, aby ho bylo možné použít jako modul plug-in. Mimo jiné se tím zkopírují všechny jeho závislosti na 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íroval PluginBase.dll do výstupního adresáře pro HelloPlugin. Pokud je sestavení PluginBase.dll ve výstupním adresáři, PluginLoadContext tam toto sestavení najde a načte je, když načte sestavení HelloPlugin.dll. V tomto okamžiku typ HelloPlugin.HelloCommand implementuje rozhraní ICommand z PluginBase.dll ve výstupním adresáři projektu HelloPlugin, nikoli rozhraní ICommand, 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í, metoda AppWithPlugin.Program.CreateCommands tyto příkazy nenajde. V důsledku toho se pro odkaz na sestavení obsahující rozhraní modulu plug-in vyžadují metadata <Private>false</Private>.

Podobně je prvek <ExcludeAssets>runtime</ExcludeAssets> důležitý také v případě, že PluginBase odkazuje na jiné balíčky. Toto nastavení má stejný účinek jako <Private>false</Private>, ale funguje na odkazech na balíčky, které může obsahovat PluginBase projekt nebo jedna z jejích závislostí.

Po dokončení projektu HelloPlugin byste měli aktualizovat projekt AppWithPlugin, abyste věděli, kde je možné najít modul plug-in HelloPlugin. Po // Paths to plugins to load komentáři přidejte @"HelloPlugin\bin\Debug\net5.0\HelloPlugin.dll" (tato cesta se může lišit podle používané verze .NET Core) jako prvek pole pluginPaths.

Plug-in se závislostmi na knihovnách

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 všechny projekty modulů plug-in měly přidat <EnableDynamicLoading>true</EnableDynamicLoading> do vlastností projektu, aby zkopírovaly všechny své závislosti na výstupu dotnet build. Publikování knihovny tříd s dotnet publish také zkopíruje všechny její závislosti do výstupního souboru 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ů chování AssemblyDependencyResolver. Například objekt AssemblyDependencyResolver může také řešit nativní knihovny a lokalizovaná satelitní sestavení zahrnutá v balíčcích NuGet. Ukázky UVPlugin a FrenchPlugin v úložišti příkladů demonstrují tyto scénáře.

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 způsobilo použití metadat <Private>false</Private> v elementu ProjectReference v souboru projektu, že se DLL nekopíroval do výstupu.

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

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

Tím zabráníte zkopírování sestavení A.PluginBase do výstupního adresáře vašeho plug-inu a zajistíte, že váš plug-in bude používat verzi A.PluginBaseod A.

Doporučení pro cílovou architekturu modulu plug-in

Vzhledem k tomu, že načítání závislostí plug-inu používá soubor .deps.json, existuje úskalí související s cílovým prostředím plug-inu. Konkrétně by vaše plug-iny měly cílit na runtime, například .NET 5, místo na verzi .NET Standard. Soubor .deps.json se generuje na základě rámce, na který cílí projekt, a protože mnoho balíčků kompatibilních s .NET Standard zahrnuje referenční sestavení pro sestavení proti .NET Standard a implementační sestavení pro konkrétní prostředí runtime, .deps.json nemusí správně zobrazit sestavení implementace nebo může namísto očekávané verze .NET Core získat verzi sestavení .NET Standard.

Odkazy na rámec pluginů

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