Skip to content

Commit 5748862

Browse files
committed
Initial commit
0 parents  commit 5748862

14 files changed

+528
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
bin/
2+
obj/
3+
out/

README.md

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Example AssemblyLoadContext-using PowerShell Module
2+
3+
This repository demonstrates a simple example for isolating module dependencies from the rest of PowerShell.
4+
5+
The `old` directory contains the pre-isolation module,
6+
and `new` represents the same module after implementing an isolation technique.
7+
8+
The main purpose of this example is to demonstrate how to use an Assembly Load Context
9+
in .NET Core/.NET 5 to isolate module dependendies in PowerShell 6+.
10+
11+
Be warned that the .NET Framework isolation technique is not ideal;
12+
there's no guarantee that the isolation technique for .NET Framework will work well.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.IO;
4+
using System.Reflection;
5+
using System.Runtime.Loader;
6+
7+
namespace JsonModule.Cmdlets
8+
{
9+
public class DependencyAssemblyLoadContext : AssemblyLoadContext
10+
{
11+
private static readonly string s_psHome = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
12+
13+
private static readonly ConcurrentDictionary<string, DependencyAssemblyLoadContext> s_dependencyLoadContexts = new ConcurrentDictionary<string, DependencyAssemblyLoadContext>();
14+
15+
internal static DependencyAssemblyLoadContext GetForDirectory(string directoryPath)
16+
{
17+
return s_dependencyLoadContexts.GetOrAdd(directoryPath, (path) => new DependencyAssemblyLoadContext(path));
18+
}
19+
20+
private readonly string _dependencyDirPath;
21+
22+
public DependencyAssemblyLoadContext(string dependencyDirPath)
23+
: base(nameof(DependencyAssemblyLoadContext))
24+
{
25+
_dependencyDirPath = dependencyDirPath;
26+
}
27+
28+
protected override Assembly Load(AssemblyName assemblyName)
29+
{
30+
string assemblyFileName = $"{assemblyName.Name}.dll";
31+
32+
// Make sure we allow other common PowerShell dependencies to be loaded by PowerShell
33+
// But specifically exclude Newtonsoft.Json since we want to use a different version here
34+
if (!assemblyName.Name.Equals("Newtonsoft.Json", StringComparison.OrdinalIgnoreCase))
35+
{
36+
string psHomeAsmPath = Path.Join(s_psHome, assemblyFileName);
37+
if (File.Exists(psHomeAsmPath))
38+
{
39+
// With this API, returning null means nothing is loaded
40+
return null;
41+
}
42+
}
43+
44+
// Now try to load the assembly from the dependency directory
45+
string dependencyAsmPath = Path.Join(_dependencyDirPath, assemblyFileName);
46+
if (File.Exists(dependencyAsmPath))
47+
{
48+
return LoadFromAssemblyPath(dependencyAsmPath);
49+
}
50+
51+
return null;
52+
}
53+
}
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>netcoreapp3.1;net461</TargetFrameworks>
5+
</PropertyGroup>
6+
7+
<PropertyGroup Condition="'$(TargetFramework)' == 'net461'">
8+
<DefineConstants>$(DefineConstants);NETFRAMEWORK</DefineConstants>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<ProjectReference Include="..\JsonModule.Engine\JsonModule.Engine.csproj" />
13+
</ItemGroup>
14+
15+
<ItemGroup Condition="'$(TargetFramework)' == 'net461'">
16+
<Compile Remove="DependencyAssemblyLoadContext.cs" />
17+
</ItemGroup>
18+
19+
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1'">
20+
<PackageReference Include="System.Management.Automation" Version="7.0.1" />
21+
</ItemGroup>
22+
23+
<ItemGroup Condition="'$(TargetFramework)' == 'net461'">
24+
<PackageReference Include="Microsoft.PowerShell.5.ReferenceAssemblies" Version="1.1.0" />
25+
</ItemGroup>
26+
27+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using System.IO;
2+
using System.Management.Automation;
3+
using System.Reflection;
4+
5+
#if NETFRAMEWORK
6+
using System;
7+
#else
8+
using System.Runtime.Loader;
9+
#endif
10+
11+
namespace JsonModule.Cmdlets
12+
{
13+
public class JsonModuleInitializer : IModuleAssemblyInitializer
14+
{
15+
private static string s_binBasePath = Path.GetFullPath(
16+
Path.Combine(
17+
Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
18+
".."));
19+
20+
private static string s_binCommonPath = Path.Combine(s_binBasePath, "Common");
21+
22+
#if NETFRAMEWORK
23+
private static string s_binFrameworkPath = Path.Combine(s_binBasePath, "Framework");
24+
#else
25+
private static string s_binCorePath = Path.Join(s_binBasePath, "Core");
26+
#endif
27+
28+
public void OnImport()
29+
{
30+
#if NETFRAMEWORK
31+
AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly_NetFramework;
32+
#else
33+
AssemblyLoadContext.Default.Resolving += ResolveAssembly_NetCore;
34+
#endif
35+
}
36+
37+
#if NETFRAMEWORK
38+
39+
private static Assembly ResolveAssembly_NetFramework(object sender, ResolveEventArgs args)
40+
{
41+
// In .NET Framework, we must try to resolve ALL assemblies under the dependency dir here
42+
// This essentially means we are combining the .NET Core ALC and resolve events into one here
43+
// Note that:
44+
// - This is not a recommended usage of Assembly.LoadFile()
45+
// - Even doing this will not bypass the GAC
46+
47+
// Parse the assembly name to get the file name
48+
var asmName = new AssemblyName(args.Name);
49+
var dllFileName = $"{asmName.Name}.dll";
50+
51+
// Look for the DLL in our .NET Framework directory
52+
string frameworkAsmPath = Path.Combine(s_binFrameworkPath, dllFileName);
53+
if (File.Exists(frameworkAsmPath))
54+
{
55+
return LoadAssemblyFile_NetFramework(frameworkAsmPath);
56+
}
57+
58+
// Now look in the dependencies directory to resolve .NET Standard dependencies
59+
string commonAsmPath = Path.Combine(s_binCommonPath, dllFileName);
60+
if (File.Exists(commonAsmPath))
61+
{
62+
return LoadAssemblyFile_NetFramework(commonAsmPath);
63+
}
64+
65+
// We've run out of places to look
66+
return null;
67+
}
68+
69+
private static Assembly LoadAssemblyFile_NetFramework(string assemblyPath)
70+
{
71+
return Assembly.LoadFile(assemblyPath);
72+
}
73+
74+
#else
75+
76+
private static Assembly ResolveAssembly_NetCore(
77+
AssemblyLoadContext assemblyLoadContext,
78+
AssemblyName assemblyName)
79+
{
80+
// In .NET Core, PowerShell deals with assembly probing so our logic is much simpler
81+
// We only care about our Engine assembly
82+
if (!assemblyName.Name.Equals("JsonModule.Engine"))
83+
{
84+
return null;
85+
}
86+
87+
// Now load the Engine assembly through the dependency ALC, and let it resolve further dependencies automatically
88+
return DependencyAssemblyLoadContext.GetForDirectory(s_binCommonPath).LoadFromAssemblyName(assemblyName);
89+
}
90+
91+
#endif
92+
}
93+
}
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System.Collections.Generic;
2+
using System.IO;
3+
using System.Management.Automation;
4+
using JsonModule.Engine;
5+
6+
namespace JsonModule.Cmdlets
7+
{
8+
[Cmdlet(VerbsData.Out, "Json")]
9+
public class OutJsonCommand : PSCmdlet
10+
{
11+
private List<object> _values;
12+
13+
public OutJsonCommand()
14+
{
15+
_values = new List<object>();
16+
}
17+
18+
[Parameter(Mandatory = true)]
19+
[ValidateNotNullOrEmpty]
20+
public string OutFile { get; set; }
21+
22+
[Parameter(Mandatory = true, ValueFromPipeline = true)]
23+
public object[] Value { get; set; }
24+
25+
protected override void ProcessRecord()
26+
{
27+
_values.AddRange(Value);
28+
}
29+
30+
protected override void EndProcessing()
31+
{
32+
if (_values.Count == 0)
33+
{
34+
return;
35+
}
36+
37+
using (FileStream fileStream = new FileStream(GetUnresolvedProviderPathFromPSPath(OutFile), FileMode.Create, FileAccess.Write, FileShare.None))
38+
{
39+
new JsonWriter().WriteToStream(fileStream, _values.Count > 1 ? _values : _values[0]);
40+
}
41+
}
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
9+
</ItemGroup>
10+
11+
</Project>

new/JsonModule.Engine/JsonWriter.cs

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.IO;
2+
using Newtonsoft.Json;
3+
4+
namespace JsonModule.Engine
5+
{
6+
public class JsonWriter
7+
{
8+
public static string NewtonsoftName { get; } = typeof(JsonConvert).Assembly.FullName;
9+
10+
public void WriteToStream(Stream stream, object value)
11+
{
12+
using (var textWriter = new StreamWriter(stream))
13+
using (var jsonWriter = new JsonTextWriter(textWriter){ Formatting = Formatting.Indented })
14+
{
15+
new JsonSerializer().Serialize(jsonWriter, value);
16+
}
17+
}
18+
}
19+
}

new/JsonModule.psd1

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
@{
2+
3+
# Script module or binary module file associated with this manifest.
4+
RootModule = if ($PSEdition -eq 'Core')
5+
{
6+
'Core/JsonModule.Cmdlets.dll'
7+
}
8+
else
9+
{
10+
'Framework/JsonModule.Cmdlets.dll'
11+
}
12+
13+
# Version number of this module.
14+
ModuleVersion = '0.0.1'
15+
16+
# Supported PSEditions
17+
CompatiblePSEditions = @('Core', 'Desktop')
18+
19+
# ID used to uniquely identify this module
20+
GUID = '228638a3-fe17-46ec-a093-edfc555a75f1'
21+
22+
# Author of this module
23+
Author = 'Me'
24+
25+
# Company or vendor of this module
26+
CompanyName = 'Unknown'
27+
28+
# Copyright statement for this module
29+
Copyright = '(c) Me. All rights reserved.'
30+
31+
# Description of the functionality provided by this module
32+
# Description = ''
33+
34+
# Minimum version of the PowerShell engine required by this module
35+
PowerShellVersion = '5.1'
36+
37+
# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
38+
DotNetFrameworkVersion = '4.6.1'
39+
40+
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
41+
FunctionsToExport = @()
42+
43+
# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
44+
CmdletsToExport = 'Out-Json'
45+
46+
# Variables to export from this module
47+
VariablesToExport = @()
48+
49+
# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
50+
AliasesToExport = @()
51+
52+
# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
53+
PrivateData = @{
54+
55+
PSData = @{
56+
} # End of PSData hashtable
57+
58+
} # End of PrivateData hashtable
59+
60+
}
61+

new/build.ps1

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
param(
2+
[ValidateSet('Debug', 'Release')]
3+
[string]
4+
$Configuration = 'Debug'
5+
)
6+
7+
$netCore = 'netcoreapp3.1'
8+
$netFramework = 'net461'
9+
10+
$outPath = "$PSScriptRoot/out/JsonModule"
11+
$commonPath = "$outPath/Common"
12+
$corePath = "$outPath/Core"
13+
$frameworkPath = "$outPath/Framework"
14+
if (Test-Path $outPath)
15+
{
16+
Remove-Item -Path $outPath -Recurse
17+
}
18+
New-Item -Path $outPath -ItemType Directory
19+
New-Item -Path $commonPath -ItemType Directory
20+
New-Item -Path $corePath -ItemType Directory
21+
New-Item -Path $frameworkPath -ItemType Directory
22+
23+
Push-Location "$PSScriptRoot/JsonModule.Engine"
24+
try
25+
{
26+
dotnet publish
27+
}
28+
finally
29+
{
30+
Pop-Location
31+
}
32+
33+
Push-Location "$PSScriptRoot/JsonModule.Cmdlets"
34+
try
35+
{
36+
dotnet publish -f $netCore
37+
dotnet publish -f $netFramework
38+
}
39+
finally
40+
{
41+
Pop-Location
42+
}
43+
44+
$commonFiles = [System.Collections.Generic.HashSet[string]]::new()
45+
Copy-Item -Path "$PSScriptRoot/JsonModule.psd1" -Destination $outPath
46+
Get-ChildItem -Path "$PSScriptRoot/JsonModule.Engine/bin/$Configuration/netstandard2.0/publish" |
47+
Where-Object { $_.Extension -in '.dll','.pdb' } |
48+
ForEach-Object { [void]$commonFiles.Add($_.Name); Copy-Item -LiteralPath $_.FullName -Destination $commonPath }
49+
Get-ChildItem -Path "$PSScriptRoot/JsonModule.Cmdlets/bin/$Configuration/$netCore/publish" |
50+
Where-Object { $_.Extension -in '.dll','.pdb' -and -not $commonFiles.Contains($_.Name) } |
51+
ForEach-Object { Copy-Item -LiteralPath $_.FullName -Destination $corePath }
52+
Get-ChildItem -Path "$PSScriptRoot/JsonModule.Cmdlets/bin/$Configuration/$netFramework/publish" |
53+
Where-Object { $_.Extension -in '.dll','.pdb' -and -not $commonFiles.Contains($_.Name) } |
54+
ForEach-Object { Copy-Item -LiteralPath $_.FullName -Destination $frameworkPath }

0 commit comments

Comments
 (0)