.NET Core-alkalmazás létrehozása beépülő modulokkal

Ez az oktatóanyag bemutatja, hogyan hozhat létre egyéni AssemblyLoadContext beépülő modulokat. Az An AssemblyDependencyResolver a beépülő modul függőségeinek feloldására szolgál. Az oktatóanyag megfelelően elkülöníti a beépülő modul függőségeit az üzemeltetési alkalmazástól. A következőket fogja megtanulni:

  • Projekt strukturálása beépülő modulok támogatásához.
  • Hozzon létre egy egyénit AssemblyLoadContext az egyes beépülő modulok betöltéséhez.
  • A típussal engedélyezheti, hogy a System.Runtime.Loader.AssemblyDependencyResolver beépülő modulok függőségekkel rendelkezzenek.
  • Egyszerűen üzembe helyezhető beépülő modulok létrehozása a buildösszetevők másolásával.

Előfeltételek

Megjegyzés

A mintakód a .NET 5-öt célozza meg, de az általa használt összes funkció a .NET Core 3.0-ban lett bevezetve, és azóta minden .NET-kiadásban elérhető.

Az alkalmazás létrehozása

Az első lépés az alkalmazás létrehozása:

  1. Hozzon létre egy új mappát, és ebben a mappában futtassa a következő parancsot:

    dotnet new console -o AppWithPlugin
    
  2. A projekt létrehozásának megkönnyítése érdekében hozzon létre egy Visual Studio-megoldásfájlt ugyanabban a mappában. Futtassa az alábbi parancsot:

    dotnet new sln
    
  3. Futtassa a következő parancsot az alkalmazásprojekt megoldáshoz való hozzáadásához:

    dotnet sln add AppWithPlugin/AppWithPlugin.csproj
    

Most már kitölthetjük az alkalmazás csontvázát. Cserélje le a kódot az AppWithPlugin/Program.cs fájlban a következő kódra:

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

A beépülő modul felületeinek létrehozása

Az alkalmazások beépülő modulokkal történő létrehozásának következő lépése annak a felületnek a meghatározása, amelyet a beépülő moduloknak implementálniuk kell. Javasoljuk, hogy hozzon létre egy osztálytárat, amely tartalmazza az alkalmazás és a beépülő modulok közötti kommunikációhoz használni kívánt típusokat. Ez a részleg lehetővé teszi, hogy a beépülő modul felületét csomagként tegye közzé anélkül, hogy a teljes alkalmazást be kellene szállítania.

A projekt gyökérmappájában futtassa a következőt dotnet new classlib -o PluginBase: . Emellett futtassa a parancsot dotnet sln add PluginBase/PluginBase.csproj a projekt megoldásfájlhoz való hozzáadásához. Törölje a PluginBase/Class1.cs fájlt, és hozzon létre egy új fájlt a PluginBase nevű mappában ICommand.cs a következő felületdefinícióval:

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

        int Execute();
    }
}

Ez ICommand a felület az a felület, amelyet az összes beépülő modul implementál.

Most, hogy a ICommand felület meg lett határozva, az alkalmazásprojekt egy kicsit többet is kitölthető. Adjon hozzá egy hivatkozást a AppWithPlugin projektből a PluginBase projekthez a dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj gyökérmappából származó paranccsal.

Cserélje le a megjegyzést // Load commands from plugins a következő kódrészletre, hogy engedélyezze a beépülő modulok betöltését a megadott fájlelérési utakról:

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

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

Ezután cserélje le a megjegyzést // Output the loaded commands a következő kódrészletre:

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

Cserélje le a megjegyzést // Execute the command with the name passed as an argument a következő kódrészletre:

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

command.Execute();

Végül adjon hozzá statikus metódusokat a és CreateCommandsnevű ProgramLoadPlugin osztályhoz, ahogy az itt látható:

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

Beépülő modulok betöltése

Az alkalmazás most már megfelelően tudja betölteni és példányosítani a parancsokat a betöltött beépülő modulszerelvényekből, de továbbra sem tudja betölteni a beépülő modulszerelvényeket. Hozzon létre egy PluginLoadContext.cs nevű fájlt az AppWithPlugin mappában a következő tartalommal:

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

A PluginLoadContext típus a következőből AssemblyLoadContextszármazik: . A AssemblyLoadContext típus egy speciális típus a futtatókörnyezetben, amely lehetővé teszi a fejlesztők számára a betöltött szerelvények különböző csoportokba való elkülönítését, hogy a szerelvényverziók ne ütközhessenek. Emellett az egyéniek AssemblyLoadContext különböző útvonalakat is választhatnak a szerelvények betöltéséhez, és felülbírálhatják az alapértelmezett viselkedést. A PluginLoadContext .NET Core 3.0-s verziójában bevezetett típuspéldányt AssemblyDependencyResolver használ a szerelvénynevek elérési utakhoz való feloldásához. Az AssemblyDependencyResolver objektum egy .NET-osztálytár elérési útjával jön létre. A rendszer feloldja a szerelvényeket és a natív kódtárakat a relatív elérési utakra annak az osztálytárnak a .deps.json fájlja alapján, amelynek az elérési útja a AssemblyDependencyResolver konstruktornak lett átadva. Az egyéni AssemblyLoadContext lehetővé teszi, hogy a beépülő modulok saját függőségekkel rendelkezzenek, és a AssemblyDependencyResolver segítségével könnyen betölthetőek a függőségek.

Most, hogy a AppWithPlugin projekt rendelkezik a PluginLoadContext típussal, frissítse a metódust Program.LoadPlugin a következő törzstel:

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

Ha minden beépülő modulhoz más-más PluginLoadContext példányt használ, a beépülő modulok különböző vagy akár ütköző függőségekkel is rendelkezhetnek probléma nélkül.

Egyszerű beépülő modul függőségek nélkül

A gyökérmappában tegye a következőket:

  1. Futtassa a következő parancsot egy új osztálytárprojekt létrehozásához:HelloPlugin

    dotnet new classlib -o HelloPlugin
    
  2. Futtassa a következő parancsot a projekt megoldáshoz való hozzáadásához AppWithPlugin :

    dotnet sln add HelloPlugin/HelloPlugin.csproj
    
  3. Cserélje le a HelloPlugin/Class1.cs fájlt egy HelloCommand.cs nevű fájlra a következő tartalommal:

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

Most nyissa meg a HelloPlugin.csproj fájlt. Ennek a következőképpen kell kinéznie:

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

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

</Project>

A címkék között <PropertyGroup> adja hozzá a következő elemet:

  <EnableDynamicLoading>true</EnableDynamicLoading>

A <EnableDynamicLoading>true</EnableDynamicLoading> előkészíti a projektet, hogy beépülő modulként lehessen használni. Ez többek között az összes függőségét a projekt kimenetére másolja. További információ: EnableDynamicLoading.

A címkék között <Project> adja hozzá a következő elemeket:

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

Az <Private>false</Private> elem fontos. Ez arra utasítja az MSBuildet, hogy ne másolja PluginBase.dll a HelloPlugin kimeneti könyvtárába. Ha a PluginBase.dll szerelvény megtalálható a kimeneti könyvtárban, ott találja meg a szerelvényt, PluginLoadContext és betölti, amikor betölti a HelloPlugin.dll szerelvényt. Ezen a ponton a HelloPlugin.HelloCommand típus a ICommand projekt kimeneti könyvtárában HelloPlugin található PluginBase.dll fogja implementálni az interfészt, nem pedig az ICommand alapértelmezett betöltési környezetbe betöltött felületet. Mivel a futtatókörnyezet ezt a két típust különböző szerelvénytípusokként látja, a AppWithPlugin.Program.CreateCommands metódus nem találja a parancsokat. Ennek eredményeképpen a <Private>false</Private> metaadatok szükségesek a beépülő modul interfészeit tartalmazó szerelvényre való hivatkozáshoz.

Hasonlóképpen, az <ExcludeAssets>runtime</ExcludeAssets> elem akkor is fontos, ha más PluginBase csomagokra hivatkozik. Ez a beállítás ugyanazzal a hatással rendelkezik, mint <Private>false</Private> a projekt vagy annak egyik függősége által tartalmazott csomaghivatkozásokon PluginBase .

Most, hogy a HelloPlugin projekt befejeződött, frissítenie kell a AppWithPlugin projektet, hogy megtudja, hol található a HelloPlugin beépülő modul. A // Paths to plugins to load megjegyzés után adja hozzá @"HelloPlugin\bin\Debug\net5.0\HelloPlugin.dll" (ez az elérési út a használt .NET Core-verziótól függően eltérő lehet) a pluginPaths tömb elemeként.

Beépülő modul könyvtárfüggőségekkel

Szinte minden beépülő modul összetettebb, mint egy egyszerű ""Helló világ!" alkalmazás", és sok beépülő modul függ más kódtáraktól. A JsonPlugin mintában szereplő és OldJsonPlugin a projektek két példát mutatnak be a beépülő modulokra, és a NuGet-csomag függőségei a következőn Newtonsoft.Json: . Emiatt minden beépülő modulprojektnek hozzá kell adnia <EnableDynamicLoading>true</EnableDynamicLoading> a projekttulajdonságokat, hogy az összes függőséget a kimenetére dotnet buildmásolja. Ha az osztálytárat a következővel dotnet publish teszi közzé, az az összes függőségét is a közzétételi kimenetbe másolja.

A minta egyéb példái

Az oktatóanyag teljes forráskódja megtalálható a dotnet/samples adattárban. A kész minta néhány további példát is tartalmaz a viselkedésre AssemblyDependencyResolver . Az objektum például AssemblyDependencyResolver a natív kódtárakat és a NuGet-csomagokban található honosított műholdszerelvényeket is fel tudja oldani. A UVPlugin és FrenchPlugin a mintaadattár ezeket a forgatókönyveket mutatja be.

Beépülő modul felületének hivatkozása NuGet-csomagból

Tegyük fel, hogy van egy A alkalmazás, amely a nevű NuGet-csomagban definiált beépülő modul felülettel rendelkezik A.PluginBase. Hogyan hivatkozhat helyesen a csomagra a beépülő modul projektjében? Projekthivatkozások esetén a <Private>false</Private> projektfájl elemének ProjectReference metaadatainak használata megakadályozta, hogy a dll átmásolható legyen a kimenetre.

A csomagra való helyes hivatkozáshoz A.PluginBase módosítsa a <PackageReference> projektfájl elemét a következőre:

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

Ez megakadályozza a A.PluginBase szerelvények másolását a beépülő modul kimeneti könyvtárába, és biztosítja, hogy a beépülő modul az A verzióját A.PluginBasehasználja.

Beépülő modul cél-keretrendszerének javaslatai

Mivel a beépülő modul függőségének betöltése a .deps.json fájlt használja, a beépülő modul cél-keretrendszeréhez kapcsolódó gotcha van. A beépülő moduloknak a .NET Standard verziója helyett egy futtatókörnyezetet kell használniuk, például a .NET 5-öt. A .deps.json fájl a projekt céljainak keretrendszere alapján jön létre, és mivel számos .NET Standard-kompatibilis csomag referenciális szerelvényeket szállít a .NET Standard és a megvalósítási szerelvények számára adott futtatókörnyezetekhez, előfordulhat, hogy a .deps.json nem látja megfelelően a megvalósítási szerelvényeket, vagy a várt .NET Core-verzió helyett a szerelvény .NET Standard verzióját fogja meg.

Beépülő modul keretrendszerének referenciái

A beépülő modulok jelenleg nem tudnak új keretrendszereket bevezetni a folyamatba. Nem tölthető be például olyan beépülő modul, amely a Microsoft.AspNetCore.App keretrendszert használja olyan alkalmazásba, amely csak a gyökér Microsoft.NETCore.App keretrendszert használja. A gazdaalkalmazásnak deklarálnia kell a beépülő modulokhoz szükséges összes keretrendszerre mutató hivatkozásokat.