.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
- Telepítse a .NET 5 SDK-t vagy egy újabb verziót.
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:
Hozzon létre egy új mappát, és ebben a mappában futtassa a következő parancsot:
dotnet new console -o AppWithPlugin
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
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 CreateCommands
nevű Program
LoadPlugin
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:
Futtassa a következő parancsot egy új osztálytárprojekt létrehozásához:
HelloPlugin
dotnet new classlib -o HelloPlugin
Futtassa a következő parancsot a projekt megoldáshoz való hozzáadásához
AppWithPlugin
:dotnet sln add HelloPlugin/HelloPlugin.csproj
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 build
má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.PluginBase
haszná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.