Membuat aplikasi .NET Core dengan plugin

Tutorial ini menunjukkan kepada Anda cara membuat kustom AssemblyLoadContext untuk memuat plugin. AssemblyDependencyResolver Digunakan untuk menyelesaikan dependensi plugin. Tutorial ini mengisolasi dependensi plugin dengan benar dari aplikasi hosting. Anda akan mempelajari cara:

Prasyarat

Catatan

Kode sampel menargetkan .NET 5, tetapi semua fitur yang digunakannya diperkenalkan di .NET Core 3.0 dan tersedia di semua rilis .NET sejak saat itu.

Buat aplikasi

Langkah pertama adalah membuat aplikasi:

  1. Buat folder baru, dan di folder tersebut jalankan perintah berikut:

    dotnet new console -o AppWithPlugin
    
  2. Untuk mempermudah pembuatan proyek, buat file solusi Visual Studio di folder yang sama. Jalankan perintah berikut:

    dotnet new sln
    
  3. Jalankan perintah berikut untuk menambahkan proyek aplikasi ke solusi:

    dotnet sln add AppWithPlugin/AppWithPlugin.csproj
    

Sekarang kita dapat mengisi kerangka aplikasi kita. Ganti kode dalam file AppWithPlugin/Program.cs dengan kode berikut:

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

Membuat antarmuka plugin

Langkah selanjutnya dalam membangun aplikasi dengan plugin adalah menentukan antarmuka yang perlu diterapkan plugin. Kami menyarankan agar Anda membuat pustaka kelas yang berisi jenis apa pun yang Anda rencanakan untuk digunakan untuk berkomunikasi antara aplikasi dan plugin Anda. Divisi ini memungkinkan Anda untuk menerbitkan antarmuka plugin Anda sebagai paket tanpa harus mengirim aplikasi lengkap Anda.

Di folder akar proyek, jalankan dotnet new classlib -o PluginBase. Selain itu, jalankan dotnet sln add PluginBase/PluginBase.csproj untuk menambahkan proyek ke file solusi. PluginBase/Class1.cs Hapus file, dan buat file baru di folder bernama PluginBaseICommand.cs dengan definisi antarmuka berikut:

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

        int Execute();
    }
}

Antarmuka ini ICommand adalah antarmuka yang akan diterapkan oleh semua plugin.

Sekarang setelah ICommand antarmuka ditentukan, proyek aplikasi dapat diisi sedikit lebih banyak. Tambahkan referensi dari AppWithPlugin proyek ke PluginBase proyek dengan dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj perintah dari folder akar.

// Load commands from plugins Ganti komentar dengan cuplikan kode berikut untuk mengaktifkannya memuat plugin dari jalur file tertentu:

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

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

Kemudian ganti // Output the loaded commands komentar dengan cuplikan kode berikut:

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

// Execute the command with the name passed as an argument Ganti komentar dengan cuplikan berikut:

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

command.Execute();

Dan akhirnya, tambahkan metode statis ke kelas bernama LoadPlugin dan CreateCommands, seperti yang Program ditunjukkan di sini:

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

Muat plugin

Sekarang aplikasi dapat memuat dan membuat instans perintah dengan benar dari rakitan plugin yang dimuat, tetapi masih tidak dapat memuat rakitan plugin. Buat file bernama PluginLoadContext.cs di folder AppWithPlugin dengan konten berikut:

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

Jenis berasal PluginLoadContext dari AssemblyLoadContext. Jenis ini AssemblyLoadContext adalah jenis khusus dalam runtime yang memungkinkan pengembang untuk mengisolasi rakitan yang dimuat ke dalam grup yang berbeda untuk memastikan bahwa versi perakitan tidak bertentangan. Selain itu, kustom AssemblyLoadContext dapat memilih jalur yang berbeda untuk memuat rakitan dari dan mengambil alih perilaku default. PluginLoadContext menggunakan instans jenis yang AssemblyDependencyResolver diperkenalkan di .NET Core 3.0 untuk mengatasi nama rakitan ke jalur. Objek AssemblyDependencyResolver dibangun dengan jalur ke pustaka kelas .NET. Ini menyelesaikan rakitan dan pustaka asli ke jalur relatif mereka berdasarkan file .deps.json untuk pustaka kelas yang jalurnya diteruskan ke AssemblyDependencyResolver konstruktor. Kustom AssemblyLoadContext memungkinkan plugin untuk memiliki dependensi mereka sendiri, dan AssemblyDependencyResolver membuatnya mudah untuk memuat dependensi dengan benar.

Sekarang setelah AppWithPlugin proyek memiliki PluginLoadContext jenisnya, perbarui Program.LoadPlugin metode dengan isi berikut:

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

Dengan menggunakan instans yang berbeda PluginLoadContext untuk setiap plugin, plugin dapat memiliki dependensi yang berbeda atau bahkan bertentangan tanpa masalah.

Plugin sederhana tanpa dependensi

Kembali ke folder akar, lakukan hal berikut:

  1. Jalankan perintah berikut untuk membuat proyek pustaka kelas baru bernama HelloPlugin:

    dotnet new classlib -o HelloPlugin
    
  2. Jalankan perintah berikut untuk menambahkan proyek ke AppWithPlugin solusi:

    dotnet sln add HelloPlugin/HelloPlugin.csproj
    
  3. Ganti file HelloPlugin/Class1.cs dengan file bernama HelloCommand.cs dengan konten berikut:

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

Sekarang, buka file HelloPlugin.csproj . Tampilannya akan terlihat seperti berikut:

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

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

</Project>

Di antara <PropertyGroup> tag, tambahkan elemen berikut:

  <EnableDynamicLoading>true</EnableDynamicLoading>

<EnableDynamicLoading>true</EnableDynamicLoading> menyiapkan proyek sehingga dapat digunakan sebagai plugin. Antara lain, ini akan menyalin semua dependensinya ke output proyek. Untuk detail selengkapnya, lihat EnableDynamicLoading.

Di antara <Project> tag, tambahkan elemen berikut:

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

Elemen <Private>false</Private> ini penting. Ini memberi tahu MSBuild untuk tidak menyalin PluginBase.dll ke direktori output untuk HelloPlugin. Jika rakitan PluginBase.dll ada di direktori output, PluginLoadContext akan menemukan rakitan di sana dan memuatnya saat memuat rakitan HelloPlugin.dll . Pada titik ini, HelloPlugin.HelloCommand jenis akan mengimplementasikan ICommand antarmuka dari PluginBase.dll di direktori HelloPlugin output proyek, bukan ICommand antarmuka yang dimuat ke dalam konteks beban default. Karena runtime melihat kedua jenis ini sebagai jenis yang berbeda dari rakitan yang berbeda, AppWithPlugin.Program.CreateCommands metode tidak akan menemukan perintah. Akibatnya, <Private>false</Private> metadata diperlukan untuk referensi ke assembly yang berisi antarmuka plugin.

Demikian pula, <ExcludeAssets>runtime</ExcludeAssets> elemen ini juga penting jika mereferensikan PluginBase paket lain. Pengaturan ini memiliki efek yang sama seperti <Private>false</Private> tetapi berfungsi pada referensi paket yang mungkin disertakan PluginBase oleh proyek atau salah satu dependensinya.

HelloPlugin Setelah proyek selesai, Anda harus memperbarui AppWithPlugin proyek untuk mengetahui di HelloPlugin mana plugin dapat ditemukan. // Paths to plugins to load Setelah komentar, tambahkan @"HelloPlugin\bin\Debug\net5.0\HelloPlugin.dll" (jalur ini bisa berbeda berdasarkan versi .NET Core yang Anda gunakan) sebagai elemen pluginPaths array.

Plugin dengan dependensi pustaka

Hampir semua plugin lebih kompleks daripada "Halo Dunia" sederhana, dan banyak plugin memiliki dependensi pada pustaka lain. Proyek JsonPlugin dan OldJsonPlugin dalam sampel menunjukkan dua contoh plugin dengan dependensi paket NuGet pada Newtonsoft.Json. Karena itu, semua proyek plugin harus ditambahkan <EnableDynamicLoading>true</EnableDynamicLoading> ke properti proyek sehingga mereka menyalin semua dependensi mereka ke output .dotnet build Menerbitkan pustaka kelas dengan dotnet publish juga akan menyalin semua dependensinya ke output penerbitan.

Contoh lain dalam sampel

Kode sumber lengkap untuk tutorial ini dapat ditemukan di repositori dotnet/samples. Sampel yang telah selesai mencakup beberapa contoh AssemblyDependencyResolver perilaku lainnya. Misalnya, AssemblyDependencyResolver objek juga dapat menyelesaikan pustaka asli serta rakitan satelit yang dilokalkan yang disertakan dalam paket NuGet. UVPlugin dan FrenchPlugin dalam repositori sampel menunjukkan skenario ini.

Mereferensikan antarmuka plugin dari paket NuGet

Katakanlah ada aplikasi A yang memiliki antarmuka plugin yang ditentukan dalam paket NuGet bernama A.PluginBase. Bagaimana Anda mereferensikan paket dengan benar dalam proyek plugin Anda? Untuk referensi proyek, menggunakan <Private>false</Private> metadata pada ProjectReference elemen dalam file proyek mencegah dll disalin ke output.

Untuk mereferensikan A.PluginBase paket dengan benar, Anda ingin mengubah <PackageReference> elemen dalam file proyek menjadi berikut:

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

Ini mencegah A.PluginBase rakitan disalin ke direktori output plugin Anda dan memastikan bahwa plugin Anda akan menggunakan versi A .A.PluginBase

Rekomendasi kerangka kerja target plugin

Karena pemuatan dependensi plugin menggunakan file .deps.json , ada gotcha yang terkait dengan kerangka kerja target plugin. Secara khusus, plugin Anda harus menargetkan runtime, seperti .NET 5, bukan versi .NET Standard. File .deps.json dihasilkan berdasarkan kerangka kerja mana yang ditargetkan proyek, dan karena banyak rakitan referensi pengiriman paket yang kompatibel dengan .NET Standard untuk membangun terhadap .NET Standard dan rakitan implementasi untuk runtime tertentu, .deps.json mungkin tidak melihat rakitan implementasi dengan benar, atau mungkin mengambil versi Standar .NET dari rakitan alih-alih versi .NET Core yang Anda harapkan.

Referensi kerangka kerja plugin

Saat ini, plugin tidak dapat memperkenalkan kerangka kerja baru ke dalam proses. Misalnya, Anda tidak dapat memuat plugin yang menggunakan Microsoft.AspNetCore.App kerangka kerja ke dalam aplikasi yang hanya menggunakan kerangka kerja akar Microsoft.NETCore.App . Aplikasi host harus mendeklarasikan referensi ke semua kerangka kerja yang diperlukan oleh plugin.