The following code creates an in-memory type, finds its assembly, then attempts to add another type referencing the first:
Add-Type -TypeDefinition 'namespace MyNamespace { public class c {}}'
$assembly =
$([System.AppDomain]::CurrentDomain.
GetAssemblies().
GetTypes() |
? {$_.Namespace -eq 'MyNamespace' } |
% Assembly |
Select-Object -Unique -First 1 )
Add-Type -TypeDefinition 'namespace MyNamespace {public class d : c {}}' `
-ReferencedAssemblies $assembly
The attempt fails with error
Add-Type: C:\repro.ps1:12
Line |
12 | Add-Type -TypeDefinition 'namespace MyNamespace {public class d : c { …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| The value cannot be an empty string. (Parameter 'path')
which suggests Add-Type
is looking for a path that probably doesn't exist since the assembly only exists in-memory.
Can a type be added that references an in-memory-only type?
The following approach works but requires the assemblies to exist in the filesystem:
Remove-Item .\c.dll,.\d.dll -ErrorAction SilentlyContinue
Add-Type `
-TypeDefinition 'namespace MyNamespace { public class c {}}' `
-OutputAssembly .\c.dll
Add-Type -Path .\c.dll
[MyNamespace.c]::new()
Add-Type -TypeDefinition 'namespace MyNamespace {public class d : c {}}' `
-ReferencedAssemblies .\c.dll `
-OutputAssembly .\d.dll
Add-Type -Path .\d.dll
[MyNamespace.d]::new()
The answer is implied by your own findings in the question, but let me spell it out:
As of PowerShell (Core) 7 v7.4.x:
Indeed, while Add-Type
's -ReferencedAssemblies
parameter does accept [System.Reflection.Assembly]
instances (which stringify to their .FullName
property in the context of binding to this [string[]]
-typed parameter) (as an alternative to passing assembly file paths), such instances are only recognized if they represent an on-disk assembly, i.e. one whose .Location
property reports a file-system path.
Since Add-Type -TypeDefinition
calls produce in-memory assemblies in the absence of an -OutputAssembly
argument, such assemblies cannot be used as -ReferencedAssemblies
arguments in later Add-Type
calls.
It seems that this constraint - which isn't currently documented - is imposed by PowerShell, not by the underlying .NET type implementing the C# compiler, which is called in-process from PowerShell.[1]
Therefore, it is conceivable that a future version of PowerShell 7 will lift this constraint - which hinges on someone stepping up to file an issue to that effect in the GitHub repository
Thus, the solution for now is indeed to create on-disk assemblies for types that must be referenced in later Add-Type
calls:
Doing so is cumbersome:
Add-Type
invariably fails if the target file passed to -OutputAssembly
already exists (there is no -Force
switch).
While using the -PassThru
switch is generally required when using -OutputAssembly
in order to also load a generated on-disk assembly into the current session, it is inexplicably also needed to ensure that a later Add-Type
call that uses an in-memory assembly that builds on the on-disk assembly via -ReferencedAssemblies
actually surfaces the generated in-memory-assembly types in the current session.[2]
On Windows, you cannot ensure that the on-disk assembly is deleted on exiting your session, because it is still locked (works fine in Unix-like environments).
if ('MyNamespace.D' -as [type]) {
Write-Verbose -Verbose 'Already loaded.'
}
else {
# Note: New-TemporaryFile works in principle, but immediately *creates* the
# file, which would cause Add-Type to fail (it has no -Force switch)
$tempFile = Join-Path ([IO.Path]::GetTempPath()) ([IO.Path]::GetRandomFileName())
# Note: -PassThru ensures that the generated assembly is also loaded into
# the current session, which is seemingly a prerequisite for the
# type created by the later, in-memory-assembly Add-Type call to actually
# surface in the current session.
$null =
Add-Type -ErrorAction Stop -PassThru -OutputAssembly $tempFile -TypeDefinition @'
namespace MyNamespace { public class C {} }
'@
# Pass the file path of the generated on-disk assembly to -ReferencedAssemblies
try {
Add-Type -ErrorAction Stop -ReferencedAssemblies $tempFile -TypeDefinition @'
namespace MyNamespace { public class D : C {} }
'@
}
finally {
Remove-Item -ErrorAction Ignore -LiteralPath $tempFile -Force
if (-not $?) { # !! This invariably happens on Windows.
Write-Warning "Failed to remove temp. assembly '$tempFile' in-session."
}
}
}
# Output the compiled types.
[MyNamespace.C]
[MyNamespace.D]
[1] The type that implements the C# compiler is Microsoft.CodeAnalysis.CSharp.CSharpCompilation
and its .Create()
method's references
parameter expects Microsoft.CodeAnalysis.MetadataReference
instances, which can be obtained from in-memory assemblies, e.g. via this constructor overload.
[2] Here's a minimal repro:
$tempDll = Join-Path (Convert-Path temp:) throwaway_$PID.dll; if (Test-Path $tempDll) { Remove-Item -ErrorAction Stop $tempDll }; $null = Add-Type -OutputAssembly $tempDll -TypeDefinition 'namespace MyNamespace { public class C {} }'; Add-Type -ReferencedAssemblies $tempDll -TypeDefinition 'namespace MyNamespace { public class D : C {} }'; [MyNamespace.D]; Remove-Item -ErrorAction Ignore $tempDll || Write-Warning "Failed to delete $tempDll"
Unless you add -PassThru
to the first Add-Type
call - be sure to start a new session first - type [MyNamespace.D]
won't be available (and neither will [MyNamespace.C]
).