Eklentilerle .NET Core uygulaması oluşturma

Bu öğreticide eklentileri yüklemek için özel AssemblyLoadContext oluşturma işlemleri gösterilmektedir. AssemblyDependencyResolver eklentinin bağımlılıklarını çözümlemek için kullanılır. Öğretici, eklentinin bağımlılıklarını barındırma uygulamasından doğru şekilde yalıtıyor. Şunları öğrenirsiniz:

  • Bir projeyi eklentileri destekleyecek şekilde yapılandırma.
  • Her eklentiyi yüklemek için bir özel AssemblyLoadContext oluşturun.
  • Eklentilerin System.Runtime.Loader.AssemblyDependencyResolver bağımlılıklara sahip olmasını sağlamak için türünü kullanın.
  • Yalnızca derleme yapıtlarını kopyalayarak kolayca dağıtabileceğiniz eklentiler yazın.

Önkoşullar

Not

Örnek kod .NET 5'i hedefler, ancak kullandığı tüm özellikler .NET Core 3.0'da kullanıma sunulmuştur ve o zamandan beri tüm .NET sürümlerinde kullanılabilir.

Uygulama oluşturma

İlk adım uygulamayı oluşturmaktır:

  1. Yeni bir klasör oluşturun ve bu klasörde aşağıdaki komutu çalıştırın:

    dotnet new console -o AppWithPlugin
    
  2. Projeyi oluşturmayı kolaylaştırmak için aynı klasörde bir Visual Studio çözüm dosyası oluşturun. Şu komutu çalıştırın:

    dotnet new sln
    
  3. Uygulama projesini çözüme eklemek için aşağıdaki komutu çalıştırın:

    dotnet sln add AppWithPlugin/AppWithPlugin.csproj
    

Artık uygulamamızın iskeletini doldurabiliriz. AppWithPlugin/Program.cs dosyasındaki kodu aşağıdaki kodla değiştirin:

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

Eklenti arabirimlerini oluşturma

Eklentilerle uygulama oluşturmanın bir sonraki adımı, eklentilerin uygulaması gereken arabirimi tanımlamaktır. Uygulamanız ve eklentileriniz arasında iletişim kurmak için kullanmayı planladığınız türleri içeren bir sınıf kitaplığı oluşturmanızı öneririz. Bu bölüm, tam uygulamanızı göndermenize gerek kalmadan eklenti arabiriminizi bir paket olarak yayımlamanıza olanak tanır.

Projenin kök klasöründe komutunu çalıştırın dotnet new classlib -o PluginBase. Ayrıca, projeyi çözüm dosyasına eklemek için komutunu çalıştırın dotnet sln add PluginBase/PluginBase.csproj . PluginBase/Class1.cs Dosyayı silin ve adlı klasörde ICommand.cs aşağıdaki arabirim tanımıyla yeni bir dosya PluginBase oluşturun:

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

        int Execute();
    }
}

Bu ICommand arabirim, tüm eklentilerin uygulayacağı arabirimdir.

Artık arabirim tanımlandığına ICommand göre, uygulama projesi biraz daha doldurulabilir. Kök klasörden AppWithPlugin komutuyla dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj projeden PluginBase projeye bir başvuru ekleyin.

// Load commands from plugins Belirli dosya yollarından eklentileri yüklemesini sağlamak için açıklamayı aşağıdaki kod parçacığıyla değiştirin:

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

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

Ardından açıklamasını // Output the loaded commands aşağıdaki kod parçacığıyla değiştirin:

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

// Execute the command with the name passed as an argument Açıklamasını aşağıdaki kod parçacığıyla değiştirin:

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

command.Execute();

Son olarak, burada gösterildiği gibi ve CreateCommandsadlı LoadPlugin sınıfına Program statik yöntemler ekleyin:

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

Eklentileri yükleme

Artık uygulama, yüklenen eklenti derlemelerinden komutları doğru bir şekilde yükleyip örnekleyebilir, ancak eklenti derlemelerini yükleyemiyor. AppWithPlugin klasöründe aşağıdaki içeriklere sahip PluginLoadContext.cs adlı bir dosya oluşturun:

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

türü PluginLoadContext türünden AssemblyLoadContexttüretilir. Tür AssemblyLoadContext , geliştiricilerin derleme sürümlerinin çakışmaması için yüklenen derlemeleri farklı gruplar halinde yalıtmasına olanak tanıyan çalışma zamanındaki özel bir türüdür. Ayrıca bir özel AssemblyLoadContext , derlemeleri yüklemek ve varsayılan davranışı geçersiz kılmak için farklı yollar seçebilir. , PluginLoadContext derleme adlarını yollara çözümlemek için .NET Core 3.0'da tanıtılan türün bir örneğini AssemblyDependencyResolver kullanır. AssemblyDependencyResolver nesnesi bir .NET sınıf kitaplığının yolu ile oluşturulur. Derlemeleri ve yerel kitaplıkları, yolu oluşturucuya geçirilen sınıf kitaplığının .deps.json dosyasına göre göreli yollarına AssemblyDependencyResolver çözümler. Özel AssemblyLoadContext , eklentilerin kendi bağımlılıklarına sahip olmasını sağlar ve AssemblyDependencyResolver bağımlılıkları doğru şekilde yüklemeyi kolaylaştırır.

Artık proje türüne AppWithPluginPluginLoadContext sahip olduğuna göre yöntemini aşağıdaki gövdeyle güncelleştirin Program.LoadPlugin :

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

Her eklenti için farklı PluginLoadContext bir örnek kullanıldığında, eklentiler sorun olmadan farklı ve hatta çakışan bağımlılıklara sahip olabilir.

Bağımlılıkları olmayan basit eklenti

Kök klasöre geri döndüğünüzde aşağıdakileri yapın:

  1. adlı HelloPluginyeni bir sınıf kitaplığı projesi oluşturmak için aşağıdaki komutu çalıştırın:

    dotnet new classlib -o HelloPlugin
    
  2. Projeyi AppWithPlugin çözüme eklemek için aşağıdaki komutu çalıştırın:

    dotnet sln add HelloPlugin/HelloPlugin.csproj
    
  3. HelloPlugin/Class1.cs dosyasını HelloCommand.cs adlı bir dosyayla aşağıdaki içerikle değiştirin:

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

Şimdi HelloPlugin.csproj dosyasını açın. Şunun gibi görünmelidir:

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

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

</Project>

Etiketlerin <PropertyGroup> arasına aşağıdaki öğeyi ekleyin:

  <EnableDynamicLoading>true</EnableDynamicLoading>

, <EnableDynamicLoading>true</EnableDynamicLoading> eklenti olarak kullanılabilmesi için projeyi hazırlar. Diğer şeylerin yanında, bu işlem tüm bağımlılıklarını projenin çıkışına kopyalar. Diğer ayrıntılar için bkz: EnableDynamicLoading.

Etiketlerin <Project> arasına aşağıdaki öğeleri ekleyin:

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

<Private>false</Private> Öğe önemlidir. Bu, MSBuild'e PluginBase.dll HelloPlugin çıkış dizinine kopyalamaması gerektiğini bildirir. PluginBase.dll derlemesi çıkış dizininde varsa, PluginLoadContext derlemeyi orada bulur ve HelloPlugin.dll derlemeyi yüklerken yükler. Bu noktada türü, HelloPlugin.HelloCommand varsayılan yük bağlamı ICommand içine yüklenen arabirimi değilICommand, projenin çıkış dizinindeki HelloPluginPluginBase.dll arabirimini uygular. Çalışma zamanı bu iki türü farklı derlemelerden farklı türler olarak gördüğünden AppWithPlugin.Program.CreateCommands , yöntemi komutları bulamaz. Sonuç olarak, <Private>false</Private> eklenti arabirimlerini içeren derlemeye başvuru için meta veriler gereklidir.

Benzer şekilde, <ExcludeAssets>runtime</ExcludeAssets> öğe diğer paketlere PluginBase başvuruyorsa da önemlidir. Bu ayar, <Private>false</Private> projenin veya bağımlılıklarından birinin içerebileceği paket başvuruları PluginBase üzerinde çalışır.

Proje tamamlandıktan sonra eklentinin HelloPluginAppWithPlugin nerede HelloPlugin bulunabileceğini öğrenmek için projeyi güncelleştirmeniz gerekir. Açıklamadan // Paths to plugins to load sonra dizinin bir öğesi pluginPaths olarak öğesini ekleyin @"HelloPlugin\bin\Debug\net5.0\HelloPlugin.dll" (bu yol kullandığınız .NET Core sürümüne göre farklı olabilir).

Kitaplık bağımlılıkları içeren eklenti

Neredeyse tüm eklentiler basit bir "Merhaba Dünya" değerinden daha karmaşıktır ve birçok eklentinin diğer kitaplıklara bağımlılıkları vardır. JsonPlugin Örnekteki ve OldJsonPlugin projeleri, üzerinde Newtonsoft.JsonNuGet paket bağımlılıklarına sahip iki eklenti örneği gösterir. Bu nedenle, tüm eklenti projelerinin tüm bağımlılıklarını çıkışına dotnet buildkopyalaması için proje özelliklerine eklemesi <EnableDynamicLoading>true</EnableDynamicLoading> gerekir. ile dotnet publish sınıf kitaplığını yayımlamak da tüm bağımlılıklarını yayımlama çıkışına kopyalar.

Örnekteki diğer örnekler

Bu öğreticinin tam kaynak kodu dotnet/samples deposunda bulunabilir. Tamamlanan örnek birkaç davranış örneği AssemblyDependencyResolver daha içerir. Örneğin, AssemblyDependencyResolver nesnesi yerel kitaplıkların yanı sıra NuGet paketlerine dahil edilen yerelleştirilmiş uydu derlemelerini de çözümleyebilir. UVPlugin örnek deposundaki ve FrenchPlugin bu senaryoları gösterir.

NuGet paketinden eklenti arabirimine başvurma

Adlı A.PluginBaseNuGet paketinde tanımlanmış eklenti arabirimine sahip bir uygulama A olduğunu varsayalım. Eklenti projenizde pakete nasıl doğru başvurursunuz? Proje başvuruları için, proje dosyasındaki ProjectReference öğesinde meta verilerin kullanılması <Private>false</Private> dll dosyasının çıkışa kopyalanmasını engelledi.

Pakete doğru şekilde başvurmak A.PluginBase için proje dosyasındaki <PackageReference> öğesini aşağıdakiyle değiştirmek istiyorsunuz:

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

Bu, derlemelerin A.PluginBase eklentinizin çıkış dizinine kopyalanmasını önler ve eklentinizin A'nın sürümünü A.PluginBasekullanmasını sağlar.

Eklenti hedef çerçevesi önerileri

Eklenti bağımlılığı yüklemesi .deps.json dosyasını kullandığından, eklentinin hedef çerçevesiyle ilgili bir gotcha vardır. Özellikle, eklentileriniz .NET Standard sürümü yerine .NET 5 gibi bir çalışma zamanını hedeflemelidir. .deps.json dosyası, projenin hedeflediği çerçeveye göre oluşturulur ve birçok .NET Standard uyumlu paket, .NET Standard'a ve belirli çalışma zamanları için uygulama derlemelerine yönelik başvuru derlemeleri sevk ettiğinden, .deps.json uygulama derlemelerini doğru göremeyebilir veya beklediğiniz .NET Core sürümü yerine bir derlemenin .NET Standard sürümünü alabilir.

Eklenti çerçevesi başvuruları

Eklentiler şu anda sürece yeni çerçeveler ekleyemiyor. Örneğin, çerçeveyi kullanan bir eklentiyi yalnızca kök Microsoft.NETCore.App çerçeveyi Microsoft.AspNetCore.App kullanan bir uygulamaya yükleyemezsiniz. Konak uygulamasının eklentiler tarafından gereken tüm çerçevelere başvuru bildirmesi gerekir.