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 -NoProfileAdd-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$enumeratorcan 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
