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:

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:

  1. Utwórz nowy folder, a w tym folderze uruchom następujące polecenie:

    dotnet new console -o AppWithPlugin
    
  2. 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
    
  3. 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:

  1. Uruchom następujące polecenie, aby utworzyć nowy projekt biblioteki klas o nazwie HelloPlugin:

    dotnet new classlib -o HelloPlugin
    
  2. Uruchom następujące polecenie, aby dodać projekt do AppWithPlugin rozwiązania:

    dotnet sln add HelloPlugin/HelloPlugin.csproj
    
  3. 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.