Tworzenie aplikacji platformy .NET Core za pomocą wtyczek
W tym samouczku pokazano, jak utworzyć niestandardowe AssemblyLoadContext wtyczki do ładowania. Element AssemblyDependencyResolver służy do rozwiązywania zależności wtyczki. Samouczek poprawnie izoluje zależności wtyczki od aplikacji hostingu. Omawiane tematy:
- Tworzenie struktury projektu do obsługi wtyczek.
- Utwórz niestandardowy element AssemblyLoadContext , aby załadować każdą wtyczkę.
- System.Runtime.Loader.AssemblyDependencyResolver Użyj typu , aby zezwolić wtyczkom na zależności.
- Twórz wtyczki, które można łatwo wdrożyć, kopiując tylko artefakty kompilacji.
Wymagania wstępne
- Zainstaluj zestaw .NET 5 SDK lub nowszą wersję.
Uwaga
Przykładowy kod jest przeznaczony dla platformy .NET 5, ale wszystkie używane funkcje zostały wprowadzone na platformie .NET Core 3.0 i są dostępne we wszystkich wersjach platformy .NET od tego czasu.
Tworzenie aplikacji
Pierwszym krokiem jest utworzenie aplikacji:
Utwórz nowy folder, a w tym folderze uruchom następujące polecenie:
dotnet new console -o AppWithPlugin
Aby ułatwić tworzenie projektu, utwórz plik rozwiązania programu Visual Studio w tym samym folderze. Uruchom następujące polecenie:
dotnet new sln
Uruchom następujące polecenie, aby dodać projekt aplikacji do rozwiązania:
dotnet sln add AppWithPlugin/AppWithPlugin.csproj
Teraz możemy wypełnić szkielet naszej aplikacji. Zastąp kod w pliku AppWithPlugin/Program.cs następującym kodem:
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);
}
}
}
}
Tworzenie interfejsów wtyczek
Następnym krokiem tworzenia aplikacji z wtyczkami jest zdefiniowanie interfejsu, który należy zaimplementować wtyczki. Zalecamy utworzenie biblioteki klas zawierającej wszelkie typy, które mają być używane do komunikowania się między aplikacją i wtyczkami. Ten podział umożliwia publikowanie interfejsu wtyczki jako pakietu bez konieczności dostarczania pełnej aplikacji.
W folderze głównym projektu uruchom polecenie dotnet new classlib -o PluginBase
. Uruchom również polecenie dotnet sln add PluginBase/PluginBase.csproj
, aby dodać projekt do pliku rozwiązania. PluginBase/Class1.cs
Usuń plik i utwórz nowy plik w PluginBase
folderze o nazwie ICommand.cs
z następującą definicją interfejsu:
namespace PluginBase
{
public interface ICommand
{
string Name { get; }
string Description { get; }
int Execute();
}
}
Ten ICommand
interfejs jest interfejsem, który zostaną zaimplementowane przez wszystkie wtyczki.
Po zdefiniowaniu interfejsu ICommand
projekt aplikacji może zostać wypełniony nieco więcej. Dodaj odwołanie z AppWithPlugin
projektu do PluginBase
projektu za dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj
pomocą polecenia z folderu głównego.
Zastąp // Load commands from plugins
komentarz następującym fragmentem kodu, aby umożliwić ładowanie wtyczek z podanych ścieżek plików:
string[] pluginPaths = new string[]
{
// Paths to plugins to load.
};
IEnumerable<ICommand> commands = pluginPaths.SelectMany(pluginPath =>
{
Assembly pluginAssembly = LoadPlugin(pluginPath);
return CreateCommands(pluginAssembly);
}).ToList();
Następnie zastąp // Output the loaded commands
komentarz następującym fragmentem kodu:
foreach (ICommand command in commands)
{
Console.WriteLine($"{command.Name}\t - {command.Description}");
}
Zastąp // Execute the command with the name passed as an argument
komentarz następującym fragmentem kodu:
ICommand command = commands.FirstOrDefault(c => c.Name == commandName);
if (command == null)
{
Console.WriteLine("No such command is known.");
return;
}
command.Execute();
Na koniec dodaj metody statyczne do Program
klasy o nazwie LoadPlugin
i CreateCommands
, jak pokazano poniżej:
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}");
}
}
Ładowanie wtyczek
Teraz aplikacja może poprawnie załadować i utworzyć wystąpienia poleceń z załadowanych zestawów wtyczek, ale nadal nie może załadować zestawów wtyczek. Utwórz plik o nazwie PluginLoadContext.cs w folderze AppWithPlugin o następującej zawartości:
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
pochodzi z klasy AssemblyLoadContext. Typ AssemblyLoadContext
jest specjalnym typem w środowisku uruchomieniowym, który umożliwia deweloperom izolowanie załadowanych zestawów do różnych grup w celu zapewnienia, że wersje zestawów nie powodują konfliktu. Ponadto niestandardowy AssemblyLoadContext
może wybrać różne ścieżki, aby załadować zestawy z i zastąpić domyślne zachowanie. Używa PluginLoadContext
wystąpienia typu wprowadzonego AssemblyDependencyResolver
na platformie .NET Core 3.0 do rozpoznawania nazw zestawów do ścieżek. Obiekt AssemblyDependencyResolver
jest konstruowany ze ścieżką do biblioteki klas platformy .NET. Rozpoznaje zestawy i biblioteki natywne do ich ścieżek względnych na podstawie pliku deps.json dla biblioteki klas, której ścieżka została przekazana do konstruktora AssemblyDependencyResolver
. AssemblyLoadContext
Niestandardowe umożliwia wtyczkom posiadanie własnych zależności i AssemblyDependencyResolver
ułatwia poprawne ładowanie zależności.
Teraz, gdy AppWithPlugin
projekt ma PluginLoadContext
typ, zaktualizuj metodę Program.LoadPlugin
przy użyciu następującej treści:
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)));
}
Używając innego PluginLoadContext
wystąpienia dla każdej wtyczki, wtyczki mogą mieć różne lub nawet sprzeczne zależności bez problemu.
Prosta wtyczka bez zależności
W folderze głównym wykonaj następujące czynności:
Uruchom następujące polecenie, aby utworzyć nowy projekt biblioteki klas o nazwie
HelloPlugin
:dotnet new classlib -o HelloPlugin
Uruchom następujące polecenie, aby dodać projekt do
AppWithPlugin
rozwiązania:dotnet sln add HelloPlugin/HelloPlugin.csproj
Zastąp plik HelloPlugin/Class1.cs plikiem o nazwie HelloCommand.cs następującą zawartością:
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;
}
}
}
Teraz otwórz plik HelloPlugin.csproj . Zawartość okna powinna wyglądać mniej więcej tak:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
</Project>
Między tagami <PropertyGroup>
dodaj następujący element:
<EnableDynamicLoading>true</EnableDynamicLoading>
Program <EnableDynamicLoading>true</EnableDynamicLoading>
przygotowuje projekt, aby można go było użyć jako wtyczki. Między innymi spowoduje to skopiowanie wszystkich jego zależności do danych wyjściowych projektu. Aby uzyskać więcej informacji, zobacz EnableDynamicLoading
.
Między tagami <Project>
dodaj następujące elementy:
<ItemGroup>
<ProjectReference Include="..\PluginBase\PluginBase.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>
</ProjectReference>
</ItemGroup>
Element <Private>false</Private>
jest ważny. Dzięki temu program MSBuild nie kopiuje PluginBase.dll do katalogu wyjściowego helloPlugin. Jeśli zestaw PluginBase.dll znajduje się w katalogu wyjściowym, PluginLoadContext
znajdzie tam zestaw i załaduje go podczas ładowania zestawu HelloPlugin.dll . W tym momencie HelloPlugin.HelloCommand
typ zaimplementuje ICommand
interfejs z PluginBase.dll w katalogu wyjściowym HelloPlugin
projektu, a nie ICommand
interfejs ładowany do domyślnego kontekstu ładowania. Ponieważ środowisko uruchomieniowe widzi te dwa typy jako różne typy z różnych zestawów, AppWithPlugin.Program.CreateCommands
metoda nie znajdzie poleceń. W związku z tym <Private>false</Private>
metadane są wymagane do odwołania do zestawu zawierającego interfejsy wtyczki.
Podobnie element jest również ważny, <ExcludeAssets>runtime</ExcludeAssets>
jeśli odwołuje się do PluginBase
innych pakietów. To ustawienie ma taki sam wpływ, jak <Private>false</Private>
w przypadku odwołań do pakietu, które PluginBase
mogą obejmować projekt lub jedną z jego zależności.
Po zakończeniu HelloPlugin
projektu należy zaktualizować AppWithPlugin
projekt, aby wiedzieć, gdzie można znaleźć wtyczkę HelloPlugin
. Po komentarzu // Paths to plugins to load
dodaj @"HelloPlugin\bin\Debug\net5.0\HelloPlugin.dll"
(ta ścieżka może być inna w zależności od używanej wersji platformy .NET Core) jako element tablicy pluginPaths
.
Wtyczka z zależnościami biblioteki
Prawie wszystkie wtyczki są bardziej złożone niż proste "Hello world", a wiele wtyczek ma zależności od innych bibliotek. Projekty JsonPlugin
i OldJsonPlugin
w przykładzie pokazują dwa przykłady wtyczek z zależnościami pakietów NuGet w systemie Newtonsoft.Json
. W związku z tym wszystkie projekty wtyczek powinny zostać dodane <EnableDynamicLoading>true</EnableDynamicLoading>
do właściwości projektu, aby skopiować wszystkie zależności do danych wyjściowych polecenia dotnet build
. Opublikowanie biblioteki klas za pomocą polecenia dotnet publish
spowoduje również skopiowanie wszystkich jej zależności do danych wyjściowych publikowania.
Inne przykłady w przykładzie
Kompletny kod źródłowy tego samouczka można znaleźć w repozytorium dotnet/samples. Ukończony przykład zawiera kilka innych przykładów AssemblyDependencyResolver
zachowania. Na przykład AssemblyDependencyResolver
obiekt może również rozpoznawać biblioteki natywne, a także zlokalizowane zestawy satelitarne zawarte w pakietach NuGet. Repozytorium UVPlugin
przykładów i FrenchPlugin
demonstruje te scenariusze.
Odwoływanie się do interfejsu wtyczki z pakietu NuGet
Załóżmy, że istnieje aplikacja A, która ma interfejs wtyczki zdefiniowany w pakiecie NuGet o nazwie A.PluginBase
. Jak poprawnie odwołujesz się do pakietu w projekcie wtyczki? W przypadku odwołań do projektu użycie <Private>false</Private>
metadanych w ProjectReference
elemencie w pliku projektu uniemożliwiło skopiowanie biblioteki DLL do danych wyjściowych.
Aby poprawnie odwołać się A.PluginBase
do pakietu, należy zmienić <PackageReference>
element w pliku projektu na następujące:
<PackageReference Include="A.PluginBase" Version="1.0.0">
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
Zapobiega to kopiowaniu A.PluginBase
zestawów do katalogu wyjściowego wtyczki i zapewnia, że wtyczka będzie używać wersji A .A.PluginBase
Zalecenia dotyczące platformy docelowej wtyczki
Ponieważ ładowanie zależności wtyczki używa pliku deps.json , istnieje gotcha powiązana ze strukturą docelową wtyczki. W szczególności wtyczki powinny być przeznaczone dla środowiska uruchomieniowego, takiego jak .NET 5, zamiast wersji platformy .NET Standard. Plik deps.json jest generowany na podstawie struktury docelowej projektu, a ponieważ wiele pakietów zgodnych z platformą .NET Standard dostarcza zestawy referencyjne do kompilowania względem platformy .NET Standard i zestawów implementacji dla określonych środowisk uruchomieniowych, plik deps.json może nie widzieć poprawnie zestawów implementacji lub może pobrać wersję zestawu .NET Standard zamiast oczekiwanej wersji platformy .NET Core.
Odwołania do struktury wtyczek
Obecnie wtyczki nie mogą wprowadzać nowych struktur do procesu. Na przykład nie można załadować wtyczki korzystającej ze Microsoft.AspNetCore.App
struktury do aplikacji, która używa tylko struktury głównej Microsoft.NETCore.App
. Aplikacja hosta musi zadeklarować odwołania do wszystkich struktur wymaganych przez wtyczki.