Bagikan melalui


Membuat aplikasi .NET Core dengan plugin

Tutorial ini menunjukkan kepada Anda cara membuat AssemblyLoadContext kustom untuk memuat plugin. AssemblyDependencyResolver digunakan untuk menyelesaikan dependensi plugin. Tutorial ini menyediakan konteks perakitan terpisah untuk dependensi plugin, memungkinkan dependensi perakitan yang berbeda antara plugin dan aplikasi hosting. Anda akan mempelajari cara:

Nota

Kode yang tidak tepercaya tidak dapat dimuat dengan aman ke dalam proses .NET tepercaya. Untuk memberikan batas keamanan atau keandalan, pertimbangkan teknologi yang disediakan oleh OS atau platform virtualisasi Anda.

Prasyarat

Membuat 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 mengandung semua tipe yang Anda rencanakan guna berkomunikasi antara aplikasi dan plugin Anda. Divisi ini memungkinkan Anda untuk menerbitkan antarmuka plugin Anda sebagai paket tanpa harus mengirimkan 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. Hapus file PluginBase/Class1.cs, dan buat file baru di folder PluginBase bernama ICommand.cs dengan definisi antarmuka berikut:

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

        int Execute();
    }
}

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

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

Ganti komentar // Load commands from plugins 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 komentar // Output the loaded commands dengan cuplikan kode berikut:

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

Ganti komentar // Execute the command with the name passed as an argument 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 Program bernama LoadPlugin dan CreateCommands, seperti yang 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 telah dimuat, tetapi masih belum dapat memuat rakitan plugin tersebut. 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 PluginLoadContext berasal dari AssemblyLoadContext. Jenis AssemblyLoadContext adalah jenis khusus dalam lingkungan runtime yang memungkinkan pengembang untuk mengisolasi kumpulan rakitan yang dimuat ke dalam grup yang berbeda untuk memastikan bahwa versi rakitan tidak bertentangan. Selain itu, AssemblyLoadContext kustom dapat memilih jalur yang berbeda untuk memuat assemblies dan mengubah perilaku default. PluginLoadContext menggunakan sebuah instans dari tipe AssemblyDependencyResolver yang diperkenalkan di .NET Core 3.0 untuk memetakan 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 konstruktor AssemblyDependencyResolver. AssemblyLoadContext kustom memungkinkan plugin untuk memiliki dependensinya sendiri, dan AssemblyDependencyResolver memudahkan untuk memuat dependensi dengan benar.

Sekarang setelah proyek AppWithPlugin memiliki jenis PluginLoadContext, perbarui metode Program.LoadPlugin 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 PluginLoadContext yang berbeda 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 solusi AppWithPlugin:

    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 tag <PropertyGroup>, 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 tag <Project>, tambahkan elemen berikut:

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

Elemen <Private>false</Private> 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, jenis HelloPlugin.HelloCommand akan mengimplementasikan antarmuka ICommand dari PluginBase.dll di direktori output proyek HelloPlugin, bukan antarmuka ICommand yang dimuat ke dalam konteks beban default. Karena runtime melihat kedua jenis ini sebagai jenis yang berbeda dari rakitan yang berbeda, metode AppWithPlugin.Program.CreateCommands tidak akan menemukan perintah. Akibatnya, metadata <Private>false</Private> diperlukan untuk referensi ke rakitan yang berisi antarmuka plugin.

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

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

Plugin dengan dependensi perpustakaan

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 menambahkan <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 perilaku AssemblyDependencyResolver lainnya. Misalnya, objek AssemblyDependencyResolver juga dapat menyelesaikan pustaka asli serta rakitan satelit lokal 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 metadata <Private>false</Private> pada elemen ProjectReference dalam file proyek mencegah dll disalin ke output.

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

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

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

Rekomendasi untuk 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 dibuat berdasarkan kerangka kerja yang ditargetkan oleh proyek, dan karena banyak paket yang kompatibel dengan .NET Standard mendistribusikan rakitan referensi untuk membangun terhadap .NET Standard dan rakitan implementasi untuk runtime tertentu, .deps.json mungkin tidak mendeteksi rakitan implementasi dengan benar, atau dapat mengambil versi .NET Standard 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 kerangka kerja Microsoft.AspNetCore.App ke dalam aplikasi yang hanya menggunakan kerangka kerja Microsoft.NETCore.App root. Aplikasi host harus mendeklarasikan referensi ke semua kerangka kerja yang diperlukan oleh plugin.