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:
- Menyusun proyek untuk mendukung plugin.
- Buat kustom AssemblyLoadContext untuk memuat setiap plugin.
- System.Runtime.Loader.AssemblyDependencyResolver Gunakan jenis untuk memungkinkan plugin memiliki dependensi.
- Plugin pembuat yang dapat dengan mudah disebarkan hanya dengan menyalin artefak build.
Prasyarat
- Instal .NET 5 SDK atau versi yang lebih baru.
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:
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 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 PluginBase
ICommand.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:
Jalankan perintah berikut untuk membuat proyek pustaka kelas baru bernama
HelloPlugin
:dotnet new classlib -o HelloPlugin
Jalankan perintah berikut untuk menambahkan proyek ke
AppWithPlugin
solusi: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 <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.
Saran dan Komentar
https://aka.ms/ContentUserFeedback.
Segera hadir: Sepanjang tahun 2024 kami akan menghentikan penggunaan GitHub Issues sebagai mekanisme umpan balik untuk konten dan menggantinya dengan sistem umpan balik baru. Untuk mengetahui informasi selengkapnya, lihat:Kirim dan lihat umpan balik untuk