├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------