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:
- General overview of what a PSCustomObject is and what it is capable.
- On the difference between a hash table and a PSCustomObject: They are similar in some aspects, but generally not the same.
- A thread on the same topic as above on StackOverflow
- … which also includes this comment on PSObject and PSCustomObject:
In other words, a PSObject is an object that you can add methods and properties to after you’ve created it. […]
[PSCustomObject] is a type accelerator. It constructs a PSObject, but does so in a way that results in hash table keys becoming properties. PSCustomObject isn’t an object type per se – it’s a process shortcut. … PSCustomObject is a placeholder that’s used when PSObject is called with no constructor parameters.
- On Object and PSObject:
Object is the parent class for all .NET classes, and PSObject inherits from Object, but is tailored specifically for PS use.
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
-
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?”. ↩︎
Film & Television (54)
How To (63)
Journal (17)
Miscellaneous (4)
News & Announcements (21)
On Software (12)
Projects (26)