C# binary PowerShell Module Type conversion

137 Views Asked by At

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.
1

There are 1 best solutions below

2
On BEST ANSWER

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