Megosztás a következőn keresztül:


.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 külön szerelvénykörnyezetet biztosít a beépülő modul függőségeihez, lehetővé téve a beépülő modulok és az üzemeltetési alkalmazás különböző szerelvényfüggőségeit. Megtudhatja, hogyan:

  • Projekt strukturálása beépülő modulok támogatásához.
  • Hozzon létre egy egyedi AssemblyLoadContext az egyes beépülő modulok betöltéséhez.
  • A System.Runtime.Loader.AssemblyDependencyResolver típust használja a beépülő modulok függőségeinek megengedésére.
  • Olyan beépülő modulok készítése, amelyek egyszerűen üzembe helyezhetők a build-artifaktumok másolásával.

Megjegyzés

A nem megbízható kód nem tölthető be biztonságosan egy megbízható .NET-folyamatba. A biztonság vagy a megbízhatóság határának biztosításához fontolja meg az operációs rendszer vagy a virtualizálási platform által biztosított technológiát.

Előfeltételek

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 a következő 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 való 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: . Futtassa a dotnet sln add PluginBase/PluginBase.csproj parancsot is 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 következő felületdefinícióval elnevezett ICommand.cs mappában:

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

        int Execute();
    }
}

Ez a ICommand interfész az, amelyet az összes beépülő modul megvalósít.

Most, hogy a ICommand felület definiálva van, az alkalmazásprojektet egy kicsit jobban ki lehet tölteni. 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 // Load commands from plugins megjegyzést a következő kódrészletre, hogy lehetővé tegye 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 // Output the loaded commands megjegyzést a következő kódrészletre:

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

Cserélje le a // Execute the command with the name passed as an argument megjegyzést 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 Program nevesített LoadPlugin osztályhoz, és CreateCommandsaz itt látható módon:

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

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

Az alkalmazás most már képes megfelelően betölteni és példányosítani a parancsokat a betöltött beépülő modul-szerelvényekből, de továbbra sem tudja betölteni a beépülő modul szerelvé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, hogy a betöltött szerelvényeket különböző csoportokba különítsék el, hogy a szerelvényverziók ne ütközhessenek. Továbbá, egy egyedi AssemblyLoadContext különböző útvonalakat választhat a szerelvények betöltéséhez, és felülírhatja az alapértelmezett viselkedést. A PluginLoadContext a AssemblyDependencyResolver típus egy példányát használja, amely a .NET Core 3.0-ban lett bevezetve, a szerelvénynevek elérési utakra való feloldására. Az AssemblyDependencyResolver objektum egy .NET-osztálytár elérési útjával jön létre. 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 elérési útját a konstruktornak AssemblyDependencyResolver adták át. Az egyéni AssemblyLoadContext lehetővé teszi, hogy a beépülő modulok saját függőségekkel rendelkezzenek, és AssemblyDependencyResolver megkönnyíti a függőségek helyes betöltését.

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

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ába visszatérve tegye a következőket:

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

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

    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. A következőhöz hasonlóan kell kinéznie:

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

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

</Project>

A <PropertyGroup> címkék között 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 használható legyen. Ez többek között az összes függőségét a projekt kimenetére másolja. További információ: EnableDynamicLoading.

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

<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 aPluginBase.dll a HelloPlugin kimeneti könyvtárába. Ha a PluginBase.dll összeállítás megtalálható a kimeneti könyvtárban, PluginLoadContext betölti, amikor betölti a HelloPlugin.dll összeállítást. Ezen a ponton a HelloPlugin.HelloCommand típus a PluginBase.dll illesztőfelületet fogja implementálni a HelloPlugin projekt kimeneti könyvtárában, nem pedig az ICommand felületet, amely az alapértelmezett terhelési környezetbe van betöltve. 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 felületeit 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 ugyanolyan hatással van, mint <Private>false</Private>, de a projekt vagy annak valamely függősége által tartalmazott csomaghivatkozások esetén működik 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ű "Hello World", és sok beépülő modul függőségekkel rendelkezik más kódtáraktól. A mintában szereplő JsonPlugin és OldJsonPlugin projektek két példát mutatnak be a Newtonsoft.Json-en keresztül NuGet-csomag függőségekkel rendelkező beépülő modulokra. Emiatt minden beépülő modulprojektnek hozzá kell adnia <EnableDynamicLoading>true</EnableDynamicLoading>-t a projekttulajdonságokhoz, hogy az összes függőséget dotnet build kimenetére másolja. Az osztálytár dotnet publish közzététele az összes függőségét is a közzétételi kimenetbe másolja.

További példák a mintában

Az oktatóanyag teljes forráskódja megtalálható a dotnet/samples adattárban. A kész minta néhány más viselkedési AssemblyDependencyResolver példát is tartalmaz. Az objektum például képes feloldani a AssemblyDependencyResolver natív kódtárakat és a NuGet-csomagokban található honosított műholdas szerelvényeket is. A UVPlugin és a FrenchPlugin mintatárban ezek a forgatókönyvek kerülnek bemutatásra.

Beépülő modul felületére hivatkozik egy NuGet-csomagból

Tegyük fel, hogy van egy A alkalmazás, amely a NuGet-csomagban definiált beépülő modul felülettel rendelkezik A.PluginBase. Hogyan hivatkozhat helyesen a csomagra a beépülő modulprojektben? Projekthivatkozások esetén a <Private>false</Private> projektfájlban lévő ProjectReference elem metaadatainak használata megakadályozta a dll kimenetbe másolását.

Ahhoz, hogy helyesen hivatkozzon a A.PluginBase csomagra, módosítania kell a <PackageReference> elemet a projekt fájlban 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 A.PluginBase változatát használja.

Beépülő modul cél-keretrendszerének ajánlásai

Mivel a beépülő modul függőségeinek betöltése a .deps.json fájlt használja, van egy buktató a beépülő modul célkeretrendszeréhez kapcsolódóan. A beépülő moduloknak a .NET Standard verziója helyett egy futtatókörnyezetet, például a .NET 5-öt kell céloznia. 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 az implementációs 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 egy szerelvény .NET Standard verzióját fogja meg.

A beépülő modul keretrendszer hivatkozásai

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ő modulok által igényelt összes keretrendszerre mutató hivatkozásokat.