├── .gitignore ├── ALCLoader ├── README.md ├── build.ps1 ├── module │ ├── ALCLoader.psd1 │ └── ALCLoader.psm1 └── src │ ├── ALCLoader.Shared │ ├── ALCLoader.Shared.csproj │ ├── LoadContext.cs │ └── SharedUtil.cs │ └── ALCLoader │ ├── ALCLoader.csproj │ ├── ConvertToNewtonsoftJsonCommand.cs │ └── ConvertToYamlDotNetCommand.cs ├── ALCPureScriptModule ├── README.md ├── build.ps1 └── module │ ├── ALCPureScriptModule.psd1 │ └── ALCPureScriptModule.psm1 ├── ALCResolver ├── README.md ├── build.ps1 ├── module │ └── ALCResolver.psd1 └── src │ ├── ALCResolver.Private │ ├── ALCResolver.Private.csproj │ ├── Json.cs │ ├── SharedUtil.cs │ └── Yaml.cs │ └── ALCResolver │ ├── ALCResolver.csproj │ ├── ConvertToNewtonsoftJsonCommand.cs │ ├── ConvertToYamlDotNetCommand.cs │ └── OnImportAndRemove.cs ├── ALCScriptLoadContext ├── README.md ├── build.ps1 ├── module │ ├── ALCScriptLoadContext.psd1 │ └── ALCScriptLoadContext.psm1 └── src │ └── ALCScriptLoadContext │ └── ALCScriptLoadContext.csproj ├── ALCScriptModule ├── README.md ├── build.ps1 ├── module │ ├── ALCScriptModule.psd1 │ └── ALCScriptModule.psm1 └── src │ └── ALCScriptModule │ ├── ALCScriptModule.csproj │ └── LoadContext.cs ├── README.md ├── common.ps1 └── test.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.vspscc 94 | *.vssscc 95 | .builds 96 | *.pidb 97 | *.svclog 98 | *.scc 99 | 100 | # Chutzpah Test files 101 | _Chutzpah* 102 | 103 | # Visual C++ cache files 104 | ipch/ 105 | *.aps 106 | *.ncb 107 | *.opendb 108 | *.opensdf 109 | *.sdf 110 | *.cachefile 111 | *.VC.db 112 | *.VC.VC.opendb 113 | 114 | # Visual Studio profiler 115 | *.psess 116 | *.vsp 117 | *.vspx 118 | *.sap 119 | 120 | # Visual Studio Trace Files 121 | *.e2e 122 | 123 | # TFS 2012 Local Workspace 124 | $tf/ 125 | 126 | # Guidance Automation Toolkit 127 | *.gpState 128 | 129 | # ReSharper is a .NET coding add-in 130 | _ReSharper*/ 131 | *.[Rr]e[Ss]harper 132 | *.DotSettings.user 133 | 134 | # TeamCity is a build add-in 135 | _TeamCity* 136 | 137 | # DotCover is a Code Coverage Tool 138 | *.dotCover 139 | 140 | # AxoCover is a Code Coverage Tool 141 | .axoCover/* 142 | !.axoCover/settings.json 143 | 144 | # Coverlet is a free, cross platform Code Coverage Tool 145 | coverage*.json 146 | coverage*.xml 147 | coverage*.info 148 | 149 | # Visual Studio code coverage results 150 | *.coverage 151 | *.coveragexml 152 | 153 | # NCrunch 154 | _NCrunch_* 155 | .*crunch*.local.xml 156 | nCrunchTemp_* 157 | 158 | # MightyMoose 159 | *.mm.* 160 | AutoTest.Net/ 161 | 162 | # Web workbench (sass) 163 | .sass-cache/ 164 | 165 | # Installshield output folder 166 | [Ee]xpress/ 167 | 168 | # DocProject is a documentation generator add-in 169 | DocProject/buildhelp/ 170 | DocProject/Help/*.HxT 171 | DocProject/Help/*.HxC 172 | DocProject/Help/*.hhc 173 | DocProject/Help/*.hhk 174 | DocProject/Help/*.hhp 175 | DocProject/Help/Html2 176 | DocProject/Help/html 177 | 178 | # Click-Once directory 179 | publish/ 180 | 181 | # Publish Web Output 182 | *.[Pp]ublish.xml 183 | *.azurePubxml 184 | # Note: Comment the next line if you want to checkin your web deploy settings, 185 | # but database connection strings (with potential passwords) will be unencrypted 186 | *.pubxml 187 | *.publishproj 188 | 189 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 190 | # checkin your Azure Web App publish settings, but sensitive information contained 191 | # in these scripts will be unencrypted 192 | PublishScripts/ 193 | 194 | # NuGet Packages 195 | *.nupkg 196 | # NuGet Symbol Packages 197 | *.snupkg 198 | # The packages folder can be ignored because of Package Restore 199 | **/[Pp]ackages/* 200 | # except build/, which is used as an MSBuild target. 201 | !**/[Pp]ackages/build/ 202 | # Uncomment if necessary however generally it will be regenerated when needed 203 | #!**/[Pp]ackages/repositories.config 204 | # NuGet v3's project.json files produces more ignorable files 205 | *.nuget.props 206 | *.nuget.targets 207 | 208 | # Microsoft Azure Build Output 209 | csx/ 210 | *.build.csdef 211 | 212 | # Microsoft Azure Emulator 213 | ecf/ 214 | rcf/ 215 | 216 | # Windows Store app package directories and files 217 | AppPackages/ 218 | BundleArtifacts/ 219 | Package.StoreAssociation.xml 220 | _pkginfo.txt 221 | *.appx 222 | *.appxbundle 223 | *.appxupload 224 | 225 | # Visual Studio cache files 226 | # files ending in .cache can be ignored 227 | *.[Cc]ache 228 | # but keep track of directories ending in .cache 229 | !?*.[Cc]ache/ 230 | 231 | # Others 232 | ClientBin/ 233 | ~$* 234 | *~ 235 | *.dbmdl 236 | *.dbproj.schemaview 237 | *.jfm 238 | *.pfx 239 | *.publishsettings 240 | orleans.codegen.cs 241 | 242 | # Including strong name files can present a security risk 243 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 244 | #*.snk 245 | 246 | # Since there are multiple workflows, uncomment next line to ignore bower_components 247 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 248 | #bower_components/ 249 | 250 | # RIA/Silverlight projects 251 | Generated_Code/ 252 | 253 | # Backup & report files from converting an old project file 254 | # to a newer Visual Studio version. Backup files are not needed, 255 | # because we have git ;-) 256 | _UpgradeReport_Files/ 257 | Backup*/ 258 | UpgradeLog*.XML 259 | UpgradeLog*.htm 260 | ServiceFabricBackup/ 261 | *.rptproj.bak 262 | 263 | # SQL Server files 264 | *.mdf 265 | *.ldf 266 | *.ndf 267 | 268 | # Business Intelligence projects 269 | *.rdl.data 270 | *.bim.layout 271 | *.bim_*.settings 272 | *.rptproj.rsuser 273 | *- [Bb]ackup.rdl 274 | *- [Bb]ackup ([0-9]).rdl 275 | *- [Bb]ackup ([0-9][0-9]).rdl 276 | 277 | # Microsoft Fakes 278 | FakesAssemblies/ 279 | 280 | # GhostDoc plugin setting file 281 | *.GhostDoc.xml 282 | 283 | # Node.js Tools for Visual Studio 284 | .ntvs_analysis.dat 285 | node_modules/ 286 | 287 | # Visual Studio 6 build log 288 | *.plg 289 | 290 | # Visual Studio 6 workspace options file 291 | *.opt 292 | 293 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 294 | *.vbw 295 | 296 | # Visual Studio LightSwitch build output 297 | **/*.HTMLClient/GeneratedArtifacts 298 | **/*.DesktopClient/GeneratedArtifacts 299 | **/*.DesktopClient/ModelManifest.xml 300 | **/*.Server/GeneratedArtifacts 301 | **/*.Server/ModelManifest.xml 302 | _Pvt_Extensions 303 | 304 | # Paket dependency manager 305 | .paket/paket.exe 306 | paket-files/ 307 | 308 | # FAKE - F# Make 309 | .fake/ 310 | 311 | # CodeRush personal settings 312 | .cr/personal 313 | 314 | # Python Tools for Visual Studio (PTVS) 315 | __pycache__/ 316 | *.pyc 317 | 318 | # Cake - Uncomment if you are using it 319 | # tools/** 320 | # !tools/packages.config 321 | 322 | # Tabs Studio 323 | *.tss 324 | 325 | # Telerik's JustMock configuration file 326 | *.jmconfig 327 | 328 | # BizTalk build output 329 | *.btp.cs 330 | *.btm.cs 331 | *.odx.cs 332 | *.xsd.cs 333 | 334 | # OpenCover UI analysis results 335 | OpenCover/ 336 | 337 | # Azure Stream Analytics local run output 338 | ASALocalRun/ 339 | 340 | # MSBuild Binary and Structured Log 341 | *.binlog 342 | 343 | # NVidia Nsight GPU debugger configuration file 344 | *.nvuser 345 | 346 | # MFractors (Xamarin productivity tool) working folder 347 | .mfractor/ 348 | 349 | # Local History for Visual Studio 350 | .localhistory/ 351 | 352 | # BeatPulse healthcheck temp database 353 | healthchecksdb 354 | 355 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 356 | MigrationBackup/ 357 | 358 | # Ionide (cross platform F# VS Code tools) working folder 359 | .ionide/ 360 | 361 | # Fody - auto-generated XML schema 362 | FodyWeavers.xsd 363 | 364 | ### Custom entries ### 365 | output/ 366 | tools/Modules 367 | -------------------------------------------------------------------------------- /ALCLoader/README.md: -------------------------------------------------------------------------------- 1 | # ALC Loader 2 | This is an example module that uses the `ALC Loader` setup. 3 | The `ALC Loader` example has two assemblies in the module: 4 | 5 | + `ALCLoader.dll` 6 | + Contains the cmdlets and any dep references inside the ALC 7 | + `ALCLoader.Shared.dll` 8 | + Shared code and where the ALC setup code is located 9 | 10 | Some pros and cons using this method over the [ALC Resolver](../ALCResolver/README.md) example are: 11 | 12 | |Pros|Cons| 13 | |-|-| 14 | |Assembly deps can be used by the cmdlet directly|Requires an internal method to support force importing a module a 2nd time| 15 | |No need for an OnImport or OnRemove to cleanup the ALC|Still requires a `.psm1` to load the assembly| 16 | |Can still share data type with the caller using the shared assembly|| 17 | |The exact dependency is loaded, it won't use any existing loaded assemblies|| 18 | 19 | ## Structure 20 | The module consists of 3 components: 21 | 22 | + PowerShell module [ALCLoader.psd1](./module/ALCLoader.psd1) and [ALCLoader.psm1](./module/ALCLoader.psm1)` 23 | + Shared Assembly util [ALCLoader.Shared](./src/ALCLoader.Shared/) 24 | + Binary module assembly [ALCLoader](./src/ALCLoader/) 25 | 26 | The names of the C# projects used here don't have to be exactly the same, the key part is the `ALCLoader.Shared` contains the `AssemblyLoadContext` setup code and `ALCLoader` contains the cmdlets and code that calls the deps which are contained in the ALC. 27 | 28 | When PowerShell loads `ALCLoader` it will run the code in [ALCLoader.psm1](./module/ALCLoader.psm1) which on PowerShell 7 will create the ALC. 29 | This is done by calling [ALCLoader.Shared.LoadContext](./src/ALCLoader.Shared/LoadContext.cs) to create the ALC, load our binary module, and return the loaded assembly inside that ALC. 30 | Finally the `psm1` will then load the assembly as normal and export the cmdlets within that assembly. 31 | -------------------------------------------------------------------------------- /ALCLoader/build.ps1: -------------------------------------------------------------------------------- 1 | . $PSScriptRoot/../common.ps1 2 | 3 | Invoke-ModuleBuild -Path $PSScriptRoot 4 | -------------------------------------------------------------------------------- /ALCLoader/module/ALCLoader.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | RootModule = 'ALCLoader.psm1' 3 | ModuleVersion = '1.0.0' 4 | GUID = '7b99120d-e9cc-4808-a607-871fd42d376c' 5 | Author = 'Jordan Borean' 6 | CompanyName = 'Community' 7 | Copyright = '(c) 2023 Jordan Borean. All rights reserved.' 8 | Description = 'ALC Loader example' 9 | PowerShellVersion = '5.1' 10 | DotNetFrameworkVersion = '4.7.2' 11 | TypesToProcess = @() 12 | FormatsToProcess = @() 13 | NestedModules = @() 14 | FunctionsToExport = @() 15 | CmdletsToExport = @( 16 | 'ConvertTo-NewtonsoftJson' 17 | 'ConvertTo-YamlDotNet' 18 | ) 19 | VariablesToExport = @() 20 | AliasesToExport = @() 21 | PrivateData = @{ 22 | PSData = @{} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ALCLoader/module/ALCLoader.psm1: -------------------------------------------------------------------------------- 1 | $importModule = Get-Command -Name Import-Module -Module Microsoft.PowerShell.Core 2 | $moduleName = [System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath) 3 | 4 | if (-not $IsCoreClr) { 5 | # PowerShell 5.1 has no concept of an Assembly Load Context so it will 6 | # just load the module assembly directly. 7 | 8 | # The type can be any type within our ALCLoader project 9 | $innerMod = if ('ALCLoader.ConvertToNewtonsoftJsonCommand' -as [type]) { 10 | $modAssembly = [ALCLoader.ConvertToNewtonsoftJsonCommand].Assembly 11 | &$importModule -Assembly $modAssembly -Force -PassThru 12 | } 13 | else { 14 | $modPath = [System.IO.Path]::Combine($PSScriptRoot, 'bin', 'net472', "$moduleName.dll") 15 | &$importModule -Name $modPath -ErrorAction Stop -PassThru 16 | } 17 | } 18 | else { 19 | # This is used to load the shared assembly in the Default ALC which then sets 20 | # an ALC for the module and any dependencies of that module to be loaded in 21 | # that ALC. 22 | 23 | $isReload = $true 24 | if (-not ('ALCLoader.Shared.LoadContext' -as [type])) { 25 | $isReload = $false 26 | 27 | Add-Type -Path ([System.IO.Path]::Combine($PSScriptRoot, 'bin', 'net5.0', "$moduleName.Shared.dll")) 28 | } 29 | 30 | $mainModule = [ALCLoader.Shared.LoadContext]::Initialize() 31 | $innerMod = &$importModule -Assembly $mainModule -PassThru:$isReload 32 | } 33 | 34 | if ($innerMod) { 35 | # Bug in pwsh, Import-Module in an assembly will pick up a cached instance 36 | # and not call the same path to set the nested module's cmdlets to the 37 | # current module scope. This is only technically needed if someone is 38 | # calling 'Import-Module -Name ALCLoader -Force' a second time. The first 39 | # import is still fine. 40 | # https://github.com/PowerShell/PowerShell/issues/20710 41 | $addExportedCmdlet = [System.Management.Automation.PSModuleInfo].GetMethod( 42 | 'AddExportedCmdlet', 43 | [System.Reflection.BindingFlags]'Instance, NonPublic' 44 | ) 45 | foreach ($cmd in $innerMod.ExportedCmdlets.Values) { 46 | $addExportedCmdlet.Invoke($ExecutionContext.SessionState.Module, @(, $cmd)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ALCLoader/src/ALCLoader.Shared/ALCLoader.Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | net472;net5.0 6 | 7 | 10.0 8 | 9 | enable 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /ALCLoader/src/ALCLoader.Shared/LoadContext.cs: -------------------------------------------------------------------------------- 1 | // AssemblyLoadContext won't work in net472 so we conditionally compile this 2 | // for net5.0 or greater. 3 | #if NET5_0_OR_GREATER 4 | using System.IO; 5 | using System.Reflection; 6 | using System.Runtime.CompilerServices; 7 | using System.Runtime.Loader; 8 | 9 | namespace ALCLoader.Shared; 10 | 11 | public class LoadContext : AssemblyLoadContext 12 | { 13 | private static LoadContext? _instance; 14 | private static object _sync = new object(); 15 | 16 | private Assembly _thisAssembly; 17 | private AssemblyName _thisAssemblyName; 18 | private Assembly _moduleAssembly; 19 | private string _assemblyDir; 20 | 21 | private LoadContext(string mainModulePathAssemblyPath) 22 | : base (name: "ALCLoader", isCollectible: false) 23 | { 24 | _assemblyDir = Path.GetDirectoryName(mainModulePathAssemblyPath) ?? ""; 25 | _thisAssembly = typeof(LoadContext).Assembly; 26 | _thisAssemblyName = _thisAssembly.GetName(); 27 | _moduleAssembly = LoadFromAssemblyPath(mainModulePathAssemblyPath); 28 | } 29 | 30 | protected override Assembly? Load(AssemblyName assemblyName) 31 | { 32 | // Checks to see if we are trying to access our current assembly 33 | // (ALCLoader.Shared). If so return the already loaded assembly object 34 | // as it provides a common interface between Pwsh and the ALC. 35 | if (AssemblyName.ReferenceMatchesDefinition(_thisAssemblyName, assemblyName)) 36 | { 37 | return _thisAssembly; 38 | } 39 | 40 | // Checks to see if the assembly exists in our path, if so load it in 41 | // the ALC. Otherwise fallback to the default loading behaviour. 42 | string asmPath = Path.Join(_assemblyDir, $"{assemblyName.Name}.dll"); 43 | if (File.Exists(asmPath)) 44 | { 45 | return LoadFromAssemblyPath(asmPath); 46 | } 47 | else 48 | { 49 | return null; 50 | } 51 | } 52 | 53 | public static Assembly Initialize() 54 | { 55 | LoadContext? instance = _instance; 56 | if (instance is not null) 57 | { 58 | return instance._moduleAssembly; 59 | } 60 | 61 | lock (_sync) 62 | { 63 | if (_instance is not null) 64 | { 65 | return _instance._moduleAssembly; 66 | } 67 | 68 | string assemblyPath = typeof(LoadContext).Assembly.Location; 69 | string assemblyName = Path.GetFileNameWithoutExtension(assemblyPath); 70 | 71 | // Removes the '.Shared' from the assembly name to refer to our main module. 72 | string moduleName = assemblyName.Substring(0, assemblyName.Length - 7); 73 | string modulePath = Path.Combine( 74 | Path.GetDirectoryName(assemblyPath)!, 75 | $"{moduleName}.dll" 76 | ); 77 | 78 | // Creates the ALC which loads our module in the ALC and returns 79 | // the loaded Assembly object for the psm1 to load. 80 | _instance = new LoadContext(modulePath); 81 | return _instance._moduleAssembly; 82 | } 83 | } 84 | } 85 | #endif 86 | -------------------------------------------------------------------------------- /ALCLoader/src/ALCLoader.Shared/SharedUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | #if NET5_0_OR_GREATER 5 | using System.Runtime.Loader; 6 | #endif 7 | 8 | namespace ALCLoader.Shared; 9 | 10 | internal class SharedUtil 11 | { 12 | public static void AddAssemblyInfo(Type type, Dictionary data) 13 | { 14 | Assembly asm = type.Assembly; 15 | 16 | data["Assembly"] = new Dictionary() 17 | { 18 | { "Name", asm.GetName().FullName }, 19 | #if NET5_0_OR_GREATER 20 | { "ALC", AssemblyLoadContext.GetLoadContext(asm)?.Name }, 21 | #endif 22 | { "Location", asm.Location } 23 | }; 24 | } 25 | } -------------------------------------------------------------------------------- /ALCLoader/src/ALCLoader/ALCLoader.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net472;net5.0 5 | 10.0 6 | true 7 | enable 8 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | 20 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /ALCLoader/src/ALCLoader/ConvertToNewtonsoftJsonCommand.cs: -------------------------------------------------------------------------------- 1 | using ALCLoader.Shared; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Management.Automation; 5 | // We can reference our ALC dependency directly 6 | using Newtonsoft.Json; 7 | 8 | namespace ALCLoader; 9 | 10 | [Cmdlet(VerbsData.ConvertTo, "NewtonsoftJson")] 11 | [OutputType(typeof(string))] 12 | public sealed class ConvertToNewtonsoftJsonCommand : PSCmdlet 13 | { 14 | private List _objs = new(); 15 | 16 | [Parameter( 17 | Mandatory = true, 18 | ValueFromPipeline = true, 19 | Position = 0 20 | )] 21 | public object[] InputObject { get; set; } = Array.Empty(); 22 | 23 | protected override void ProcessRecord() 24 | { 25 | foreach (object obj in InputObject) 26 | { 27 | _objs.Add(obj); 28 | } 29 | } 30 | 31 | protected override void EndProcessing() 32 | { 33 | Dictionary finalObj = new() 34 | { 35 | { 36 | "Object", _objs 37 | } 38 | }; 39 | SharedUtil.AddAssemblyInfo(typeof(JsonConvert), finalObj); 40 | 41 | string outString = JsonConvert.SerializeObject( 42 | finalObj, 43 | Formatting.Indented); 44 | WriteObject(outString); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ALCLoader/src/ALCLoader/ConvertToYamlDotNetCommand.cs: -------------------------------------------------------------------------------- 1 | using ALCLoader.Shared; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Management.Automation; 5 | // We can reference our ALC dependency directly 6 | using YamlDotNet.Serialization; 7 | 8 | namespace ALCLoader; 9 | 10 | [Cmdlet(VerbsData.ConvertTo, "YamlDotNet")] 11 | [OutputType(typeof(string))] 12 | public sealed class ConvertToYamlDotNetCommand : PSCmdlet 13 | { 14 | private List _objs = new(); 15 | 16 | [Parameter( 17 | Mandatory = true, 18 | ValueFromPipeline = true, 19 | Position = 0 20 | )] 21 | public object[] InputObject { get; set; } = Array.Empty(); 22 | 23 | protected override void ProcessRecord() 24 | { 25 | foreach (object obj in InputObject) 26 | { 27 | _objs.Add(obj); 28 | } 29 | } 30 | 31 | protected override void EndProcessing() 32 | { 33 | Dictionary finalObj = new() 34 | { 35 | { 36 | "Object", _objs 37 | } 38 | }; 39 | SharedUtil.AddAssemblyInfo(typeof(SerializerBuilder), finalObj); 40 | 41 | SerializerBuilder builder = new SerializerBuilder(); 42 | ISerializer serializer = builder.Build(); 43 | string outString = serializer.Serialize(finalObj); 44 | 45 | WriteObject(outString); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ALCPureScriptModule/README.md: -------------------------------------------------------------------------------- 1 | # ALC Pure ScriptModule 2 | This is an example module that shows how to use an ALC with a Script based module in PowerShell without any csproj or `dotnet publish` step. 3 | I would highly recommend using [ALCScriptModule](../ALCScriptModule/README.md) over this method as it greatly simplifies the build process and ALC setup. 4 | 5 | In this example module we have a `.psm1` that loads the dependencies `Newtonsoft.Json` and `YamlDotNet`. 6 | In Windows PowerShell (5.1) the dependencies will just be loaded directly while in PowerShell (7+) the dependencies will be loaded inside an ALC. 7 | 8 | Some pros and cons for a script module using an ALC over a binary one are: 9 | 10 | |Pros|Cons| 11 | |-|-| 12 | |No need to code in C#|Syntax to refer to ALC types is verbose and uncommon| 13 | |Can migrate only certain parts of the code to an ALC|Build process may be slightly more complicated| 14 | 15 | ## Structure 16 | The module consists of 2 parts: 17 | 18 | + [ALCPureScriptModule.psd1](./module/ALCPureScriptModule.psd1) 19 | + Module manifest, no difference from normal 20 | + [ALCPureScriptModule.psm1](./module/ALCPureScriptModule.psm1) 21 | + Module that sets up the ALC and defines the functions needed 22 | 23 | As with all script modules the module can either define the public/private functions in-line or load them from another file. 24 | How it chooses to do this is outside the scope of this example. 25 | 26 | The ALC logic is all contained in [ALCPureScriptModule.psm1](./module/ALCPureScriptModule.psm1) and consists of three main parts: 27 | 28 | + Setting up the ALC and loading the assemblies 29 | + PowerShell (7+) sets up the ALC and loads the dependencies inside it 30 | + Windows PowerShell (5.1) just loads the assemblies 31 | + Setting up the static type mapping for the module functions to reference 32 | + Defines the module functions. 33 | -------------------------------------------------------------------------------- /ALCPureScriptModule/build.ps1: -------------------------------------------------------------------------------- 1 | . $PSScriptRoot/../common.ps1 2 | 3 | Write-Host "Getting build information" 4 | $Build = Get-BuildInfo -Path $PSSCriptRoot 5 | 6 | if (-not (Test-Path -LiteralPath $Build.BuildDir)) { 7 | New-Item -Path $Build.BuildDir -ItemType Directory -Force | Out-Null 8 | } 9 | 10 | Write-Host "Downloading assemblies" 11 | $newtonsoftJson = Get-NugetAssembly -Name Newtonsoft.Json -Version 13.0.3 12 | $yamlDotNet = Get-NugetAssembly -Name YamlDotNet 13.7.1 13 | 14 | Write-Host "Build PowerShell module result" 15 | Copy-Item -Path ([Path]::Combine($Build.PowerShellSource, "*")) -Destination $Build.BuildDir -Recurse 16 | 17 | $binDir = [Path]::Combine($build.BuildDir, "bin") 18 | if (-not (Test-Path -LiteralPath $binDir)) { 19 | New-Item -Path $binDir -ItemType Directory | Out-Null 20 | } 21 | 22 | $net45Bin = [Path]::Combine($binDir, "net45") 23 | if (-not (Test-Path -LiteralPath $net45Bin)) { 24 | New-Item -Path $net45Bin -ItemType Directory | Out-Null 25 | } 26 | Copy-Item -Path ([Path]::Combine($newtonsoftJson, "net45", "*.dll")) -Destination $net45Bin 27 | Copy-Item -Path ([Path]::Combine($yamlDotNet, "net45", "*.dll")) -Destination $net45Bin 28 | 29 | $netstandard20Bin = [Path]::Combine($binDir, "netstandard2.0") 30 | if (-not (Test-Path -LiteralPath $netstandard20Bin)) { 31 | New-Item -Path $netstandard20Bin -ItemType Directory | Out-Null 32 | } 33 | Copy-Item -Path ([Path]::Combine($newtonsoftJson, "netstandard2.0", "*.dll")) -Destination $netstandard20Bin 34 | Copy-Item -Path ([Path]::Combine($yamlDotNet, "netstandard2.0", "*.dll")) -Destination $netstandard20Bin 35 | -------------------------------------------------------------------------------- /ALCPureScriptModule/module/ALCPureScriptModule.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | RootModule = 'ALCPureScriptModule.psm1' 3 | ModuleVersion = '1.0.0' 4 | GUID = 'e77b5490-ff5b-4d71-9c0c-e33d32ceb82b' 5 | Author = 'Jordan Borean' 6 | CompanyName = 'Community' 7 | Copyright = '(c) 2023 Jordan Borean. All rights reserved.' 8 | Description = 'ALC with Script Module' 9 | PowerShellVersion = '5.1' 10 | DotNetFrameworkVersion = '4.7.2' 11 | TypesToProcess = @() 12 | FormatsToProcess = @() 13 | NestedModules = @() 14 | FunctionsToExport = @( 15 | 'ConvertTo-NewtonsoftJson' 16 | 'ConvertTo-YamlDotNet' 17 | ) 18 | CmdletsToExport = @() 19 | VariablesToExport = @() 20 | AliasesToExport = @() 21 | PrivateData = @{ 22 | PSData = @{} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ALCPureScriptModule/module/ALCPureScriptModule.psm1: -------------------------------------------------------------------------------- 1 | if ($IsCoreCLR) { 2 | <# 3 | Loads the assembly using an ALC that is defined in the psm1 using Add-Type. 4 | A future improvement could try and define all this using a Linq Expression 5 | to avoid the compilation hit but that will make things harder to maintain. 6 | #> 7 | 8 | $binFolder = [System.IO.Path]::Combine($PSScriptRoot, "bin", "netstandard2.0") 9 | 10 | Add-Type -TypeDefinition @' 11 | using System; 12 | using System.IO; 13 | using System.Reflection; 14 | using System.Runtime.Loader; 15 | 16 | namespace ALCPureScriptModule 17 | { 18 | public class LoadContext : AssemblyLoadContext 19 | { 20 | private static LoadContext _instance; 21 | 22 | private readonly string _assemblyPath; 23 | 24 | private LoadContext(string assemblyPath) 25 | : base(name: "ALCPureScriptModule", isCollectible: false) 26 | { 27 | _assemblyPath = assemblyPath; 28 | } 29 | 30 | protected override Assembly Load(AssemblyName assemblyName) 31 | { 32 | string asmPath = null; 33 | if (IsOurAssembly(assemblyName, out asmPath)) 34 | { 35 | return LoadFromAssemblyPath(asmPath); 36 | } 37 | else 38 | { 39 | return null; 40 | } 41 | } 42 | 43 | internal Assembly ResolveAssembly( 44 | AssemblyLoadContext defaultAlc, 45 | AssemblyName assemblyName 46 | ) { 47 | string asmPath = null; 48 | if (IsOurAssembly(assemblyName, out asmPath)) 49 | { 50 | return LoadFromAssemblyName(assemblyName); 51 | } 52 | else 53 | { 54 | return null; 55 | } 56 | } 57 | 58 | private bool IsOurAssembly(AssemblyName name, out string assemblyToLoad) 59 | { 60 | string asmPath = Path.Join(_assemblyPath, $"{name.Name}.dll"); 61 | if (File.Exists(asmPath)) 62 | { 63 | assemblyToLoad = asmPath; 64 | return true; 65 | } 66 | else 67 | { 68 | assemblyToLoad = null; 69 | return false; 70 | } 71 | } 72 | 73 | public static LoadContext OnImport(string assemblyPath) 74 | { 75 | _instance = new LoadContext(assemblyPath); 76 | AssemblyLoadContext.Default.Resolving += _instance.ResolveAssembly; 77 | 78 | return _instance; 79 | } 80 | 81 | public static void OnRemove() 82 | { 83 | if (_instance != null) 84 | { 85 | AssemblyLoadContext.Default.Resolving -= _instance.ResolveAssembly; 86 | } 87 | } 88 | } 89 | } 90 | '@ 91 | 92 | # Setup the OnRemove method to remove our custom loader 93 | $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { 94 | [ALCPureScriptModule.LoadContext]::OnRemove() 95 | } 96 | 97 | # Create an instance of our ALC 98 | $loadContext = [ALCPureScriptModule.LoadContext]::OnImport($binFolder) 99 | 100 | # Load our desired deps into the ALC. Any deps of this library will be 101 | # resolved inside the ALC as well as long as the assembly is present. It is 102 | # important to load the assembly through the ALC method or else PowerShell 103 | # might attempt to use an existing assembly that has been loaded. 104 | $newtonAssemblyName = [System.Reflection.AssemblyName]::GetAssemblyName( 105 | [System.IO.Path]::Combine($binFolder, "Newtonsoft.Json.dll")) 106 | $NewtonsoftAssembly = $loadContext.LoadFromAssemblyName($newtonAssemblyName) 107 | 108 | $yamlAssemblyName = [System.Reflection.AssemblyName]::GetAssemblyName( 109 | [System.IO.Path]::Combine($binFolder, "YamlDotNet.dll")) 110 | $YamlDotNetAssembly = $loadContext.LoadFromAssemblyName($yamlAssemblyName) 111 | } 112 | else { 113 | <# 114 | For .NET Framework we just load as normal. There are three scenarios here: 115 | 116 | 1. The assembly is in the GAC 117 | 118 | The GAC version is always favoured, we cannot do anything about this 119 | 120 | 2. The assembly is already loaded elsewhere 121 | 122 | The already loaded assembly is used 123 | 124 | 3. The assembly is not loaded 125 | 126 | The AppDomain will load it using the filepath in the $assemblyName 127 | 128 | If the same assembly is loaded but at a different version that is 129 | incompatible with the one we are trying to load, .NET Framework will 130 | load our own assembly. 131 | #> 132 | $binFolder = [System.IO.Path]::Combine($PSScriptRoot, "bin", "net45") 133 | 134 | $newtonAssemblyName = [System.Reflection.AssemblyName]::GetAssemblyName( 135 | [System.IO.Path]::Combine($binFolder, "Newtonsoft.Json.dll")) 136 | $NewtonsoftAssembly = [System.Reflection.Assembly]::Load($newtonAssemblyName) 137 | 138 | $yamlAssemblyName = [System.Reflection.AssemblyName]::GetAssemblyName( 139 | [System.IO.Path]::Combine($binFolder, "YamlDotNet.dll")) 140 | $YamlDotNetAssembly = [System.Reflection.Assembly]::Load($yamlAssemblyName) 141 | } 142 | 143 | # As PowerShell cannot safely reference the type by name we need to save an 144 | # instance of the types we use in the module. We use a script scoped variable 145 | # as an easy way to reference it later. 146 | $script:Newtonsoft = @{ 147 | Json = @{ 148 | JsonConvert = $NewtonsoftAssembly.GetType("Newtonsoft.Json.JsonConvert") 149 | Formatting = $NewtonsoftAssembly.GetType("Newtonsoft.Json.Formatting") 150 | } 151 | } 152 | $script:YamlDotNet = @{ 153 | Serialization = @{ 154 | SerializerBuilder = $YamlDotNetAssembly.GetType("YamlDotNet.Serialization.SerializerBuilder") 155 | } 156 | } 157 | 158 | Function ConvertTo-NewtonsoftJson { 159 | [CmdletBinding()] 160 | param ( 161 | [Parameter(Mandatory, ValueFromPipeline)] 162 | [object[]] 163 | $InputObject 164 | ) 165 | 166 | begin { 167 | $objs = [System.Collections.Generic.List[object]]::new() 168 | } 169 | 170 | process { 171 | $InputObject | ForEach-Object { $objs.Add($_) } 172 | } 173 | 174 | end { 175 | $finalObj = [Ordered]@{ 176 | AssemblyInfo = [Ordered]@{ 177 | Name = $NewtonsoftAssembly.GetName().FullName 178 | Location = $NewtonsoftAssembly.Location 179 | } 180 | Object = $objs 181 | } 182 | 183 | <# 184 | When refering to the type loaded in our ALC we need to refer to the 185 | type from the assembly rather than use PowerShell to look it up. There 186 | are many ways of doing this, this is just one example. 187 | #> 188 | $Newtonsoft.Json.JsonConvert::SerializeObject( 189 | $finalObj, 190 | $Newtonsoft.Json.Formatting::Indented) 191 | } 192 | } 193 | 194 | Function ConvertTo-YamlDotNet { 195 | [CmdletBinding()] 196 | param ( 197 | [Parameter(Mandatory, ValueFromPipeline)] 198 | [object[]] 199 | $InputObject 200 | ) 201 | 202 | begin { 203 | $objs = [System.Collections.Generic.List[object]]::new() 204 | } 205 | 206 | process { 207 | $InputObject | ForEach-Object { $objs.Add($_) } 208 | } 209 | 210 | end { 211 | $finalObj = [Ordered]@{ 212 | AssemblyInfo = [Ordered]@{ 213 | Name = $YamlDotNetAssembly.GetName().FullName 214 | Location = $YamlDotNetAssembly.Location 215 | } 216 | Object = $objs 217 | } 218 | 219 | <# 220 | When refering to the type loaded in our ALC we need to refer to the 221 | type from the assembly rather than use PowerShell to look it up. There 222 | are many ways of doing this, this is just one example. 223 | #> 224 | $builder = $YamlDotNet.Serialization.SerializerBuilder::new() 225 | $serializer = $builder.Build() 226 | 227 | $serializer.Serialize($finalObj) 228 | } 229 | } 230 | 231 | Export-ModuleMember -Function ConvertTo-NewtonsoftJson, ConvertTo-YamlDotNet 232 | -------------------------------------------------------------------------------- /ALCResolver/README.md: -------------------------------------------------------------------------------- 1 | # ALC Resolver 2 | This is an example module that uses the `ALC Resolver` setup. 3 | The `ALC Resolver` example has two assemblies in the module: 4 | 5 | + `ALCResolver.dll` 6 | + Contains the cmdlets and ALC setup code 7 | + `ALCResolver.Private.dll` 8 | + Will be loaded into the ALC and contains the code calling the deps that should be loaded in the ALC 9 | 10 | Some pros and cons using this method over the [ALC Loader](../ALCLoader/README.md) example are: 11 | 12 | |Pros|Cons| 13 | |-|-| 14 | |No need for a `.psm1`|No guarantees deps will be loaded in the ALC, if dep is resolved before `OnModuleImportAndRemove` is called then it will not be loaded in the ALC| 15 | |Works find with second `Import-Module -Force`, no reflection needed|Need to explicitly structure cmdlets to call methods in the private assembly to use deps| 16 | 17 | Due to the edge case where a dep being loaded or the complexities of adding a shim assembly with this setup I would highly recommend avoiding it in favour of the [ALCLoader](../ALCLoader/README.md) example. 18 | `ALCLoader` will ensure all the module's dependencies are loaded in the ALC and the structure of the code is easier to deal with. 19 | 20 | ## Structure 21 | The module consists of 3 components: 22 | 23 | + PowerShell module [ALCResolver.psd1](./module/ALCResolver.psd1) and [ALCResolver.psm1](./module/ALCResolver.psm1)` 24 | + Binary module assembly [ALCResolver](./src/ALCResolver/) 25 | + ALC'd Assembly [ALCResolver.Private](./src/ALCResolver.Private/) 26 | 27 | The names of the C# projects used here don't have to be exactly the same, the key part is the `ALCResolver` contains the `OnModuleImportAndRemove` handler to setup the `ALC` and `ALCResolver.Private` contains deps that should be placed in the ALC. 28 | 29 | When PowerShell loads `ALCResolver` it will run the import the `ALCResolver` assembly and load any cmdlets inside that in the module. 30 | It will also find the [OnImportAndRemove](./src/ALCResolver/OnImportAndRemove.cs) instance and call the `OnImport()` method during the import. 31 | This method sets up the `ALC` and then loads `ALCResolver.Private` into that ALC. 32 | Any assemblies `ALCResolver.Private` needs during runtime will load the assembly from the bin directory of the module into the ALC. 33 | It is possible to just use the one assembly where it sets up the `OnImportAndRemove` logic with the ACL but this runs the risk of loading the dependency if it is resolved by the runtime before `OnImportAndRemove` is called. 34 | For example this may happen if you have a type that is a subclass of one in a dependency. 35 | -------------------------------------------------------------------------------- /ALCResolver/build.ps1: -------------------------------------------------------------------------------- 1 | . $PSScriptRoot/../common.ps1 2 | 3 | Invoke-ModuleBuild -Path $PSScriptRoot 4 | -------------------------------------------------------------------------------- /ALCResolver/module/ALCResolver.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | RootModule = if ($PSEdition -eq 'core') { 'bin/net5.0/ALCResolver.dll' } else { 'bin/net472/ALCResolver.dll' } 3 | ModuleVersion = '1.0.0' 4 | GUID = '31bb040b-32c8-4c89-a65e-876222ec94bc' 5 | Author = 'Jordan Borean' 6 | CompanyName = 'Community' 7 | Copyright = '(c) 2023 Jordan Borean. All rights reserved.' 8 | Description = 'ALC Resolver example' 9 | PowerShellVersion = '5.1' 10 | DotNetFrameworkVersion = '4.7.2' 11 | TypesToProcess = @() 12 | FormatsToProcess = @() 13 | NestedModules = @() 14 | FunctionsToExport = @() 15 | CmdletsToExport = @( 16 | 'ConvertTo-NewtonsoftJson' 17 | 'ConvertTo-YamlDotNet' 18 | ) 19 | VariablesToExport = @() 20 | AliasesToExport = @() 21 | PrivateData = @{ 22 | PSData = @{} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ALCResolver/src/ALCResolver.Private/ALCResolver.Private.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net472;net5.0 5 | 10.0 6 | enable 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /ALCResolver/src/ALCResolver.Private/Json.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | // We can reference our ALC dependency directly 4 | using Newtonsoft.Json; 5 | 6 | namespace ALCResolver.Private; 7 | 8 | internal static class Json 9 | { 10 | public static string ConvertToJson(Dictionary data) 11 | { 12 | SharedUtil.AddAssemblyInfo(typeof(JsonConvert), data); 13 | 14 | return JsonConvert.SerializeObject( 15 | data, 16 | Formatting.Indented); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ALCResolver/src/ALCResolver.Private/SharedUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | #if NET5_0_OR_GREATER 5 | using System.Runtime.Loader; 6 | #endif 7 | 8 | namespace ALCResolver.Private; 9 | 10 | internal class SharedUtil 11 | { 12 | public static void AddAssemblyInfo(Type type, Dictionary data) 13 | { 14 | Assembly asm = type.Assembly; 15 | 16 | data["Assembly"] = new Dictionary() 17 | { 18 | { "Name", asm.GetName().FullName }, 19 | #if NET5_0_OR_GREATER 20 | { "ALC", AssemblyLoadContext.GetLoadContext(asm)?.Name }, 21 | #endif 22 | { "Location", asm.Location } 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ALCResolver/src/ALCResolver.Private/Yaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | // We can reference our ALC dependency directly 4 | using YamlDotNet.Serialization; 5 | 6 | namespace ALCResolver.Private; 7 | 8 | internal static class Yaml 9 | { 10 | public static string ConvertToYaml(Dictionary data) 11 | { 12 | SharedUtil.AddAssemblyInfo(typeof(SerializerBuilder), data); 13 | 14 | SerializerBuilder builder = new SerializerBuilder(); 15 | ISerializer serializer = builder.Build(); 16 | return serializer.Serialize(data); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ALCResolver/src/ALCResolver/ALCResolver.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net472;net5.0 5 | 10.0 6 | true 7 | enable 8 | 9 | 10 | 11 | 16 | 17 | 18 | 19 | 20 | 25 | 26 | 27 | 28 | 29 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ALCResolver/src/ALCResolver/ConvertToNewtonsoftJsonCommand.cs: -------------------------------------------------------------------------------- 1 | using ALCResolver.Private; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Management.Automation; 5 | 6 | namespace ALCResolver; 7 | 8 | [Cmdlet(VerbsData.ConvertTo, "NewtonsoftJson")] 9 | [OutputType(typeof(string))] 10 | public sealed class ConvertToNewtonsoftJsonCommand : PSCmdlet 11 | { 12 | private List _objs = new(); 13 | 14 | [Parameter( 15 | Mandatory = true, 16 | ValueFromPipeline = true, 17 | Position = 0 18 | )] 19 | public object[] InputObject { get; set; } = Array.Empty(); 20 | 21 | protected override void ProcessRecord() 22 | { 23 | foreach (object obj in InputObject) 24 | { 25 | _objs.Add(obj); 26 | } 27 | } 28 | 29 | protected override void EndProcessing() 30 | { 31 | Dictionary finalObj = new() 32 | { 33 | { 34 | "Object", _objs 35 | } 36 | }; 37 | WriteObject(Json.ConvertToJson(finalObj)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ALCResolver/src/ALCResolver/ConvertToYamlDotNetCommand.cs: -------------------------------------------------------------------------------- 1 | using ALCResolver.Private; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Management.Automation; 5 | 6 | namespace ALCResolver; 7 | 8 | [Cmdlet(VerbsData.ConvertTo, "YamlDotNet")] 9 | [OutputType(typeof(string))] 10 | public sealed class ConvertToYamlDotNetCommand : PSCmdlet 11 | { 12 | private List _objs = new(); 13 | 14 | [Parameter( 15 | Mandatory = true, 16 | ValueFromPipeline = true, 17 | Position = 0 18 | )] 19 | public object[] InputObject { get; set; } = Array.Empty(); 20 | 21 | protected override void ProcessRecord() 22 | { 23 | foreach (object obj in InputObject) 24 | { 25 | _objs.Add(obj); 26 | } 27 | } 28 | 29 | protected override void EndProcessing() 30 | { 31 | Dictionary finalObj = new() 32 | { 33 | { 34 | "Object", _objs 35 | } 36 | }; 37 | WriteObject(Yaml.ConvertToYaml(finalObj)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ALCResolver/src/ALCResolver/OnImportAndRemove.cs: -------------------------------------------------------------------------------- 1 | // We only need to configure the ALC for PSv7+. 2 | #if NET5_0_OR_GREATER 3 | using System; 4 | using System.IO; 5 | using System.Management.Automation; 6 | using System.Reflection; 7 | using System.Runtime.Loader; 8 | 9 | namespace ALCResolver; 10 | 11 | internal class LoadContext : AssemblyLoadContext 12 | { 13 | private readonly string _assemblyDir; 14 | 15 | public LoadContext(string assemblyDir) 16 | : base (name: "ALCResolver", isCollectible: false) 17 | { 18 | _assemblyDir = assemblyDir; 19 | } 20 | 21 | protected override Assembly? Load(AssemblyName assemblyName) 22 | { 23 | string asmPath = Path.Join(_assemblyDir, $"{assemblyName.Name}.dll"); 24 | if (File.Exists(asmPath)) 25 | { 26 | return LoadFromAssemblyPath(asmPath); 27 | } 28 | else 29 | { 30 | return null; 31 | } 32 | } 33 | } 34 | 35 | public class OnModuleImportAndRemove : IModuleAssemblyInitializer, IModuleAssemblyCleanup 36 | { 37 | private static readonly string _assemblyDir = Path.GetDirectoryName( 38 | typeof(OnModuleImportAndRemove).Assembly.Location)!; 39 | 40 | private static readonly LoadContext _alc = new LoadContext(_assemblyDir); 41 | 42 | public void OnImport() 43 | { 44 | AssemblyLoadContext.Default.Resolving += ResolveAlc; 45 | } 46 | 47 | public void OnRemove(PSModuleInfo module) 48 | { 49 | AssemblyLoadContext.Default.Resolving -= ResolveAlc; 50 | } 51 | 52 | private static Assembly? ResolveAlc(AssemblyLoadContext defaultAlc, AssemblyName assemblyToResolve) 53 | { 54 | string asmPath = Path.Join(_assemblyDir, $"{assemblyToResolve.Name}.dll"); 55 | if (IsSatisfyingAssembly(assemblyToResolve, asmPath)) 56 | { 57 | return _alc.LoadFromAssemblyName(assemblyToResolve); 58 | } 59 | else 60 | { 61 | return null; 62 | } 63 | } 64 | 65 | private static bool IsSatisfyingAssembly(AssemblyName requiredAssemblyName, string assemblyPath) 66 | { 67 | if (requiredAssemblyName.Name == "ALCResolver" || !File.Exists(assemblyPath)) 68 | { 69 | return false; 70 | } 71 | 72 | AssemblyName asmToLoadName = AssemblyName.GetAssemblyName(assemblyPath); 73 | 74 | return string.Equals(asmToLoadName.Name, requiredAssemblyName.Name, StringComparison.OrdinalIgnoreCase) 75 | && asmToLoadName.Version >= requiredAssemblyName.Version; 76 | } 77 | } 78 | #endif -------------------------------------------------------------------------------- /ALCScriptLoadContext/README.md: -------------------------------------------------------------------------------- 1 | # ALC ScriptModule LoadContext 2 | This is similar to [ALCPureScriptModule](../ALCPureScriptModule/README.md) but shows how an assembly and its dependencies can be loaded in an Assembly Load Context and used in a script module. 3 | While it does have a `csproj` this is only used to download the dependencies for the module, it is a completly optional step if downloading them another way. 4 | 5 | It can be further expanded to put any common helper code in the C# project like callbacks or anything else that might need access to the assemblies that are being depended on. 6 | This might make things clearer or easier to do if you are comfortable doing it in C#. 7 | 8 | This approach has some benefits over the other ScriptModule examples as: 9 | 10 | + All dependencies of the assembly is loaded into the ALC, no sharing with whatever PowerShell might have already loaded 11 | + It can be easily expanded with your own custom C# code if needed (callbacks/helper code to simplify things) 12 | 13 | Some downsides: 14 | 15 | + The PowerShell cannot refer to the types in the assemblies in the ALC - they need to be retrieved through the assembly (see `$ALCTypes`) 16 | + It uses an inline `Add-Type` which can be problematic with some AVs and adds some slight import delays 17 | + This can certainly be built into another assembly to save runtime or to share it with other modules 18 | 19 | In this example module we have a `.psm1` that loads the dependencies `Microsoft.Identity.Client` and its dependencies. 20 | In Windows PowerShell (5.1) the dependencies will just be loaded directly while in PowerShell (7+) the dependencies will be loaded inside an ALC. 21 | 22 | You can run the following code after running the `Get-MsalToken` function to see how the `Microsoft.Identity.Client` assembly is loaded into the ALC 23 | 24 | ```powershell 25 | [System.AppDomain]::CurrentDomain.GetAssemblies() | 26 | ForEach-Object { 27 | $alc = [Runtime.Loader.AssemblyLoadContext]::GetLoadContext($_) 28 | [PSCustomObject]@{ 29 | Name = $_.GetName().Name 30 | ALC = $alc.Name 31 | Location = $_.Location 32 | } 33 | } | Sort-Object Name 34 | ``` 35 | 36 | ## Structure 37 | The module consists of 4 parts: 38 | 39 | + [ALCScriptLoadContext.psd1](./module/ALCScriptLoadContext.psd1) 40 | + Module manifest, no difference from normal 41 | + [ALCScriptLoadContext.psm1](./module/ALCScriptLoadContext.psm1) 42 | + Module that sets up the ALC and defines the functions needed 43 | + [ALCScriptLoadContext.csproj](./src/ALCScriptLoadContext/ALCScriptLoadContext.csproj) 44 | + Our C# project file, this is completely optional and is used to gather the assembly dlls and its dependencies 45 | 46 | As with all script modules the module can either define the public/private functions in-line or load them from another file. 47 | How it chooses to do this is outside the scope of this example. 48 | 49 | The ALC logic is all contained in [ALCScriptLoadContext.psm1](./module/ALCScriptLoadContext.psm1) and consists of three main parts: 50 | 51 | + Setting up the ALC and loading the assemblies 52 | + PowerShell (7+) sets up the ALC and loads the dependencies inside it 53 | + Windows PowerShell (5.1) just loads the assemblies 54 | + Setting up the static type mapping for the module functions to reference 55 | + Defines the module functions. 56 | -------------------------------------------------------------------------------- /ALCScriptLoadContext/build.ps1: -------------------------------------------------------------------------------- 1 | . $PSScriptRoot/../common.ps1 2 | 3 | Invoke-ModuleBuild -Path $PSScriptRoot 4 | -------------------------------------------------------------------------------- /ALCScriptLoadContext/module/ALCScriptLoadContext.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | RootModule = 'ALCScriptLoadContext.psm1' 3 | ModuleVersion = '1.0.0' 4 | GUID = '911143c1-f652-458c-af65-bcf3b8c64183' 5 | Author = 'Jordan Borean' 6 | CompanyName = 'Community' 7 | Copyright = '(c) 2024 Jordan Borean. All rights reserved.' 8 | Description = 'ALC with Script Module that uses the LoadContext directly' 9 | PowerShellVersion = '5.1' 10 | DotNetFrameworkVersion = '4.7.2' 11 | TypesToProcess = @() 12 | FormatsToProcess = @() 13 | NestedModules = @() 14 | FunctionsToExport = @( 15 | 'Get-MsalToken' 16 | ) 17 | CmdletsToExport = @() 18 | VariablesToExport = @() 19 | AliasesToExport = @() 20 | PrivateData = @{ 21 | PSData = @{} 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ALCScriptLoadContext/module/ALCScriptLoadContext.psm1: -------------------------------------------------------------------------------- 1 | if ($IsCoreCLR) { 2 | # As the Assembly Load code is run in a separate thread we need to use 3 | # Add-Type to define the class. This class simply creates the 4 | # AssemblyLoadContext for our module and has it load any assemblies into 5 | # the ALC and not the main app domain. 6 | Add-Type -TypeDefinition @' 7 | #nullable enable 8 | using System.IO; 9 | using System.Reflection; 10 | using System.Runtime.CompilerServices; 11 | using System.Runtime.Loader; 12 | 13 | namespace MyModuleAlc; 14 | 15 | public class LoadContext : AssemblyLoadContext 16 | { 17 | private readonly string _assemblyDir; 18 | 19 | public LoadContext(string alcName, string assemblyDir) 20 | : base (name: alcName, isCollectible: false) 21 | { 22 | _assemblyDir = assemblyDir; 23 | } 24 | 25 | protected override Assembly? Load(AssemblyName assemblyName) 26 | { 27 | // Checks to see if the assembly exists in our path, if so load it in 28 | // the ALC. Otherwise fallback to the default loading behaviour. 29 | string asmPath = Path.Join(_assemblyDir, $"{assemblyName.Name}.dll"); 30 | if (File.Exists(asmPath)) 31 | { 32 | return LoadFromAssemblyPath(asmPath); 33 | } 34 | else 35 | { 36 | return null; 37 | } 38 | } 39 | } 40 | '@ 41 | 42 | # This example uses netstandard2.0 as the target framework deps, this can 43 | # be set to whatever is relevant for your module setup. 44 | $binDir = [System.IO.Path]::Combine($PSSCriptRoot, "bin", "netstandard2.0") 45 | 46 | # Create an instance of the AssemblyLoadContext. We call it 'MyAlc' but you 47 | # cal call it whatever you want as long as it's unique (use your module 48 | # name). We also provide the assembly directory to find assemblies from, 49 | # this allows the ALC to find the dependencies and use that. 50 | $loadContext = [MyModuleAlc.LoadContext]::new("MyAlc", $binDir) 51 | 52 | # We use LoadFromAssemblyPath to load our direct dependency and get the 53 | # Assembly object back. From this object we can get the types used in our 54 | # module (see after the else branch). 55 | $miClientAssembly = $loadContext.LoadFromAssemblyPath((Join-Path $binDir "Microsoft.Identity.Client.dll")) 56 | } 57 | else { 58 | # This is code run on WinPS (5.1), the bin dir is the same because this 59 | # example uses netstandard2.0, this will be different if you use separate 60 | # frameworks for your assemblies. 61 | $binDir = [System.IO.Path]::Combine($PSSCriptRoot, "bin", "netstandard2.0") 62 | 63 | # There is no AssemblyLoadContext on .NET Framework so we just load 64 | # directly and hope for the best. You can of course add more complex logic 65 | # if you wish but I recommend just leaving as is and tell people to use 66 | # pwsh 7+ or run inside a job to avoid conflicts. 67 | Add-Type -LiteralPath (Join-Path $binDir "Microsoft.Identity.Client.dll") 68 | 69 | # As the type is loaded directly we can reference it like normal. We save 70 | # the assembly only for this dll for the next step. 71 | $miClientAssembly = [Microsoft.Identity.Client.ConfidentialClientApplicationOptions].Assembly 72 | } 73 | 74 | # We store the types in our assembly in a hashtable to make it easier to 75 | # retrieve after. This example just stores it under the full type name but you 76 | # can add whatever logic to shorten or alias the name. Any types that were not 77 | # found will be raised as an error later on. 78 | $script:ALCTypes = @{} 79 | $unknownTypes = foreach ($typeName in @( 80 | 'Microsoft.Identity.Client.ConfidentialClientApplicationOptions' 81 | 'Microsoft.Identity.Client.ConfidentialClientApplicationBuilder' 82 | )) { 83 | 84 | # GetType returns $null if the Assembly.GetType(string name) can't find 85 | # the assembly. We output that into $unknownTypes for erroring later. 86 | # Otherwise we add it to our hashtable for referencing later in our module. 87 | $foundType = $miClientAssembly.GetType($typeName) 88 | if ($foundType) { 89 | $ALCTypes[$typeName] = $foundType 90 | } 91 | else { 92 | $typeName 93 | } 94 | } 95 | if ($unknownTypes) { 96 | $msg = "Failed to find the following types in Microsoft.Identity.Client: '$($unknownTypes -join "', '")'" 97 | Write-Error -Message $msg -ErrorAction Stop 98 | } 99 | 100 | Function Get-MsalToken { 101 | [CmdletBinding()] 102 | param( 103 | [Parameter(Mandatory)][string]$ClientID, 104 | [Parameter(Mandatory)][string]$TenantID, 105 | [Parameter(Mandatory)][string]$ClientSecret 106 | ) 107 | 108 | # We cannot refer to the type using the normal [TypeName] syntax as it's 109 | # loaded in the ALC and PowerShell doesn't know about it. Use our $ALCTypes 110 | # hashtable which stores the type object under the type name instead. The 111 | # Type object acts the same way as [TypeName], it's just retrieved 112 | # differently. 113 | $Options = $ALCTypes['Microsoft.Identity.Client.ConfidentialClientApplicationOptions']::new() 114 | $Options.ClientId = $ClientID 115 | $Options.TenantId = $TenantID 116 | $Options.ClientSecret = $ClientSecret 117 | 118 | # Same deal here, we need to get the type from the $ALCTypes hashtable. 119 | $authApp = $ALCTypes['Microsoft.Identity.Client.ConfidentialClientApplicationBuilder']::CreateWithApplicationOptions( 120 | $Options).Build() 121 | 122 | # From here the code is as per usual. 123 | $authTask = $authApp.AcquireTokenForClient( 124 | [string[]]@('https://graph.microsoft.com/.default')).ExecuteAsync([System.Threading.CancellationToken]::None) 125 | while (-not $authTask.AsyncWaitHandle.WaitOne(200)) {} 126 | $authTask.GetAwaiter().GetResult() 127 | } 128 | -------------------------------------------------------------------------------- /ALCScriptLoadContext/src/ALCScriptLoadContext/ALCScriptLoadContext.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ALCScriptModule/README.md: -------------------------------------------------------------------------------- 1 | # ALC ScriptModule 2 | This is an example module that shows how to use an ALC with a Script based module in PowerShell when combined with a `.csroj`. 3 | While this does require `dotnet` to publish the assembly it is beneficial over [ALCPureScriptModule](../ALCPureScriptModule/README.md) for a few reasons: 4 | 5 | + No need to manually download the assembly dependencies, dotnet will do this for you 6 | + The complex ALC code is moved out of the `psm1` into the C# assembly generated 7 | + Importing the module no longer has to compile the ALC code, it will be quicker this way 8 | + The `psm1` is simplified by moving the complex code out 9 | + It is now possible to re-use the binary assembly to embed other C# code your module may need 10 | 11 | The core module is still written in PowerShell so it will still have the same disadvantages as `ALCPureScriptModule` when it comes to referencing the ALC type. 12 | In this example module we have a `.psm1` that loads the dependencies `Newtonsoft.Json` and `YamlDotNet`. 13 | In Windows PowerShell (5.1) the dependencies will just be loaded directly while in PowerShell (7+) the dependencies will be loaded inside an ALC. 14 | 15 | Some pros and cons for a script module using an ALC over a binary one are: 16 | 17 | |Pros|Cons| 18 | |-|-| 19 | |No need to code in C#|Syntax to refer to ALC types is verbose and uncommon| 20 | |Can migrate only certain parts of the code to an ALC|Build process may be slightly more complicated| 21 | 22 | ## Structure 23 | The module consists of 4 parts: 24 | 25 | + [ALCScriptModule.psd1](./module/ALCScriptModule.psd1) 26 | + Module manifest, no difference from normal 27 | + [ALCScriptModule.psm1](./module/ALCScriptModule.psm1) 28 | + Module that sets up the ALC and defines the functions needed 29 | + [ALCScriptModule.csproj](./src/ALCScriptModule/ALCScriptModule.csproj) 30 | + Our C# project file, defines the frameworks we want to work with 31 | + [LoadContext.cs](./src/ALCScriptModule/LoadContext.cs) 32 | + Our `AssemblyLoadContext` implementation that will be used in the psm1 33 | 34 | As with all script modules the module can either define the public/private functions in-line or load them from another file. 35 | How it chooses to do this is outside the scope of this example. 36 | 37 | The ALC logic is all contained in [ALCScriptModule.psm1](./module/ALCScriptModule.psm1) and consists of three main parts: 38 | 39 | + Setting up the ALC and loading the assemblies 40 | + PowerShell (7+) sets up the ALC and loads the dependencies inside it 41 | + Windows PowerShell (5.1) just loads the assemblies 42 | + Setting up the static type mapping for the module functions to reference 43 | + Defines the module functions. 44 | -------------------------------------------------------------------------------- /ALCScriptModule/build.ps1: -------------------------------------------------------------------------------- 1 | . $PSScriptRoot/../common.ps1 2 | 3 | Invoke-ModuleBuild -Path $PSScriptRoot 4 | -------------------------------------------------------------------------------- /ALCScriptModule/module/ALCScriptModule.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | RootModule = 'ALCScriptModule.psm1' 3 | ModuleVersion = '1.0.0' 4 | GUID = 'c54ad6bd-2e14-4768-9d80-6f8f2c28cd30' 5 | Author = 'Jordan Borean' 6 | CompanyName = 'Community' 7 | Copyright = '(c) 2023 Jordan Borean. All rights reserved.' 8 | Description = 'ALC with Script Module' 9 | PowerShellVersion = '5.1' 10 | DotNetFrameworkVersion = '4.7.2' 11 | TypesToProcess = @() 12 | FormatsToProcess = @() 13 | NestedModules = @() 14 | FunctionsToExport = @( 15 | 'ConvertTo-NewtonsoftJson' 16 | 'ConvertTo-YamlDotNet' 17 | ) 18 | CmdletsToExport = @() 19 | VariablesToExport = @() 20 | AliasesToExport = @() 21 | PrivateData = @{ 22 | PSData = @{} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ALCScriptModule/module/ALCScriptModule.psm1: -------------------------------------------------------------------------------- 1 | if ($IsCoreCLR) { 2 | <# 3 | This shows how the LoadContext.cs code can be defined in the psm1 which 4 | removes the need to compile the code. I would not recommend this as it 5 | increases the import time and complicates the code here. 6 | #> 7 | 8 | $binFolder = [System.IO.Path]::Combine($PSScriptRoot, "bin", "net5.0") 9 | $moduleName = [System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath) 10 | 11 | # Loading the ALC setup code through an assembly is the 12 | # recommended route. There's no import hit to compiling the code 13 | # and the psm1 is cleaner. 14 | if (-not ('ALCScriptModule.LoadContext' -as [type])) { 15 | $alcAssembly = [System.IO.Path]::Combine($binFolder, "$moduleName.dll") 16 | Add-Type -Path $alcAssembly 17 | } 18 | 19 | # Setup the OnRemove method to remove our custom loader 20 | $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { 21 | [ALCScriptModule.LoadContext]::OnRemove() 22 | } 23 | 24 | # Create an instance of our ALC defined in our C# assembly 25 | $loadContext = [ALCScriptModule.LoadContext]::OnImport() 26 | 27 | # Load our desired deps into the ALC. Any deps of this library will be 28 | # resolved inside the ALC as well as long as the assembly is present. It is 29 | # important to load the assembly through the ALC method or else PowerShell 30 | # might attempt to use an existing assembly that has been loaded. 31 | $newtonAssemblyName = [System.Reflection.AssemblyName]::GetAssemblyName( 32 | [System.IO.Path]::Combine($binFolder, "Newtonsoft.Json.dll")) 33 | $NewtonsoftAssembly = $loadContext.LoadFromAssemblyName($newtonAssemblyName) 34 | 35 | $yamlAssemblyName = [System.Reflection.AssemblyName]::GetAssemblyName( 36 | [System.IO.Path]::Combine($binFolder, "YamlDotNet.dll")) 37 | $YamlDotNetAssembly = $loadContext.LoadFromAssemblyName($yamlAssemblyName) 38 | } 39 | else { 40 | <# 41 | For .NET Framework we just load as normal. There are three scenarios here: 42 | 43 | 1. The assembly is in the GAC 44 | 45 | The GAC version is always favoured, we cannot do anything about this 46 | 47 | 2. The assembly is already loaded elsewhere 48 | 49 | The already loaded assembly is used 50 | 51 | 3. The assembly is not loaded 52 | 53 | The AppDomain will load it using the filepath in the $assemblyName 54 | 55 | If the same assembly is loaded but at a different version that is 56 | incompatible with the one we are trying to load, .NET Framework will 57 | load our own assembly. 58 | #> 59 | $binFolder = [System.IO.Path]::Combine($PSScriptRoot, "bin", "net472") 60 | 61 | $newtonAssemblyName = [System.Reflection.AssemblyName]::GetAssemblyName( 62 | [System.IO.Path]::Combine($binFolder, "Newtonsoft.Json.dll")) 63 | $NewtonsoftAssembly = [System.Reflection.Assembly]::Load($newtonAssemblyName) 64 | 65 | $yamlAssemblyName = [System.Reflection.AssemblyName]::GetAssemblyName( 66 | [System.IO.Path]::Combine($binFolder, "YamlDotNet.dll")) 67 | $YamlDotNetAssembly = [System.Reflection.Assembly]::Load($yamlAssemblyName) 68 | } 69 | 70 | # As PowerShell cannot safely reference the type by name we need to save an 71 | # instance of the types we use in the module. We use a script scoped variable 72 | # as an easy way to reference it later. 73 | $script:Newtonsoft = @{ 74 | Json = @{ 75 | JsonConvert = $NewtonsoftAssembly.GetType("Newtonsoft.Json.JsonConvert") 76 | Formatting = $NewtonsoftAssembly.GetType("Newtonsoft.Json.Formatting") 77 | } 78 | } 79 | $script:YamlDotNet = @{ 80 | Serialization = @{ 81 | SerializerBuilder = $YamlDotNetAssembly.GetType("YamlDotNet.Serialization.SerializerBuilder") 82 | } 83 | } 84 | 85 | Function ConvertTo-NewtonsoftJson { 86 | [CmdletBinding()] 87 | param ( 88 | [Parameter(Mandatory, ValueFromPipeline)] 89 | [object[]] 90 | $InputObject 91 | ) 92 | 93 | begin { 94 | $objs = [System.Collections.Generic.List[object]]::new() 95 | } 96 | 97 | process { 98 | $InputObject | ForEach-Object { $objs.Add($_) } 99 | } 100 | 101 | end { 102 | $finalObj = [Ordered]@{ 103 | AssemblyInfo = [Ordered]@{ 104 | Name = $NewtonsoftAssembly.GetName().FullName 105 | Location = $NewtonsoftAssembly.Location 106 | } 107 | Object = $objs 108 | } 109 | 110 | <# 111 | When refering to the type loaded in our ALC we need to refer to the 112 | type from the assembly rather than use PowerShell to look it up. There 113 | are many ways of doing this, this is just one example. 114 | #> 115 | $Newtonsoft.Json.JsonConvert::SerializeObject( 116 | $finalObj, 117 | $Newtonsoft.Json.Formatting::Indented) 118 | } 119 | } 120 | 121 | Function ConvertTo-YamlDotNet { 122 | [CmdletBinding()] 123 | param ( 124 | [Parameter(Mandatory, ValueFromPipeline)] 125 | [object[]] 126 | $InputObject 127 | ) 128 | 129 | begin { 130 | $objs = [System.Collections.Generic.List[object]]::new() 131 | } 132 | 133 | process { 134 | $InputObject | ForEach-Object { $objs.Add($_) } 135 | } 136 | 137 | end { 138 | $finalObj = [Ordered]@{ 139 | AssemblyInfo = [Ordered]@{ 140 | Name = $YamlDotNetAssembly.GetName().FullName 141 | Location = $YamlDotNetAssembly.Location 142 | } 143 | Object = $objs 144 | } 145 | 146 | <# 147 | When refering to the type loaded in our ALC we need to refer to the 148 | type from the assembly rather than use PowerShell to look it up. There 149 | are many ways of doing this, this is just one example. 150 | #> 151 | $builder = $YamlDotNet.Serialization.SerializerBuilder::new() 152 | $serializer = $builder.Build() 153 | 154 | $serializer.Serialize($finalObj) 155 | } 156 | } 157 | 158 | Export-ModuleMember -Function ConvertTo-NewtonsoftJson, ConvertTo-YamlDotNet 159 | -------------------------------------------------------------------------------- /ALCScriptModule/src/ALCScriptModule/ALCScriptModule.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net472;net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /ALCScriptModule/src/ALCScriptModule/LoadContext.cs: -------------------------------------------------------------------------------- 1 | #if NET5_0_OR_GREATER 2 | using System; 3 | using System.IO; 4 | using System.Reflection; 5 | using System.Runtime.Loader; 6 | 7 | namespace ALCScriptModule 8 | { 9 | public class LoadContext : AssemblyLoadContext 10 | { 11 | private static LoadContext _instance; 12 | 13 | private readonly string _assemblyPath; 14 | 15 | private LoadContext(string assemblyPath) 16 | : base(name: "ALCScriptModule", isCollectible: false) 17 | { 18 | _assemblyPath = assemblyPath; 19 | } 20 | 21 | protected override Assembly Load(AssemblyName assemblyName) 22 | { 23 | string asmPath = null; 24 | if (IsOurAssembly(assemblyName, out asmPath)) 25 | { 26 | return LoadFromAssemblyPath(asmPath); 27 | } 28 | else 29 | { 30 | return null; 31 | } 32 | } 33 | 34 | internal Assembly ResolveAssembly( 35 | AssemblyLoadContext defaultAlc, 36 | AssemblyName assemblyName 37 | ) 38 | { 39 | string asmPath = null; 40 | if (IsOurAssembly(assemblyName, out asmPath)) 41 | { 42 | return LoadFromAssemblyName(assemblyName); 43 | } 44 | else 45 | { 46 | return null; 47 | } 48 | } 49 | 50 | private bool IsOurAssembly(AssemblyName name, out string assemblyToLoad) 51 | { 52 | string asmPath = Path.Join(_assemblyPath, $"{name.Name}.dll"); 53 | if (File.Exists(asmPath)) 54 | { 55 | assemblyToLoad = asmPath; 56 | return true; 57 | } 58 | else 59 | { 60 | assemblyToLoad = null; 61 | return false; 62 | } 63 | } 64 | 65 | public static LoadContext OnImport() 66 | { 67 | string assemblyPath = typeof(LoadContext).Assembly.Location; 68 | _instance = new LoadContext(Path.GetDirectoryName(assemblyPath)); 69 | AssemblyLoadContext.Default.Resolving += _instance.ResolveAssembly; 70 | 71 | return _instance; 72 | } 73 | 74 | public static void OnRemove() 75 | { 76 | if (_instance != null) 77 | { 78 | AssemblyLoadContext.Default.Resolving -= _instance.ResolveAssembly; 79 | } 80 | } 81 | } 82 | } 83 | #endif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerShell Assembly Load Contexts 2 | This repo is designed to go through the various ways to use an Assembly Load Context (`ALC`) in a PowerShell module. 3 | An ALC is a new mechanism introduced with .NET 5 that provides a way to load multiple versions of the same assembly into the same process. 4 | It can be used in PowerShell to build a module with a dependency that might conflict with something provided by PowerShell or another module that has already been imported. 5 | 6 | I found that the guides online didn't cover all the mechanisms or showed real working examples of it all setup. 7 | I would like to call out the following links which do cover a lot of the ground here and are great fundamentals for understanding the concept behind an ALC: 8 | 9 | + https://learn.microsoft.com/en-us/powershell/scripting/dev-cross-plat/resolving-dependency-conflicts?view=powershell-7.4 10 | + https://pipe.how/get-assemblyloadcontext/ 11 | 12 | There are two ways I know how to use an ALC in PowerShell modules: 13 | 14 | + [ALC Loader](./ALCLoader/README.md) - loads our binary module in a ALC directly 15 | + [ALC Resolver](./ALCLoader/README.md) - loads our binary module normally and the deps with an ALC resolver 16 | 17 | These are not official names but how I will refer to these methods going forward. 18 | I personally recommend the [ALC Loader](./ALCLoader/README.md) method when starting a new project as: 19 | 20 | + It ensures all deps are placed in the ALC and nothing is missed 21 | + Cmdlets can interact directly with the deps without a wrapper assembly making the code simpler 22 | 23 | An ALC typically needs to be used as a binary module but it is technically possible to use it in a pure PowerShell script module. 24 | See [ALC ScriptModule](./ALCScriptModule/README.md), [ALC Pure ScriptModule](./ALCPureScriptModule/README.md), or [ALC Script LoadContext](./ALCScriptLoadContext/README.md) for more details on this approach. 25 | 26 | ## Testing 27 | The `./test.ps1` script can be used to build an example module and run through some basic scenarios. 28 | 29 | ```powershell 30 | pwsh.exe -File ./test.ps1 -Name ALCLoader 31 | pwsh.exe -File ./test.ps1 -Name ALCResolver 32 | pwsh.exe -File ./test.ps1 -Name ALCScriptModule 33 | pwsh.exe -File ./test.ps1 -Name ALCPureScriptModule 34 | ``` 35 | These scenarios are all the same in each example and show how the assemblies are loaded and that the code actually works. 36 | Please note the `GAC` tests require you to be running as admin. 37 | The tests go through 6 different scenarios: 38 | 39 | |Scenario|Outcome| 40 | |-|-| 41 | |WinPS|Loads the module's assembly| 42 | |WinPS Same version already loaded|Already loaded assembly| 43 | |WinPS Older version already loaded|Loads the module's assembly| 44 | |PS|Loads the module's assembly in an ALC| 45 | |PS Same version already loaded|Loads the module's assembly in an ALC| 46 | |PS Older version already loaded|Loads the module's assembly in an ALC| 47 | 48 | The main difference between WinPS and PS here is that PS will always load our dependencies in the ALC while WinPS only load our assembly if there is not an existing assembly that matches the name/version. 49 | WinPS can load the same assembly at different versions so this should avoid any version conflicts in the majority of cases. 50 | 51 | ## Things to Avoid 52 | When using an ALC you should avoid: 53 | 54 | + Use an ALC type as your cmdlet's parameters or outputs 55 | 56 | It is possible to output an object of a type from an assembly loaded in an ALC but it should be avoided as much as possible. 57 | This won't cause PowerShell to then load the assembly but it will be unable to reference the type. 58 | For example this won't work because `[Assembly.In.Alc.Type]` is not resolvable in PowerShell and if it was the type won't match the type loaded in the ALC. 59 | 60 | ```ps 61 | $obj = Test-Function 62 | $obj -is [Assembly.In.Alc.Type] 63 | ``` 64 | 65 | This will also be problematic if you try to create a function with a parameter of a type in the ALC 66 | 67 | ```ps 68 | Function Test-Function { 69 | [CmdletBinding()] 70 | param ( 71 | [Assembly.In.Alc.Type]$Object 72 | ) 73 | } 74 | 75 | $obj = Get-ModuleItem 76 | Test-Function -Object $obj 77 | ``` 78 | 79 | The same problem applies where the type may not be resolvable in PowerShell and if it was will be for a different assembly than the one from the ALC that created the type giving you the confusing error: 80 | 81 | ``` 82 | The type 'Assembly.In.Alc.Type' cannot be casted to the type 'Assembly.In.Alc.Type' 83 | ``` 84 | 85 | ## Real World Examples 86 | Some real world examples of modules that use an ALC are: 87 | 88 | + [OpenAuthenticode](https://github.com/jborean93/PowerShell-OpenAuthenticode) 89 | + [PowerShellEditorServices](https://github.com/PowerShell/PowerShellEditorServices) 90 | + [PSOpenAD](https://github.com/jborean93/PSOpenAD) 91 | + [PSToml](https://github.com/jborean93/PSToml) 92 | + [SecretManagement.DpapiNG](https://github.com/jborean93/SecretManagement.DpapiNG) 93 | + This also is a [SecretManagement](https://github.com/PowerShell/SecretManagement) implementation 94 | + [Yayaml](https://github.com/jborean93/PowerShell-Yayaml) 95 | 96 | Thanks to 97 | 98 | + @seeminglyscience who came up with the `ALC Loader` example and helped me through some questions I had 99 | + @JustinGrote who came up with some ideas to try out and talk through some scenarios to test 100 | + @mdgrs-mei for finding a bug in my `ALCResolver` example and working through some of the edge cases 101 | -------------------------------------------------------------------------------- /common.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.IO 2 | using namespace System.Net 3 | using namespace System.Runtime.InteropServices 4 | 5 | # Common code used in the build.ps1 scripts of each process 6 | 7 | $ErrorActionPreference = 'Stop' 8 | 9 | Function Get-NugetAssembly { 10 | <# 11 | .SYNOPSIS 12 | Downloads the assembly. 13 | #> 14 | [OutputType([string])] 15 | [CmdletBinding()] 16 | param ( 17 | [Parameter(Mandatory)] 18 | [string] 19 | $Name, 20 | 21 | [Parameter(Mandatory)] 22 | [string] 23 | $Version 24 | ) 25 | 26 | $targetFolder = Join-Path $PSScriptRoot bin 27 | if (-not (Test-Path -LiteralPath $targetFolder)) { 28 | New-Item -Path $targetFolder -ItemType Directory | Out-Null 29 | } 30 | 31 | $downloadUrl = "https://globalcdn.nuget.org/packages/$($Name.ToLowerInvariant()).$Version.nupkg" 32 | $targetFile = Join-Path $targetFolder "$Name.$Version.zip" 33 | 34 | $assemblyFolder = Join-Path $targetFolder "$Name.$Version" 35 | if (-not (Test-Path -LiteralPath $assemblyFolder)) { 36 | New-Item -Path $assemblyFolder -ItemType Directory | Out-Null 37 | } 38 | 39 | if (-not (Test-Path -LiteralPath $targetFile)) { 40 | $oldSecurityProtocol = [ServicePointManager]::SecurityProtocol 41 | try { 42 | & { 43 | $ProgressPreference = 'SilentlyContinue' 44 | [ServicePointManager]::SecurityProtocol = 'Tls12' 45 | Invoke-WebRequest -UseBasicParsing -Uri $downloadUrl -OutFile $targetFile 46 | } 47 | } 48 | finally { 49 | [ServicePointManager]::SecurityProtocol = $oldSecurityProtocol 50 | } 51 | } 52 | 53 | Add-Type -As System.IO.Compression.FileSystem 54 | 55 | $archive = [System.IO.Compression.ZipFile]::Open( 56 | $targetFile, 57 | "Read") 58 | try { 59 | $archive.Entries | Where-Object { 60 | $_.FullName -like "lib/*/*.dll" 61 | } | ForEach-Object { 62 | $dllName = Split-Path -Path $_.FullName -Leaf 63 | $dllFolder = (Split-Path -Path $_.FullName -Parent).Substring(4) 64 | 65 | $binFolder = Join-Path $assemblyFolder $dllFolder 66 | if (-not (Test-Path -LiteralPath $binFolder)) { 67 | New-Item -Path $binFolder -ItemType Directory | Out-Null 68 | } 69 | 70 | $dllPath = Join-Path $binFolder $dllName 71 | if (-not (Test-Path -LiteralPath $dllPath)) { 72 | [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $dllPath) 73 | } 74 | } 75 | } 76 | finally { 77 | $archive.Dispose() 78 | } 79 | 80 | $assemblyFolder 81 | } 82 | 83 | Function Get-PowerShell { 84 | <# 85 | .SYNOPSIS 86 | Downloads the version of PowerShell specified. 87 | 88 | .PARAMETER Version 89 | The version of PowerShell to download. 90 | #> 91 | [OutputType([string])] 92 | [CmdletBinding()] 93 | param( 94 | [Parameter(Mandatory)] 95 | [ValidateNotNullOrEmpty()] 96 | [string]$Version 97 | ) 98 | 99 | $releaseArch = switch ([RuntimeInformation]::ProcessArchitecture) { 100 | X64 { 'x64' } 101 | X86 { 'x86' } 102 | ARM64 { 'arm64' } 103 | default { 104 | $err = [ErrorRecord]::new( 105 | [Exception]::new("Unsupported archecture requests '$_'"), 106 | "UnknownArch", 107 | [ErrorCategory]::InvalidArgument, 108 | $_ 109 | ) 110 | $PSCmdlet.ThrowTerminatingError($err) 111 | } 112 | } 113 | 114 | $targetFolder = Join-Path $PSScriptRoot bin 115 | if (-not (Test-Path -LiteralPath $targetFolder)) { 116 | New-Item -Path $targetFolder -ItemType Directory | Out-Null 117 | } 118 | 119 | if (-not $IsCoreCLR -or $IsWindows) { 120 | $downloadUrl = "https://github.com/PowerShell/PowerShell/releases/download/v$Version/PowerShell-$Version-win-$releaseArch.zip" 121 | $fileName = "pwsh-$Version.zip" 122 | $nativeExt = ".exe" 123 | } 124 | else { 125 | $downloadUrl = "https://github.com/PowerShell/PowerShell/releases/download/v$Version/powershell-$Version-linux-$releaseArch.tar.gz" 126 | $fileName = "pwsh-$Version.tar.gz" 127 | $nativeExt = "" 128 | } 129 | 130 | $targetFile = Join-Path $targetFolder $fileName 131 | if (-not (Test-Path -LiteralPath $targetFile)) { 132 | $oldSecurityProtocol = [ServicePointManager]::SecurityProtocol 133 | try { 134 | & { 135 | $ProgressPreference = 'SilentlyContinue' 136 | [ServicePointManager]::SecurityProtocol = 'Tls12' 137 | Invoke-WebRequest -UseBasicParsing -Uri $downloadUrl -OutFile $targetFile 138 | } 139 | } 140 | finally { 141 | [ServicePointManager]::SecurityProtocol = $oldSecurityProtocol 142 | } 143 | } 144 | 145 | $pwshFolder = Join-Path $targetFolder "pwsh-$Version" 146 | if (-not (Test-Path -LiteralPath $pwshFolder)) { 147 | New-Item -Path $pwshFolder -ItemType Directory | Out-Null 148 | } 149 | 150 | $pwshFile = Join-Path $pwshFolder "pwsh$nativeExt" 151 | if (-not (Test-Path -LiteralPath $pwshFile)) { 152 | if (-not $IsCoreCLR -or $IsWindows) { 153 | $oldPreference = $global:ProgressPreference 154 | try { 155 | $global:ProgressPreference = 'SilentlyContinue' 156 | Expand-Archive -LiteralPath $targetFile -DestinationPath $pwshFolder 157 | } 158 | finally { 159 | $global:ProgressPreference = $oldPreference 160 | } 161 | } 162 | else { 163 | tar -xf $targetFile --directory $pwshFolder 164 | if ($LASTEXITCODE) { 165 | throw "Failed to extract pwsh tar for $Version" 166 | } 167 | 168 | chmod +x $pwshFile 169 | if ($LASTEXITCODE) { 170 | throw "Failed to set pwsh as executable at $pwshFile" 171 | } 172 | } 173 | } 174 | 175 | $pwshFile 176 | } 177 | 178 | Function Get-BuildInfo { 179 | <# 180 | .SYNOPSIS 181 | Gets the module build information. 182 | 183 | .PARAMETER Path 184 | The module directory. 185 | #> 186 | [CmdletBinding()] 187 | param ( 188 | [Parameter(Mandatory)] 189 | [string] 190 | $Path 191 | ) 192 | 193 | $moduleSrc = [Path]::Combine($Path, 'module') 194 | $manifestItem = Get-Item -Path ([Path]::Combine($moduleSrc, '*.psd1')) 195 | $manifest = Test-ModuleManifest -Path $manifestItem.FullName -ErrorAction Ignore -WarningAction Ignore 196 | $moduleName = $manifest.Name 197 | $moduleVersion = $manifest.Version 198 | 199 | $dotnetSrc = [Path]::Combine($Path, "src", $moduleName) 200 | if (Test-Path -LiteralPath $dotnetSrc) { 201 | [xml]$csharpProjectInfo = Get-Content -Path ([Path]::Combine($dotnetSrc, '*.csproj')) 202 | $targetFrameworks = @(@($csharpProjectInfo.Project.PropertyGroup)[0].TargetFrameworks.Split( 203 | ';', [StringSplitOptions]::RemoveEmptyEntries)) 204 | } 205 | else { 206 | $dotnetSrc = $null 207 | $targetFrameworks = @() 208 | } 209 | 210 | [Ordered]@{ 211 | ModuleName = $moduleName 212 | Version = $moduleVersion 213 | PowerShellSource = $moduleSrc 214 | DotnetSource = $dotnetSrc 215 | Configuration = "Release" 216 | TargetFrameworks = $targetFrameworks 217 | BuildDir = [Path]::Combine($Path, 'output', $build.ModuleName, $build.Version) 218 | } 219 | } 220 | 221 | Function Invoke-ModuleBuild { 222 | <# 223 | .SYNOPSIS 224 | Builds the module. 225 | 226 | .PARAMETER Path 227 | The module directory to build. 228 | #> 229 | [CmdletBinding()] 230 | param ( 231 | [Parameter(Mandatory)] 232 | [string] 233 | $Path 234 | ) 235 | 236 | Write-Host "Getting build information" 237 | $Build = Get-BuildInfo -Path $Path 238 | 239 | if (-not (Test-Path -LiteralPath $Build.BuildDir)) { 240 | New-Item -Path $Build.BuildDir -ItemType Directory -Force | Out-Null 241 | } 242 | 243 | Write-Host "Compiling Dotnet assemblies" 244 | Push-Location -LiteralPath $Build.DotnetSource 245 | try { 246 | $dotnetArgs = @( 247 | 'publish' 248 | '--configuration', $Build.Configuration, 249 | '--verbosity', 'q', 250 | '-nologo', 251 | "-p:Version=$($Build.Version)" 252 | ) 253 | 254 | foreach ($framework in $Build.TargetFrameworks) { 255 | dotnet @dotnetArgs --framework $framework 256 | if ($LASTEXITCODE) { 257 | throw "Failed to compile code for $framework" 258 | } 259 | } 260 | } 261 | finally { 262 | Pop-Location 263 | } 264 | 265 | Write-Host "Build PowerShell module result" 266 | Copy-Item -Path ([Path]::Combine($Build.PowerShellSource, "*")) -Destination $Build.BuildDir -Recurse 267 | 268 | foreach ($framework in $Build.TargetFrameworks) { 269 | $publishFolder = [Path]::Combine($Build.DotnetSource, "bin", $Build.Configuration, $framework, "publish") 270 | $binFolder = [Path]::Combine($Build.BuildDir, "bin", $framework) 271 | if (-not (Test-Path -LiteralPath $binFolder)) { 272 | New-Item -Path $binFolder -ItemType Directory | Out-Null 273 | } 274 | Copy-Item ([Path]::Combine($publishFolder, "*")) -Destination $binFolder -Recurse 275 | } 276 | } 277 | 278 | if (-not $IsCoreCLR -or $IsWindows) { 279 | Function Add-GacAssembly { 280 | [CmdletBinding()] 281 | param ( 282 | [Parameter(Mandatory)] 283 | [string] 284 | $Path 285 | ) 286 | 287 | if (-not (Test-Path -LiteralPath $Path)) { 288 | throw "Assembly does not exist at path '$Path'" 289 | } 290 | 291 | $invokeParams = @{} 292 | if ($IsCoreCLR) { 293 | $s = New-PSSession -UseWindowsPowerShell 294 | $invokeParams.Session = $s 295 | } 296 | 297 | try { 298 | Invoke-Command @invokeParams -ScriptBlock { 299 | $ErrorActionPreference = 'Stop' 300 | 301 | [System.Reflection.Assembly]::LoadWithPartialName("System.EnterpriseServices") | Out-Null 302 | $publish = [System.EnterpriseServices.Internal.Publish]::new() 303 | $publish.GacInstall($args[0]) 304 | } -ArgumentList $Path 305 | } 306 | finally { 307 | if ($s) { Remove-PSSession -Session $s } 308 | } 309 | } 310 | 311 | Function Remove-GacAssembly { 312 | [CmdletBinding()] 313 | param ( 314 | [Parameter(Mandatory)] 315 | [string] 316 | $Path 317 | ) 318 | 319 | if (-not (Test-Path -LiteralPath $Path)) { 320 | throw "Assembly does not exist at path '$Path'" 321 | } 322 | 323 | $invokeParams = @{} 324 | if ($IsCoreCLR) { 325 | $s = New-PSSession -UseWindowsPowerShell 326 | $invokeParams.Session = $s 327 | } 328 | 329 | try { 330 | Invoke-Command @invokeParams -ScriptBlock { 331 | $ErrorActionPreference = 'Stop' 332 | 333 | [System.Reflection.Assembly]::LoadWithPartialName("System.EnterpriseServices") | Out-Null 334 | $publish = [System.EnterpriseServices.Internal.Publish]::new() 335 | $publish.GacRemove($args[0]) 336 | } -ArgumentList $Path 337 | } 338 | finally { 339 | if ($s) { Remove-PSSession -Session $s } 340 | } 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /test.ps1: -------------------------------------------------------------------------------- 1 | using namespace System.IO 2 | using namespace System.Reflection 3 | 4 | param ( 5 | [Parameter(Mandatory)] 6 | [string] 7 | $Name 8 | ) 9 | 10 | $ErrorActionPreference = 'Stop' 11 | 12 | . $PSScriptRoot/common.ps1 13 | 14 | ######### 15 | # SETUP # 16 | ######### 17 | 18 | # Comes with Newtonsoft.Json 12 19 | $pwsh_7_1 = Get-PowerShell -Version 7.1.7 20 | 21 | # Comes with Newtonsoft.Json 13 22 | $pwsh_7_4 = Get-PowerShell -Version 7.4.0 23 | 24 | Get-NugetAssembly -Name YamlDotNet -Version 12.3.1 | Out-Null 25 | Get-NugetAssembly -Name YamlDotNet -Version 13.7.1 | Out-Null 26 | $newtonsoft_12 = Get-NugetAssembly -Name Newtonsoft.Json -Version 12.0.3 27 | $newtonsoft_13 = Get-NugetAssembly -Name Newtonsoft.Json -Version 13.0.3 28 | 29 | # Build our module 30 | & $pwsh_7_4 -File ([Path]::Combine($PSScriptRoot, $Name, "build.ps1")) 31 | 32 | ######### 33 | # TESTS # 34 | ######### 35 | 36 | Push-Location -LiteralPath $PSScriptRoot/$Name 37 | try { 38 | Write-Host "Test: PS - Loading with assembly not loaded" 39 | & $pwsh_7_4 { 40 | Import-Module (Get-Item -Path ./output/ALC*).FullName 41 | 42 | # Newtonsoft.Json is always loaded in pwsh so we are just testing YamlDotNet 43 | ConvertTo-YamlDotNet foo 44 | 45 | @( 46 | foreach ($asm in [System.AppDomain]::CurrentDomain.GetAssemblies()) { 47 | if (-not ( 48 | $asm.GetName().Name -like "*newtonsoft*" -or 49 | $asm.GetName().Name -like "*yaml*" 50 | ) 51 | ) { 52 | continue 53 | } 54 | 55 | $alc = [Runtime.Loader.AssemblyLoadContext]::GetLoadContext($asm) 56 | [PSCustomObject]@{ 57 | Name = $asm.FullName 58 | Location = $asm.Location 59 | ALC = $alc 60 | } 61 | } 62 | ) | Format-List 63 | } | Out-Host 64 | 65 | Write-Host "Test: PS - Loading with older assembly already loaded" 66 | & $pwsh_7_1 { 67 | Import-Module (Get-Item -Path ./output/ALC*).FullName 68 | 69 | Add-Type -Path "../bin/YamlDotNet.12.3.1/netstandard2.0/YamlDotNet.dll" 70 | 71 | ConvertTo-NewtonsoftJson foo 72 | ConvertTo-YamlDotNet foo 73 | 74 | @( 75 | foreach ($asm in [System.AppDomain]::CurrentDomain.GetAssemblies()) { 76 | if (-not ( 77 | $asm.GetName().Name -like "*newtonsoft*" -or 78 | $asm.GetName().Name -like "*yaml*" 79 | ) 80 | ) { 81 | continue 82 | } 83 | 84 | $alc = [Runtime.Loader.AssemblyLoadContext]::GetLoadContext($asm) 85 | [PSCustomObject]@{ 86 | Name = $asm.FullName 87 | Location = $asm.Location 88 | ALC = $alc 89 | } 90 | } 91 | ) | Format-List 92 | } | Out-Host 93 | 94 | Write-Host "Test: PS - Loading with same assembly version already loaded" 95 | & $pwsh_7_4 { 96 | Import-Module (Get-Item -Path ./output/ALC*).FullName 97 | 98 | Add-Type -Path "../bin/YamlDotNet.13.7.1/netstandard2.0/YamlDotNet.dll" 99 | 100 | ConvertTo-NewtonsoftJson foo 101 | ConvertTo-YamlDotNet foo 102 | 103 | @( 104 | foreach ($asm in [System.AppDomain]::CurrentDomain.GetAssemblies()) { 105 | if (-not ( 106 | $asm.GetName().Name -like "*newtonsoft*" -or 107 | $asm.GetName().Name -like "*yaml*" 108 | ) 109 | ) { 110 | continue 111 | } 112 | 113 | $alc = [Runtime.Loader.AssemblyLoadContext]::GetLoadContext($asm) 114 | [PSCustomObject]@{ 115 | Name = $asm.FullName 116 | Location = $asm.Location 117 | ALC = $alc 118 | } 119 | } 120 | ) | Format-List 121 | } | Out-Host 122 | 123 | if ($IsCoreCLR -and -not $IsWindows) { 124 | return 125 | } 126 | 127 | Write-Host "Test: WinPS - Assembly not already loaded" 128 | powershell.exe { 129 | Import-Module (Get-Item -Path ./output/ALC*).FullName 130 | 131 | ConvertTo-NewtonsoftJson foo 132 | ConvertTo-YamlDotNet foo 133 | 134 | @( 135 | foreach ($asm in [System.AppDomain]::CurrentDomain.GetAssemblies()) { 136 | if (-not ( 137 | $asm.GetName().Name -like "*newtonsoft*" -or 138 | $asm.GetName().Name -like "*yaml*" 139 | ) 140 | ) { 141 | continue 142 | } 143 | 144 | [PSCustomObject]@{ 145 | Name = $asm.FullName 146 | Location = $asm.Location 147 | } 148 | } 149 | ) | Format-List 150 | } | Out-Host 151 | 152 | Write-Host "Test: WinPS - Older assembly already loaded" 153 | powershell.exe { 154 | Add-Type -Path "../bin/Newtonsoft.Json.12.0.3/net45/Newtonsoft.Json.dll" 155 | Add-Type -Path "../bin/YamlDotNet.12.3.1/net45/YamlDotNet.dll" 156 | 157 | Import-Module (Get-Item -Path ./output/ALC*).FullName 158 | 159 | ConvertTo-NewtonsoftJson foo 160 | ConvertTo-YamlDotNet foo 161 | 162 | @( 163 | foreach ($asm in [System.AppDomain]::CurrentDomain.GetAssemblies()) { 164 | if (-not ( 165 | $asm.GetName().Name -like "*newtonsoft*" -or 166 | $asm.GetName().Name -like "*yaml*" 167 | ) 168 | ) { 169 | continue 170 | } 171 | 172 | [PSCustomObject]@{ 173 | Name = $asm.FullName 174 | Location = $asm.Location 175 | } 176 | } 177 | ) | Format-List 178 | } | Out-Host 179 | 180 | Write-Host "Test: WinPS - Same assembly already loaded" 181 | powershell.exe { 182 | Add-Type -Path "../bin/Newtonsoft.Json.13.0.3/net45/Newtonsoft.Json.dll" 183 | Add-Type -Path "../bin/YamlDotNet.13.7.1/net45/YamlDotNet.dll" 184 | 185 | Import-Module (Get-Item -Path ./output/ALC*).FullName 186 | 187 | ConvertTo-NewtonsoftJson foo 188 | ConvertTo-YamlDotNet foo 189 | 190 | @( 191 | foreach ($asm in [System.AppDomain]::CurrentDomain.GetAssemblies()) { 192 | if (-not ( 193 | $asm.GetName().Name -like "*newtonsoft*" -or 194 | $asm.GetName().Name -like "*yaml*" 195 | ) 196 | ) { 197 | continue 198 | } 199 | 200 | [PSCustomObject]@{ 201 | Name = $asm.FullName 202 | Location = $asm.Location 203 | } 204 | } 205 | ) | Format-List 206 | } | Out-Host 207 | 208 | # We need to be running as admin to add/remove from the GAC 209 | $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent() 210 | if (-not 211 | (([System.Security.Principal.WindowsPrincipal]$currentUser).IsInRole( 212 | [System.Security.Principal.WindowsBuiltinRole]::Administrator 213 | )) 214 | ) { 215 | return 216 | } 217 | 218 | $newton12Assembly = "$newtonsoft_12\net45\Newtonsoft.Json.dll" 219 | $newton13Assembly = "$newtonsoft_13\net45\Newtonsoft.Json.dll" 220 | 221 | Add-GacAssembly -Path $newton12Assembly 222 | try { 223 | Write-Host "Test: WinPS GAC - Older assembly version" 224 | powershell.exe { 225 | Import-Module (Get-Item -Path ./output/ALC*).FullName 226 | 227 | ConvertTo-NewtonsoftJson foo 228 | } | Out-Host 229 | } 230 | finally { 231 | Remove-GacAssembly -Path $newton12Assembly 232 | } 233 | 234 | Add-GacAssembly -Path $newton13Assembly 235 | try { 236 | Write-Host "Test: WinPS GAC - Same assembly version" 237 | powershell.exe { 238 | Import-Module (Get-Item -Path ./output/ALC*).FullName 239 | 240 | ConvertTo-NewtonsoftJson foo 241 | } | Out-Host 242 | } 243 | finally { 244 | Remove-GacAssembly -Path $newton13Assembly 245 | } 246 | } 247 | finally { 248 | Pop-Location 249 | } 250 | --------------------------------------------------------------------------------