Search code examples
c#powershellpowershell-cmdlet

What is '<Invoke>d__40'?


Using Powershell, System.Management.Automation.Cmdlet.Invoke() returns object of type '<Invoke>d__40' rather than specified OutputType.

To reproduce:

  1. Copy SendGreeting example cmdlet to .\ExampleCmdlet.cs
  2. powershell -NoProfile
  3. Add-Type -Path .\ExampleCmdlet.cs
  4. $command = [SendGreeting.SendGreetingCommand]::new()
  5. $command.Name = 'Person'
  6. $invoke = $command.Invoke()
  7. $invoke.GetType()

Expected: [string]
Actual: [<Invoke>d__40]

$PSVersionTable:

Name                           Value
----                           -----
PSVersion                      5.1.19041.1237
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.19041.1237
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1

Get-Member -InputObject $invoke reveals this to implement IEnumerator and some playing with .MoveNext() and .Current will sometimes output the expected "Hello Person!" result.

What is this <Invoke>d__40 type?
Why is $command.Invoke() not returning the expected string output directly?


Solution

  • To complement Guru Stron's helpful answer, which explains that the name of the specific type returned is just an implementation detail; what matters is that the type implements the System.Collections.IEnumerable interface:

    The fact that the type also implements the System.Collections.IEnumerator interface, as you've discovered, makes it a lazy (on-demand) enumerable: that is, the object returned doesn't itself contain data, it retrieves / generates data when enumerated.

    If you output $invoke, PowerShell implicitly enumerates the enumerable, and you should see the expected outcome:

    PS> $invoke  # enumeration happens here.
    Hello Person!
    

    Note that an attempt to access $invoke again produces no output, because the enumeration has completed (and even trying to reset it with .Reset() doesn't work, because the type implementing the interface doesn't support it).

    • Note: It is not unusual for lazy enumerables to support repeat enumeration, despite also not implementing the .Reset() method; e.g., in the following examples $enumerator can be enumerated repeatedly, and yields the same results every time: $enumerator = [System.Linq.Enumerable]::Range(1,10) and $enumerator = [System.IO.File]::ReadLines("$PWD/test.txt")

    By contrast, assigning $invoke to a variable does not cause enumeration: $result = $invoke merely creates another reference to the enumerator itself.

    In order to capture the actual object(s) to be enumerated, you must force enumeration via $(), the subexpression operator or @(), the array-subexpression operator; e.g.:

    # Note: This assumes you haven't output $invoke by itself before.
    $result = $($invoke) # force enumeration and store the enumerated object(s)
    

    Taking a step back:

    Lazy enumerables aren't that common in normal PowerShell code, and if you use them in an enumeration context - notably in the pipeline or in a foreach statement - they'll work as expected.

    When you assign a lazy enumerable to a variable, you need to be aware that you're storing just the enumerator, not the data it will enumerate.

    If you use your sample cmdlet as it is meant to be used - by invoking it as command Send-Greeting with a -Name argument - the lazy enumerable is eliminated from the picture, because cmdlets output actual data:

    # Directly outputs string 'Hello Person!'
    Send-Greeting -Name Person
    

    To make your sample cmdlet callable this way, you need to not only load the implementing type's assembly into your session with Add-Type, you must additionally import it as a PowerShell module, with Import-Module:

    # Compile and load the assembly, and also import it as a PowerShell module,
    # so the cmdlet that is implemented surfaces as such.
    (Add-type -PassThru -LiteralPath .\ExampleCmdlet.cs).Assembly | Import-Module
    
    # Now you can call your Send-Greeting cmdlet.
    Send-Greeting -Name Person