فصل ۱۷: Legacy Loading و ساخت Plug-In System با AssemblyLoadContext
فصل ۱۷: Legacy Loading و ساخت Plug-In System با AssemblyLoadContext
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
ادامهٔ LoadFile و Load(byte[])
دو Caveat ادامهٔ بحث مقالهٔ قبل:
- اگر
LoadFile را دوباره با Path کاملاً یکسان فراخوانی کنید، Assemblyای که قبلاً Load شده بود برگردانده میشود. - در .NET Framework، هر دو Method ابتدا GAC را بررسی میکنند و اگر Assembly آنجا باشد، از GAC Load میکنند.
با LoadFile و Load(byte[])، با کنار گذاشتن برخی Caveatها، برای هر Assembly یک ALC جدا خواهید داشت. این Isolation را ممکن میکند، هرچند Management را دشوارتر میسازد.
برای Resolve کردن Dependencyها Event مربوط به AppDomain را Handle میکنید که در همهٔ ALCها Fire میشود:
AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
{
string fullAssemblyName = args.Name;
// return an Assembly object or null
...
};
متغیر args property دیگری به نام RequestingAssembly دارد که مشخص میکند کدام Assembly Resolution را Trigger کرده است.
بعد از پیدا کردن Assembly میتوانید Assembly.LoadFile را برای Load کردن آن فراخوانی کنید.
AssemblyLoadContext.All.SelectMany (a => a.Assemblies)
نوشتن یک Plug-In System
برای نمایش کامل Conceptهایی که در این بخش توضیح داده شد، یک Plug-In System مینویسیم که از ALCهای قابل Unload برای Isolate کردن هر Plug-In استفاده میکند.
Demo ابتدا شامل سه Project در .NET است:
- Plugin.Common — Library
- Interfaceای را تعریف میکند که Plug-Inها Implement میکنند.
- Capitalizer — Library
- Plug-Inی که Text را Capitalize میکند.
- Plugin.Host — Console Application
- Plug-Inها را پیدا و Invoke میکند.
فرض میکنیم Projectها در Directoryهای زیر هستند:
c:\source\PluginDemo\Plugin.Common
c:\source\PluginDemo\Capitalizer
c:\source\PluginDemo\Plugin.Host
همهٔ Projectها Library به نام Plugin.Common را Reference میکنند و هیچ Interproject Reference دیگری وجود نخواهد داشت.
Plugin.Common
از Plugin.Common شروع میکنیم. Plug-Inهای ما وظیفهٔ بسیار سادهای دارند: Transform کردن یک String. Interface را چنین تعریف میکنیم:
namespace Plugin.Common
{
public interface ITextPlugin
{
string TransformText (string input);
}
}
تمام Plugin.Common همین است.
Capitalizer، Plug-In
Plug-In به نام Capitalizer به Plugin.Common Reference میدهد و فقط یک Class دارد. فعلاً Logic را ساده نگه میداریم تا Plug-In هیچ Dependency اضافی نداشته باشد:
public class CapitalizerPlugin : Plugin.Common.ITextPlugin
{
public string TransformText (string input) => input.ToUpper();
}
اگر هر دو Project را Build و Output Folder مربوط به Capitalizer را ببینید، دو Assembly زیر وجود دارد:
Capitalizer.dll // Our plug-in assembly
Plugin.Common.dll // Referenced assembly
Plugin.Host
Plugin.Host یک Console Application با دو Class است. Class اول Custom ALC برای Load کردن Plug-Inها است:
class PluginLoadContext : AssemblyLoadContext
{
AssemblyDependencyResolver _resolver;
public PluginLoadContext (string pluginPath, bool collectible)
// Give it a friendly name to help with debugging:
: base (name: Path.GetFileName (pluginPath), collectible)
{
// Create a resolver to help us find dependencies.
_resolver = new AssemblyDependencyResolver (pluginPath);
}
protected override Assembly Load (AssemblyName assemblyName)
{
// See below
if (assemblyName.Name == typeof (ITextPlugin).Assembly.GetName().Name)
return null;
string target = _resolver.ResolveAssemblyToPath (assemblyName);
if (target != null)
return LoadFromAssemblyPath (target);
// Could be a BCL assembly. Allow the default context to resolve.
return null;
}
protected override IntPtr LoadUnmanagedDll (string unmanagedDllName)
{
string path = _resolver.ResolveUnmanagedDllToPath (unmanagedDllName);
return path == null
? IntPtr.Zero
: LoadUnmanagedDllFromPath (path);
}
}
در Constructor، Path مربوط به Main Plug-In Assembly و Flagای برای مشخص کردن Collectible بودن ALC میدهیم تا بتوان آن را Unload کرد.
در Method به نام Load، Dependency Resolution را مدیریت میکنیم. همهٔ Plug-Inها باید Plugin.Common را Reference کنند تا ITextPlugin را Implement کنند. بنابراین در نقطهای Load برای Resolve کردن Plugin.Common اجرا میشود. باید مراقب باشیم، چون Output Folder مربوط به Plug-In احتمالاً علاوه بر Capitalizer.dll، Copy خودش از Plugin.Common.dll را هم دارد. اگر این Copy را در PluginLoadContext Load کنیم دو Copy از Assembly خواهیم داشت: یکی در Default Context مربوط به Host و دیگری در PluginLoadContext. این Assemblyها Incompatible خواهند بود و Host شکایت میکند که Plug-In، ITextPlugin را Implement نمیکند.
برای حل این مشکل صریحاً این Condition را بررسی میکنیم:
if (assemblyName.Name == typeof (ITextPlugin).Assembly.GetName().Name)
return null;
برگرداندن null اجازه میدهد Default ALC مربوط به Host Assembly را Resolve کند.
بعد از Check کردن Common Assembly، از AssemblyDependencyResolver برای پیدا کردن Private Dependencyهایی که Plug-In ممکن است داشته باشد استفاده میکنیم. در حال حاضر Dependency خصوصی وجود ندارد.
همچنین LoadUnmanagedDll را Override میکنیم تا اگر Plug-In Unmanaged Dependency داشته باشد آنها نیز صحیح Load شوند.
Class دوم در Plugin.Host خود Main Program است. برای سادگی Path مربوط به Capitalizer Plug-In را Hard-Code میکنیم؛ در دنیای واقعی ممکن است Pathها را با جستوجوی DLL در Locationهای شناختهشده یا خواندن Configuration File پیدا کنید:
class Program
{
const bool UseCollectibleContexts = true;
static void Main()
{
const string capitalizer = @"C:\source\PluginDemo\"
+ @"Capitalizer\bin\Debug\netcoreapp3.0\Capitalizer.dll";
Console.WriteLine (TransformText ("big apple", capitalizer));
}
static string TransformText (string text, string pluginPath)
{
var alc = new PluginLoadContext (pluginPath, UseCollectibleContexts);
try
{
Assembly assem = alc.LoadFromAssemblyPath (pluginPath);
// Locate the type in the assembly that implements ITextPlugin:
Type pluginType = assem.ExportedTypes.Single (t =>
typeof (ITextPlugin).IsAssignableFrom (t));
// Instantiate the ITextPlugin implementation:
var plugin = (ITextPlugin)Activator.CreateInstance (pluginType);
// Call the TransformText method
return plugin.TransformText (text);
}
finally
{
if (UseCollectibleContexts) alc.Unload(); // unload the ALC
}
Method به نام TransformText را بررسی کنیم. ابتدا ALC جدیدی برای Plug-In میسازیم و از آن میخواهیم Main Plug-In Assembly را Load کند. سپس با Reflection Typeای را پیدا میکنیم که ITextPlugin را Implement میکند. بعد Plug-In را نمونهسازی، Method به نام TransformText را فراخوانی و ALC را Unload میکنیم.
خروجی:
افزودن Dependencyها
Code ما کاملاً قادر به Resolve و Isolate کردن Dependencyها است. برای نمایش، ابتدا NuGet Reference به Humanizer.Core Version 2.6.2 اضافه میکنیم. این کار را از UI مربوط به Visual Studio یا با افزودن Element زیر به Capitalizer.csproj انجام میدهیم:
<ItemGroup>
<PackageReference Include="Humanizer.Core" Version="2.6.2" />
</ItemGroup>
اکنون CapitalizerPlugin را چنین تغییر میدهیم:
using Humanizer;
namespace Capitalizer
{
public class CapitalizerPlugin : Plugin.Common.ITextPlugin
{
public string TransformText (string input) => input.Pascalize();
}
}
اگر Program را دوباره Run کنید، خروجی:
سپس Plug-In دیگری به نام Pluralizer میسازیم. یک .NET Library Project جدید ایجاد کرده و NuGet Reference به Humanizer.Core Version 2.7.9 اضافه میکنیم:
<ItemGroup>
<PackageReference Include="Humanizer.Core" Version="2.7.9" />
</ItemGroup>
Classی به نام PluralizerPlugin اضافه میکنیم. شبیه Capitalizer Plug-In است، ولی Method به نام Pluralize را فراخوانی میکند:
using Humanizer;
namespace Pluralizer
{
public class PluralizerPlugin : Plugin.Common.ITextPlugin
{
public string TransformText (string input) => input.Pluralize();
}
}
در نهایت باید Codeی به Method به نام Main در Plugin.Host اضافه کنیم تا Plug-In به نام Pluralizer را Load و Run کند:
static void Main()
{
const string capitalizer = @"C:\source\PluginDemo\"
+ @"Capitalizer\bin\Debug\netcoreapp3.0\Capitalizer.dll";
Console.WriteLine (TransformText ("big apple", capitalizer));
const string pluralizer = @"C:\source\PluginDemo\"
+ @"Pluralizer\bin\Debug\netcoreapp3.0\Pluralizer.dll";
Console.WriteLine (TransformText ("big apple", pluralizer));
}
خروجی اکنون:
برای دیدن کامل اتفاقی که میافتد، Constant به نام UseCollectibleContexts را false کنید و Code زیر را به Main اضافه کنید تا ALCها و Assemblyهایشان Enumerate شوند:
foreach (var context in AssemblyLoadContext.All)
{
Console.WriteLine ($"Context: {context.GetType().Name} {context.Name}");
foreach (var assembly in context.Assemblies)
Console.WriteLine ($" Assembly: {assembly.FullName}");
}
در Output دو Version متفاوت از Humanizer را میبینید که هرکدام در ALC خودش Load شده است:
Context: PluginLoadContext Capitalizer.dll
Assembly: Capitalizer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=...
Assembly: Humanizer, Version=2.6.0.0, Culture=neutral, PublicKeyToken=...
Context: PluginLoadContext Pluralizer.dll
Assembly: Pluralizer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=...
Assembly: Humanizer, Version=2.7.0.0, Culture=neutral, PublicKeyToken=...
Context: DefaultAssemblyLoadContext Default
Assembly: System.Private.CoreLib, Version=4.0.0.0, Culture=neutral,...
Assembly: Host, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
...