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.
Saat menulis modul PowerShell biner di C#, wajar untuk mengambil dependensi pada paket atau pustaka lain untuk menyediakan fungsionalitas. Mengambil dependensi pada pustaka lain diinginkan untuk penggunaan kembali kode. PowerShell selalu memuat rakitan ke dalam konteks yang sama. Ini menyajikan masalah ketika dependensi modul berkonflik dengan DLL yang sudah dimuat dan dapat mencegah penggunaan dua modul yang tidak terkait dalam sesi PowerShell yang sama.
Jika Anda mengalami masalah ini, Anda telah melihat pesan kesalahan seperti ini:
pesan kesalahan konflik beban Rakitan
Artikel ini melihat beberapa cara konflik dependensi terjadi di PowerShell dan cara untuk mengurangi masalah konflik dependensi. Bahkan jika Anda bukan penulis modul, ada beberapa trik di sini yang mungkin membantu Anda dengan konflik dependensi yang terjadi dalam modul yang Anda gunakan.
Mengapa konflik dependensi terjadi?
Di .NET, konflik dependensi terjadi ketika dua versi rakitan yang sama dimuat ke dalam Assembly Load Context yang sama. Istilah ini berarti hal-hal yang sedikit berbeda pada platform .NET yang berbeda, yang dibahas nanti dalam artikel ini. Konflik ini adalah masalah umum yang terjadi di perangkat lunak mana pun di mana dependensi versi digunakan.
Masalah konflik diperparah oleh fakta bahwa proyek hampir tidak pernah sengaja atau langsung tergantung pada dua versi dependensi yang sama. Sebagai gantinya, proyek memiliki dua atau beberapa dependensi yang masing-masing memerlukan versi dependensi yang sama yang berbeda.
Misalnya, katakanlah aplikasi .NET Anda, DuckBuilder
, membawa dua dependensi, untuk melakukan bagian dari fungsionalitasnya dan terlihat seperti ini:
Karena Contoso.ZipTools
dan Fabrikam.FileHelpers
keduanya bergantung pada versi Newtonsoft.Json yang berbeda, mungkin ada konflik dependensi tergantung pada bagaimana setiap dependensi dimuat.
Bertentangan dengan dependensi PowerShell
Di PowerShell, masalah konflik dependensi diperbesar karena dependensi PowerShell sendiri dimuat ke dalam konteks bersama yang sama. Ini berarti mesin PowerShell dan semua modul PowerShell yang dimuat tidak boleh memiliki dependensi yang bertentangan. Contoh klasiknya adalah Newtonsoft.Json:
Dalam contoh ini, modul FictionalTools
bergantung pada Newtonsoft.Json versi12.0.3
, yang merupakan versi Newtonsoft.Json yang lebih baru daripada 11.0.2
yang dikirim dalam contoh PowerShell.
Nota
Ini adalah contoh. PowerShell 7.0 saat ini dikirim dengan Newtonsoft.Json 12.0.3. Versi PowerShell yang lebih baru memiliki versi Newtonsoft.Json yang lebih baru.
Karena modul bergantung pada versi rakitan yang lebih baru, modul tidak akan menerima versi yang sudah dimuat PowerShell. Tetapi karena PowerShell telah memuat versi rakitan, modul tidak dapat memuat versinya sendiri menggunakan mekanisme beban konvensional.
Bertentangan dengan dependensi modul lain
Skenario umum lainnya di PowerShell adalah modul dimuat yang bergantung pada satu versi rakitan, lalu modul lain dimuat nanti yang bergantung pada versi rakitan yang berbeda.
Ini sering terlihat seperti berikut:
Dalam hal ini, modul FictionalTools
memerlukan versi Microsoft.Extensions.Logging
yang lebih baru daripada modul FilesystemManager
.
Bayangkan modul ini memuat dependensi mereka dengan menempatkan rakitan dependensi di direktori yang sama dengan rakitan modul akar. Ini memungkinkan .NET untuk secara implisit memuatnya berdasarkan nama. Jika kita menjalankan PowerShell 7.0 (di atas .NET Core 3.1), kita dapat memuat dan menjalankan FictionalTools
, lalu memuat dan menjalankan FilesystemManager
tanpa masalah. Namun, dalam sesi baru, jika kita memuat dan menjalankan FilesystemManager
, maka muat FictionalTools
, kita mendapatkan FileLoadException
dari perintah FictionalTools
karena memerlukan versi Microsoft.Extensions.Logging
yang lebih baru daripada yang dimuat.
FictionalTools
tidak dapat memuat versi yang diperlukan karena rakitan dengan nama yang sama telah dimuat.
PowerShell dan .NET
PowerShell berjalan pada platform .NET, yang bertanggung jawab untuk menyelesaikan dan memuat dependensi perakitan. Kita harus memahami bagaimana .NET beroperasi di sini untuk memahami konflik dependensi.
Kita juga harus menghadapi fakta bahwa versi PowerShell yang berbeda berjalan pada implementasi .NET yang berbeda. Secara umum, PowerShell 5.1 ke bawah berjalan pada .NET Framework, sementara PowerShell 6 ke atas berjalan pada .NET Core. Kedua implementasi beban .NET ini dan menangani rakitan secara berbeda. Ini berarti bahwa menyelesaikan konflik dependensi dapat bervariasi tergantung pada platform .NET yang mendasar.
Konteks Pemuatan Rakitan
Di .NET, Assembly Load Context (ALC) adalah namespace runtime tempat rakitan dimuat. Nama rakitan harus unik. Konsep ini memungkinkan rakitan diselesaikan secara unik berdasarkan nama di setiap ALC.
Pemuatan referensi rakitan di .NET
Semantik pemuatan rakitan tergantung pada implementasi .NET (.NET Core vs .NET Framework) dan .NET API yang digunakan untuk memuat rakitan tertentu. Daripada masuk ke detail di sini, ada tautan di bagian Bacaan lebih lanjut yang masuk ke detail besar tentang cara kerja pemuatan rakitan .NET di setiap implementasi .NET.
Dalam artikel ini kita akan merujuk ke mekanisme berikut:
- Pemuatan rakitan implisit (secara efektif
Assembly.Load(AssemblyName)
), ketika .NET secara implisit mencoba memuat rakitan berdasarkan nama dari referensi rakitan statis dalam kode .NET. -
Assembly.LoadFrom()
, API pemuatan berorientasi plugin yang menambahkan handler untuk mengatasi dependensi DLL yang dimuat. Metode ini mungkin tidak menyelesaikan dependensi seperti yang kita inginkan. -
Assembly.LoadFile()
, API pemuatan dasar yang dimaksudkan untuk memuat hanya rakitan yang diminta dan tidak menangani dependensi apa pun.
Perbedaan dalam .NET Framework vs .NET Core
Cara kerja API ini telah berubah dengan cara yang halus antara .NET Core dan .NET Framework, jadi ada baiknya membaca tautan yang disertakan. Yang penting, Assembly Load Contexts dan mekanisme resolusi rakitan lainnya telah berubah antara .NET Framework dan .NET Core.
Secara khusus, .NET Framework memiliki fitur berikut:
- Cache Perakitan Global, untuk resolusi perakitan di seluruh komputer
- Domain Aplikasi, yang berfungsi seperti kotak pasir dalam proses untuk isolasi perakitan, tetapi juga menyajikan lapisan serialisasi untuk digabungkan dengan
- Model konteks pemuatan rakitan terbatas yang memiliki serangkaian konteks beban rakitan tetap, masing-masing dengan perilakunya sendiri:
- Konteks beban default, di mana rakitan dimuat secara default
- Konteks load-from, untuk memuat rakitan secara manual saat runtime
- Konteks khusus refleksi, untuk memuat rakitan dengan aman untuk membaca metadata mereka tanpa menjalankannya
- Kekosongan misterius yang dimuat rakitan dengan
Assembly.LoadFile(string path)
danAssembly.Load(byte[] asmBytes)
tinggal di
Untuk informasi selengkapnya, lihat praktik terbaik untuk Assembly Loading.
.NET Core (dan .NET 5+) telah mengganti kompleksitas ini dengan model yang lebih sederhana:
- Tidak ada Singgahan Rakitan Global. Aplikasi membawa semua dependensi mereka sendiri. Ini menghapus faktor eksternal untuk resolusi dependensi dalam aplikasi, membuat resolusi dependensi lebih dapat direproduksi.
PowerShell, sebagai host plugin, sedikit mempersulit ini untuk modul. Dependensinya dalam
$PSHOME
dibagikan dengan semua modul. - Hanya satu Domain Aplikasi, dan tidak ada kemampuan untuk membuat yang baru. Konsep Domain Aplikasi dipertahankan di .NET untuk menjadi status global proses .NET.
- Model Assembly Load Context (ALC) baru yang dapat diperluas. Resolusi rakitan dapat diberi namespace dengan memasukkannya ke dalam ALC baru. Proses .NET dimulai dengan satu ALC default tempat semua rakitan dimuat (kecuali yang dimuat dengan
Assembly.LoadFile(string)
danAssembly.Load(byte[])
). Tetapi prosesnya dapat membuat dan menentukan ALC kustomnya sendiri dengan logika pemuatannya sendiri. Ketika assembly dimuat, ALC pertama yang dimuat bertanggung jawab untuk menyelesaikan dependensinya. Ini menciptakan peluang untuk menerapkan mekanisme pemuatan plugin .NET yang kuat.
Dalam kedua implementasi, rakitan dimuat dengan malas. Ini berarti bahwa mereka dimuat ketika metode yang memerlukan jenisnya dijalankan untuk pertama kalinya.
Misalnya, berikut adalah dua versi kode yang sama yang memuat dependensi pada waktu yang berbeda.
Yang pertama selalu memuat dependensinya ketika Program.GetRange()
dipanggil, karena referensi dependensi secara leksikal ada dalam metode :
using Dependency.Library;
public static class Program
{
public static List<int> GetRange(int limit)
{
var list = new List<int>();
for (int i = 0; i < limit; i++)
{
if (i >= 20)
{
// Dependency.Library will be loaded when GetRange is run
// because the dependency call occurs directly within the method
DependencyApi.Use();
}
list.Add(i);
}
return list;
}
}
Yang kedua memuat dependensinya hanya jika parameter limit
adalah 20 atau lebih, karena tidak langsung internal melalui metode:
using Dependency.Library;
public static class Program
{
public static List<int> GetNumbers(int limit)
{
var list = new List<int>();
for (int i = 0; i < limit; i++)
{
if (i >= 20)
{
// Dependency.Library is only referenced within
// the UseDependencyApi() method,
// so will only be loaded when limit >= 20
UseDependencyApi();
}
list.Add(i);
}
return list;
}
private static void UseDependencyApi()
{
// Once UseDependencyApi() is called, Dependency.Library is loaded
DependencyApi.Use();
}
}
Ini adalah praktik yang baik karena meminimalkan I/O memori dan sistem file dan menggunakan sumber daya secara lebih efisien. Sayangnya efek samping dari ini adalah bahwa kita tidak akan tahu bahwa assembly gagal dimuat sampai kita mencapai jalur kode yang mencoba memuat assembly.
Ini juga dapat membuat kondisi waktu untuk konflik beban perakitan. Jika dua bagian dari program yang sama mencoba memuat versi yang berbeda dari rakitan yang sama, versi yang dimuat bergantung pada jalur kode mana yang dijalankan terlebih dahulu.
Untuk PowerShell, ini berarti bahwa faktor-faktor berikut dapat memengaruhi konflik beban perakitan:
- Modul mana yang dimuat terlebih dahulu?
- Apakah jalur kode yang menggunakan pustaka dependensi dijalankan?
- Apakah PowerShell memuat dependensi yang bertentangan saat startup atau hanya di bawah jalur kode tertentu?
Perbaikan cepat dan batasannya
Dalam beberapa kasus, Anda dapat melakukan penyesuaian kecil pada modul Anda dan memperbaiki hal-hal dengan upaya minimal. Tetapi solusi ini cenderung datang dengan peringatan. Meskipun dapat berlaku untuk modul Anda, modul tersebut tidak akan berfungsi untuk setiap modul.
Mengubah versi dependensi Anda
Cara paling sederhana untuk menghindari konflik dependensi adalah dengan menyetujui dependensi. Ini mungkin dimungkinkan ketika:
- Konflik Anda adalah dengan dependensi langsung modul Anda dan Anda mengontrol versi.
- Konflik Anda adalah dengan dependensi tidak langsung, tetapi Anda dapat mengonfigurasi dependensi langsung Anda untuk menggunakan versi dependensi tidak langsung yang dapat dikerjakan.
- Anda tahu versi yang bertentangan dan dapat mengandalkannya tidak berubah.
Paket
Misalnya, PowerShell 6.2.6 dan PowerShell 7.0.2 saat ini menggunakan Newtonsoft.Json versi 12.0.3. Untuk membuat modul yang menargetkan Windows PowerShell, PowerShell 6, dan PowerShell 7, Anda akan menargetkan Newtonsoft.Json 12.0.3 sebagai dependensi dan menyertakannya dalam modul bawaan Anda. Saat modul dimuat di PowerShell 6 atau 7, rakitan Newtonsoft.Json PowerShell sendiri sudah dimuat. Karena ini adalah versi yang diperlukan untuk modul Anda, resolusi berhasil. Di Windows PowerShell, rakitan belum ada di PowerShell, sehingga dimuat dari folder modul Anda sebagai gantinya.
Umumnya, saat menargetkan paket PowerShell konkret, seperti Microsoft.PowerShell.Sdk atau System.Management.Automation, NuGet harus dapat menyelesaikan versi dependensi yang tepat yang diperlukan. Menargetkan Windows PowerShell dan PowerShell 6+ menjadi lebih sulit karena Anda harus memilih antara menargetkan beberapa kerangka kerja atau PowerShellStandard.Library.
Keadaan saat menyematkan ke versi dependensi umum tidak akan berfungsi meliputi:
- Konfliknya adalah dengan dependensi tidak langsung, dan tidak ada dependensi Anda yang dapat dikonfigurasi untuk menggunakan versi umum.
- Versi dependensi lainnya cenderung sering berubah, jadi penetapan pada versi umum hanyalah perbaikan jangka pendek.
Gunakan dependensi di luar proses
Solusi ini lebih untuk pengguna modul daripada penulis modul. Ini adalah solusi untuk digunakan ketika dihadapkan dengan modul yang tidak akan berfungsi karena konflik dependensi yang ada.
Konflik dependensi terjadi karena dua versi rakitan yang sama dimuat ke dalam proses .NET yang sama. Solusi sederhana adalah memuatnya ke dalam proses yang berbeda, selama Anda masih dapat menggunakan fungsionalitas dari keduanya bersama-sama.
Di PowerShell, ada beberapa cara untuk mencapai hal ini:
Memanggil PowerShell sebagai subprosces
Untuk menjalankan perintah PowerShell dari proses saat ini, mulai proses PowerShell baru secara langsung dengan panggilan perintah:
pwsh -c 'Invoke-ConflictingCommand'
Batasan utama di sini adalah bahwa merestrukturisasi hasilnya dapat lebih sulit atau lebih rentan kesalahan daripada opsi lain.
Sistem pekerjaan PowerShell
Sistem pekerjaan PowerShell juga menjalankan perintah di luar proses, dengan mengirim perintah ke proses PowerShell baru dan mengembalikan hasilnya:
$result = Start-Job { Invoke-ConflictingCommand } | Receive-Job -Wait
Dalam hal ini, Anda hanya perlu memastikan bahwa variabel dan status apa pun diteruskan dengan benar.
Sistem pekerjaan juga bisa sedikit rumit saat menjalankan perintah kecil.
PowerShell jarak jauh
Saat tersedia, remoting PowerShell dapat menjadi cara yang berguna untuk menjalankan perintah di luar proses. Dengan jarak jauh, Anda dapat membuat PSSession
baru dalam proses baru, memanggil perintahnya melalui remoting PowerShell, lalu menggunakan hasilnya secara lokal dengan modul lain yang berisi dependensi yang bertentangan. Contohnya mungkin terlihat seperti ini:
# Create a local PowerShell session # where the module with conflicting assemblies will be loaded $s = New-PSSession # Import the module with the conflicting dependency via remoting, # exposing the commands locally Import-Module -PSSession $s -Name ConflictingModule # Run a command from the module with the conflicting dependencies Invoke-ConflictingCommand
Jarak jauh implisit ke Windows PowerShell
Opsi lain di PowerShell 7 adalah menggunakan bendera
-UseWindowsPowerShell
padaImport-Module
. Ini mengimpor modul melalui sesi jarak jauh lokal ke Windows PowerShell:Import-Module -Name ConflictingModule -UseWindowsPowerShell
Ketahuilah bahwa modul mungkin tidak kompatibel dengan atau mungkin bekerja secara berbeda dengan Windows PowerShell.
Ketika pemanggilan di luar proses tidak boleh digunakan
Sebagai penulis modul, pemanggilan perintah di luar proses sulit untuk dipanggang ke dalam modul dan mungkin memiliki kasus tepi yang menyebabkan masalah. Secara khusus, jarak jauh dan pekerjaan mungkin tidak tersedia di semua lingkungan tempat modul Anda perlu bekerja. Namun, prinsip umum memindahkan implementasi di luar proses dan memungkinkan modul PowerShell menjadi klien yang lebih tipis, mungkin masih berlaku.
Sebagai pengguna modul, ada kasus di mana pemanggilan di luar proses tidak akan berfungsi:
- Saat remoting PowerShell tidak tersedia karena Anda tidak memiliki hak istimewa untuk menggunakannya atau tidak diaktifkan.
- Ketika jenis .NET tertentu diperlukan dari output sebagai input ke metode atau perintah lain. Perintah yang berjalan di atas PowerShell memancarkan objek yang deserialisasi daripada objek .NET yang sangat ditik. Ini berarti bahwa panggilan metode dan API yang di ketik dengan kuat tidak berfungsi dengan output perintah yang diimpor melalui jarak jauh.
Solusi yang lebih kuat
Solusi sebelumnya semuanya memiliki skenario dan modul yang tidak berfungsi. Namun, mereka juga memiliki kebajikan yang relatif sederhana untuk diimplementasikan dengan benar. Solusi berikut lebih kuat, tetapi membutuhkan lebih banyak upaya untuk menerapkan dengan benar dan dapat memperkenalkan bug halus jika tidak ditulis dengan hati-hati.
Memuat melalui .NET Core Assembly Load Contexts
Assembly Load Contexts (ALC) diperkenalkan dalam .NET Core 1.0 untuk secara khusus mengatasi kebutuhan untuk memuat beberapa versi rakitan yang sama ke dalam runtime yang sama.
Dalam .NET, mereka menawarkan solusi yang paling kuat untuk masalah pemuatan versi perakitan yang bertentangan. Namun, ALC kustom tidak tersedia di .NET Framework. Ini berarti bahwa solusi ini hanya berfungsi di PowerShell 6 ke atas.
Saat ini, contoh terbaik menggunakan ALC untuk isolasi dependensi di PowerShell ada di PowerShell Editor Services, server bahasa untuk ekstensi PowerShell untuk Visual Studio Code. ALC digunakan untuk mencegah dependensi PowerShell Editor Services berbenturan dengan yang ada di modul PowerShell.
Menerapkan isolasi dependensi modul dengan ALC secara konseptual sulit, tetapi kita akan bekerja melalui contoh minimal. Bayangkan kita memiliki modul sederhana yang hanya dimaksudkan untuk bekerja di PowerShell 7. Kode sumber diatur sebagai berikut:
+ AlcModule.psd1
+ src/
+ TestAlcModuleCommand.cs
+ AlcModule.csproj
Implementasi cmdlet terlihat seperti ini:
using Shared.Dependency;
namespace AlcModule
{
[Cmdlet(VerbsDiagnostic.Test, "AlcModule")]
public class TestAlcModuleCommand : Cmdlet
{
protected override void EndProcessing()
{
// Here's where our dependency gets used
Dependency.Use();
// Something trivial to make our cmdlet do *something*
WriteObject("done!");
}
}
}
Manifes (sangat disederhanakan), terlihat seperti ini:
@{
Author = 'Me'
ModuleVersion = '0.0.1'
RootModule = 'AlcModule.dll'
CmdletsToExport = @('Test-AlcModule')
PowerShellVersion = '7.0'
}
Dan csproj
terlihat seperti ini:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Shared.Dependency" Version="1.0.0" />
<PackageReference Include="Microsoft.PowerShell.Sdk" Version="7.0.1" PrivateAssets="all" />
</ItemGroup>
</Project>
Ketika kita membangun modul ini, output yang dihasilkan memiliki tata letak berikut:
AlcModule/
+ AlcModule.psd1
+ AlcModule.dll
+ Shared.Dependency.dll
Dalam contoh ini, masalah kita ada di perakitan Shared.Dependency.dll
, yang merupakan dependensi konflik imajiner kita. Ini adalah dependensi yang perlu kita letakkan di belakang ALC sehingga kita dapat menggunakan versi khusus modul.
Kita perlu merekayasa ulang modul sehingga:
- Dependensi modul hanya dimuat ke dalam ALC kustom kami, dan bukan ke ALC PowerShell, sehingga tidak mungkin ada konflik. Selain itu, karena kami menambahkan lebih banyak dependensi ke proyek kami, kami tidak ingin terus menambahkan lebih banyak kode untuk terus memuat berfungsi. Sebagai gantinya, kami ingin logika resolusi dependensi generik yang dapat digunakan kembali.
- Memuat modul masih berfungsi seperti biasa di PowerShell. Cmdlet dan jenis lain yang dibutuhkan sistem modul PowerShell ditentukan dalam ALC PowerShell sendiri.
Untuk menengahi kedua persyaratan ini, kita harus memecah modul menjadi dua rakitan:
- Perakitan cmdlet,
AlcModule.Cmdlets.dll
, yang berisi definisi dari semua jenis yang diperlukan sistem modul PowerShell untuk memuat modul kami dengan benar. Yaitu, implementasi apa pun dari kelas dasarCmdlet
dan kelas yang mengimplementasikanIModuleAssemblyInitializer
, yang menyiapkan penanganan aktivitas untukAssemblyLoadContext.Default.Resolving
memuatAlcModule.Engine.dll
dengan benar melalui ALC kustom kami. Karena PowerShell 7 sengaja menyembunyikan jenis yang ditentukan dalam rakitan yang dimuat di ALC lain, jenis apa pun yang dimaksudkan untuk diekspos secara publik ke PowerShell juga harus didefinisikan di sini. Akhirnya, definisi ALC kustom kami perlu didefinisikan dalam rakitan ini. Di luar itu, kode sesedikitan mungkin harus hidup di perakitan ini. - Perakitan mesin,
AlcModule.Engine.dll
, yang menangani implementasi aktual modul. Jenis dari ini tersedia di PowerShell ALC, tetapi awalnya dimuat melalui ALC kustom kami. Dependensinya hanya dimuat ke dalam ALC kustom. Secara efektif, ini menjadi jembatan antara dua ALC.
Menggunakan konsep jembatan ini, situasi perakitan baru kami terlihat seperti ini:
Diagram
Untuk memastikan logika pemeriksaan dependensi ALC default tidak menyelesaikan dependensi yang akan dimuat ke dalam ALC kustom, kita perlu memisahkan kedua bagian modul ini di direktori yang berbeda. Tata letak modul baru memiliki struktur berikut:
AlcModule/
AlcModule.Cmdlets.dll
AlcModule.psd1
Dependencies/
| + AlcModule.Engine.dll
| + Shared.Dependency.dll
Untuk melihat bagaimana implementasi berubah, kita akan mulai dengan implementasi AlcModule.Engine.dll
:
using Shared.Dependency;
namespace AlcModule.Engine
{
public class AlcEngine
{
public static void Use()
{
Dependency.Use();
}
}
}
Ini adalah kontainer sederhana untuk dependensi, Shared.Dependency.dll
, tetapi Anda harus menganggapnya sebagai .NET API untuk fungsionalitas Anda yang dibungkus cmdlet di rakitan lain untuk PowerShell.
Cmdlet di AlcModule.Cmdlets.dll
terlihat seperti ini:
// Reference our module's Engine implementation here
using AlcModule.Engine;
namespace AlcModule.Cmdlets
{
[Cmdlet(VerbsDiagnostic.Test, "AlcModule")]
public class TestAlcModuleCommand : Cmdlet
{
protected override void EndProcessing()
{
AlcEngine.Use();
WriteObject("done!");
}
}
}
Pada titik ini, jika kita memuat AlcModule dan menjalankan Test-AlcModule
, kita mendapatkan FileNotFoundException ketika ALC default mencoba memuat Alc.Engine.dll
untuk menjalankan EndProcessing()
. Ini bagus, karena itu berarti ALC default tidak dapat menemukan dependensi yang ingin kita sembunyikan.
Sekarang kita perlu menambahkan kode ke AlcModule.Cmdlets.dll
sehingga tahu cara menyelesaikan AlcModule.Engine.dll
. Pertama, kita harus mendefinisikan ALC kustom kita untuk menyelesaikan rakitan dari direktori Dependencies
modul kita:
namespace AlcModule.Cmdlets
{
internal class AlcModuleAssemblyLoadContext : AssemblyLoadContext
{
private readonly string _dependencyDirPath;
public AlcModuleAssemblyLoadContext(string dependencyDirPath)
{
_dependencyDirPath = dependencyDirPath;
}
protected override Assembly Load(AssemblyName assemblyName)
{
// We do the simple logic here of looking for an assembly of the given name
// in the configured dependency directory.
string assemblyPath = Path.Combine(
_dependencyDirPath,
$"{assemblyName.Name}.dll");
if (File.Exists(assemblyPath))
{
// The ALC must use inherited methods to load assemblies.
// Assembly.Load*() won't work here.
return LoadFromAssemblyPath(assemblyPath);
}
// For other assemblies, return null to allow other resolutions to continue.
return null;
}
}
}
Kemudian kita perlu menghubungkan ALC kustom kita ke peristiwa Resolving
ALC default, yang merupakan versi ALC dari peristiwa AssemblyResolve
pada Domain Aplikasi. Peristiwa ini diaktifkan untuk menemukan AlcModule.Engine.dll
ketika EndProcessing()
dipanggil.
namespace AlcModule.Cmdlets
{
public class AlcModuleResolveEventHandler : IModuleAssemblyInitializer, IModuleAssemblyCleanup
{
// Get the path of the dependency directory.
// In this case we find it relative to the AlcModule.Cmdlets.dll location
private static readonly string s_dependencyDirPath = Path.GetFullPath(
Path.Combine(
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
"Dependencies"));
private static readonly AlcModuleAssemblyLoadContext s_dependencyAlc =
new AlcModuleAssemblyLoadContext(s_dependencyDirPath);
public void OnImport()
{
// Add the Resolving event handler here
AssemblyLoadContext.Default.Resolving += ResolveAlcEngine;
}
public void OnRemove(PSModuleInfo psModuleInfo)
{
// Remove the Resolving event handler here
AssemblyLoadContext.Default.Resolving -= ResolveAlcEngine;
}
private static Assembly ResolveAlcEngine(AssemblyLoadContext defaultAlc, AssemblyName assemblyToResolve)
{
// We only want to resolve the Alc.Engine.dll assembly here.
// Because this will be loaded into the custom ALC,
// all of *its* dependencies will be resolved
// by the logic we defined for that ALC's implementation.
//
// Note that we are safe in our assumption that the name is enough
// to distinguish our assembly here,
// since it's unique to our module.
// There should be no other AlcModule.Engine.dll on the system.
if (!assemblyToResolve.Name.Equals("AlcModule.Engine"))
{
return null;
}
// Allow our ALC to handle the directory discovery concept
//
// This is where Alc.Engine.dll is loaded into our custom ALC
// and then passed through into PowerShell's ALC,
// becoming the bridge between both
return s_dependencyAlc.LoadFromAssemblyName(assemblyToResolve);
}
}
}
Dengan implementasi baru, lihat urutan panggilan yang terjadi ketika modul dimuat dan Test-AlcModule
dijalankan:
Beberapa tempat menarik adalah:
-
IModuleAssemblyInitializer
dijalankan terlebih dahulu saat modul dimuat dan mengatur peristiwaResolving
. - Kami tidak memuat dependensi sampai
Test-AlcModule
dijalankan dan metodeEndProcessing()
dipanggil. - Ketika
EndProcessing()
dipanggil, ALC default gagal menemukanAlcModule.Engine.dll
dan mengaktifkan peristiwaResolving
. - Penanganan aktivitas kami menghubungkan ALC kustom ke ALC default dan memuat
AlcModule.Engine.dll
saja. - Ketika
AlcEngine.Use()
dipanggil dalamAlcModule.Engine.dll
, ALC kustom kembali masuk untuk menyelesaikanShared.Dependency.dll
. Secara khusus, itu selalu memuatShared.Dependency.dll
kami karena tidak pernah bertentangan dengan apa pun di ALC default dan hanya terlihat di direktoriDependencies
kami.
Merakit implementasi, tata letak kode sumber baru kami terlihat seperti ini:
+ AlcModule.psd1
+ src/
+ AlcModule.Cmdlets/
| + AlcModule.Cmdlets.csproj
| + TestAlcModuleCommand.cs
| + AlcModuleAssemblyLoadContext.cs
| + AlcModuleInitializer.cs
|
+ AlcModule.Engine/
| + AlcModule.Engine.csproj
| + AlcEngine.cs
AlcModule.Cmdlets.csproj terlihat seperti:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\AlcModule.Engine\AlcModule.Engine.csproj" />
<PackageReference Include="Microsoft.PowerShell.Sdk" Version="7.0.1" PrivateAssets="all" />
</ItemGroup>
</Project>
AlcModule.Engine.csproj terlihat seperti ini:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Shared.Dependency" Version="1.0.0" />
</ItemGroup>
</Project>
Jadi, ketika kita membangun modul, strategi kita adalah:
- Membangun
AlcModule.Engine
- Membangun
AlcModule.Cmdlets
- Salin semuanya dari
AlcModule.Engine
ke direktoriDependencies
, dan ingat apa yang kami salin - Salin semuanya dari
AlcModule.Cmdlets
yang tidak ada dalamAlcModule.Engine
ke direktori modul dasar
Karena tata letak modul di sini sangat penting untuk pemisahan dependensi, berikut adalah skrip build yang akan digunakan dari akar sumber:
param(
# The .NET build configuration
[ValidateSet('Debug', 'Release')]
[string]
$Configuration = 'Debug'
)
# Convenient reusable constants
$mod = "AlcModule"
$netcore = "netcoreapp3.1"
$copyExtensions = @('.dll', '.pdb')
# Source code locations
$src = "$PSScriptRoot/src"
$engineSrc = "$src/$mod.Engine"
$cmdletsSrc = "$src/$mod.Cmdlets"
# Generated output locations
$outDir = "$PSScriptRoot/out/$mod"
$outDeps = "$outDir/Dependencies"
# Build AlcModule.Engine
Push-Location $engineSrc
dotnet publish -c $Configuration
Pop-Location
# Build AlcModule.Cmdlets
Push-Location $cmdletsSrc
dotnet publish -c $Configuration
Pop-Location
# Ensure out directory exists and is clean
Remove-Item -Path $outDir -Recurse -ErrorAction Ignore
New-Item -Path $outDir -ItemType Directory
New-Item -Path $outDeps -ItemType Directory
# Copy manifest
Copy-Item -Path "$PSScriptRoot/$mod.psd1"
# Copy each Engine asset and remember it
$deps = [System.Collections.Generic.Hashtable[string]]::new()
Get-ChildItem -Path "$engineSrc/bin/$Configuration/$netcore/publish/" |
Where-Object { $_.Extension -in $copyExtensions } |
ForEach-Object { [void]$deps.Add($_.Name); Copy-Item -Path $_.FullName -Destination $outDeps }
# Now copy each Cmdlets asset, not taking any found in Engine
Get-ChildItem -Path "$cmdletsSrc/bin/$Configuration/$netcore/publish/" |
Where-Object { -not $deps.Contains($_.Name) -and $_.Extension -in $copyExtensions } |
ForEach-Object { Copy-Item -Path $_.FullName -Destination $outDir }
Terakhir, kami memiliki cara umum untuk mengisolasi dependensi modul kami dalam Konteks Beban Perakitan yang tetap kuat dari waktu ke waktu karena lebih banyak dependensi ditambahkan.
Untuk contoh yang lebih rinci, buka repositori GitHub ini. Contoh ini menunjukkan cara memigrasikan modul untuk menggunakan ALC, sambil menjaga modul tersebut berfungsi di .NET Framework. Ini juga menunjukkan cara menggunakan .NET Standard dan PowerShell Standard untuk menyederhanakan implementasi inti.
Solusi ini juga digunakan oleh modul Bicep PowerShell, dan posting blog Mengatasi Konflik Modul PowerShell adalah bacaan bagus lainnya tentang solusi ini.
Penanganan penyelesaian rakitan untuk pemuatan berdampingan
Meskipun kuat, solusi yang dijelaskan di atas mengharuskan perakitan modul untuk tidak secara langsung mereferensikan rakitan dependensi, tetapi sebaliknya, mereferensikan rakitan pembungkus yang mereferensikan rakitan dependensi. Rakitan pembungkus bertindak seperti jembatan, meneruskan panggilan dari rakitan modul ke rakitan dependensi. Ini membuatnya biasanya jumlah pekerjaan yang tidak sepele untuk mengadopsi solusi ini:
- Untuk modul baru, ini akan menambahkan kompleksitas tambahan ke desain dan implementasi
- Untuk modul yang ada, ini akan memerlukan pemfaktoran ulang yang signifikan
Ada solusi yang disederhanakan untuk mencapai pemuatan rakitan berdampingan, dengan menghubungkan peristiwa Resolving
dengan instans AssemblyLoadContext
kustom. Menggunakan metode ini lebih mudah bagi penulis modul tetapi memiliki dua batasan. Lihat repositori PowerShell-ALC-Samples untuk kode sampel dan dokumentasi yang menjelaskan batasan dan skenario terperinci ini untuk solusi ini.
Penting
Jangan gunakan Assembly.LoadFile
untuk tujuan isolasi dependensi. Menggunakan AssemblyLoadContext
terpisah, rakitan yang dimuat dapat ditemukan oleh kode resolusi jenis PowerShell. Oleh karena itu, mungkin ada jenis duplikat dengan nama jenis yang sepenuhnya memenuhi syarat yang sama yang tersedia dari dua ALC yang berbeda.
Domain Aplikasi Kustom
Opsi akhir dan paling ekstrem untuk isolasi perakitan adalah menggunakan Domain Aplikasi kustom. Domain Aplikasi hanya tersedia di .NET Framework. Mereka digunakan untuk menyediakan isolasi dalam proses antara bagian aplikasi .NET. Salah satu kegunaannya adalah mengisolasi beban rakitan satu sama lain dalam proses yang sama.
Namun, Domain Aplikasiadalah batas serialisasi. Objek dalam satu domain aplikasi tidak dapat dirujuk dan digunakan langsung oleh objek di domain aplikasi lain. Anda dapat mengatasinya dengan menerapkan MarshalByRefObject
. Tetapi ketika Anda tidak mengontrol jenis, seperti yang sering terjadi dengan dependensi, tidak mungkin untuk memaksa implementasi di sini. Satu-satunya solusi adalah membuat perubahan arsitektur besar. Batas serialisasi juga memiliki implikasi performa yang serius.
Karena Domain Aplikasi memiliki batasan serius ini, rumit untuk diterapkan, dan hanya berfungsi di .NET Framework, kami tidak akan memberikan contoh bagaimana Anda dapat menggunakannya di sini. Meskipun mereka layak disebut sebagai kemungkinan, mereka tidak direkomendasikan.
Jika Anda tertarik untuk mencoba menggunakan domain aplikasi kustom, tautan berikut mungkin membantu:
- dokumentasi konseptual tentang Domain Aplikasi
- Contoh untuk menggunakan Domain Aplikasi
Solusi untuk konflik dependensi yang tidak berfungsi untuk PowerShell
Terakhir, kita akan mengatasi beberapa kemungkinan yang muncul saat meneliti konflik dependensi .NET di .NET yang dapat terlihat menjanjikan, tetapi umumnya tidak akan berfungsi untuk PowerShell.
Solusi ini memiliki tema umum bahwa mereka adalah perubahan pada konfigurasi penyebaran untuk lingkungan tempat Anda mengontrol aplikasi dan mungkin seluruh komputer. Solusi ini berorientasi pada skenario seperti server web dan aplikasi lain yang disebarkan ke lingkungan server, di mana lingkungan dimaksudkan untuk menghosting aplikasi dan bebas untuk dikonfigurasi oleh pengguna yang menyebarkan. Mereka juga cenderung sangat berorientasi .NET Framework, yang berarti mereka tidak bekerja dengan PowerShell 6 atau yang lebih tinggi.
Jika Anda tahu bahwa modul Anda hanya digunakan di lingkungan Windows PowerShell 5.1 yang memiliki kontrol penuh, beberapa di antaranya mungkin merupakan opsi. Namun secara umum, modul tidak boleh memodifikasi status komputer global seperti ini. Ini dapat memutus konfigurasi yang menyebabkan masalah dalam powershell.exe
, modul lain, atau aplikasi dependen lainnya yang menyebabkan modul Anda gagal dengan cara yang tidak terduga.
Pengalihan pengikatan statis dengan app.config untuk memaksa menggunakan versi dependensi yang sama
Aplikasi .NET Framework dapat memanfaatkan file app.config
untuk mengonfigurasi beberapa perilaku aplikasi secara deklaratif. Anda dapat menulis entri app.config
yang mengonfigurasi pengikatan perakitan untuk mengalihkan pemuatan rakitan ke versi tertentu.
Dua masalah dengan ini untuk PowerShell adalah:
- .NET Core tidak mendukung
app.config
, sehingga solusi ini hanya berlaku untukpowershell.exe
. -
powershell.exe
adalah aplikasi bersama yang berada di direktoriSystem32
. Kemungkinan modul Anda tidak akan dapat memodifikasi kontennya di banyak sistem. Bahkan jika bisa, memodifikasiapp.config
dapat merusak konfigurasi yang ada atau memengaruhi pemuatan modul lain.
Mengatur codebase
dengan app.config
Untuk alasan yang sama, mencoba mengonfigurasi pengaturan codebase
di app.config
tidak akan berfungsi di modul PowerShell.
Menginstal dependensi ke Global Assembly Cache (GAC)
Cara lain untuk mengatasi konflik versi dependensi di .NET Framework adalah dengan menginstal dependensi ke GAC, sehingga versi yang berbeda dapat dimuat berdampingan dari GAC.
Sekali lagi, untuk modul PowerShell, masalah utama di sini adalah:
- GAC hanya berlaku untuk .NET Framework, sehingga ini tidak membantu di PowerShell 6 ke atas.
- Menginstal rakitan ke GAC adalah modifikasi status komputer global dan dapat menyebabkan efek samping di aplikasi lain atau ke modul lain. Mungkin juga sulit untuk dilakukan dengan benar, bahkan ketika modul Anda memiliki hak istimewa akses yang diperlukan. Kesalahan dapat menyebabkan masalah serius di seluruh komputer di aplikasi .NET lainnya.
Bacaan lebih lanjut
Ada banyak lagi untuk dibaca pada konflik dependensi versi rakitan .NET. Berikut adalah beberapa titik lompat bagus:
- .NET: Rakitan di .NET
- .NET Core: Algoritma pemuatan perakitan terkelola
- .NET Core: Memahami System.Runtime.Loader.AssemblyLoadContext
- .NET Core: Diskusi tentang solusi pemuatan rakitan berdampingan
- .NET Framework: Mengalihkan versi rakitan
- .NET Framework: Praktik terbaik untuk pemuatan perakitan
- .NET Framework: Bagaimana runtime menemukan rakitan
- .NET Framework: Mengatasi beban rakitan
- Stack Overflow: Pengalihan pengikatan perakitan, bagaimana dan mengapa?
- PowerShell: Diskusi tentang menerapkan AssemblyLoadContexts
-
PowerShell:
Assembly.LoadFile()
tidak dimuat ke assemblyLoadContext default - Rick Strahl: Kapan dependensi rakitan .NET dimuat?
- Jon Skeet: Ringkasan penerapan versi di .NET
- Nate McMaster: Mendalami primitif .NET Core