Search code examples
c#powershellpowershell-cmdletpscustomobject

How can I output a PSCustomObject from a compiled Cmdlet?


I have a compiled Cmdlet that works with Hashtables. I would like to write a [pscustomobject] of the hashtable to the pipeline. In a PowerShell script block that would be accomplished using the following cast:

[pscustomobject]$hashtable

I would like to accomplish the same from a compiled Cmdlet. The natural implementation would be

using System.Collections;
using System.Management.Automation;

[Cmdlet(VerbsData.Out,"PsCustomObject")]
public class OutPsCustomObjectCommand : PSCmdlet {
    [Parameter(Mandatory = true)]
    public Hashtable Hashtable { get; set; }
    protected override void ProcessRecord() {
        WriteObject(new PSCustomObject(Hashtable));
    }
}

however, PSCustomObject has no public constructor.

I thought new PSObject() might be equivalent, but WriteObject(new PSObject(Hashtable)) differs enough from [pscustomobject]$hashtable that they aren't even shown the same in the console:

PS\> Out-PsCustomObject -Hashtable @{ a='aye' }
Name                           Value
----                           -----
a                              aye

PS\> [pscustomobject]@{ a = 'aye' }
a
-
aye

How can I output a PSCustomObject from a compiled Cmdlet?


Solution

  • You're kinda close, first you need to create a PSObject and then, for each DictionaryEntry on the hash table, you need to add a new PSNoteProperty to said object and lastly output it.

    NOTE: The following does not ensure ordering on the PSCustomObject properties, what [pscustomobject]@{ .... } achieves preserving the order of the hash table key / value pairs is all done via AST parsing at runtime. Cannot be achieved via already created hash table.

    If you needed to preserve the order then you could change the parameter to:

    public IDictionary IDictionary { get; set; }
    

    And pass-in an OrderedDictionary, like so:

    Out-PsCustomObject ([ordered]@{ foo = 'bar'; baz = 'hello' })
    

    Also worth noting, what the [ordered] accelerator does has the same AST parsing used in [pscustomobject] to preserve ordering via hash table literal (@{ ... }) instantiation.

    using System.Collections;
    using System.Management.Automation;
    
    [Cmdlet(VerbsData.Out, "PsCustomObject")]
    public class OutPsCustomObjectCommand : PSCmdlet
    {
        [Parameter(Mandatory = true, Position = 0)]
        public Hashtable Hashtable { get; set; }
    
        protected override void ProcessRecord()
        {
            PSObject output = new();
            foreach (DictionaryEntry kv in Hashtable)
            {
                output.Properties.Add(new PSNoteProperty(
                    LanguagePrimitives.ConvertTo<string>(kv.Key), kv.Value));
            }
    
            WriteObject(output);
        }
    }
    

    EDIT

    After further inspecting the PowerShell source (see Mshexpression.cs#L353-L371), there is a perhaps shorter way to create a pscustomobject from a hash table or any other IDictionary type using LanguagePrimitives.ConvertPSObjectToType, however, the notes mentioned before still remain, this does not ensure property ordering.

    using System.Collections;
    using System.Management.Automation;
    
    [Cmdlet(VerbsData.Out, "PsCustomObject")]
    public class OutPsCustomObjectCommand : PSCmdlet
    {
        [Parameter(Mandatory = true, Position = 0)]
        public IDictionary IDictionary { get; set; }
    
        protected override void ProcessRecord() =>
            WriteObject(LanguagePrimitives.ConvertPSObjectToType(
                valueToConvert: PSObject.AsPSObject(IDictionary),
                resultType: typeof(PSObject),
                recursion: false,
                formatProvider: null,
                ignoreUnknownMembers: true));
    }