Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
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:
- Menyusun proyek untuk mendukung plugin.
- Buat AssemblyLoadContext kustom untuk memuat setiap plugin.
- Gunakan jenis System.Runtime.Loader.AssemblyDependencyResolver untuk memungkinkan plugin memiliki dependensi.
- Plugin yang ditulis dapat dengan mudah disebarkan hanya dengan menyalin file hasil build.
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
- .NET SDK terbaru
- Visual Studio Code editor
- The C# DevKit
Membuat aplikasi
Langkah pertama adalah membuat aplikasi:
Buat folder baru, dan di folder tersebut jalankan perintah berikut:
dotnet new console -o AppWithPlugin
Untuk mempermudah pembuatan proyek, buat file solusi Visual Studio di folder yang sama. Jalankan perintah berikut:
dotnet new sln
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:
Jalankan perintah berikut untuk membuat proyek pustaka kelas baru bernama
HelloPlugin
:dotnet new classlib -o HelloPlugin
Jalankan perintah berikut untuk menambahkan proyek ke solusi
AppWithPlugin
:dotnet sln add HelloPlugin/HelloPlugin.csproj
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.