Skip to content

Commit

Permalink
Use OrderedDictionary to dynamically create new objects (#11090)
Browse files Browse the repository at this point in the history
* updating links to relative. fixing typo
* More editorial changes

---------

Co-authored-by: Sean Wheeler <sean.wheeler@microsoft.com>
  • Loading branch information
santisq and sdwheeler committed May 9, 2024
1 parent 6557952 commit 4b8cfcc
Showing 1 changed file with 176 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,179 @@ Unwrapped = 42.92 ms
The unwrapped example is **372 times faster**. Also, notice that the first implementation requires
the **Append** parameter, which isn't required for the later implementation.

## Use OrderedDictionary to dynamically create new objects

There are situations where we may need to dynamically create objects based on some input, the
perhaps most commonly used way to create a new **PSObject** and then add new properties using the
`Add-Member` cmdlet. The performance cost for small collections using this technique may be
negligible however it can become very noticeable for big collections. In that case, the recommended
approach is to use an `[OrderedDictionary]` and then convert it to a **PSObject** using the
`[pscustomobject]` type accelerator. For more information, see the _Creating ordered dictionaries_
section of [about_Hash_Tables][19].

Assume you have the following API response stored in the variable `$json`.

```json
{
"tables": [
{
"name": "PrimaryResult",
"columns": [
{ "name": "Type", "type": "string" },
{ "name": "TenantId", "type": "string" },
{ "name": "count_", "type": "long" }
],
"rows": [
[ "Usage", "63613592-b6f7-4c3d-a390-22ba13102111", "1" ],
[ "Usage", "d436f322-a9f4-4aad-9a7d-271fbf66001c", "1" ],
[ "BillingFact", "63613592-b6f7-4c3d-a390-22ba13102111", "1" ],
[ "BillingFact", "d436f322-a9f4-4aad-9a7d-271fbf66001c", "1" ],
[ "Operation", "63613592-b6f7-4c3d-a390-22ba13102111", "7" ],
[ "Operation", "d436f322-a9f4-4aad-9a7d-271fbf66001c", "5" ]
]
}
]
}
```

Now, suppose you want to export this data to a CSV. First you need to create new objects and add the
properties and values using the `Add-Member` cmdlet.

```powershell
$data = $json | ConvertFrom-Json
$columns = $data.tables.columns
$result = foreach ($row in $data.tables.rows) {
$obj = [psobject]::new()
$index = 0
foreach ($column in $columns) {
$obj | Add-Member -MemberType NoteProperty -Name $column.name -Value $row[$index++]
}
$obj
}
```

Using an `OrderedDictionary`, the code can be translated to:

```powershell
$data = $json | ConvertFrom-Json
$columns = $data.tables.columns
$result = foreach ($row in $data.tables.rows) {
$obj = [ordered]@{}
$index = 0
foreach ($column in $columns) {
$obj[$column.name] = $row[$index++]
}
[pscustomobject] $obj
}
```

In both cases the `$result` output would be same:

```output
Type TenantId count_
---- -------- ------
Usage 63613592-b6f7-4c3d-a390-22ba13102111 1
Usage d436f322-a9f4-4aad-9a7d-271fbf66001c 1
BillingFact 63613592-b6f7-4c3d-a390-22ba13102111 1
BillingFact d436f322-a9f4-4aad-9a7d-271fbf66001c 1
Operation 63613592-b6f7-4c3d-a390-22ba13102111 7
Operation d436f322-a9f4-4aad-9a7d-271fbf66001c 5
```

The latter approach becomes exponentially more efficient as the number of objects and member
properties increases.

Here is a performance comparison of three techniques for creating objects with 5 properties:

```powershell
$tests = @{
'[ordered] into [pscustomobject] cast' = {
param([int] $iterations, [string[]] $props)
foreach ($i in 1..$iterations) {
$obj = [ordered]@{}
foreach ($prop in $props) {
$obj[$prop] = $i
}
[pscustomobject] $obj
}
}
'Add-Member' = {
param([int] $iterations, [string[]] $props)
foreach ($i in 1..$iterations) {
$obj = [psobject]::new()
foreach ($prop in $props) {
$obj | Add-Member -MemberType NoteProperty -Name $prop -Value $i
}
$obj
}
}
'PSObject.Properties.Add' = {
param([int] $iterations, [string[]] $props)
# this is how, behind the scenes, `Add-Member` attaches
# new properties to our PSObject.
# Worth having it here for performance comparison
foreach ($i in 1..$iterations) {
$obj = [psobject]::new()
foreach ($prop in $props) {
$obj.PSObject.Properties.Add(
[psnoteproperty]::new($prop, $i))
}
$obj
}
}
}
$properties = 'Prop1', 'Prop2', 'Prop3', 'Prop4', 'Prop5'
1kb, 10kb, 100kb | ForEach-Object {
$groupResult = foreach ($test in $tests.GetEnumerator()) {
$ms = Measure-Command { & $test.Value -iterations $_ -props $properties }
[pscustomobject]@{
Iterations = $_
Test = $test.Key
TotalMilliseconds = [math]::Round($ms.TotalMilliseconds, 2)
}
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
}
$groupResult = $groupResult | Sort-Object TotalMilliseconds
$groupResult | Select-Object *, @{
Name = 'RelativeSpeed'
Expression = {
$relativeSpeed = $_.TotalMilliseconds / $groupResult[0].TotalMilliseconds
[math]::Round($relativeSpeed, 2).ToString() + 'x'
}
}
}
```

And these are the results:

```output
Iterations Test TotalMilliseconds RelativeSpeed
---------- ---- ----------------- -------------
1024 [ordered] into [pscustomobject] cast 22.00 1x
1024 PSObject.Properties.Add 153.17 6.96x
1024 Add-Member 261.96 11.91x
10240 [ordered] into [pscustomobject] cast 65.24 1x
10240 PSObject.Properties.Add 1293.07 19.82x
10240 Add-Member 2203.03 33.77x
102400 [ordered] into [pscustomobject] cast 639.83 1x
102400 PSObject.Properties.Add 13914.67 21.75x
102400 Add-Member 23496.08 36.72x
```

## Related links

- [`$null`][03]
Expand All @@ -643,6 +816,7 @@ the **Append** parameter, which isn't required for the later implementation.
- [`[StreamReader]`][15]
- [`[File]::ReadLines()` method][16]
- [Write-Host][17]
- [Add-Member][18]

<!-- Link reference definitions -->
[02]: ../../learn/deep-dives/everything-about-hashtable.md
Expand All @@ -661,3 +835,5 @@ the **Append** parameter, which isn't required for the later implementation.
[15]: xref:System.IO.StreamReader
[16]: xref:System.IO.File.ReadLines*
[17]: xref:Microsoft.PowerShell.Utility.Write-Host
[18]: xref:Microsoft.PowerShell.Utility.Add-Member
[19]: /powershell/module/microsoft.powershell.core/about/about_hash_tables#creating-ordered-dictionaries

0 comments on commit 4b8cfcc

Please sign in to comment.