2

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]:

  • Date is cast as a DateTime object.
  • Int32 is undefined by me and is converted to an int32 by PowerShell
  • TypedInt is cast as an int
  • Text is undefined by me and is converted to a string by PowerShell

When I debug the code in Visual Studio, I get this: enter image description here

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.
7
  • It is the way PS works. Often I find PS casts object to psObject and I have to cast back to the type I want. What I use often to help debug is : Write-Host myInt32.GetType(). PS created a hash table where the key name is Int32. So the integer is really myInt32.Value.Int32 Commented Oct 28, 2023 at 12:26
  • That is exactly what I do in my code here: var myIntType = myInt32.Value.GetType().Name. The issue is, that it returns PSObject and not int, as you het in a PowerShell console. Commented Oct 28, 2023 at 12:50
  • What is returned is a hash table. That is why it is in a curly bracket in the debug window. The key to the hash is "Int32=1" where Int32 is the key and 1 is the value. Try myInt32.Int32. Commented Oct 28, 2023 at 14:30
  • In this case mklement0 are personal. You can read about pscustomobjects here : learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/…. This is the first time mklement0 has admitted to the inconsistencies with PS that I've been complaining about for a long time. Commented Oct 29, 2023 at 8:56
  • @jdweng, to be fair: "I find PS casts object to psObject" is correct, but all your other claims ("PS created a hash table where the key name is Int32", ...) are incorrect and confusing distractions. As for "the first time mklement0 has admitted to the inconsistencies with PS": that is both obviously untrue (otherwise the linked GitHub issues wouldn't exist, for instance) and truly personal. Commented Oct 29, 2023 at 12:20

1 Answer 1

1

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
Sign up to request clarification or add additional context in comments.

1 Comment

Thank you SO much for this concise explanation. It is spot on.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.