Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Podczas pisania binarnego modułu programu PowerShell w języku C# naturalne jest podjęcie zależności od innych pakietów lub bibliotek w celu zapewnienia funkcjonalności. Pobieranie zależności od innych bibliotek jest pożądane w przypadku ponownego użycia kodu. Program PowerShell zawsze ładuje zestawy do tego samego kontekstu. W ten sposób występują problemy, gdy zależności modułu powodują konflikt z już załadowanymi bibliotekami DLL i mogą uniemożliwić używanie dwóch w inny sposób niepowiązanych modułów w tej samej sesji programu PowerShell.
Jeśli wystąpił ten problem, został wyświetlony komunikat o błędzie podobny do następującego:
komunikat o błędzie ładowania zestawu
W tym artykule omówiono niektóre sposoby występowania konfliktów zależności w programie PowerShell i sposoby rozwiązywania problemów z konfliktami zależności. Nawet jeśli nie jesteś autorem modułu, w tym miejscu istnieją pewne wskazówki, które mogą pomóc w przypadku konfliktów zależności występujących w modułach, których używasz.
Dlaczego występują konflikty zależności?
Na platformie .NET konflikty zależności występują, gdy dwa wersje tego samego zestawu są ładowane do tego samego kontekstu ładowania zestawu . Ten termin oznacza nieco inne elementy na różnych platformach .NET, które zostały omówione później w tym artykule. Ten konflikt jest typowym problemem występującym w dowolnym oprogramowaniu, w którym są używane zależności w wersji.
Problemy powodujące konflikty są złożone przez fakt, że projekt prawie nigdy celowo lub bezpośrednio nie zależy od dwóch wersji tej samej zależności. Zamiast tego projekt ma co najmniej dwie zależności, które wymagają innej wersji tej samej zależności.
Załóżmy na przykład, że aplikacja .NET, DuckBuilder
, wprowadza dwie zależności, aby wykonać części jej funkcji i wygląda następująco:
Newtonsoft.Json
Ponieważ Contoso.ZipTools
i Fabrikam.FileHelpers
zależą od różnych wersji Newtonsoft.Json, może wystąpić konflikt zależności w zależności od sposobu ładowania każdej zależności.
Konflikt z zależnościami programu PowerShell
W programie PowerShell problem z konfliktem zależności jest powiększony, ponieważ własne zależności programu PowerShell są ładowane do tego samego kontekstu udostępnionego. Oznacza to, że aparat programu PowerShell i wszystkie załadowane moduły programu PowerShell nie mogą mieć zależności powodujące konflikt. Klasycznym przykładem jest Newtonsoft.Json:
moduł
W tym przykładzie moduł FictionalTools
zależy od wersji 12.0.3
Newtonsoft.Json, która jest nowszą wersją Newtonsoft.Json niż 11.0.2
dostarczanej w przykładowym programie PowerShell.
Nuta
Jest to przykład. Program PowerShell 7.0 jest obecnie dostarczany z programem Newtonsoft.Json 12.0.3. Nowsze wersje programu PowerShell mają nowsze wersje Newtonsoft.Json.
Ponieważ moduł zależy od nowszej wersji zestawu, nie zaakceptuje wersji, którą program PowerShell już załadował. Ponieważ program PowerShell załadował już wersję zestawu, moduł nie może załadować własnej wersji przy użyciu konwencjonalnego mechanizmu ładowania.
Konflikt z zależnościami innego modułu
Innym typowym scenariuszem w programie PowerShell jest załadowanie modułu zależnego od jednej wersji zestawu, a następnie załadowanie innego modułu później, który zależy od innej wersji tego zestawu.
Często wygląda to następująco:
W takim przypadku moduł FictionalTools
wymaga nowszej wersji Microsoft.Extensions.Logging
niż moduł FilesystemManager
.
Wyobraź sobie, że te moduły ładują zależności, umieszczając zestawy zależności w tym samym katalogu co zestaw modułu głównego. Dzięki temu platforma .NET może niejawnie ładować je według nazwy. Jeśli używamy programu PowerShell 7.0 (na platformie .NET Core 3.1), możemy załadować i uruchomić FictionalTools
, a następnie załadować i uruchomić FilesystemManager
bez problemu. Jednak w nowej sesji, jeśli załadujemy i uruchomimy FilesystemManager
, załadujemy FictionalTools
, otrzymamy FileLoadException
z polecenia FictionalTools
, ponieważ wymaga nowszej wersji Microsoft.Extensions.Logging
niż załadowany.
FictionalTools
nie można załadować wymaganej wersji, ponieważ zestaw o tej samej nazwie został już załadowany.
Program PowerShell i platforma .NET
Program PowerShell działa na platformie .NET, która jest odpowiedzialna za rozpoznawanie i ładowanie zależności zestawu. Musimy zrozumieć, jak platforma .NET działa tutaj, aby zrozumieć konflikty zależności.
Musimy również zmierzyć się z faktem, że różne wersje programu PowerShell działają w różnych implementacjach platformy .NET. Ogólnie rzecz biorąc, program PowerShell 5.1 lub nowszy działa w programie .NET Framework, podczas gdy program PowerShell 6 i nowsze są uruchamiane na platformie .NET Core. Te dwie implementacje ładowania platformy .NET i obsługują zestawy inaczej. Oznacza to, że rozwiązywanie konfliktów zależności może się różnić w zależności od bazowej platformy .NET.
Konteksty ładowania zestawów
Na platformie .NET kontekst ładowania zestawu (ALC) to przestrzeń nazw środowiska uruchomieniowego, do której są ładowane zestawy. Nazwy zestawów muszą być unikatowe. Ta koncepcja umożliwia unikatowe rozpoznawanie zestawów według nazwy w każdym ALC.
Ładowanie odwołań do zestawów na platformie .NET
Semantyka ładowania zestawu zależy od implementacji platformy .NET (.NET Core i .NET Framework) oraz interfejsu API platformy .NET używanego do ładowania określonego zestawu. Zamiast szczegółowo poznać tutaj, w sekcji Więcej informacji na temat znajdują się szczegółowe informacje na temat sposobu działania ładowania zestawu platformy .NET w każdej implementacji platformy .NET.
W tym artykule odwołujemy się do następujących mechanizmów:
- Niejawne ładowanie zestawu (skutecznie
Assembly.Load(AssemblyName)
), gdy platforma .NET niejawnie próbuje załadować zestaw według nazwy ze statycznego odwołania do zestawu w kodzie platformy .NET. -
Assembly.LoadFrom()
interfejs API ładowania zorientowanego na wtyczkę, który dodaje programy obsługi w celu rozpoznawania zależności załadowanej biblioteki DLL. Ta metoda może nie rozpoznawać zależności w żądany sposób. -
Assembly.LoadFile()
, podstawowy interfejs API ładowania przeznaczony do załadowania tylko zestawu wymaganego i nie obsługuje żadnych zależności.
Różnice w programie .NET Framework a .NET Core
Sposób działania tych interfejsów API zmienił się w subtelny sposób między platformami .NET Core i .NET Framework, dlatego warto przeczytać zawarte linki . Co ważne, konteksty ładowania zestawów i inne mechanizmy rozwiązywania zestawów zmieniły się między programem .NET Framework i platformą .NET Core.
W szczególności program .NET Framework ma następujące funkcje:
- GlobalNa pamięć podręczna zestawów do rozpoznawania zestawów dla całego komputera
- Domeny aplikacji, które działają jak piaskownice procesów w celu izolacji zestawu, ale także przedstawiają warstwę serializacji, z którą można się zmierzyć
- Ograniczony model kontekstu obciążenia zestawu, który ma stały zestaw kontekstów ładowania zestawów, z których każdy ma własne zachowanie:
- Domyślny kontekst ładowania, w którym zestawy są domyślnie ładowane
- Ładowanie z kontekstu do ręcznego ładowania zestawów w czasie wykonywania
- Kontekst tylko odbicia w celu bezpiecznego ładowania zestawów do odczytywania metadanych bez ich uruchamiania
- Tajemnicza pustka, w którą żyją zestawy załadowane
Assembly.LoadFile(string path)
iAssembly.Load(byte[] asmBytes)
Aby uzyskać więcej informacji, zobacz Best Practices for Assembly Loading.
Platforma .NET Core (i platforma .NET 5+) zastąpiła tę złożoność prostszym modelem:
- Brak globalnej pamięci podręcznej zestawów. Aplikacje przynoszą wszystkie własne zależności. Spowoduje to usunięcie czynnika zewnętrznego do rozwiązywania zależności w aplikacjach, dzięki czemu rozpoznawanie zależności będzie bardziej powtarzalne.
Program PowerShell, jako host wtyczki, komplikuje to nieco w przypadku modułów. Jego zależności w
$PSHOME
są współużytkowane ze wszystkimi modułami. - Tylko jedna domena aplikacji i brak możliwości tworzenia nowych. Koncepcja domeny aplikacji jest utrzymywana na platformie .NET jako globalny stan procesu .NET.
- Nowy, rozszerzalny model kontekstu ładowania zestawów (ALC). Rozpoznawanie zestawów może być przestrzeń nazw, umieszczając ją w nowym ALC. Procesy platformy .NET rozpoczynają się od jednego domyślnego ALC, do którego są ładowane wszystkie zestawy (z wyjątkiem tych załadowanych z
Assembly.LoadFile(string)
iAssembly.Load(byte[])
). Jednak proces może tworzyć i definiować własne niestandardowe kontrolery ALC przy użyciu własnej logiki ładowania. Po załadowaniu zestawu pierwszy element ALC, do niego załadowany, jest odpowiedzialny za rozpoznawanie jego zależności. Dzięki temu można zaimplementować zaawansowane mechanizmy ładowania wtyczek platformy .NET.
W obu implementacjach zestawy są ładowane leniwie. Oznacza to, że są ładowane po pierwszym uruchomieniu metody wymagającej ich typu.
Na przykład poniżej przedstawiono dwie wersje tego samego kodu, które ładują zależność w różnym czasie.
Pierwszy zawsze ładuje zależność, gdy jest wywoływana Program.GetRange()
, ponieważ odwołanie zależności jest leksykalnie obecne w metodzie:
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;
}
}
Drugi ładuje zależność tylko wtedy, gdy parametr limit
wynosi 20 lub więcej, ze względu na wewnętrzną pośredniość za pomocą metody:
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();
}
}
Jest to dobra praktyka, ponieważ minimalizuje pamięć i operacje we/wy systemu plików i wykorzystują zasoby wydajniej. Niefortunny efekt uboczny jest taki, że nie będziemy wiedzieć, że nie można załadować zestawu, dopóki nie osiągniemy ścieżki kodu, która próbuje załadować zestaw.
Może również utworzyć warunek chronometrażu dla konfliktów obciążenia zestawu. Jeśli dwie części tego samego programu spróbują załadować różne wersje tego samego zestawu, załadowana wersja zależy od tego, która ścieżka kodu jest uruchamiana jako pierwsza.
W przypadku programu PowerShell oznacza to, że następujące czynniki mogą mieć wpływ na konflikt obciążenia zestawu:
- Który moduł został załadowany jako pierwszy?
- Czy ścieżka kodu korzystająca z biblioteki zależności została uruchomiona?
- Czy program PowerShell ładuje zależność powodującą konflikt podczas uruchamiania lub tylko w ramach określonych ścieżek kodu?
Szybkie poprawki i ich ograniczenia
W niektórych przypadkach można wprowadzić niewielkie korekty w module i naprawić elementy przy minimalnym nakładzie pracy. Jednak te rozwiązania mają tendencję do tworzenia zastrzeżeń. Chociaż mogą one dotyczyć modułu, nie będą działać dla każdego modułu.
Zmienianie wersji zależności
Najprostszym sposobem uniknięcia konfliktów zależności jest uzgodnienie zależności. Może to być możliwe w następujących przypadkach:
- Twój konflikt jest bezpośrednią zależnością modułu i kontrolujesz wersję.
- Konflikt dotyczy zależności pośredniej, ale możesz skonfigurować bezpośrednie zależności tak, aby korzystały z wersji zależności pośredniej z możliwością działania.
- Znasz wersję powodującą konflikt i możesz polegać na niej, że nie zmienia się.
Pakiet
Na przykład program PowerShell 6.2.6 i program PowerShell 7.0.2 obecnie używają Newtonsoft.Json w wersji 12.0.3. Aby utworzyć moduł przeznaczony dla programu Windows PowerShell, programu PowerShell 6 i programu PowerShell 7, należy kierować Newtonsoft.Json 12.0.3 jako zależność i dołączyć go do wbudowanego modułu. Po załadowaniu modułu w programie PowerShell 6 lub 7 jest już załadowany własny zestaw Newtonsoft.Json programu PowerShell. Ponieważ jest to wersja wymagana dla modułu, rozwiązanie powiedzie się. W programie Windows PowerShell zestaw nie jest jeszcze obecny w programie PowerShell, więc zamiast tego jest ładowany z folderu modułu.
Ogólnie rzecz biorąc, w przypadku określania konkretnego pakietu programu PowerShell, takiego jak Microsoft.PowerShell.Sdk lub System.Management.Automation, pakiet NuGet powinien mieć możliwość rozwiązania odpowiednich wymaganych wersji zależności. Określanie wartości docelowych zarówno programu Windows PowerShell, jak i programu PowerShell 6+ staje się trudniejsze, ponieważ należy wybrać między określaniem wartości docelowej wielu platform lub PowerShellStandard.Library.
Okoliczności, w których przypinanie do typowej wersji zależności nie będzie działać:
- Konflikt dotyczy zależności pośredniej, a żadna z zależności nie może być skonfigurowana do używania wspólnej wersji.
- Inna wersja zależności może się często zmienić, więc osiedlenie się na wspólnej wersji jest tylko krótkoterminową poprawką.
Używanie zależności poza procesem
To rozwiązanie jest bardziej przeznaczone dla użytkowników modułów niż autorzy modułów. Jest to rozwiązanie używane podczas konfrontacji z modułem, który nie będzie działać z powodu istniejącego konfliktu zależności.
Konflikty zależności występują, ponieważ dwie wersje tego samego zestawu są ładowane do tego samego procesu platformy .NET. Prostym rozwiązaniem jest załadowanie ich do różnych procesów, o ile nadal można używać funkcji z obu tych procesów.
W programie PowerShell istnieje kilka sposobów, aby to osiągnąć:
Wywoływanie programu PowerShell jako podprocesu
Aby uruchomić polecenie programu PowerShell poza bieżącym procesem, uruchom nowy proces programu PowerShell bezpośrednio za pomocą wywołania polecenia:
pwsh -c 'Invoke-ConflictingCommand'
Głównym ograniczeniem jest to, że restrukturyzacja wyniku może być trudniejsza lub bardziej podatna na błędy niż inne opcje.
System zadań programu PowerShell
System zadań programu PowerShell uruchamia również polecenia poza procesem, wysyłając polecenia do nowego procesu programu PowerShell i zwracając wyniki:
$result = Start-Job { Invoke-ConflictingCommand } | Receive-Job -Wait
W takim przypadku wystarczy upewnić się, że wszystkie zmienne i stan są prawidłowo przekazywane.
System zadań może być również nieco kłopotliwy podczas uruchamiania małych poleceń.
Komunikacja zdalna programu PowerShell
Gdy jest dostępna, komunikacja zdalna programu PowerShell może być przydatnym sposobem uruchamiania poleceń poza procesem. Za pomocą komunikacji zdalnej można utworzyć nową PSSession w nowym procesie, wywołać polecenia za pośrednictwem komunikacji zdalnej programu PowerShell, a następnie użyć wyników lokalnie z innymi modułami zawierającymi zależności powodujące konflikt.
Przykład może wyglądać następująco:
# 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
Niejawna komunikacja zdalna z programem Windows PowerShell
Inną opcją w programie PowerShell 7 jest użycie flagi
-UseWindowsPowerShell
naImport-Module
. Spowoduje to zaimportowanie modułu za pośrednictwem lokalnej sesji komunikacji zdalnej do programu Windows PowerShell:Import-Module -Name ConflictingModule -UseWindowsPowerShell
Należy pamiętać, że moduły mogą nie być zgodne z programem Lub mogą działać inaczej w programie Windows PowerShell.
W przypadku wywołania poza procesem nie należy używać
Jako autor modułu wywołanie polecenia poza procesem jest trudne do pieczenia w module i może mieć przypadki brzegowe, które powodują problemy. W szczególności komunikacja zdalna i zadania mogą nie być dostępne we wszystkich środowiskach, w których moduł musi działać. Jednak ogólna zasada przenoszenia implementacji poza proces i umożliwienie modułu programu PowerShell być klientem cieńszym, może nadal mieć zastosowanie.
Jako użytkownik modułu istnieją przypadki, w których wywołanie poza procesem nie będzie działać:
- Gdy komunikacja zdalna programu PowerShell jest niedostępna, ponieważ nie masz uprawnień do jej używania lub nie jest włączona.
- Gdy określony typ platformy .NET jest wymagany z danych wyjściowych jako dane wejściowe do metody lub innego polecenia. Polecenia uruchamiane za pośrednictwem komunikacji zdalnej programu PowerShell emitują obiekty deserializowane, a nie silnie typizowane obiekty platformy .NET. Oznacza to, że wywołania metod i silnie typizowane interfejsy API nie działają z danymi wyjściowymi poleceń zaimportowanych za pośrednictwem komunikacji telefonicznej.
Bardziej niezawodne rozwiązania
Poprzednie rozwiązania miały wszystkie scenariusze i moduły, które nie działają. Jednak mają one również zaletę stosunkowo prostego wdrożenia. Poniższe rozwiązania są bardziej niezawodne, ale wymagają większego nakładu pracy w celu poprawnego zaimplementowania i mogą wprowadzać subtelne błędy, jeśli nie zostały starannie napisane.
Ładowanie za pośrednictwem kontekstów ładowania zestawów platformy .NET Core
konteksty ładowania zestawów (ALCs) zostały wprowadzone na platformie .NET Core 1.0, aby w szczególności rozwiązać konieczność załadowania wielu wersji tego samego zestawu do tego samego środowiska uruchomieniowego.
Na platformie .NET oferują najbardziej niezawodne rozwiązanie problemu ładowania wersji zestawu powodującego konflikt. Niestandardowe kontrolery ALC nie są jednak dostępne w programie .NET Framework. Oznacza to, że to rozwiązanie działa tylko w programie PowerShell 6 lub nowszym.
Obecnie najlepszym przykładem użycia usługi ALC na potrzeby izolacji zależności w programie PowerShell są usługi edytora programu PowerShell, serwer językowy rozszerzenia programu PowerShell dla programu Visual Studio Code. ALC jest używany, aby zapobiec starciu własnych zależności usług edytora programu PowerShell z tymi w modułach programu PowerShell.
Implementowanie izolacji zależności modułu za pomocą usługi ALC jest koncepcyjnie trudne, ale będziemy pracować z minimalnym przykładem. Załóżmy, że mamy prosty moduł przeznaczony tylko do pracy w programie PowerShell 7. Kod źródłowy jest zorganizowany w następujący sposób:
+ AlcModule.psd1
+ src/
+ TestAlcModuleCommand.cs
+ AlcModule.csproj
Implementacja polecenia cmdlet wygląda następująco:
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!");
}
}
}
Manifest (mocno uproszczony) wygląda następująco:
@{
Author = 'Me'
ModuleVersion = '0.0.1'
RootModule = 'AlcModule.dll'
CmdletsToExport = @('Test-AlcModule')
PowerShellVersion = '7.0'
}
A csproj
wygląda następująco:
<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>
Podczas kompilowanie tego modułu wygenerowane dane wyjściowe mają następujący układ:
AlcModule/
+ AlcModule.psd1
+ AlcModule.dll
+ Shared.Dependency.dll
W tym przykładzie naszym problemem jest zestaw Shared.Dependency.dll
, który jest naszą wyimaginowaną zależnością powodującą konflikt. Jest to zależność, którą musimy umieścić za usługą ALC, aby można było użyć wersji specyficznej dla modułu.
Musimy ponownie zaprojektować moduł, aby:
- Zależności modułów są ładowane tylko do niestandardowego ALC, a nie do usługi ALC programu PowerShell, więc nie może występować konflikt. Ponadto w miarę dodawania większej liczby zależności do naszego projektu nie chcemy stale dodawać więcej kodu, aby kontynuować ładowanie. Zamiast tego chcemy używać logiki rozpoznawania zależności wielokrotnego użytku.
- Ładowanie modułu nadal działa normalnie w programie PowerShell. Polecenia cmdlet i inne typy, których wymaga system modułu programu PowerShell, są definiowane we własnym ALC programu PowerShell.
Aby pośrednicować w tych dwóch wymaganiach, musimy podzielić moduł na dwa zestawy:
- Zestaw poleceń cmdlet,
AlcModule.Cmdlets.dll
, który zawiera definicje wszystkich typów, które system modułów programu PowerShell musi poprawnie załadować nasz moduł. Mianowicie wszystkie implementacje klasy bazowejCmdlet
i klasy implementująceIModuleAssemblyInitializer
, która konfiguruje program obsługi zdarzeń dlaAssemblyLoadContext.Default.Resolving
, aby prawidłowo załadowaćAlcModule.Engine.dll
za pośrednictwem naszego niestandardowego ALC. Ponieważ program PowerShell 7 celowo ukrywa typy zdefiniowane w zestawach załadowanych w innych bibliotekach ALC, wszystkie typy, które mają być publicznie widoczne dla programu PowerShell, muszą być również zdefiniowane w tym miejscu. Na koniec nasza niestandardowa definicja ALC musi być zdefiniowana w tym zestawie. Poza tym tak mało kodu, jak to możliwe, powinno istnieć w tym zestawie. - Zestaw aparatu,
AlcModule.Engine.dll
, który obsługuje rzeczywistą implementację modułu. Typy z tego typu są dostępne w usłudze PowerShell ALC, ale są one początkowo ładowane za pośrednictwem naszego niestandardowego ALC. Jego zależności są ładowane tylko do niestandardowego ALC. W rzeczywistości staje się to mostem między dwiema ALC.
Korzystając z tej koncepcji mostu, nasza nowa sytuacja montażu wygląda następująco:
diagram
Aby upewnić się, że domyślna logika sondowania zależności usługi ALC nie rozwiązuje problemów z zależnościami, które mają zostać załadowane do niestandardowego zestawu ALC, musimy oddzielić te dwie części modułu w różnych katalogach. Nowy układ modułu ma następującą strukturę:
AlcModule/
AlcModule.Cmdlets.dll
AlcModule.psd1
Dependencies/
| + AlcModule.Engine.dll
| + Shared.Dependency.dll
Aby zobaczyć, jak zmienia się implementacja, zaczniemy od implementacji AlcModule.Engine.dll
:
using Shared.Dependency;
namespace AlcModule.Engine
{
public class AlcEngine
{
public static void Use()
{
Dependency.Use();
}
}
}
Jest to prosty kontener dla zależności, Shared.Dependency.dll
, ale należy traktować go jako interfejs API platformy .NET dla funkcji, które polecenia cmdlet w innym zestawie zawijają dla programu PowerShell.
Polecenie cmdlet w AlcModule.Cmdlets.dll
wygląda następująco:
// 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!");
}
}
}
W tym momencie, jeśli mieliśmy załadować AlcModule i uruchomić Test-AlcModule
, otrzymujemy FileNotFoundException, gdy domyślna aplikacja ALC próbuje załadować Alc.Engine.dll
do uruchomienia EndProcessing()
. Jest to dobre, ponieważ oznacza to, że domyślna aplikacja ALC nie może odnaleźć zależności, które chcemy ukryć.
Teraz musimy dodać kod do AlcModule.Cmdlets.dll
, aby wiedzieć, jak rozwiązać AlcModule.Engine.dll
. Najpierw musimy zdefiniować nasz niestandardowy zestaw ALC, aby rozpoznawać zestawy z katalogu Dependencies
modułu:
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;
}
}
}
Następnie musimy podłączyć nasze niestandardowe ALC do domyślnego zdarzenia Resolving
ALC, który jest wersją ALC zdarzenia AssemblyResolve
w domenach aplikacji. To zdarzenie jest wyzwalane w celu znalezienia AlcModule.Engine.dll
po wywołaniu EndProcessing()
.
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);
}
}
}
W przypadku nowej implementacji przyjrzyj się sekwencji wywołań, która występuje po załadowaniu modułu i Test-AlcModule
jest uruchamiana:
Niektóre punkty orientacyjne to:
-
IModuleAssemblyInitializer
jest uruchamiany jako pierwszy, gdy moduł ładuje i ustawia zdarzenieResolving
. - Nie ładujemy zależności, dopóki nie zostanie uruchomiona
Test-AlcModule
, a wywoływana jest jej metodaEndProcessing()
. - Po wywołaniu
EndProcessing()
domyślna usługa ALC nie może odnaleźćAlcModule.Engine.dll
i uruchamia zdarzenieResolving
. - Nasza procedura obsługi zdarzeń podłącza niestandardowy element ALC do domyślnego ALC i ładuje tylko
AlcModule.Engine.dll
. - Gdy
AlcEngine.Use()
jest wywoływana wAlcModule.Engine.dll
, niestandardowy ALC ponownie rozpoczyna się w celu rozwiązaniaShared.Dependency.dll
. W szczególności zawsze ładuje naszychShared.Dependency.dll
, ponieważ nigdy nie powoduje konfliktów z niczym w domyślnym ALC i wygląda tylko w naszym kataloguDependencies
.
Zestaw implementacji nowy układ kodu źródłowego wygląda następująco:
+ AlcModule.psd1
+ src/
+ AlcModule.Cmdlets/
| + AlcModule.Cmdlets.csproj
| + TestAlcModuleCommand.cs
| + AlcModuleAssemblyLoadContext.cs
| + AlcModuleInitializer.cs
|
+ AlcModule.Engine/
| + AlcModule.Engine.csproj
| + AlcEngine.cs
Plik AlcModule.Cmdlets.csproj wygląda następująco:
<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>
Plik AlcModule.Engine.csproj wygląda następująco:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Shared.Dependency" Version="1.0.0" />
</ItemGroup>
</Project>
Dlatego podczas tworzenia modułu nasza strategia to:
- Kompilowanie
AlcModule.Engine
- Kompilowanie
AlcModule.Cmdlets
- Skopiuj wszystko, od
AlcModule.Engine
do kataloguDependencies
i zapamiętaj skopiowane elementy - Skopiuj wszystkie elementy z
AlcModule.Cmdlets
, które nie zostałyAlcModule.Engine
do katalogu podstawowego modułu
Ponieważ układ modułu w tym miejscu ma kluczowe znaczenie dla separacji zależności, oto skrypt kompilacji do użycia z katalogu głównego źródła:
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 }
Na koniec mamy ogólny sposób izolowania zależności modułu w kontekście ładowania zestawu, który pozostaje niezawodny w miarę dodawania większej liczby zależności.
Aby uzyskać bardziej szczegółowy przykład, przejdź do tego repozytorium GitHub. W tym przykładzie pokazano, jak przeprowadzić migrację modułu do korzystania z usługi ALC, zachowując jednocześnie działanie tego modułu w programie .NET Framework. Pokazano również, jak używać platformy .NET Standard i programu PowerShell Standard w celu uproszczenia podstawowej implementacji.
To rozwiązanie jest również używane przez moduł Bicep PowerShell, a wpis w blogu rozwiązywanie konfliktów modułów programu PowerShell jest kolejnym dobrym rozwiązaniem.
Procedura obsługi rozpoznawania zestawów na potrzeby ładowania równoległego
Mimo że jest niezawodne, opisane powyżej rozwiązanie wymaga, aby zestaw modułu nie odwołył się bezpośrednio do zestawów zależności, ale zamiast tego odwołał się do zestawu otoki odwołującego się do zestawów zależności. Zestaw otoki działa jak most, przekazując wywołania z zestawu modułu do zestawów zależności. Dzięki temu zwykle nie jest to prosta ilość pracy w celu wdrożenia tego rozwiązania:
- W przypadku nowego modułu spowoduje to dodanie dodatkowej złożoności do projektowania i implementacji
- W przypadku istniejącego modułu wymagałoby to znacznej refaktoryzacji
Istnieje uproszczone rozwiązanie, które umożliwia równoległe ładowanie zestawów, podłączając zdarzenie Resolving
z niestandardowym wystąpieniem AssemblyLoadContext
. Użycie tej metody jest łatwiejsze dla autora modułu, ale ma dwa ograniczenia. Zapoznaj się z repozytorium PowerShell-ALC-Samples, aby zapoznać się z przykładowym kodem i dokumentacją, która opisuje te ograniczenia i szczegółowe scenariusze dla tego rozwiązania.
Ważny
Nie należy używać Assembly.LoadFile
w celu izolacji zależności. Użycie AssemblyLoadContext
, załadowane zestawy są wykrywalne przez kod rozpoznawania typu programu PowerShell. W związku z tym mogą istnieć zduplikowane typy o tej samej pełnej nazwie typu qualifed dostępnej z dwóch różnych ALC.
Niestandardowe domeny aplikacji
Ostateczną i najbardziej ekstremalną opcją izolacji zestawu jest użycie niestandardowych domen aplikacji. domeny aplikacji są dostępne tylko w programie .NET Framework. Są one używane do zapewnienia izolacji w procesie między częściami aplikacji platformy .NET. Jednym z zastosowań jest odizolowanie obciążeń zestawów od siebie w ramach tego samego procesu.
Jednak domeny aplikacjisą granicami serializacji. Nie można odwoływać się do obiektów w jednej domenie aplikacji i używać ich bezpośrednio w innej domenie aplikacji. Można to obejść, implementując MarshalByRefObject
. Ale jeśli nie kontrolujesz typów, tak jak często w przypadku zależności, nie można wymusić implementacji w tym miejscu. Jedynym rozwiązaniem jest wprowadzenie dużych zmian architektury. Granica serializacji ma również poważne konsekwencje dla wydajności.
Ponieważ domeny aplikacji mają to poważne ograniczenie, są skomplikowane do zaimplementowania i działają tylko na platformie .NET Framework, nie przedstawimy przykładu sposobu ich użycia w tym miejscu. Chociaż warto wspomnieć o możliwości, nie są zalecane.
Jeśli interesuje Cię próba użycia domeny aplikacji niestandardowej, poniższe linki mogą pomóc:
- dokumentacja koncepcyjna dotycząca domen aplikacji
- przykłady dotyczące używania domen aplikacji
Rozwiązania konfliktów zależności, które nie działają w programie PowerShell
Na koniec zajmiemy się pewnymi możliwościami, które pojawiają się podczas badania konfliktów zależności platformy .NET na platformie .NET, które mogą wyglądać obiecująco, ale ogólnie nie będą działać w programie PowerShell.
Te rozwiązania mają wspólny motyw, który są zmianami konfiguracji wdrażania dla środowiska, w którym kontrolujesz aplikację i prawdopodobnie całą maszynę. Te rozwiązania są ukierunkowane na scenariusze, takie jak serwery internetowe i inne aplikacje wdrożone w środowiskach serwera, w których środowisko ma obsługiwać aplikację i jest bezpłatne do skonfigurowania przez użytkownika wdrażającego. Zwykle są one również bardzo zorientowane na platformę .NET Framework, co oznacza, że nie działają z programem PowerShell 6 lub nowszym.
Jeśli wiesz, że moduł jest używany tylko w środowiskach programu Windows PowerShell 5.1, nad którymi masz całkowitą kontrolę, niektóre z nich mogą być opcjami. Ogólnie jednak moduły nie powinny modyfikować globalnego stanu maszyny, takiego jak ten. Może przerwać konfiguracje, które powodują problemy w powershell.exe
, innych modułach lub innych aplikacjach zależnych, które powodują niepowodzenie modułu w nieoczekiwany sposób.
Przekierowanie powiązania statycznego z app.config w celu wymuszenia użycia tej samej wersji zależności
Aplikacje .NET Framework mogą korzystać z pliku app.config
w celu deklaratywnego konfigurowania niektórych zachowań aplikacji. Istnieje możliwość zapisania wpisu app.config
, który konfiguruje powiązanie zestawu w celu przekierowania ładowania zestawu do określonej wersji.
Dwa problemy z tym związane z programem PowerShell to:
- Platforma .NET Core nie obsługuje
app.config
, dlatego to rozwiązanie dotyczy tylkopowershell.exe
. -
powershell.exe
to aplikacja udostępniona, która znajduje się w kataloguSystem32
. Prawdopodobnie moduł nie będzie mógł modyfikować jego zawartości w wielu systemach. Nawet jeśli to możliwe, modyfikowanieapp.config
może spowodować przerwanie istniejącej konfiguracji lub wpłynąć na ładowanie innych modułów.
Ustawianie codebase
przy użyciu app.config
Z tych samych powodów próba skonfigurowania ustawienia codebase
w app.config
nie będzie działać w modułach programu PowerShell.
Instalowanie zależności do globalnej pamięci podręcznej zestawów (GAC)
Innym sposobem rozwiązywania konfliktów wersji zależności w programie .NET Framework jest zainstalowanie zależności do GAC, dzięki czemu różne wersje mogą być ładowane równolegle z GAC.
Ponownie w przypadku modułów programu PowerShell główne problemy są następujące:
- Funkcja GAC ma zastosowanie tylko do programu .NET Framework, więc nie pomaga to w programie PowerShell 6 lub nowszym.
- Instalowanie zestawów w GAC jest modyfikacją globalnego stanu maszyny i może spowodować skutki uboczne w innych aplikacjach lub innych modułach. Może to być również trudne do wykonania prawidłowo, nawet jeśli moduł ma wymagane uprawnienia dostępu. Nieprawidłowe rozwiązanie może spowodować poważne problemy dotyczące całej maszyny w innych aplikacjach platformy .NET.
Linki zewnętrzne
Istnieje o wiele więcej informacji na temat konfliktów zależności wersji zestawu platformy .NET. Oto kilka miłych skoków punktów:
- .NET: zestawy na platformie .NET
- .NET Core: algorytm ładowania zestawu zarządzanego
- .NET Core: Opis elementu System.Runtime.Loader.AssemblyLoadContext
- .NET Core: omówienie rozwiązań ładowania zestawów równoległych
- .NET Framework: przekierowywanie wersji zestawów
- .NET Framework: najlepsze rozwiązania dotyczące ładowania zestawów
- .NET Framework: jak środowisko uruchomieniowe lokalizuje zestawy
- .NET Framework: Rozwiązywanie problemów z ładowaniem zestawów
- Stack Overflow: przekierowanie powiązania zestawu, jak i dlaczego?
- programu PowerShell: omówienie implementowania elementu AssemblyLoadContexts
-
programu PowerShell:
Assembly.LoadFile()
nie jest ładowana do domyślnego AssemblyLoadContext - Rick Strahl: Kiedy jest ładowana zależność zestawu .NET?
- Jon Skeet: podsumowanie wersji w programie .NET
- Nate McMaster: szczegółowe informacje na temat elementów pierwotnych platformy .NET Core