Powershell: PSCustomObject

Powershell’s PSCustomObject type has been a great tool, but since I don’t use it every day, I jotted down some sample code and links for my future self (or any other interested reader 😄 ), as a reminder.

Background & Reading Material

I’m using my “own types”, created as a PSCustomObjects, in several scripts and functions.

The main drive is mostly that I can first gather data from multiple, different data sources and then can bring that information into a more manageable and coherent format — for example by adding additional custom properties (key/value pairs), and with values which are often constructed dynamically, e.g. by using calculated expressions.

Here are a few links to introductory and helpful articles and comments on the differences between Object, PSObject, PSCustomObject and also hash tables:

Basic operations

So, here’s a small script that shows some common operations (create, access, change, remove), in different stages and variants.

More details and some advanced topics can be found in the articles that are linked above.

# Helper function to generate an array of some PSCustomObjects:

Function generateArrayOfPSCustomObjects
{
    Param
    (
        [switch] $NoDescription,
        [switch] $JustEnumerateProperties
    )
    
    $data   = @(1..10)
    $result = @()
    
    ForEach ($i in $data)
    {
        $object = [PSCustomObject] @{
            ID          = $i
            Description = "Default text"
        }

        $object.Description = "This is item $i of $(@($data).count)"
        
        # Add an additional property ("Data") to the custom object format:
        $data_value = @($data).count % $i | % { if ($_ -eq 0) { $true } else { $false } }
        $object | Add-Member -MemberType NoteProperty -Name 'Data' -Value $data_value
        
        # Remove the default property "Description":
        if ($NoDescription)
        {
            $object.psobject.properties.remove('Description')
                # 'psobject' is a hidden property that gives you access to base object metadata.
        }

        $result += $object
    }
    
    if ($JustEnumerateProperties) { $result[0].psobject.properties.name }
    else                          { $result }
}

# The actual main script:

$array = generateArrayOfPSCustomObjects

write-host "---- The pure array of PSCustomObjects: ----"
$array | out-string

write-host "---- Select only the properties 'ID' and 'Data': ----"
$array | select ID, Data | out-string

write-host "---- Just item no. 4: ----"
$array | ? { $_.ID -eq "4" } | out-string

write-host "---- Access only 'ID' directly: ----"
$key = 'ID'
$array | % { $_.$key }

write-host "---- Changed value of property 'Description': ----"
$array | % { $_.Description = "New description" }
$array | out-string

write-host "---- Created without default property 'Description': ----"
$another_array = generateArrayOfPSCustomObjects -NoDescription
$another_array | out-string

write-host "---- Enumerate property names - Version 1: ----"
$array | Get-Member -MemberType NoteProperty | Select -ExpandProperty Name

write-host "---- Enumerate property names - Version 2: ----"
generateArrayOfPSCustomObjects -JustEnumerateProperties

Using it as a template for other variables

I have scripts where I want to use the same PSCustomObject format/layout without repeating it multiple times (e.g. in several loops), because that would also come with the burden of updating the code at multiple locations, in case the common PSCustomObject changes (e.g. by adding, removing, or renaming properties).

My first, naive approach was to define such a common PSCustomObject only one time in the script, with null values; using this as a “template”, by assigning this empty shell to a new variable. Then one could overwrite the properties of thes new variable if needed — at least that was the idea… (which, spoiler alert, didn’t work out):

#
# How NOT to do it!
#

$Template = [PSCustomObject] @{
    ID = $null
}

$A = $Template
$A.ID = '123'

$B = $Template
$B.ID = 'ABC'

"Expected: '123'     -> Real value: '$($A.ID)'"
"Expected: 'ABC'     -> Real value: '$($B.ID)'"
"Expected: (nothing) -> Real value: '$($Template.ID)'"

You’ll get some unexpected results when reading the variable’s properties:

Expected: '123'     -> Real value: 'ABC'
Expected: 'ABC'     -> Real value: 'ABC'
Expected: (nothing) -> Real value: 'ABC'

That is because these new variables will not hold copies of the empty template, but references to the “empty template” PSCustomObject.
Meaning: By writing to variable $A or $B, you’ll actually write to the referenced variable instead, i.e. to the original instance of $Template!

Solution: Use the method *.PSObject.Copy() to create a shallow(!) copy (see also):

#
# The right way.
#

$Template = [PSCustomObject] @{
    ID = $null
}

$A = $Template.PSObject.Copy()    # <-- !!!
$A.ID = '123'

$B = $Template.PSObject.Copy()    # <-- !!!
$B.ID = 'ABC'

"Expected: '123'     -> Real value: '$($A.ID)'"
"Expected: 'ABC'     -> Real value: '$($B.ID)'"
"Expected: (nothing) -> Real value: '$($Template.ID)'"

And this time, the output is as expected:

Expected: '123'     -> Real value: '123'
Expected: 'ABC'     -> Real value: 'ABC'
Expected: (nothing) -> Real value: ''

How to check whether the PSCustomObject is empty (without properties)1

This tests (returns $true or $false) if it is a regular empty object, and also tests if it’s a PSCustomObject, without any properties:

[bool] (Get-Member -InputObject $SomeObject -MemberType NoteProperty -ErrorAction Ignore)

Note: The -ErrorAction Ignore is needed, so that no exception is thrown (additional to the boolean result) when a simple $null object is checked.

Example

$o1 = [PSCustomObject] @{ Something = "Value" }
$o2 = [PSCustomObject] @{ Something = "" }
$o3 = [PSCustomObject] @{ Something = $null }
$o4 = [PSCustomObject] @{ }                     # Valid PSCustomObject, but propertyless.
$o5 = $null                                     # Regular NULL object.
    
"o1: " + ([bool] (Get-Member -InputObject $o1 -MemberType NoteProperty -ErrorAction Ignore)).ToString()
"o2: " + ([bool] (Get-Member -InputObject $o2 -MemberType NoteProperty -ErrorAction Ignore)).ToString()
"o3: " + ([bool] (Get-Member -InputObject $o3 -MemberType NoteProperty -ErrorAction Ignore)).ToString()
"o4: " + ([bool] (Get-Member -InputObject $o4 -MemberType NoteProperty -ErrorAction Ignore)).ToString()
"o5: " + ([bool] (Get-Member -InputObject $o5 -MemberType NoteProperty -ErrorAction Ignore)).ToString()

That should produce an output like this:

o1: True
o2: True
o3: True
o4: False
o5: False

Looping through the properties

By using PSObject.Properties (Source):

$YourPSCustomObject.PSObject.Properties | ForEach-Object { "$($_.Name) = $($_.Value)" }

Performance

One tip is to use the new() constructor to improve the performance (see version B: in the results below):

$PSCO = [PSCustomObject] @{ ID = $null }
"A: {0} TotalMilliseconds" -f (measure-command { $A = $PSCO.PSObject.Copy() }).TotalMilliseconds
"B: {0} TotalMilliseconds" -f (measure-command { $B = [PSCustomObject]::new() }).TotalMilliseconds

Measuring with measure-command showed quite a difference, especially on the first run and when comparing Powershell 5.1 with 7.2: (Note that I also had a few extraordinary values, where it was 18 or 11 Milliseconds on the very first run…)

Windows Powershell 5.1.19041.4170

Run #1
A: 9,4621 TotalMilliseconds
B: 3,321 TotalMilliseconds

Run #2
A: 0,2795 TotalMilliseconds
B: 0,0406 TotalMilliseconds

Run #3
A: 1,6811 TotalMilliseconds
B: 0,0421 TotalMilliseconds

Powershell 7.4.2

Run #1
A: 8,421 TotalMilliseconds
B: 3,3449 TotalMilliseconds

Run #2
A: 0,4269 TotalMilliseconds
B: 0,0681 TotalMilliseconds

Run #3
A: 0,2715 TotalMilliseconds
B: 0,0622 TotalMilliseconds

  1. Based on a particular post (but not the one that was accepted as the best answer) from “What operator should be used to detect an empty psobject?”. ↩︎