├── .gitignore ├── Targeting-NetStandard2.0 ├── scenario-1 │ ├── images │ │ └── screen.jpg │ ├── conflict.psd1 │ ├── build.ps1 │ ├── README.md │ ├── conflict.csproj │ └── Class1.cs ├── scenario-2 │ ├── images │ │ └── screen.jpg │ ├── conflict │ │ ├── conflict.psd1 │ │ ├── conflict.csproj │ │ └── Class1.cs │ ├── resolver │ │ ├── resolver.csproj │ │ └── Class1.cs │ ├── build.ps1 │ └── README.md └── README.md ├── Resolving-Event-with-ALC ├── nuget-packages │ ├── SharedDependency.0.7.0.nupkg │ ├── SharedDependency.1.0.0.nupkg │ └── SharedDependency.1.5.0.nupkg ├── nuget.config ├── src │ ├── SampleModule │ │ ├── LocalDependency │ │ │ ├── LocalDependency.cs │ │ │ └── LocalDependency.csproj │ │ ├── SampleModule.psd1 │ │ ├── build.ps1 │ │ ├── Commands │ │ │ ├── Greeting.Commands.csproj │ │ │ ├── Command.cs │ │ │ └── CustomALC.cs │ │ ├── SampleModule.sln │ │ └── README.md │ ├── LowerDependencyConflict │ │ ├── LowerConflict.cs │ │ └── ConflictWithLowerDeps.csproj │ ├── HigherDependencyConflict │ │ ├── HigherConflict.cs │ │ └── ConflictWithHigherDeps.csproj │ └── shared-dependency │ │ ├── SharedDependency.csproj │ │ └── SharedDependency.cs ├── build.ps1 ├── scenario-demos │ ├── 1-SampleModule-only.md │ ├── 2-HigherConflict-then-SampleModule.md │ ├── 5-SampleModule-then-LowerConflict.md │ ├── 3-LowerConflict-then-SampleModule.md │ └── 4-SampleModule-then-HigherConflict.md └── README.md ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | .vscode/ 4 | 5 | -------------------------------------------------------------------------------- /Targeting-NetStandard2.0/scenario-1/images/screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daxian-dbw/PowerShell-ALC-Samples/HEAD/Targeting-NetStandard2.0/scenario-1/images/screen.jpg -------------------------------------------------------------------------------- /Targeting-NetStandard2.0/scenario-2/images/screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daxian-dbw/PowerShell-ALC-Samples/HEAD/Targeting-NetStandard2.0/scenario-2/images/screen.jpg -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/nuget-packages/SharedDependency.0.7.0.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daxian-dbw/PowerShell-ALC-Samples/HEAD/Resolving-Event-with-ALC/nuget-packages/SharedDependency.0.7.0.nupkg -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/nuget-packages/SharedDependency.1.0.0.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daxian-dbw/PowerShell-ALC-Samples/HEAD/Resolving-Event-with-ALC/nuget-packages/SharedDependency.1.0.0.nupkg -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/nuget-packages/SharedDependency.1.5.0.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daxian-dbw/PowerShell-ALC-Samples/HEAD/Resolving-Event-with-ALC/nuget-packages/SharedDependency.1.5.0.nupkg -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/src/SampleModule/LocalDependency/LocalDependency.cs: -------------------------------------------------------------------------------- 1 | namespace MyModule 2 | { 3 | public class LocalDependency 4 | { 5 | public static string GetGreetingMessage() 6 | { 7 | string msg = Shared.Dependency.GetNextGreeting(); 8 | return $"From : {msg}"; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/src/LowerDependencyConflict/LowerConflict.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace LowerDeps 4 | { 5 | [Cmdlet("Test", "ConflictWithLowerDeps")] 6 | public class ConflictCommand : PSCmdlet 7 | { 8 | protected override void EndProcessing() 9 | { 10 | WriteObject(Shared.Dependency.GetNextGreeting()); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/src/HigherDependencyConflict/HigherConflict.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace HigherDeps 4 | { 5 | [Cmdlet("Test", "ConflictWithHigherDeps")] 6 | public class ConflictCommand : PSCmdlet 7 | { 8 | protected override void EndProcessing() 9 | { 10 | WriteObject(Shared.Dependency.GetNextGreeting()); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Targeting-NetStandard2.0/scenario-1/conflict.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'conflict' 3 | # 4 | 5 | @{ 6 | ModuleVersion = '0.0.1' 7 | GUID = '703c37b6-06c1-449a-9290-7a3316a53275' 8 | Author = 'dongbo' 9 | 10 | NestedModules = @('conflict.dll') 11 | FunctionsToExport = @() 12 | CmdletsToExport = @('Test-DummyCommand') 13 | VariablesToExport = '*' 14 | AliasesToExport = @() 15 | } 16 | -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/src/SampleModule/SampleModule.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'SampleModule' 3 | # 4 | 5 | @{ 6 | ModuleVersion = '0.0.1' 7 | GUID = 'bbcbf8b1-e3ea-408f-8823-1041810f6658' 8 | Author = 'dongbo' 9 | 10 | NestedModules = @('Greeting.Commands.dll') 11 | FunctionsToExport = @() 12 | CmdletsToExport = @('Get-Greeting') 13 | VariablesToExport = '*' 14 | AliasesToExport = @() 15 | } 16 | -------------------------------------------------------------------------------- /Targeting-NetStandard2.0/scenario-2/conflict/conflict.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'conflict' 3 | # 4 | 5 | @{ 6 | ModuleVersion = '0.0.1' 7 | GUID = 'dd949fab-e6a8-4d02-b73f-39469bd1c9fa' 8 | Author = 'dongbo' 9 | 10 | NestedModules = @('resolver.dll', 'conflict.dll') 11 | FunctionsToExport = @() 12 | CmdletsToExport = @('Test-DummyCommand') 13 | VariablesToExport = '*' 14 | AliasesToExport = @() 15 | } 16 | -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/src/shared-dependency/SharedDependency.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 0.7.0.0 8 | 0.7.0 9 | true 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/src/shared-dependency/SharedDependency.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Loader; 3 | 4 | namespace Shared 5 | { 6 | public class Dependency 7 | { 8 | public static string GetNextGreeting() 9 | { 10 | string asmFullName = typeof(Dependency).Assembly.FullName; 11 | string contextName = AssemblyLoadContext.GetLoadContext(typeof(Dependency).Assembly).Name; 12 | return $"Greetings! -- from '{asmFullName}', loaded in '{contextName}'"; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/src/SampleModule/LocalDependency/LocalDependency.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | false 8 | None 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/src/SampleModule/build.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [ValidateSet("Debug", "Release")] 3 | [string] $Configuration = "Debug" 4 | ) 5 | 6 | dotnet publish "$PSScriptRoot\SampleModule.sln" 7 | 8 | if ($LASTEXITCODE -eq 0) { 9 | $target = Join-Path $PSScriptRoot 'bin' 'SampleModule' 'Dependencies' 10 | if (Test-Path $target) { 11 | Remove-Item $target -Recurse -Force 12 | } 13 | 14 | $null = New-Item -ItemType Directory $target 15 | Move-Item "$PSScriptRoot\bin\SampleModule\SharedDependency.dll" $target 16 | } 17 | -------------------------------------------------------------------------------- /Targeting-NetStandard2.0/scenario-1/build.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [ValidateSet("Debug", "Release")] 3 | [string] $Configuration = "Debug" 4 | ) 5 | 6 | dotnet publish -c $Configuration 7 | 8 | if ($LASTEXITCODE -eq 0) { 9 | $target = Join-Path $PSScriptRoot 'bin' 'conflict' 'Dependencies' 10 | if (Test-Path $target) { 11 | Remove-Item $target -Recurse -Force 12 | } 13 | 14 | $null = New-Item -ItemType Directory $target 15 | Move-Item "$PSScriptRoot\bin\conflict\Newtonsoft.Json.dll" $target 16 | 17 | $moduleDir = Join-Path $PSScriptRoot 'bin' 'conflict' 18 | $moduleDir = Resolve-Path $moduleDir -Relative 19 | Write-Host " Module deployed to: $moduleDir" -ForegroundColor Green 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Samples for isolating dependencies in the PowerShell module 2 | 3 | This repository includes sample code to show the techniques that can be used 4 | to isolate dependency assemblies for a PowerShell module, 5 | so as to avoid facing conflicts in the assembly resolution when loading the module. 6 | 7 | Currently, two overall scenarios are discussed: 8 | 9 | 1. [Resolving-Event-with-ALC](./Resolving-Event-with-ALC) -- for PowerShell modules that targets .NET 3.1 and above, 10 | where the type `AssemblyLoadContext` is available. 11 | 12 | 2. [Targeting-NetStandard2.0](./Targeting-NetStandard2.0) -- for PowerShell modules that targets .NET Standard 2.0, 13 | where the type `AssemblyLoadContext` is NOT available. 14 | 15 | Check out the `README.md` in each of these 2 folders to see the detailed information. 16 | -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/src/LowerDependencyConflict/ConflictWithLowerDeps.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | false 8 | None 9 | 10 | 11 | false 12 | 13 | 14 | bin\LowerConflict 15 | true 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Targeting-NetStandard2.0/scenario-2/resolver/resolver.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | resolver 6 | 10.0 7 | 8 | 9 | false 10 | None 11 | 12 | 13 | false 14 | 15 | 16 | ..\bin\resolver 17 | true 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/src/HigherDependencyConflict/ConflictWithHigherDeps.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | false 8 | None 9 | 10 | 11 | false 12 | 13 | 14 | bin\HigherConflict 15 | true 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Targeting-NetStandard2.0/scenario-1/README.md: -------------------------------------------------------------------------------- 1 | ## Scenario 1 2 | 3 | Basically, the registration of the `AssemblyResolve` event needs to happen before the first loading request of `Newtonsoft.Json.dll` gets triggered by the `conflict.dll` assembly. 4 | 5 | In this scenario, the dependency of `Newtonsoft.Json.dll` is delay-loaded when the `Test-DummyCommand` is invoked. 6 | So, the registration of the `AssemblyResolve` event can be done in the same assembly, on module importing. 7 | 8 | ### Build 9 | 10 | To build the sample module, run `build.ps1`. 11 | The module folder `conflict` will be deployed to `.\bin\conflict`. 12 | 13 | ### Run in PowerShell 7.0.x 14 | 15 | PowerShell 7.0.x loads the version `12.0.0.0` of `Newtonsoft.Json` upon startup. 16 | The `conflict` module depends on the version `13.0.0.0` of `Newtonsoft.Json`. 17 | 18 | By leveraging `AppDomain.AssemblyResolve` and `AssemblyLoadContextProxy`, 19 | the `conflict` module can work as expected in PowerShell 7.0.x. 20 | 21 | ![screenshot](./images/screen.jpg) 22 | -------------------------------------------------------------------------------- /Targeting-NetStandard2.0/scenario-2/build.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [ValidateSet("Debug", "Release")] 3 | [string] $Configuration = "Debug" 4 | ) 5 | 6 | try { 7 | Push-Location $PSScriptRoot/conflict 8 | dotnet publish -c $Configuration 9 | } finally { 10 | Pop-Location 11 | } 12 | 13 | if ($LASTEXITCODE -eq 0) { 14 | try { 15 | Push-Location $PSScriptRoot/resolver 16 | dotnet publish -c $Configuration 17 | } finally { 18 | Pop-Location 19 | } 20 | 21 | $moduleDir = Join-Path $PSScriptRoot 'bin' 'conflict' 22 | $target = Join-Path $PSScriptRoot 'bin' 'conflict' 'Dependencies' 23 | if (Test-Path $target) { 24 | Remove-Item $target -Recurse -Force 25 | } 26 | 27 | $null = New-Item -ItemType Directory $target 28 | Move-Item "$PSScriptRoot\bin\conflict\Newtonsoft.Json.dll" $target 29 | Move-Item "$PSScriptRoot\bin\resolver\resolver.dll" $moduleDir -Force 30 | Remove-Item "$PSScriptRoot\bin\resolver" -Recurse 31 | 32 | $moduleDir = Resolve-Path $moduleDir -Relative 33 | Write-Host " Module deployed to: $moduleDir" -ForegroundColor Green 34 | } 35 | -------------------------------------------------------------------------------- /Targeting-NetStandard2.0/scenario-1/conflict.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | conflict 6 | 10.0 7 | 8 | 9 | false 10 | None 11 | 12 | 13 | false 14 | 15 | 16 | bin\conflict 17 | true 18 | 19 | 20 | 21 | 22 | 23 | 24 | PreserveNewest 25 | PreserveNewest 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Dongbo Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Targeting-NetStandard2.0/scenario-2/conflict/conflict.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | conflict 6 | 10.0 7 | 8 | 9 | false 10 | None 11 | 12 | 13 | false 14 | 15 | 16 | ..\bin\conflict 17 | true 18 | 19 | 20 | 21 | 22 | 23 | 24 | PreserveNewest 25 | PreserveNewest 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/src/SampleModule/Commands/Greeting.Commands.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | false 8 | None 9 | 10 | 11 | false 12 | 13 | 14 | ..\bin\SampleModule 15 | true 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | PreserveNewest 24 | PreserveNewest 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Targeting-NetStandard2.0/scenario-2/README.md: -------------------------------------------------------------------------------- 1 | ## Scenario 2 2 | 3 | In this scenario, `Newtonsoft.Json` types are directly used in the class level, such as attribute and base type. 4 | This will trigger the loading of the version `13.0.0.0` of `Newtonsoft.Json` as soon as the `conflict.dll` assembly gets loaded. 5 | In that case, registering `AssemblyResolve` within the same assembly will be too late and thus won't work. 6 | 7 | In this case, the registration of `AssemblyResolve` needs to happen in a separate assembly (`resolver.dll` in this sample), 8 | which needs to be loaded before the above assembly, so the handler can kick in when the `conflict.dll` assembly gets loaded. 9 | 10 | ### Build 11 | 12 | To build the sample module, run `build.ps1`. 13 | The module folder `conflict` will be deployed to `.\bin\conflict`. 14 | 15 | ### Run in PowerShell 7.0.x 16 | 17 | PowerShell 7.0.x loads the version `12.0.0.0` of `Newtonsoft.Json` upon startup. 18 | The `conflict` module depends on the version `13.0.0.0` of `Newtonsoft.Json`. 19 | 20 | By leveraging `AppDomain.AssemblyResolve` and `AssemblyLoadContextProxy`, 21 | the `conflict` module can work as expected in PowerShell 7.0.x. 22 | 23 | ![screenshot](./images/screen.jpg) 24 | -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/src/SampleModule/Commands/Command.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace MyModule 4 | { 5 | [Cmdlet("Get", "Greeting")] 6 | public class MyCommand : PSCmdlet 7 | { 8 | [Parameter(Mandatory = true, ParameterSetName = "UseSharedDependency")] 9 | public SwitchParameter UseSharedDependency { get; set; } 10 | 11 | [Parameter(Mandatory = true, ParameterSetName = "UseLocalDependency")] 12 | public SwitchParameter UseLocalDependency { get; set; } 13 | 14 | protected override void EndProcessing() 15 | { 16 | switch (ParameterSetName) 17 | { 18 | case "UseSharedDependency": 19 | WriteObject(GetMessageFromSharedDependency()); 20 | break; 21 | case "UseLocalDependency": 22 | WriteObject(GetMessageFromLocalDependency()); 23 | break; 24 | default: 25 | throw new System.Exception("Unreachable code."); 26 | } 27 | } 28 | 29 | private string GetMessageFromSharedDependency() 30 | { 31 | return Shared.Dependency.GetNextGreeting(); 32 | } 33 | 34 | private string GetMessageFromLocalDependency() 35 | { 36 | return LocalDependency.GetGreetingMessage(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/build.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [ValidateSet('Debug', 'Release')] 3 | [string] $Configuration = 'Debug' 4 | ) 5 | 6 | $target = Join-Path $PSScriptRoot 'bin' 7 | if (Test-Path $target) { 8 | Remove-Item $target -Recurse -Force 9 | } 10 | $null = New-Item -ItemType Directory $target 11 | 12 | try { 13 | $higherDeps = Join-Path $PSScriptRoot 'src' 'HigherDependencyConflict' 14 | Push-Location $higherDeps 15 | dotnet publish -c $Configuration 16 | 17 | if ($LASTEXITCODE -eq 0) { 18 | $source = Join-Path $higherDeps 'bin' 'HigherConflict' 19 | Copy-Item $source -Recurse $target 20 | } else { 21 | return 22 | } 23 | } finally { 24 | Pop-Location 25 | } 26 | 27 | try { 28 | $lowerDeps = Join-Path $PSScriptRoot 'src' 'LowerDependencyConflict' 29 | Push-Location -Path $lowerDeps 30 | dotnet publish -c $Configuration 31 | 32 | if ($LASTEXITCODE -eq 0) { 33 | $source = Join-Path $lowerDeps 'bin' 'LowerConflict' 34 | Copy-Item $source -Recurse $target 35 | } else { 36 | return 37 | } 38 | } finally { 39 | Pop-Location 40 | } 41 | 42 | $sampleModule = Join-Path $PSScriptRoot 'src' 'SampleModule' 43 | & "$sampleModule/build.ps1" 44 | if ($?) { 45 | $source = Join-Path $sampleModule 'bin' 'SampleModule' 46 | Copy-Item $source -Recurse $target 47 | } 48 | 49 | Write-Host "`nAll modules are published to '$target'" -ForegroundColor Green 50 | -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/src/SampleModule/SampleModule.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Greeting.Commands", "Commands\Greeting.Commands.csproj", "{5AD037C0-5666-4C87-B5E5-7D9810DAE6B5}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(SolutionProperties) = preSolution 14 | HideSolutionNode = FALSE 15 | EndGlobalSection 16 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 17 | {BE13A734-79DC-4645-8AEE-0AFEB61FC3A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 18 | {BE13A734-79DC-4645-8AEE-0AFEB61FC3A5}.Debug|Any CPU.Build.0 = Debug|Any CPU 19 | {BE13A734-79DC-4645-8AEE-0AFEB61FC3A5}.Release|Any CPU.ActiveCfg = Release|Any CPU 20 | {BE13A734-79DC-4645-8AEE-0AFEB61FC3A5}.Release|Any CPU.Build.0 = Release|Any CPU 21 | {5AD037C0-5666-4C87-B5E5-7D9810DAE6B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {5AD037C0-5666-4C87-B5E5-7D9810DAE6B5}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {5AD037C0-5666-4C87-B5E5-7D9810DAE6B5}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {5AD037C0-5666-4C87-B5E5-7D9810DAE6B5}.Release|Any CPU.Build.0 = Release|Any CPU 25 | EndGlobalSection 26 | EndGlobal 27 | -------------------------------------------------------------------------------- /Targeting-NetStandard2.0/scenario-2/conflict/Class1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Management.Automation; 3 | using Newtonsoft.Json; 4 | 5 | namespace assembly.conflict 6 | { 7 | [JsonConverter(typeof(DummyConverter))] 8 | public enum TestEnum 9 | { 10 | Abc, 11 | Def 12 | } 13 | 14 | public class DummyConverter : JsonConverter 15 | { 16 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 17 | { 18 | throw new NotImplementedException(); 19 | } 20 | 21 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 22 | { 23 | throw new NotImplementedException(); 24 | } 25 | 26 | public override bool CanConvert(Type objectType) 27 | { 28 | throw new NotImplementedException(); 29 | } 30 | } 31 | 32 | [Cmdlet("Test", "DummyCommand")] 33 | public class DummyCommand : PSCmdlet 34 | { 35 | protected override void EndProcessing() 36 | { 37 | string typeName = typeof(JsonConvert).FullName; 38 | Console.WriteLine($"Using '{typeName}' from '{GetAssemblyName()}'"); 39 | } 40 | 41 | private string GetAssemblyName() 42 | { 43 | return typeof(JsonConvert).Assembly.FullName; 44 | } 45 | 46 | public static void PrintConverterName() 47 | { 48 | Console.WriteLine(typeof(DummyConverter).FullName); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/scenario-demos/1-SampleModule-only.md: -------------------------------------------------------------------------------- 1 | ## SampleModule only 2 | 3 | > NOTE: This assumes you have built and generated the 3 modules successfully with `.\build.ps1` 4 | and have `Set-Location PowerShell-ALC-Samples\Resolving-Event-with-ALC`. 5 | 6 | When `SampleModule` is the only module in the picture, 7 | its resolving handler will serve the loading request of the `1.0.0.0` version of `SharedDependency.dll`. 8 | 9 | ```powershell 10 | ## PowerShell 7.2 11 | 12 | PS:1> Import-Module .\bin\SampleModule\SampleModule.psd1 13 | 14 | PS:2> Get-Greeting -UseSharedDependency ## triggers loading request of 'SharedDependency' from 'Greeting.Commands.dll'. 15 | <*** Fall in 'ResolvingHandler': SharedDependency, Version=1.0.0.0 -- Loaded! ***> 16 | Greetings! -- from 'SharedDependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null', loaded in 'MyCustomALC' 17 | 18 | PS:3> Get-Greeting -UseSharedDependency ## an assembly can trigger the loading of its reference assembly only once. 19 | Greetings! -- from 'SharedDependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null', loaded in 'MyCustomALC' 20 | 21 | PS:4> Get-Greeting -UseLocalDependency ## triggers loading request of 'SharedDependency' from 'LocalDependency.dll'. 22 | <*** Fall in 'ResolvingHandler': SharedDependency, Version=1.0.0.0 -- Loaded! ***> 23 | From : Greetings! -- from 'SharedDependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null', loaded in 'MyCustomALC' 24 | 25 | PS:5> Get-Greeting -UseLocalDependency ## again, 'LocalDependency.dll' can trigger the loading of 'SharedDependency' only once. 26 | From : Greetings! -- from 'SharedDependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null', loaded in 'MyCustomALC' 27 | ``` 28 | -------------------------------------------------------------------------------- /Targeting-NetStandard2.0/README.md: -------------------------------------------------------------------------------- 1 | ## Target netstandard2.0 2 | 3 | Some modules are built targeting `netstandard2.0` (or `net462`), 4 | so that a single build can work on both .NET (PowerShell 7+) and .NET Framework (Windows PowerShell 5.1). 5 | Also, it's quite common for a module to depend on `Newtonsoft.Json`, 6 | and it happens a lot that a module starts to have assembly conflicts with PowerShell because it depends on a higher version of the `Newtonsoft.Json` assembly. 7 | 8 | The type `AssemblyLoadContext` is not available when targeting `netstandard2.0` or `net462`, 9 | but it's easy to wrap a few reflection API calls to create a custom `AssemblyLoadContext` and load an assembly into it from path 10 | when the module runs in the .NET (PowerShell 7+) environment. 11 | So, we will create the `AssemblyLoadContextProxy` type that encapsulate those reflection operations, 12 | and we will show how to use `AppDomain.AssemblyResolve` and `AssemblyLoadContextProxy` to work around this assembly conflict issue. 13 | 14 | > **NOTE:** Do not use `Assembly.LoadFile` for the dependency isolation purpose.
15 | > This API does load an assembly to a separate `AssemblyLoadContext` instance, but assemblies loaded by 16 | > this API are discoverable by PowerShell's type resolution code (see code [here](https://github.com/PowerShell/PowerShell/blob/918bb8c952af1d461abfc98bc709a1d359168a1c/src/System.Management.Automation/utils/ClrFacade.cs#L56-L61)). 17 | > So, your module could run into the "_Type Identity_" issue when loading an assembly by `Assembly.LoadFile` while another module 18 | > loads a different version of the same assembly into the default `AssemblyLoadContext`. 19 | 20 | ### Two scenarios 21 | 22 | Samples here are for 2 scenarios: 23 | 24 | 1. The dependency of `Newtonsoft.Json` is delay-loaded only when the module business code gets to run. 25 | 2. The dependency of `Newtonsoft.Json` is at the class-level, and will be loaded as soon as the assembly gets loaded. 26 | 27 | Please see the sub-folders for each of these 2 scenarios. 28 | -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/src/SampleModule/Commands/CustomALC.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Runtime.Loader; 5 | using System.Management.Automation; 6 | 7 | namespace CustomAlc 8 | { 9 | public class MyAlc : AssemblyLoadContext 10 | { 11 | private static MyAlc s_myAlc = new( 12 | Path.Combine( 13 | Path.GetDirectoryName(typeof(MyAlc).Assembly.Location), 14 | "Dependencies")); 15 | private string dependencyFolder; 16 | 17 | private MyAlc(string folder) 18 | : base("MyCustomALC", isCollectible: false) 19 | { 20 | dependencyFolder = folder; 21 | } 22 | 23 | protected override Assembly Load(AssemblyName assemblyName) 24 | { 25 | string path = Path.Combine(dependencyFolder, assemblyName.Name) + ".dll"; 26 | 27 | if (File.Exists(path)) 28 | { 29 | return LoadFromAssemblyPath(path); 30 | } 31 | 32 | return null; 33 | } 34 | 35 | internal static Assembly ResolvingHandler(AssemblyLoadContext context, AssemblyName name) 36 | { 37 | if (name.FullName.Equals("SharedDependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", StringComparison.OrdinalIgnoreCase)) 38 | { 39 | Console.WriteLine($"<*** Fall in 'ResolvingHandler': SharedDependency, Version=1.0.0.0 -- Loaded! ***>"); 40 | return s_myAlc.LoadFromAssemblyName(name); 41 | } 42 | 43 | return null; 44 | } 45 | } 46 | 47 | public class Init : IModuleAssemblyInitializer, IModuleAssemblyCleanup 48 | { 49 | public void OnImport() 50 | { 51 | AssemblyLoadContext.Default.Resolving += MyAlc.ResolvingHandler; 52 | } 53 | 54 | public void OnRemove(PSModuleInfo module) 55 | { 56 | AssemblyLoadContext.Default.Resolving -= MyAlc.ResolvingHandler; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/scenario-demos/2-HigherConflict-then-SampleModule.md: -------------------------------------------------------------------------------- 1 | ## HigherConflict module loaded before SampleModule 2 | 3 | > NOTE: This assumes you have built and generated the 3 modules successfully with `.\build.ps1` 4 | and have `Set-Location PowerShell-ALC-Samples\Resolving-Event-with-ALC`. 5 | 6 | When the `HigherConflict` module gets loaded, the `1.5.0.0` version of `SharedDependency.dll` will be loaded into the default `AssemblyLoadContext`. 7 | Then when loading and using `SampleModule`, the loading request for `1.0.0.0` version of `SharedDependency.dll` will be triggered, 8 | which will be served directly by the default `AssemblyLoadContext` with the `1.5.0.0` version of `SharedDependency.dll` because the same or higher version of the requested assembly is already available. 9 | 10 | ```powershell 11 | ## PowerShell 7.2 12 | 13 | PS:1> Import-Module .\bin\HigherConflict\ConflictWithHigherDeps.dll 14 | PS:2> Get-Module ConflictWithHigherDeps 15 | 16 | ModuleType Version PreRelease Name ExportedCommands 17 | ---------- ------- ---------- ---- ---------------- 18 | Binary 1.0.0.0 ConflictWithHigherDeps Test-ConflictWithHigherDeps 19 | 20 | PS:3> Test-ConflictWithHigherDeps ## the '1.5.0.0' version of 'SharedDependency' gets loaded in default ALC. 21 | Greetings! -- from 'SharedDependency, Version=1.5.0.0, Culture=neutral, PublicKeyToken=null', loaded in 'Default' 22 | 23 | PS:4> Import-Module .\bin\SampleModule\SampleModule.psd1 24 | 25 | PS:5> Get-Greeting -UseSharedDependency ## the resolving handler is not triggered, because the request is served by '1.5.0.0' version of 'SharedDependency'. 26 | Greetings! -- from 'SharedDependency, Version=1.5.0.0, Culture=neutral, PublicKeyToken=null', loaded in 'Default' 27 | 28 | PS:6> Get-Greeting -UseLocalDependency ## same here, the loading request is served by '1.5.0.0' version of 'SharedDependency'. 29 | From : Greetings! -- from 'SharedDependency, Version=1.5.0.0, Culture=neutral, PublicKeyToken=null', loaded in 'Default' 30 | 31 | ## manually loading 'SharedDependency, Version=1.0.0.0' will be served by '1.5.0.0', not triggering the resolving handler. 32 | PS:7> [System.Reflection.Assembly]::Load('SharedDependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null') | % FullName 33 | SharedDependency, Version=1.5.0.0, Culture=neutral, PublicKeyToken=null 34 | ``` 35 | -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/src/SampleModule/README.md: -------------------------------------------------------------------------------- 1 | ## Module structure 2 | 3 | The `SampleModule` has the following folder structure: 4 | 5 | ```sh 6 | └───SampleModule 7 | │ Greeting.Commands.dll 8 | │ LocalDependency.dll 9 | │ SampleModule.psd1 10 | │ 11 | └───Dependencies 12 | SharedDependency.dll 13 | ``` 14 | 15 | `SampleModule.psd1` declares a nested module: `NestedModules = @('Greeting.Commands.dll')`. 16 | 17 | `Greeting.Commands.dll` references 2 dependencies: `SharedDependency.dll` and `LocalDependency.dll`. It contains: 18 | 19 | 1. [`CustomALC.cs`](./Commands/CustomALC.cs) - A custom implementation of `AssemblyLoadContext` and the code that registers a handler to `AssemblyLoadContext.Default.Resolving` event when the module is loaded (of course, unregister when the module is removed). 20 | 2. [`Command.cs`](./Commands/Command.cs) - The real business logic of the module that exposes the `Get-Greeting` cmdlet. 21 | 22 | `LocalDependency.dll` contains the real business logic of the module, which acts like a utility assembly needed by `Greeting.Commands.dll`. 23 | It also references `SharedDependency.dll`. 24 | This utility assembly is added to this sample intentionally, to demonstrate the behaviors when a module needs to request for loading the same assembly more than once. 25 | 26 | `SharedDependency.dll` is the conflicting dependency assembly. The version referenced by `SampleModule` is `1.0.0.0`. 27 | 28 | ## How it works 29 | 30 | During the loading of the nested module `Greeting.Commands.dll`, 31 | its `OnImport` implementation will be called, which will register a handler to the `AssemblyLoadContext.Default.Resolving` event. 32 | 33 | The handler only reacts to the loading request of the `1.0.0.0` version of `SharedDependency.dll`, because that's the version this module depends on. 34 | The handler uses a singleton instance of the custom `AssemblyLoadContext` to serve all loading requests, 35 | so that it's guaranteed to return the same assembly instance for all loading requests it serves. 36 | The custom `AssemblyLoadContext` looks for the requested assembly from the `Dependencies` folder under the module base, 37 | and that's why `SharedDependency.dll` is placed there. 38 | 39 | The syntax of `Get-Greeting` is 40 | 41 | ```powershell 42 | Get-Greeting -UseSharedDependency [] 43 | 44 | Get-Greeting -UseLocalDependency [] 45 | ``` 46 | 47 | When `-UseSharedDependency` is specified, the loading of `SharedDependency.dll` will be triggered. 48 | When `-UseLocalDependency` is specified, the loading of `LocalDependency.dll` will be triggered, 49 | which will then trigger another loading request of `SharedDependency.dll`. 50 | -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/scenario-demos/5-SampleModule-then-LowerConflict.md: -------------------------------------------------------------------------------- 1 | ## LowerConflict module loaded after SampleModule 2 | 3 | > NOTE: This assumes you have built and generated the 3 modules successfully with `.\build.ps1` 4 | and have `Set-Location PowerShell-ALC-Samples\Resolving-Event-with-ALC`. 5 | 6 | When the `SampleModule` gets loaded and used first, 7 | the loading request for the `1.0.0.0` version of `SharedDependency.dll` will be served by its resolving handler. 8 | 9 | When the `LowerConflict` module gets loaded, the `0.7.0.0` version of `SharedDependency.dll` will be loaded into the default `AssemblyLoadContext`. 10 | The subsequent loading request for the `1.0.0.0` version of `SharedDependency.dll` will continue to be served by the resolving handler 11 | 12 | ```PowerShell 13 | ## PowerShell 7.2 14 | 15 | PS:1> Import-Module .\bin\SampleModule\SampleModule.psd1 16 | 17 | PS:2> Get-Greeting -UseSharedDependency ## triggers loading request of 'SharedDependency' from 'Greeting.Commands.dll'. 18 | <*** Fall in 'ResolvingHandler': SharedDependency, Version=1.0.0.0 -- Loaded! ***> 19 | Greetings! -- from 'SharedDependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null', loaded in 'MyCustomALC' 20 | 21 | PS:3> Get-Greeting -UseSharedDependency ## an assembly can trigger the loading of its reference assembly only once. 22 | Greetings! -- from 'SharedDependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null', loaded in 'MyCustomALC' 23 | 24 | PS:4> Import-Module .\bin\lowerConflict\ConflictWithLowerDeps.dll 25 | 26 | PS:5> Test-ConflictWithLowerDeps ## this cmdlet has '0.7.0.0' version of 'SharedDependency' loaded in default ALC. 27 | Greetings! -- from 'SharedDependency, Version=0.7.0.0, Culture=neutral, PublicKeyToken=null', loaded in 'Default' 28 | 29 | PS:6> Get-Greeting -UseLocalDependency ## triggers loading request of 'SharedDependency' from 'LocalDependency.dll'. 30 | <*** Fall in 'ResolvingHandler': SharedDependency, Version=1.0.0.0 -- Loaded! ***> 31 | From : Greetings! -- from 'SharedDependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null', loaded in 'MyCustomALC' 32 | 33 | PS:7> Get-Greeting -UseLocalDependency ## again, 'LocalDependency.dll' can trigger the loading of 'SharedDependency' only once. 34 | From : Greetings! -- from 'SharedDependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null', loaded in 'MyCustomALC' 35 | 36 | ## manually loading 'SharedDependency, Version=1.0.0.0' will trigger the resolving handler again. 37 | PS:8> [System.Reflection.Assembly]::Load('SharedDependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null') | % FullName 38 | <*** Fall in 'ResolvingHandler': SharedDependency, Version=1.0.0.0 -- Loaded! ***> 39 | SharedDependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null 40 | ``` 41 | -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/scenario-demos/3-LowerConflict-then-SampleModule.md: -------------------------------------------------------------------------------- 1 | ## LowerConflict module loaded before SampleModule 2 | 3 | > NOTE: This assumes you have built and generated the 3 modules successfully with `.\build.ps1` 4 | and have `Set-Location PowerShell-ALC-Samples\Resolving-Event-with-ALC`. 5 | 6 | When the `LowerConflict` module gets loaded, the `0.7.0.0` version of `SharedDependency.dll` will be loaded into the default `AssemblyLoadContext`. 7 | Then when loading and using `SampleModule`, the loading request for `1.0.0.0` version of `SharedDependency.dll` will be triggered, 8 | which cannot be served by the `0.7.0.0` version of `SharedDependency.dll` that is already available in the default `AssemblyLoadContext` 9 | because the version is lower than the requested version. 10 | So, the registered resolving handler will be triggered, to serve the loading request for the `1.0.0.0` version of `SharedDependency.dll`. 11 | 12 | ```powershell 13 | ## PowerShell 7.2 14 | 15 | PS:1> Import-Module .\bin\lowerConflict\ConflictWithLowerDeps.dll 16 | 17 | PS:2> Get-Module ConflictWithLowerDeps 18 | 19 | ModuleType Version PreRelease Name ExportedCommands 20 | ---------- ------- ---------- ---- ---------------- 21 | Binary 1.0.0.0 ConflictWithLowerDeps Test-ConflictWithLowerDeps 22 | 23 | PS:3> Test-ConflictWithLowerDeps ## the '0.7.0.0' version of 'SharedDependency' gets loaded in default ALC. 24 | Greetings! -- from 'SharedDependency, Version=0.7.0.0, Culture=neutral, PublicKeyToken=null', loaded in 'Default' 25 | 26 | PS:4> Import-Module .\bin\SampleModule\SampleModule.psd1 27 | 28 | PS:5> Get-Greeting -UseSharedDependency ## triggers loading request of 'SharedDependency' from 'Greeting.Commands.dll'. 29 | <*** Fall in 'ResolvingHandler': SharedDependency, Version=1.0.0.0 -- Loaded! ***> 30 | Greetings! -- from 'SharedDependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null', loaded in 'MyCustomALC' 31 | 32 | PS:6> Get-Greeting -UseSharedDependency ## an assembly can trigger the loading of its reference assembly only once. 33 | Greetings! -- from 'SharedDependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null', loaded in 'MyCustomALC' 34 | 35 | PS:7> Get-Greeting -UseLocalDependency ## triggers loading request of 'SharedDependency' from 'LocalDependency.dll'. 36 | <*** Fall in 'ResolvingHandler': SharedDependency, Version=1.0.0.0 -- Loaded! ***> 37 | From : Greetings! -- from 'SharedDependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null', loaded in 'MyCustomALC' 38 | 39 | PS:8> Get-Greeting -UseLocalDependency ## again, 'LocalDependency.dll' can trigger the loading of 'SharedDependency' only once. 40 | From : Greetings! -- from 'SharedDependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null', loaded in 'MyCustomALC' 41 | ``` 42 | -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/scenario-demos/4-SampleModule-then-HigherConflict.md: -------------------------------------------------------------------------------- 1 | ## HigherConflict module loaded after SampleModule 2 | 3 | > NOTE: This assumes you have built and generated the 3 modules successfully with `.\build.ps1` 4 | and have `Set-Location PowerShell-ALC-Samples\Resolving-Event-with-ALC`. 5 | 6 | When the `SampleModule` gets loaded and used first, 7 | the loading request for the `1.0.0.0` version of `SharedDependency.dll` will be served by its resolving handler. 8 | 9 | Note that, the default `AssemblyLoadContext` will not cache the assembly instance of the `1.0.0.0` version of `SharedDependency.dll` returned from the resolving handler. However, it will "take a note" of it -- I saw the `1.0.0.0` version of `SharedDependency.dll` got served by a handler, so requests for the same assembly should be sent to the handlers, even if a higher version of the assembly becomes available later in the default `AssemblyLoadContext`. 10 | 11 | When the `HigherConflict` module gets loaded later, the `1.5.0.0` version of `SharedDependency.dll` will be loaded into the default `AssemblyLoadContext`. 12 | However, when the loading request for the `1.0.0.0` version of `SharedDependency.dll` gets triggered again by the `LocalDependency.dll`, 13 | it will not be served by the `1.5.0.0` version of `SharedDependency.dll` that is already available in the default `AssemblyLoadContext`, 14 | but instead, the loading request will be sent to the resolving handler. 15 | So, `LocalDependency.dll` will get the `1.0.0.0` version `SharedDependency.dll` returned from the resolving handler. 16 | 17 | ```powershell 18 | ## PowerShell 7.2 19 | 20 | PS:1> Import-Module .\bin\SampleModule\SampleModule.psd1 21 | 22 | PS:2> Get-Greeting -UseSharedDependency ## triggers loading request of 'SharedDependency' from 'Greeting.Commands.dll'. 23 | <*** Fall in 'ResolvingHandler': SharedDependency, Version=1.0.0.0 -- Loaded! ***> 24 | Greetings! -- from 'SharedDependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null', loaded in 'MyCustomALC' 25 | 26 | PS:3> Get-Greeting -UseSharedDependency ## an assembly can trigger the loading of its reference assembly only once. 27 | Greetings! -- from 'SharedDependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null', loaded in 'MyCustomALC' 28 | 29 | PS:4> Import-Module .\bin\HigherConflict\ConflictWithHigherDeps.dll 30 | 31 | PS:5> Test-ConflictWithHigherDeps ## this cmdlet has '1.5.0.0' version of 'SharedDependency' loaded in default ALC. 32 | Greetings! -- from 'SharedDependency, Version=1.5.0.0, Culture=neutral, PublicKeyToken=null', loaded in 'Default' 33 | 34 | PS:6> Get-Greeting -UseLocalDependency ## triggers loading request of 'SharedDependency' from 'LocalDependency.dll'. 35 | <*** Fall in 'ResolvingHandler': SharedDependency, Version=1.0.0.0 -- Loaded! ***> 36 | From : Greetings! -- from 'SharedDependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null', loaded in 'MyCustomALC' 37 | 38 | PS:7> Get-Greeting -UseLocalDependency ## again, 'LocalDependency.dll' can trigger the loading of 'SharedDependency' only once. 39 | From : Greetings! -- from 'SharedDependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null', loaded in 'MyCustomALC' 40 | 41 | ## manually loading 'SharedDependency, Version=1.0.0.0' will trigger the resolving handler again. 42 | PS:8> [System.Reflection.Assembly]::Load('SharedDependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null') | % FullName 43 | <*** Fall in 'ResolvingHandler': SharedDependency, Version=1.0.0.0 -- Loaded! ***> 44 | SharedDependency, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null 45 | ``` 46 | -------------------------------------------------------------------------------- /Targeting-NetStandard2.0/scenario-2/resolver/Class1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Management.Automation; 5 | using System.Reflection; 6 | 7 | namespace assembly.resolver 8 | { 9 | public class Init : IModuleAssemblyInitializer, IModuleAssemblyCleanup 10 | { 11 | private static readonly string s_dependencyFolder; 12 | private static readonly HashSet s_dependencies; 13 | private static readonly AssemblyLoadContextProxy s_proxy; 14 | 15 | static Init() 16 | { 17 | s_dependencyFolder = Path.Combine(Path.GetDirectoryName(typeof(Init).Assembly.Location), "Dependencies"); 18 | s_dependencies = new(StringComparer.Ordinal); 19 | s_proxy = AssemblyLoadContextProxy.CreateLoadContext("platyPS-load-context"); 20 | 21 | foreach (string filePath in Directory.EnumerateFiles(s_dependencyFolder, "*.dll")) 22 | { 23 | s_dependencies.Add(AssemblyName.GetAssemblyName(filePath).FullName); 24 | } 25 | } 26 | 27 | public void OnImport() 28 | { 29 | AppDomain.CurrentDomain.AssemblyResolve += ResolvingHandler; 30 | } 31 | 32 | public void OnRemove(PSModuleInfo module) 33 | { 34 | AppDomain.CurrentDomain.AssemblyResolve -= ResolvingHandler; 35 | } 36 | 37 | private static bool IsAssemblyMatching(AssemblyName assemblyName, Assembly requestingAssembly) 38 | { 39 | // The requesting assembly is always available in .NET, but could be null in .NET Framework. 40 | // - When the requesting assembly is available, we check whether the loading request came from this 41 | // module (the 'conflict' assembly in this case), so as to make sure we only act on the request 42 | // from this module. 43 | // - When the requesting assembly is not available, we just have to depend on the assembly name only. 44 | return requestingAssembly is not null 45 | ? requestingAssembly.FullName.StartsWith("conflict,") && s_dependencies.Contains(assemblyName.FullName) 46 | : s_dependencies.Contains(assemblyName.FullName); 47 | } 48 | 49 | internal static Assembly ResolvingHandler(object sender, ResolveEventArgs args) 50 | { 51 | var assemblyName = new AssemblyName(args.Name); 52 | if (IsAssemblyMatching(assemblyName, args.RequestingAssembly)) 53 | { 54 | string fileName = assemblyName.Name + ".dll"; 55 | string filePath = Path.Combine(s_dependencyFolder, fileName); 56 | 57 | if (File.Exists(filePath)) 58 | { 59 | Console.WriteLine($"<*** Fall in 'ResolvingHandler': Newtonsoft.Json, Version=13.0.0.0 -- Loaded! ***>"); 60 | // - In .NET, load the assembly into the custom assembly load context. 61 | // - In .NET Framework, assembly conflict is not a problem, so we load the assembly 62 | // by 'Assembly.LoadFrom', the same as what powershell.exe would do. 63 | return s_proxy is not null 64 | ? s_proxy.LoadFromAssemblyPath(filePath) 65 | : Assembly.LoadFrom(filePath); 66 | } 67 | } 68 | 69 | return null; 70 | } 71 | } 72 | 73 | internal class AssemblyLoadContextProxy 74 | { 75 | private readonly object _customContext; 76 | private readonly MethodInfo _loadFromAssemblyPath; 77 | 78 | private AssemblyLoadContextProxy(Type alc, string loadContextName) 79 | { 80 | var ctor = alc.GetConstructor(new[] { typeof(string), typeof(bool) }); 81 | _loadFromAssemblyPath = alc.GetMethod("LoadFromAssemblyPath", new[] { typeof(string) }); 82 | _customContext = ctor.Invoke(new object[] { loadContextName, false }); 83 | } 84 | 85 | internal Assembly LoadFromAssemblyPath(string assemblyPath) 86 | { 87 | return (Assembly) _loadFromAssemblyPath.Invoke(_customContext, new[] { assemblyPath }); 88 | } 89 | 90 | internal static AssemblyLoadContextProxy CreateLoadContext(string name) 91 | { 92 | if (string.IsNullOrEmpty(name)) 93 | { 94 | throw new ArgumentNullException(nameof(name)); 95 | } 96 | 97 | var alc = typeof(object).Assembly.GetType("System.Runtime.Loader.AssemblyLoadContext"); 98 | return alc is not null 99 | ? new AssemblyLoadContextProxy(alc, name) 100 | : null; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Targeting-NetStandard2.0/scenario-1/Class1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Management.Automation; 5 | using System.Reflection; 6 | using Newtonsoft.Json; 7 | 8 | namespace assembly.conflict 9 | { 10 | [Cmdlet("Test", "DummyCommand")] 11 | public class DummyCommand : PSCmdlet 12 | { 13 | protected override void EndProcessing() 14 | { 15 | string typeName = typeof(JsonConvert).FullName; 16 | Console.WriteLine($"Using '{typeName}' from '{GetAssemblyName()}'"); 17 | } 18 | 19 | private string GetAssemblyName() 20 | { 21 | return typeof(JsonConvert).Assembly.FullName; 22 | } 23 | } 24 | 25 | public class Init : IModuleAssemblyInitializer, IModuleAssemblyCleanup 26 | { 27 | private static readonly Assembly s_self; 28 | private static readonly string s_dependencyFolder; 29 | private static readonly HashSet s_dependencies; 30 | private static readonly AssemblyLoadContextProxy s_proxy; 31 | 32 | static Init() 33 | { 34 | s_self = typeof(Init).Assembly; 35 | s_dependencyFolder = Path.Combine(Path.GetDirectoryName(s_self.Location), "Dependencies"); 36 | s_dependencies = new(StringComparer.Ordinal); 37 | s_proxy = AssemblyLoadContextProxy.CreateLoadContext("platyPS-load-context"); 38 | 39 | foreach (string filePath in Directory.EnumerateFiles(s_dependencyFolder, "*.dll")) 40 | { 41 | s_dependencies.Add(AssemblyName.GetAssemblyName(filePath).FullName); 42 | } 43 | } 44 | 45 | public void OnImport() 46 | { 47 | AppDomain.CurrentDomain.AssemblyResolve += ResolvingHandler; 48 | } 49 | 50 | public void OnRemove(PSModuleInfo module) 51 | { 52 | AppDomain.CurrentDomain.AssemblyResolve -= ResolvingHandler; 53 | } 54 | 55 | private static bool IsAssemblyMatching(AssemblyName assemblyName, Assembly requestingAssembly) 56 | { 57 | // The requesting assembly is always available in .NET, but could be null in .NET Framework. 58 | // - When the requesting assembly is available, we check whether the loading request came from this 59 | // module, so as to make sure we only act on the request from this module. 60 | // - When the requesting assembly is not available, we just have to depend on the assembly name only. 61 | return requestingAssembly is not null 62 | ? requestingAssembly == s_self && s_dependencies.Contains(assemblyName.FullName) 63 | : s_dependencies.Contains(assemblyName.FullName); 64 | } 65 | 66 | internal static Assembly ResolvingHandler(object sender, ResolveEventArgs args) 67 | { 68 | var assemblyName = new AssemblyName(args.Name); 69 | if (IsAssemblyMatching(assemblyName, args.RequestingAssembly)) 70 | { 71 | string fileName = assemblyName.Name + ".dll"; 72 | string filePath = Path.Combine(s_dependencyFolder, fileName); 73 | 74 | if (File.Exists(filePath)) 75 | { 76 | Console.WriteLine($"<*** Fall in 'ResolvingHandler': Newtonsoft.Json, Version=13.0.0.0 -- Loaded! ***>"); 77 | // - In .NET, load the assembly into the custom assembly load context. 78 | // - In .NET Framework, assembly conflict is not a problem, so we load the assembly 79 | // by 'Assembly.LoadFrom', the same as what powershell.exe would do. 80 | return s_proxy is not null 81 | ? s_proxy.LoadFromAssemblyPath(filePath) 82 | : Assembly.LoadFrom(filePath); 83 | } 84 | } 85 | 86 | return null; 87 | } 88 | } 89 | 90 | internal class AssemblyLoadContextProxy 91 | { 92 | private readonly object _customContext; 93 | private readonly MethodInfo _loadFromAssemblyPath; 94 | 95 | private AssemblyLoadContextProxy(Type alc, string loadContextName) 96 | { 97 | var ctor = alc.GetConstructor(new[] { typeof(string), typeof(bool) }); 98 | _loadFromAssemblyPath = alc.GetMethod("LoadFromAssemblyPath", new[] { typeof(string) }); 99 | _customContext = ctor.Invoke(new object[] { loadContextName, false }); 100 | } 101 | 102 | internal Assembly LoadFromAssemblyPath(string assemblyPath) 103 | { 104 | return (Assembly) _loadFromAssemblyPath.Invoke(_customContext, new[] { assemblyPath }); 105 | } 106 | 107 | internal static AssemblyLoadContextProxy CreateLoadContext(string name) 108 | { 109 | if (string.IsNullOrEmpty(name)) 110 | { 111 | throw new ArgumentNullException(nameof(name)); 112 | } 113 | 114 | var alc = typeof(object).Assembly.GetType("System.Runtime.Loader.AssemblyLoadContext"); 115 | return alc is not null 116 | ? new AssemblyLoadContextProxy(alc, name) 117 | : null; 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Resolving-Event-with-ALC/README.md: -------------------------------------------------------------------------------- 1 | # Resolving assembly conflicts 2 | 3 | The article [Resolving PowerShell module assembly dependency conflicts][the-article] contains great content about what this problem is, why it happens, and different ways for a module author to mitigate the issue. 4 | The [most robust solution][most-robust-solution] described in the article leverages the `AssemblyLoadContext` to handle the loading requests of all a module's dependencies, 5 | which makes sure the module gets the exact version of the dependency assemblies that it requests for. 6 | 7 | This technique presents a clean solution for a module to avoid dependency conflicts. 8 | It is used by the [Bicep PowerShell module](https://github.com/PSBicep/PSBicep), 9 | and is also documented with a great example in Emanuel Palm's blog post: [Resolving PowerShell Module Conflicts](https://pipe.how/get-assemblyloadcontext/). 10 | 11 | However, this technique requires the module assembly to not directly reference the dependency assemblies, 12 | but instead, to reference a wrapper assembly which then references the dependency assemblies. 13 | The wrapper assembly acts like a bridge, forwarding the calls from the module assembly to the dependency assemblies. 14 | This makes it usually a non-trivial amount of work to apply this technique -- 15 | 16 | - For a new module, this would add additional complexity to the design and implementation; 17 | - For an existing module, this would require significant refactoring. 18 | 19 | Here I want to introduce a simplified solution to mitigate the problem, 20 | which comes with [two limitations](#limitations) comparing to the [above solution][most-robust-solution] but requires way less effort from the module author. 21 | 22 | ## AssemblyLoadContext.Default.Resolving + AssemblyLoadContext 23 | 24 | The use of the assembly resolving event is quite common for redirecting loading requests. 25 | You can register an assembly resolving handler for the exact versions of your dependency assemblies, 26 | and then leverage `AssemblyLoadContext` in the handler to deal with the loading. 27 | With this, there is no need to have a wrapper assembly, 28 | and the handler is guaranteed to return the same assembly instance for all the loading requests it receives for the same assembly. 29 | 30 | > **NOTE:** Do not use `Assembly.LoadFrom` in the event handler.
31 | > That API always loads an assembly file to the default `AssemblyLoadContext`, 32 | > which is actually the source of this assembly-conflict problem. 33 | 34 | > **NOTE:** Do not use `Assembly.LoadFile` for the dependency isolation purpose.
35 | > This API does load an assembly to a separate `AssemblyLoadContext` instance, but assemblies loaded by 36 | > this API are discoverable by PowerShell's type resolution code (see code [here](https://github.com/PowerShell/PowerShell/blob/918bb8c952af1d461abfc98bc709a1d359168a1c/src/System.Management.Automation/utils/ClrFacade.cs#L56-L61)). 37 | > So, your module could run into the "_Type Identity_" issue when loading an assembly by `Assembly.LoadFile` while another module 38 | > loads a different version of the same assembly into the default `AssemblyLoadContext`. 39 | 40 | To leverage `AssemblyLoadContext`, 41 | you need to create a custom `AssemblyLoadContext` class and directly use it to load assembly files. 42 | 43 | We have the module `SampleModule` to demonstrate this solution. 44 | The whole sample is organized as follows: 45 | 46 | - [shared-dependency](./src/shared-dependency/): it's a project to produce different versions of NuGet packages for `SharedDependency.dll`. 47 | Three such packages of the versions `0.7.0`, `1.0.0`, and `1.5.0` are available under the folder [nuget-packages](./nuget-packages/). 48 | - [SampleModule](./src/SampleModule/): it produces the `SampleModule` that uses _"`Resolving` event + custom `AssemblyLoadContext`"_ to handle the conflicting `SharedDependency.dll`. 49 | See its [README](./src/SampleModule/README.md) for details on the module structure and how it works. 50 | - [ConflictWithHigherDeps](./src/HigherDependencyConflict/): it's a module that depends on a higher version of `SharedDependency.dll` 51 | - [ConflictWithLowerDeps](./src/LowerDependencyConflict/): it's a module that depends on a lower version of `SharedDependency.dll` 52 | - [scenario-demos](./scenario-demos/): it contains the demos for five scenarios that `SampleModule` can run into with the modules `ConflictWithHigherDeps` and `ConflictWithLowerDeps`. 53 | 54 | To build and generate all the 3 modules needed for the demos, 55 | run the `.\build.ps1` within this folder. 56 | 57 | The generated modules will be placed in `.\bin`. 58 | Please make sure `.NET SDK 6` is installed and available in `PATH` before building. 59 | The version of the SDK should be `6.0.100` or newer. 60 | 61 | Once the 3 modules are generated under `.\bin`, 62 | go ahead to [scenario-demos](./scenario-demos/) to review the behaviors of `SampleModule` for those five scenarios. 63 | 64 | ## Limitations 65 | 66 | Comparing to technique adopted by the Bicep module, there are 2 limitations with this solution: 67 | 68 | 1. If a higher version of the dependency is already loaded in the default `AssemblyLoadContext`, 69 | that version will be used by your module, and the resolving handler will never be triggered. 70 | 1. If another module uses the same technique to handle the same version of the same dependency, 71 | and it's loaded before your module, then your module's request for that dependency will be served by that module's resolving handler. 72 | This's OK as long as that module is still loaded, but could potentially be a problem if that module is removed and unregistered the resolving handler that served your previous loading request. 73 | This is because if your module happens to have a new request for the same dependency after that point, the new request might then be served by your module's resolving handler with a new assembly instance, which could cause the type identity issue. 74 | 75 | Please make sure you evaluate the limitations before going forward with this solution: 76 | 77 | - For the 1st limitation, it may be acceptable to depend on a higher version dependency assembly at run time for some modules. 78 | For those modules, this solution could be a good fit. 79 | - For the 2nd limitation, it would be rare to happen in practice, given that most workflows don't involve removing a loaded module. 80 | 81 | [the-article]: https://docs.microsoft.com/powershell/scripting/dev-cross-plat/resolving-dependency-conflicts 82 | [most-robust-solution]: https://docs.microsoft.com/powershell/scripting/dev-cross-plat/resolving-dependency-conflicts#loading-through-net-core-assembly-load-contexts 83 | --------------------------------------------------------------------------------