Aracılığıyla paylaş


Eklentilerle .NET Core uygulaması oluşturma

Bu öğreticide eklentileri yüklemek için özel bir AssemblyLoadContext nasıl oluşturulacağı gösterilmektedir. Eklentinin bağımlılıklarını çözmek için bir AssemblyDependencyResolver kullanılır. Öğretici, eklenti bağımlılıkları için ayrı bir derleme bağlamı sağlayarak eklentilerle barındırma uygulaması arasında farklı derleme bağımlılıklarına olanak sağlar. Nasıl yapılacağını öğreneceksiniz:

  • Bir projeyi eklentileri destekleyecek şekilde yapılandırma.
  • Her eklentiyi yüklemek için özel bir AssemblyLoadContext oluşturun.
  • Eklentilerin bağımlılıklara sahip olmasını sağlamak için System.Runtime.Loader.AssemblyDependencyResolver türünü kullanın.
  • Sadece derleme çıktıları kopyalanarak kolayca dağıtılabilen eklentiler geliştirin.

Uyarı

Güvenilmeyen kod güvenilir bir .NET işlemine güvenli bir şekilde yüklenemez. Güvenlik veya güvenilirlik sınırı sağlamak için işletim sisteminiz veya sanallaştırma platformunuz tarafından sağlanan bir teknolojiyi göz önünde bulundurun.

Önkoşullar

Uygulamayı 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. Aşağıdaki 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ı için 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öndermek zorunda kalmadan eklenti arabiriminizi bir paket olarak yayımlamanıza olanak tanır.

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

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

        int Execute();
    }
}

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

artık ICommand arabirimi tanımlandığına göre, uygulama projesi biraz daha doldurulabilir. Kök klasörden dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj komutuyla AppWithPlugin projesinden PluginBase projesine bir referans ekleyin.

// Load commands from plugins açıklamasını aşağıdaki kod parçacığıyla değiştirerek belirli dosya yollarından eklentileri yüklemesini sağlayın:

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 // Output the loaded commands açıklamasını 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 LoadPlugin ve CreateCommandsadlı Program sınıfına 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ükleyebilir ve başlatabilir, 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;
        }
    }
}

PluginLoadContext türü AssemblyLoadContext'den türetilmiştir. AssemblyLoadContext türü, geliştiricilerin derleme sürümlerinin çakışmamasını sağlamak için yüklenen derlemeleri farklı gruplar halinde yalıtmasına olanak tanıyan çalışma zamanındaki özel bir türdür. Ayrıca, özel bir AssemblyLoadContext derlemeleri yüklemek ve varsayılan davranışı geçersiz kılmak için farklı yollar seçebilir. PluginLoadContext, derleme adlarını yollara dönüştürmek için .NET Core 3.0'da tanıtılmış olan AssemblyDependencyResolver türünün bir örneğini kullanıyor. AssemblyDependencyResolver nesnesi bir .NET sınıf kitaplığının yolu kullanılarak oluşturulur. Derlemeleri ve yerel kitaplıkları, yolu AssemblyDependencyResolver oluşturucuya geçirilen sınıf kitaplığının .deps.json dosyasına göre göreli yollarına çö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.

AppWithPlugin projesi PluginLoadContext türüne sahip olduğuna göre, Program.LoadPlugin yöntemini aşağıdaki gövdeyle güncelleştirin:

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ı bir PluginLoadContext örneği kullanarak, eklentiler sorun olmadan farklı veya çakışan bağımlılıklara sahip olabilir.

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

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

  1. HelloPluginadlı yeni 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ümüne eklemek için aşağıdaki komutu çalıştırın:

    dotnet sln add HelloPlugin/HelloPlugin.csproj
    
  3. HelloPlugin/Class1.cs dosyasını aşağıdaki içeriklerle HelloCommand.cs adlı bir dosyayla 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. Aşağıdakine benzer görünmelidir:

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

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

</Project>

<PropertyGroup> etiketlerinin 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.

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

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

<Private>false</Private> öğesi önemlidir. Bu, MSBuild'e HelloPlugin için çıkış dizinine PluginBase.dll kopyalamaması gerektiğini bildirir. PluginBase.dll derlemesi çıkış dizininde varsa, PluginLoadContext derlemeyi orada bulur ve HelloPlugin.dll derlemesini yüklediğinde yükler. Bu noktada, HelloPlugin.HelloCommand türü varsayılan yük bağlamı içine yüklenen ICommand arabirimini değil, HelloPlugin projesinin çıkış dizinindeki PluginBase.dllICommand 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, eklenti arabirimlerini içeren derlemeye başvuru için <Private>false</Private> meta verileri gerekir.

Benzer şekilde, PluginBase diğer paketlere başvuruyorsa <ExcludeAssets>runtime</ExcludeAssets> öğesi de önemlidir. Bu ayar, <Private>false</Private> ile aynı etkiye sahiptir, ancak PluginBase projesinin veya bağımlılıklarından birinin içerebileceği paket başvuruları üzerinde çalışır.

HelloPlugin projesi tamamlandıktan sonra, HelloPlugin eklentinin nerede bulunabileceğini öğrenmek için AppWithPlugin projesini güncelleştirmeniz gerekir. // Paths to plugins to load açıklamasının ardından pluginPaths dizisinin öğesi olarak @"HelloPlugin\bin\Debug\net5.0\HelloPlugin.dll" (bu yol kullandığınız .NET Core sürümüne göre farklı olabilir) ekleyin.

Kitaplık bağımlılıklarına sahip eklenti

Neredeyse tüm eklentiler basit bir "Merhaba Dünya" yerine daha karmaşıktır ve birçok eklentinin diğer kitaplıklara bağımlılıkları vardır. Örnekteki JsonPlugin ve OldJsonPlugin projeleri, Newtonsoft.Jsonüzerinde NuGet 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ı dotnet buildçıkışına kopyalaması için proje özelliklerine <EnableDynamicLoading>true</EnableDynamicLoading> eklemesi gerekir. Sınıf kitaplığını dotnet publish ile yayımlamak, tüm bağımlılıklarıyla birlikte yayımlama çıktısına kopyalanmasını sağlar.

Örnekteki diğer örnekler

Bu öğreticinin kaynak kodunun tamamı dotnet/samples deposundabulunabilir. Tamamlanan örnek, AssemblyDependencyResolver davranışına ilişkin birkaç örnek 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. Örnek deposundaki UVPlugin ve FrenchPlugin bu senaryoları gösterir.

NuGet paketinden eklenti arabirimine başvurma

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

A.PluginBase paketine doğru şekilde başvurmak 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, A.PluginBase derlemelerinin eklentinizin çıkış dizinine kopyalanmasını önler ve eklentinizin A'nın A.PluginBasesürümünü kullanmasını sağlar.

Eklenti hedef çerçevesi önerileri

Eklenti bağımlılığı yükleme .deps.json dosyasını kullandığından, eklentinin hedef çerçevesiyle ilgili bir gotcha vardır. Özel olarak, 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 Standart uyumlu paket, belirli çalışma zamanları için .NET Standard ve uygulama derlemelerine karşı derlemeler göndermek için başvuru derlemelerini sevk ettiğinden, .deps.json uygulama derlemelerini doğru şekilde göremeyebilir veya beklediğiniz .NET Core sürümü yerine bir derlemenin .NET Standard sürümünü alabilir.

Eklenti çerçevesi referansları

Eklentiler şu anda sürece yeni çerçeveler ekleyemiyor. Örneğin, Microsoft.AspNetCore.App çerçevesini kullanan bir eklentiyi yalnızca kök Microsoft.NETCore.App çerçevesini kullanan bir uygulamaya yükleyemezsiniz. Ana uygulamanın, eklentiler için gereken tüm çerçevelere referansları bildirmesi gerekir.