c# - Can a type be added that references an in-memory assembly? - Stack Overflow

The following code creates an in-memory type, finds its assembly, then attempts to add another type ref

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?

What works

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 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?

What works

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()
Share Improve this question edited Nov 16, 2024 at 17:28 fuz 93.4k27 gold badges212 silver badges383 bronze badges asked Nov 16, 2024 at 17:23 alx9ralx9r 4,3036 gold badges33 silver badges56 bronze badges 7
  • 2 No, the (C#) compiler won't have access to in-memory modules, you'll need to compile them at the same time, or rely on c.dll for storing the first one on disk (d.dll shouldn't be necessary here though) – Mathias R. Jessen Commented Nov 16, 2024 at 21:07
  • Raw C# can do that in memory. Not sure how you would translate that to powershell though. – Jeremy Lakeman Commented Nov 19, 2024 at 0:12
  • 1 Hmmm .... there's this hack for creating a MetadataReference from an assembly github/dotnet/runtime/issues/36590#issuecomment-689883856. Written as a workaround for when an application is published as a single executable . Not sure if there's any new support in the runtime. Then I'm sure you can find a CSharpCompilation.Create(...).Emit(...), new AssemblyLoadContext(...).LoadFromStream(...) example. – Jeremy Lakeman Commented Nov 19, 2024 at 0:37
  • 1 Found a relevant question and posted my version of that comment as an answer stackoverflow/a/79201797/4139809. – Jeremy Lakeman Commented Nov 19, 2024 at 0:50
  • 1 Note that if you Emit the first type, then you can probably ModuleMetadata.CreateFromImage from the assembly bytes without the unsafe code. – Jeremy Lakeman Commented Nov 19, 2024 at 1:12
 |  Show 2 more comments

1 Answer 1

Reset to default 2

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

    • Update: Commendably, you've since filed GitHub issue #24612

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]

    • This smells like a bug: a type generated in an in-memory assembly should invariably be loaded into the current session, along with any on-disk assemblies it depends on, given that not loading an in-memory assembly into the current session is obviously pointless (it will disappear when the session exits).
  • 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]).

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1745648935a4638140.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信