I'm working on a project, where I use the input properties to create parameters on objects with strong types. I have this sample code:
using System;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Linq;
namespace TestExtractedData
{
public class ExtractData
{
public Type Type { get; set; }
public string Parameter { get; set; }
public dynamic Data { get; set; }
}
[Cmdlet("Get", "ExtractedData")]
[OutputType(typeof(ExtractData))]
public class GetExtractedDataCommand : PSCmdlet
{
[Parameter(
Mandatory = true,
Position = 1,
ValueFromPipeline = true,
ValueFromPipelineByPropertyName = false)]
public PSObject[] InputObject { get; set; }
// This method gets called once for each cmdlet in the pipeline when the pipeline starts executing
protected override void BeginProcessing()
{
WriteVerbose("Begin!");
}
// This method will be called for each input received from the pipeline to this cmdlet; if no input is received, this method is not called
protected override void ProcessRecord()
{
var properties = InputObject[0].Members.Where(w => w.GetType() == typeof(PSNoteProperty)).ToList();
var extractedData = properties.Select(s => new ExtractData
{
Parameter = s.Name,
Type = Type.GetType(s.TypeNameOfValue),
Data = (from o in InputObject select o.Properties[s.Name].Value).ToArray()
}).ToList();
var myDate = InputObject[0].Properties["Date"];
var myInt32 = InputObject[0].Properties["Int32"];
var myTypedInt = InputObject[0].Properties["TypedInt"];
var myText = InputObject[0].Properties["Text"];
var myDateType = myDate.Value.GetType().Name;
var myIntType = myInt32.Value.GetType().Name;
var myTypedIntType = myTypedInt.Value.GetType().Name;
var myTextType = myText.Value.GetType().Name;
WriteObject(extractedData);
}
// This method will be called once at the end of pipeline execution; if no input is received, this method is not called
protected override void EndProcessing()
{
WriteVerbose("End!");
}
}
}
I can debug the module by running this command:
1..100 | foreach { [pscustomobject]@{Date = (Get-Date).AddHours($_);Int32 = $_;TypedInt = [int]$_ ; Text = "Iteration $_"}} | Get-ExtractedData
I know the following about the parameters in my [pscustomobject]
:
When I debug the code in Visual Studio, I get this:
I expected the value of Int32 to be of type Int32, but instead it is PSObject.
My question is, why does this happen and does this only happen to ints or also to other types, that are not cast in the hashtable fed to [pscustomobject]
?
I really want Int32 to be an int32 in my C# code, but I'm not sure how to make it so. I tried this:
var change = Convert.ChangeType(myInt32.Value, typeof(int));
but that fails with this error:
Get-ExtractedData: Object must implement IConvertible.
You're seeing a fundamental - but unfortunate - behavior of PowerShell's pipeline:
Objects sent through the pipeline are invariably wrapped in [psobject]
instances.
These wrappers are meant to be invisible helper objects, i.e. a mere implementation detail, but all too frequently they are not, as discussed in GitHub issue #5579 and - more closely related to your issue - GitHub issue #14394.
A simple illustration of the problem:
# -> $true, 'Int32'
42 | ForEach-Object { $_ -is [psobject]; $_.GetType().Name }
That is, the [int]
instance was invisibly wrapped in [psobject]
.
This does not happen with the intrinsic .ForEach()
method:
# -> $false, 'Int32'
(42).ForEach({ $_ -is [psobject]; $_.GetType().Name })
The upshot is:
Dealing with a [pscustomobject]
instance whose properties either were or potentially were populated from pipeline input objects requires your binary cmdlet to (conditionally) remove the [psobject]
wrapper by accessing the latter's .BaseObject
property.
Here's a simplified example:
# Ad hoc-compile a sample cmdlet, Invoke-Foo, that echoes the
# type of the .Prop property of its input object(s).
Add-Type @'
using System;
using System.Management.Automation;
[Cmdlet("Invoke", "Foo")]
public class InvokeFooCommand : PSCmdlet {
[Parameter(ValueFromPipeline=true)]
public PSObject InputObject { get; set; }
protected override void ProcessRecord() {
// Get the value of property .Prop
object propValue = InputObject.Properties["Prop"].Value;
// If the value is of type PSObject, get its base object
// (the wrapped .NET instance), via the .BaseObject property.
if (propValue is PSObject) { propValue = ((PSObject)propValue).BaseObject; }
WriteObject(propValue.GetType().Name);
}
}
'@ -PassThru | ForEach-Object Assembly | Import-Module
# Invocation via the pipeline.
# -> 'Int32'
42 | ForEach-Object { [pscustomobject] @{ Prop = $_ } } | Invoke-Foo
# Invocation via the .ForEach() method
# -> 'Int32'
(42).ForEach({ [pscustomobject] @{ Prop = $_ } }) | Invoke-Foo