[玩转系统] 解决 PowerShell 模块程序集依赖性冲突
作者:精品下载站 日期:2024-12-14 02:56:44 浏览:14 分类:玩电脑
解决 PowerShell 模块程序集依赖性冲突
当用 C# 编写二进制 PowerShell 模块时,很自然地需要依赖其他包或库来提供功能。为了代码重用,需要依赖其他库。 PowerShell 始终将程序集加载到同一上下文中。当模块的依赖项与已加载的 DLL 发生冲突时,这会出现问题,并且可能会阻止在同一 PowerShell 会话中使用两个不相关的模块。
如果您遇到此问题,您会看到如下错误消息:
本文探讨了 PowerShell 中发生依赖关系冲突的一些方式以及缓解依赖关系冲突问题的方法。即使您不是模块作者,这里也有一些技巧可以帮助您解决您使用的模块中发生的依赖项冲突。
为什么会出现依赖冲突呢?
在 .NET 中,当同一程序集的两个版本加载到同一个程序集加载上下文中时,就会发生依赖性冲突。该术语在不同 .NET 平台上的含义略有不同,本文稍后将对此进行介绍。这种冲突是任何使用版本依赖项的软件中都会出现的常见问题。
项目几乎从不故意或直接依赖于同一依赖项的两个版本,这一事实使冲突问题变得更加复杂。相反,该项目具有两个或多个依赖项,每个依赖项都需要同一依赖项的不同版本。
例如,假设您的 .NET 应用程序 DuckBuilder
引入了两个依赖项来执行其部分功能,如下所示:
由于 Contoso.ZipTools
和 Fabrikam.FileHelpers
均依赖于不同版本的 Newtonsoft.Json,因此可能会出现依赖项冲突,具体取决于每个依赖项的方式已加载。
与 PowerShell 的依赖项冲突
在PowerShell中,依赖冲突问题被放大,因为PowerShell自己的依赖项被加载到同一个共享上下文中。这意味着 PowerShell 引擎和所有加载的 PowerShell 模块不得具有冲突的依赖项。一个典型的例子是 Newtonsoft.Json:
在此示例中,模块 FictionalTools
依赖于 Newtonsoft.Json 版本 12.0.3
,它是 Newtonsoft.Json 的较新版本 比示例 PowerShell 中附带的 11.0.2
更高。
笔记
这是一个例子。 PowerShell 7.0 目前附带 Newtonsoft.Json 12.0.3。较新版本的 PowerShell 具有较新版本的 Newtonsoft.Json。
由于该模块依赖于较新版本的程序集,因此它不会接受 PowerShell 已加载的版本。但由于 PowerShell 已经加载了程序集的一个版本,因此该模块无法使用传统的加载机制加载自己的版本。
与另一个模块的依赖项冲突
PowerShell 中的另一种常见情况是加载依赖于程序集的一个版本的模块,然后稍后加载依赖于该程序集的不同版本的另一个模块。
这通常如下所示:
在这种情况下,FictionalTools
模块需要比 FilesystemManager
模块更新版本的 Microsoft.Extensions.Logging
。
想象一下,这些模块通过将依赖项程序集放置在与根模块程序集相同的目录中来加载它们的依赖项。这允许 .NET 按名称隐式加载它们。如果我们运行 PowerShell 7.0(在 .NET Core 3.1 之上),我们可以加载并运行 FictionalTools
,然后加载并运行 FilesystemManager
,不会出现任何问题。但是,在新会话中,如果我们加载并运行 FilesystemManager
,然后加载 FictionalTools
,我们会从 FictionalToolsFileLoadException
。 /code> 命令,因为它需要比加载版本更新的 Microsoft.Extensions.Logging
版本。 FictionalTools
无法加载所需的版本,因为已加载同名的程序集。
PowerShell 和 .NET
PowerShell运行在.NET平台上,负责解析和加载程序集依赖项。我们必须了解 .NET 在这里如何运行才能理解依赖冲突。
我们还必须面对这样一个事实:不同版本的 PowerShell 运行在不同的 .NET 实现上。一般来说,PowerShell 5.1及更低版本在.NET Framework上运行,而PowerShell 6及更高版本在.NET Core上运行。 .NET 的这两种实现加载和处理程序集的方式不同。这意味着解决依赖冲突可能会因底层 .NET 平台而异。
程序集加载上下文
在 .NET 中,程序集加载上下文 (ALC) 是程序集加载到的运行时命名空间。程序集的名称必须是唯一的。此概念允许在每个 ALC 中通过名称唯一地解析程序集。
.NET 中的程序集引用加载
程序集加载的语义取决于 .NET 实现(.NET Core 与 .NET Framework)和用于加载特定程序集的 .NET API。此处不再详细介绍,进一步阅读部分中的链接详细介绍了 .NET 程序集加载在每个 .NET 实现中的工作原理。
在本文中,我们将参考以下机制:
- 当 .NET 隐式尝试从 .NET 代码中的静态程序集引用按名称加载程序集时,隐式程序集加载(实际上是
Assembly.Load(AssemblyName)
)。 Assembly.LoadFrom()
,一个面向插件的加载 API,它添加处理程序来解析加载的 DLL 的依赖关系。此方法可能无法按照我们想要的方式解决依赖关系。Assembly.LoadFile()
,一个基本加载 API,旨在仅加载所需的程序集,不处理任何依赖项。
.NET Framework 与 .NET Core 的差异
这些 API 的工作方式在 .NET Core 和 .NET Framework 之间发生了微妙的变化,因此值得阅读其中包含的链接。重要的是,程序集加载上下文和其他程序集解析机制在 .NET Framework 和 .NET Core 之间发生了变化。
具体来说,.NET Framework具有以下特点:
- 全局程序集缓存,用于机器范围的程序集解析
- 应用程序域,其工作方式类似于用于程序集隔离的进程内沙箱,但也提供了一个序列化层来应对
有限的程序集加载上下文模型,具有一组固定的程序集加载上下文,每个上下文都有自己的行为:
- 默认加载上下文,默认加载程序集
- 加载源上下文,用于在运行时手动加载程序集
- 仅反射上下文,用于安全加载程序集以读取其元数据而不运行它们
- 使用
Assembly.LoadFile(string path)
和Assembly.Load(byte[] asmBytes)
加载的程序集所在的神秘空间
有关详细信息,请参阅程序集加载的最佳实践。
.NET Core(和 .NET 5+)用更简单的模型取代了这种复杂性:
- 没有全局程序集缓存。应用程序带来了它们自己的所有依赖项。这消除了应用程序中依赖性解析的外部因素,使依赖性解析更具可重复性。 PowerShell 作为插件主机,使模块的情况稍微复杂化。它在
$PSHOME
中的依赖项与所有模块共享。 - 只有一个应用程序域,并且无法创建新的应用程序域。应用程序域概念在 .NET 中维护为 .NET 进程的全局状态。
- 一种新的、可扩展的程序集加载上下文 (ALC) 模型。程序集解析可以通过将其放入新的 ALC 中来命名。 .NET 进程以单个默认 ALC 开始,所有程序集都加载到其中(使用
Assembly.LoadFile(string)
和Assembly.Load(byte[])
加载的程序集除外)。但该进程可以使用自己的加载逻辑创建和定义自己的自定义 ALC。加载程序集时,加载到的第一个 ALC 负责解决其依赖性。这为实现强大的 .NET 插件加载机制创造了机会。
在这两种实现中,程序集都是延迟加载的。这意味着它们会在第一次运行需要其类型的方法时加载。
例如,以下是同一代码的两个版本,它们在不同时间加载依赖项。
第一个总是在调用 Program.GetRange()
时加载其依赖项,因为依赖项引用按词法存在于方法中:
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;
}
}
由于通过方法进行内部间接寻址,第二个仅当 limit
参数为 20 或更大时才加载其依赖项:
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();
}
}
这是一个很好的实践,因为它最大限度地减少了内存和文件系统 I/O,并更有效地使用资源。不幸的是,这样做的一个副作用是,在到达尝试加载程序集的代码路径之前,我们不会知道程序集加载失败。
它还可以为程序集加载冲突创建计时条件。如果同一程序的两个部分尝试加载同一程序集的不同版本,则加载的版本取决于首先运行的代码路径。
对于 PowerShell,这意味着以下因素可能会影响程序集加载冲突:
- 首先加载哪个模块?
- 使用依赖库的代码路径是否运行?
- PowerShell 是否在启动时或仅在某些代码路径下加载冲突的依赖项?
快速修复及其局限性
在某些情况下,可以对模块进行小的调整并以最小的努力修复问题。但这些解决方案往往带有一些警告。虽然它们可能适用于您的模块,但它们并不适用于每个模块。
更改您的依赖版本
避免依赖性冲突的最简单方法是就依赖性达成一致。在以下情况下这可能是可能的:
- 您的冲突与您的模块的直接依赖关系以及您控制的版本有关。
- 您的冲突与间接依赖项有关,但您可以配置直接依赖项以使用可行的间接依赖项版本。
- 您知道冲突的版本并且可以相信它不会改变。
Newtonsoft.Json 包是最后一个场景的一个很好的例子。这是 PowerShell 6 及更高版本的依赖项,在 Windows PowerShell 中不使用。这意味着解决版本控制冲突的一个简单方法是在您希望定位的 PowerShell 版本中定位最低版本的 Newtonsoft.Json。
例如,PowerShell 6.2.6 和 PowerShell 7.0.2 目前都使用 Newtonsoft.Json 版本 12.0.3。要创建面向 Windows PowerShell、PowerShell 6 和 PowerShell 7 的模块,您可以将 Newtonsoft.Json 12.0.3 定位为依赖项,并将其包含在构建的模块中。当在 PowerShell 6 或 7 中加载模块时,PowerShell 自己的 Newtonsoft.Json 程序集已被加载。由于它是您的模块所需的版本,因此解析成功。在 Windows PowerShell 中,该程序集尚未出现在 PowerShell 中,因此它是从模块文件夹中加载的。
通常,当针对具体的 PowerShell 包(例如 Microsoft.PowerShell.Sdk 或 System.Management.Automation)时,NuGet 应该能够解析所需的正确依赖项版本。同时定位 Windows PowerShell 和 PowerShell 6+ 变得更加困难,因为您必须在定位多个框架或 PowerShellStandard.Library 之间进行选择。
固定到通用依赖项版本不起作用的情况包括:
- 冲突与间接依赖项有关,并且您的任何依赖项都无法配置为使用通用版本。
- 另一个依赖版本可能会经常更改,因此选择通用版本只是一个短期修复。
使用进程外的依赖关系
该解决方案更适合模块用户而不是模块作者。当遇到由于现有依赖冲突而无法工作的模块时,可以使用此解决方案。
由于同一程序集的两个版本被加载到同一 .NET 进程中,因此会发生依赖关系冲突。一个简单的解决方案是将它们加载到不同的进程中,只要您仍然可以一起使用两个进程的功能即可。
在 PowerShell 中,有多种方法可以实现此目的:
作为子进程调用 PowerShell
要在当前进程之外运行 PowerShell 命令,请直接使用命令调用启动新的 PowerShell 进程:
pwsh -c 'Invoke-ConflictingCommand'
这里的主要限制是重组结果可能比其他选项更棘手或更容易出错。
PowerShell 作业系统
PowerShell 作业系统还通过将命令发送到新的 PowerShell 进程并返回结果来在进程外运行命令:
$result = Start-Job { Invoke-ConflictingCommand } | Receive-Job -Wait
在这种情况下,您只需要确保正确传入任何变量和状态即可。
当运行小命令时,作业系统也可能有点麻烦。
PowerShell 远程处理
当 PowerShell 远程处理可用时,它可以成为在进程外运行命令的有用方法。通过远程处理,您可以在新进程中创建一个新的 PSSession,通过 PowerShell 远程处理调用其命令,然后在本地将结果与包含冲突依赖项的其他模块一起使用。
一个例子可能如下所示:
# 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
隐式远程处理 Windows PowerShell
PowerShell 7 中的另一个选项是在
Import-Module
上使用-UseWindowsPowerShell
标志。这将通过本地远程处理会话将该模块导入到 Windows PowerShell 中:Import-Module -Name ConflictingModule -UseWindowsPowerShell
请注意,模块可能与 Windows PowerShell 不兼容或工作方式不同。
何时不应使用进程外调用
作为模块作者,进程外命令调用很难融入模块中,并且可能存在导致问题的边缘情况。特别是,远程处理和作业可能无法在模块需要工作的所有环境中可用。然而,将实现移出进程并允许 PowerShell 模块成为瘦客户端的一般原则可能仍然适用。
作为模块用户,在某些情况下进程外调用不起作用:
- 当 PowerShell 远程处理因您无权使用它或未启用而不可用时。
- 当需要将输出中的特定 .NET 类型作为方法或另一个命令的输入时。通过 PowerShell 远程处理运行的命令会发出反序列化对象,而不是强类型的 .NET 对象。这意味着方法调用和强类型 API 不适用于通过远程处理导入的命令的输出。
为了满足这两个要求,我们必须将模块分解为两个程序集:
- cmdlet 程序集
AlcModule.Cmdlets.dll
,其中包含 PowerShell 模块系统正确加载模块所需的所有类型的定义。即,Cmdlet
基类和实现IModuleAssemblyInitializer
的类的任何实现,该类为AssemblyLoadContext.Default.Resolving
设置事件处理程序通过我们的自定义 ALC 正确加载AlcModule.Engine.dll
。由于 PowerShell 7 故意隐藏在其他 ALC 中加载的程序集中定义的类型,因此还必须在此处定义任何要公开暴露给 PowerShell 的类型。最后,我们的自定义 ALC 定义需要在此程序集中定义。除此之外,该程序集中应包含尽可能少的代码。 - 引擎程序集
AlcModule.Engine.dll
,用于处理模块的实际实现。 PowerShell ALC 中提供了来自此的类型,但它最初是通过我们的自定义 ALC 加载的。它的依赖项仅加载到自定义 ALC 中。实际上,这成为两个 ALC 之间的桥梁。
使用这个桥梁概念,我们的新装配情况如下所示:
为了确保默认 ALC 的依赖关系探测逻辑不会解析要加载到自定义 ALC 中的依赖关系,我们需要将模块的这两部分放在不同的目录中。新的模块布局具有以下结构:
AlcModule/
AlcModule.Cmdlets.dll
AlcModule.psd1
Dependencies/
| + AlcModule.Engine.dll
| + Shared.Dependency.dll
要了解实现如何变化,我们将从 AlcModule.Engine.dll 的实现开始:
using Shared.Dependency;
namespace AlcModule.Engine
{
public class AlcEngine
{
public static void Use()
{
Dependency.Use();
}
}
}
这是依赖项 Shared.Dependency.dll
的简单容器,但您应该将其视为您的功能的 .NET API,其他程序集中的 cmdlet 为 PowerShell 包装了该功能。
AlcModule.Cmdlets.dll
中的 cmdlet 如下所示:
// 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!");
}
}
}
此时,如果我们要加载 AlcModule 并运行 Test-AlcModule
,当默认 ALC 尝试加载 时,我们会收到 FileNotFoundException >Alc.Engine.dll
运行 EndProcessing()
。这很好,因为这意味着默认的 ALC 无法找到我们想要隐藏的依赖项。
现在我们需要向 AlcModule.Cmdlets.dll
添加代码,以便它知道如何解析 AlcModule.Engine.dll
。首先,我们必须定义自定义 ALC 以解析模块的 Dependencies
目录中的程序集:
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;
}
}
}
然后,我们需要将自定义 ALC 连接到默认 ALC 的 Resolving
事件,该事件是应用程序域上 AssemblyResolve
事件的 ALC 版本。当调用 EndProcessing()
时,会触发此事件来查找 AlcModule.Engine.dll
。
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);
}
}
}
使用新的实现,看一下加载模块和运行 Test-AlcModule 时发生的调用顺序:
一些兴趣点是:
- 当模块加载并设置
Resolving
事件时,首先运行IModuleAssemblyInitializer
。 - 在运行
Test-AlcModule
并调用其EndProcessing()
方法之前,我们不会加载依赖项。 - 当调用
EndProcessing()
时,默认 ALC 无法找到AlcModule.Engine.dll
并触发Resolving
事件。 - 我们的事件处理程序将自定义 ALC 连接到默认 ALC 并仅加载
AlcModule.Engine.dll
。 - 当在
AlcModule.Engine.dll
中调用AlcEngine.Use()
时,自定义 ALC 会再次启动以解析Shared.Dependency.dll
。具体来说,它总是加载我们的Shared.Dependency.dll
,因为它永远不会与默认 ALC 中的任何内容发生冲突,并且只在我们的Dependency
目录中查找。
组装实现后,我们的新源代码布局如下所示:
+ AlcModule.psd1
+ src/
+ AlcModule.Cmdlets/
| + AlcModule.Cmdlets.csproj
| + TestAlcModuleCommand.cs
| + AlcModuleAssemblyLoadContext.cs
| + AlcModuleInitializer.cs
|
+ AlcModule.Engine/
| + AlcModule.Engine.csproj
| + AlcEngine.cs
AlcModule.Cmdlets.csproj 看起来像:
<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 看起来像这样:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Shared.Dependency" Version="1.0.0" />
</ItemGroup>
</Project>
因此,当我们构建模块时,我们的策略是:
- 构建AlcModule.Engine
- 构建 AlcModule.Cmdlet
- 将
AlcModule.Engine
中的所有内容复制到Dependencies
目录中,并记住我们复制的内容 - 将
AlcModule.Cmdlets
中不属于AlcModule.Engine
的所有内容复制到基本模块目录中
由于这里的模块布局对于依赖关系分离非常重要,因此这里有一个从源根目录使用的构建脚本:
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 }
最后,我们有一种通用方法可以在程序集加载上下文中隔离模块的依赖项,随着时间的推移,随着添加更多依赖项,该方法仍然保持健壮。
有关更详细的示例,请访问此 GitHub 存储库。此示例演示如何迁移模块以使用 ALC,同时保持该模块在 .NET Framework 中运行。它还展示了如何使用.NET Standard和PowerShell Standard来简化核心实现。
Bicep PowerShell 模块也使用此解决方案,博客文章解决 PowerShell 模块冲突是有关此解决方案的另一本好读物。
用于并排加载的程序集解析处理程序
虽然稳健,但上述解决方案要求模块程序集不直接引用依赖程序集,而是引用引用依赖程序集的包装器程序集。包装程序集就像一座桥梁,将调用从模块程序集转发到依赖程序集。这使得采用此解决方案通常需要大量工作:
- 对于新模块,这会增加设计和实现的额外复杂性
- 对于现有模块,这将需要大量重构
有一个简化的解决方案可以通过将 Resolving
事件与自定义 AssemblyLoadContext
实例挂钩来实现并行程序集加载。使用此方法对于模块作者来说更容易,但有两个限制。查看 PowerShell-ALC-Samples 存储库,获取描述此解决方案的这些限制和详细场景的示例代码和文档。
这很重要
请勿使用 Assembly.LoadFile
来实现依赖项隔离目的。当另一个模块将同一程序集的不同版本加载到默认的 AssemblyLoadContext
中时,使用 Assembly.LoadFile
会产生类型标识问题。虽然此 API 将程序集加载到单独的 AssemblyLoadContext
实例,但加载的程序集可由 PowerShell 的类型解析代码发现。因此,两个不同的 ALC 可能存在具有相同完全限定类型名称的重复类型。
自定义应用程序域
程序集隔离的最后也是最极端的选项是使用自定义应用程序域。 应用程序域仅在 .NET Framework 中可用。它们用于在 .NET 应用程序的各个部分之间提供进程内隔离。用途之一是在同一流程中将装配负载相互隔离。
但是,应用程序域是序列化边界。一个应用程序域中的对象不能被另一应用程序域中的对象直接引用和使用。您可以通过实现 MarshalByRefObject
来解决此问题。但是,当您不控制类型时(通常是依赖关系的情况),就不可能在此处强制实现。唯一的解决方案是进行大规模的架构更改。序列化边界还具有严重的性能影响。
由于应用程序域有这个严重的限制,实现起来很复杂,并且只能在 .NET Framework 中工作,因此我们不会在此处给出如何使用它们的示例。虽然它们作为一种可能性值得一提,但不推荐它们。
如果您有兴趣尝试使用自定义应用程序域,以下链接可能会有所帮助:
- 应用程序域的概念文档
- 使用应用程序域的示例
不适用于 PowerShell 的依赖冲突解决方案
最后,我们将解决在研究 .NET 中的 .NET 依赖冲突时出现的一些可能性,这些冲突看起来很有希望,但通常不适用于 PowerShell。
这些解决方案有一个共同的主题,即它们是对您控制应用程序甚至整个计算机的环境的部署配置的更改。这些解决方案面向 Web 服务器和部署到服务器环境的其他应用程序等场景,其中该环境旨在托管应用程序,并且可由部署用户自由配置。它们也往往非常面向 .NET Framework,这意味着它们不能与 PowerShell 6 或更高版本一起使用。
如果您知道您的模块仅在您拥有完全控制权的 Windows PowerShell 5.1 环境中使用,则其中一些可能是选项。但一般来说,模块不应该像这样修改全局机器状态。它可能会破坏配置,从而导致 powershell.exe
、其他模块或其他依赖应用程序出现问题,从而导致模块以意外方式失败。
使用 app.config 进行静态绑定重定向以强制使用相同的依赖项版本
.NET Framework 应用程序可以利用 app.config
文件以声明方式配置某些应用程序行为。可以编写一个 app.config
条目来配置程序集绑定,以将程序集加载重定向到特定版本。
PowerShell 的两个问题是:
- .NET Core 不支持
app.config
,因此此解决方案仅适用于powershell.exe
。 powershell.exe
是一个共享应用程序,位于System32
目录中。您的模块可能无法在许多系统上修改其内容。即使可以,修改app.config
也可能会破坏现有配置或影响其他模块的加载。
使用 app.config 设置codebase
出于同样的原因,尝试在 app.config
中配置 codebase
设置在 PowerShell 模块中不起作用。
安装全局程序集缓存 (GAC) 的依赖项
解决 .NET Framework 中依赖项版本冲突的另一种方法是将依赖项安装到 GAC,以便可以从 GAC 并行加载不同版本。
同样,对于 PowerShell 模块,这里的主要问题是:
- GAC 仅适用于 .NET Framework,因此这在 PowerShell 6 及更高版本中没有帮助。
- 将程序集安装到 GAC 是对全局计算机状态的修改,可能会对其他应用程序或其他模块产生副作用。即使您的模块具有所需的访问权限,也可能很难正确执行。出错可能会导致其他 .NET 应用程序出现严重的机器范围问题。
进一步阅读
关于 .NET 程序集版本依赖性冲突,还有很多内容可供阅读。以下是一些不错的起点:
- .NET:.NET 中的程序集
- .NET Core:托管程序集加载算法
- .NET Core:了解 System.Runtime.Loader.AssemblyLoadContext
- .NET Core:关于并行程序集加载解决方案的讨论
- .NET Framework:重定向程序集版本
- .NET Framework:程序集加载的最佳实践
- .NET Framework:运行时如何定位程序集
- .NET Framework:解决程序集加载
- StackOverflow:程序集绑定重定向,如何以及为什么?
- PowerShell:有关实现 AssemblyLoadContexts 的讨论
- PowerShell:
Assembly.LoadFile()
未加载到默认的 AssemblyLoadContext - Rick Strahl:.NET 程序集依赖项何时加载?
- Jon Skeet:.NET 版本控制总结
- Nate McMaster:深入探讨 .NET Core 原语
猜你还喜欢
- 03-30 [玩转系统] 如何用批处理实现关机,注销,重启和锁定计算机
- 02-14 [系统故障] Win10下报错:该文件没有与之关联的应用来执行该操作
- 01-07 [系统问题] Win10--解决锁屏后会断网的问题
- 01-02 [系统技巧] Windows系统如何关闭防火墙保姆式教程,超详细
- 12-15 [玩转系统] 如何在 Windows 10 和 11 上允许多个 RDP 会话
- 12-15 [玩转系统] 查找 Exchange/Microsoft 365 中不活动(未使用)的通讯组列表
- 12-15 [玩转系统] 如何在 Windows 上安装远程服务器管理工具 (RSAT)
- 12-15 [玩转系统] 如何在 Windows 上重置组策略设置
- 12-15 [玩转系统] 如何获取计算机上的本地管理员列表?
- 12-15 [玩转系统] 在 Visual Studio Code 中连接到 MS SQL Server 数据库
- 12-15 [玩转系统] 如何降级 Windows Server 版本或许可证
- 12-15 [玩转系统] 如何允许非管理员用户在 Windows 中启动/停止服务
取消回复欢迎 你 发表评论:
- 精品推荐!
-
- 最新文章
- 热门文章
- 热评文章
[影视] 黑道中人 Alto Knights(2025)剧情 犯罪 历史 电影
[古装剧] [七侠五义][全75集][WEB-MP4/76G][国语无字][1080P][焦恩俊经典]
[实用软件] 虚拟手机号 电话 验证码 注册
[电视剧] 安眠书店/你 第五季 You Season 5 (2025) 【全10集】
[电视剧] 棋士(2025) 4K 1080P【全22集】悬疑 犯罪 王宝强 陈明昊
[软件合集] 25年6月5日 精选软件22个
[软件合集] 25年6月4日 精选软件36个
[短剧] 2025年06月04日 精选+付费短剧推荐33部
[短剧] 2025年06月03日 精选+付费短剧推荐25部
[软件合集] 25年6月3日 精选软件44个
[剧集] [央视][笑傲江湖][2001][DVD-RMVB][高清][40集全]李亚鹏、许晴、苗乙乙
[电视剧] 欢乐颂.5部全 (2016-2024)
[电视剧] [突围] [45集全] [WEB-MP4/每集1.5GB] [国语/内嵌中文字幕] [4K-2160P] [无水印]
[影视] 【稀有资源】香港老片 艺坛照妖镜之96应召名册 (1996)
[剧集] 神经风云(2023)(完结).4K
[剧集] [BT] [TVB] [黑夜彩虹(2003)] [全21集] [粤语中字] [TV-RMVB]
[实用软件] 虚拟手机号 电话 验证码 注册
[资源] B站充电视频合集,包含多位重量级up主,全是大佬真金白银买来的~【99GB】
[影视] 内地绝版高清录像带 [mpg]
[书籍] 古今奇书禁书三教九流资料大合集 猎奇必备珍藏资源PDF版 1.14G
[电视剧] [突围] [45集全] [WEB-MP4/每集1.5GB] [国语/内嵌中文字幕] [4K-2160P] [无水印]
[剧集] [央视][笑傲江湖][2001][DVD-RMVB][高清][40集全]李亚鹏、许晴、苗乙乙
[电影] 美国队长4 4K原盘REMUX 杜比视界 内封简繁英双语字幕 49G
[电影] 死神来了(1-6)大合集!
[软件合集] 25年05月13日 精选软件16个
[精品软件] 25年05月15日 精选软件18个
[绝版资源] 南与北 第1-2季 合集 North and South (1985) /美国/豆瓣: 8.8[1080P][中文字幕]
[软件] 25年05月14日 精选软件57个
[短剧] 2025年05月14日 精选+付费短剧推荐39部
[短剧] 2025年05月15日 精选+付费短剧推荐36部
- 最新评论
-
- 热门tag