Using Powershell, System.Management.Automation.Cmdlet.Invoke() returns object of type '<Invoke>d__40'
rather than specified OutputType.
To reproduce:
- Copy SendGreeting example cmdlet to .\ExampleCmdlet.cs
powershell -NoProfile
Add-Type -Path .\ExampleCmdlet.cs
$command = [SendGreeting.SendGreetingCommand]::new()
$command.Name = 'Person'
$invoke = $command.Invoke()
$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?
CodePudding user response:
<Invoke>d__40
is name of compiler generated class:
Cmdlet.Invoke
/Cmdlet.Invoke<T>
returns IEnumerable
/IEnumerable<T>
and is implemented using yield return
which results in compiler generating as special named class (compiler can use <
and >
symbols in identifiers while developers can't) which implements IEnumerable
/IEnumerable<T>
(check out for example this decompilation).
CodePudding user response:
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