@@ -627,6 +627,179 @@ Unwrapped = 42.92 ms
627627The unwrapped example is ** 372 times faster** . Also, notice that the first implementation requires
628628the ** Append** parameter, which isn't required for the later implementation.
629629
630+ ## Use OrderedDictionary to dynamically create new objects
631+
632+ There are situations where we may need to dynamically create objects based on some input, the
633+ perhaps most commonly used way to create a new ** PSObject** and then add new properties using the
634+ ` Add-Member ` cmdlet. The performance cost for small collections using this technique may be
635+ negligible however it can become very noticeable for big collections. In that case, the recommended
636+ approach is to use an ` [OrderedDictionary] ` and then convert it to a ** PSObject** using the
637+ ` [pscustomobject] ` type accelerator. For more information, see the _ Creating ordered dictionaries_
638+ section of [ about_Hash_Tables] [ 19 ] .
639+
640+ Assume you have the following API response stored in the variable ` $json ` .
641+
642+ ``` json
643+ {
644+ "tables" : [
645+ {
646+ "name" : " PrimaryResult" ,
647+ "columns" : [
648+ { "name" : " Type" , "type" : " string" },
649+ { "name" : " TenantId" , "type" : " string" },
650+ { "name" : " count_" , "type" : " long" }
651+ ],
652+ "rows" : [
653+ [ " Usage" , " 63613592-b6f7-4c3d-a390-22ba13102111" , " 1" ],
654+ [ " Usage" , " d436f322-a9f4-4aad-9a7d-271fbf66001c" , " 1" ],
655+ [ " BillingFact" , " 63613592-b6f7-4c3d-a390-22ba13102111" , " 1" ],
656+ [ " BillingFact" , " d436f322-a9f4-4aad-9a7d-271fbf66001c" , " 1" ],
657+ [ " Operation" , " 63613592-b6f7-4c3d-a390-22ba13102111" , " 7" ],
658+ [ " Operation" , " d436f322-a9f4-4aad-9a7d-271fbf66001c" , " 5" ]
659+ ]
660+ }
661+ ]
662+ }
663+ ```
664+
665+ Now, suppose you want to export this data to a CSV. First you need to create new objects and add the
666+ properties and values using the ` Add-Member ` cmdlet.
667+
668+ ``` powershell
669+ $data = $json | ConvertFrom-Json
670+ $columns = $data.tables.columns
671+ $result = foreach ($row in $data.tables.rows) {
672+ $obj = [psobject]::new()
673+ $index = 0
674+
675+ foreach ($column in $columns) {
676+ $obj | Add-Member -MemberType NoteProperty -Name $column.name -Value $row[$index++]
677+ }
678+
679+ $obj
680+ }
681+ ```
682+
683+ Using an ` OrderedDictionary ` , the code can be translated to:
684+
685+ ``` powershell
686+ $data = $json | ConvertFrom-Json
687+ $columns = $data.tables.columns
688+ $result = foreach ($row in $data.tables.rows) {
689+ $obj = [ordered]@{}
690+ $index = 0
691+
692+ foreach ($column in $columns) {
693+ $obj[$column.name] = $row[$index++]
694+ }
695+
696+ [pscustomobject] $obj
697+ }
698+ ```
699+
700+ In both cases the ` $result ` output would be same:
701+
702+ ``` output
703+ Type TenantId count_
704+ ---- -------- ------
705+ Usage 63613592-b6f7-4c3d-a390-22ba13102111 1
706+ Usage d436f322-a9f4-4aad-9a7d-271fbf66001c 1
707+ BillingFact 63613592-b6f7-4c3d-a390-22ba13102111 1
708+ BillingFact d436f322-a9f4-4aad-9a7d-271fbf66001c 1
709+ Operation 63613592-b6f7-4c3d-a390-22ba13102111 7
710+ Operation d436f322-a9f4-4aad-9a7d-271fbf66001c 5
711+ ```
712+
713+ The latter approach becomes exponentially more efficient as the number of objects and member
714+ properties increases.
715+
716+ Here is a performance comparison of three techniques for creating objects with 5 properties:
717+
718+ ``` powershell
719+ $tests = @{
720+ '[ordered] into [pscustomobject] cast' = {
721+ param([int] $iterations, [string[]] $props)
722+
723+ foreach ($i in 1..$iterations) {
724+ $obj = [ordered]@{}
725+ foreach ($prop in $props) {
726+ $obj[$prop] = $i
727+ }
728+ [pscustomobject] $obj
729+ }
730+ }
731+ 'Add-Member' = {
732+ param([int] $iterations, [string[]] $props)
733+
734+ foreach ($i in 1..$iterations) {
735+ $obj = [psobject]::new()
736+ foreach ($prop in $props) {
737+ $obj | Add-Member -MemberType NoteProperty -Name $prop -Value $i
738+ }
739+ $obj
740+ }
741+ }
742+ 'PSObject.Properties.Add' = {
743+ param([int] $iterations, [string[]] $props)
744+
745+ # this is how, behind the scenes, `Add-Member` attaches
746+ # new properties to our PSObject.
747+ # Worth having it here for performance comparison
748+
749+ foreach ($i in 1..$iterations) {
750+ $obj = [psobject]::new()
751+ foreach ($prop in $props) {
752+ $obj.PSObject.Properties.Add(
753+ [psnoteproperty]::new($prop, $i))
754+ }
755+ $obj
756+ }
757+ }
758+ }
759+
760+ $properties = 'Prop1', 'Prop2', 'Prop3', 'Prop4', 'Prop5'
761+
762+ 1kb, 10kb, 100kb | ForEach-Object {
763+ $groupResult = foreach ($test in $tests.GetEnumerator()) {
764+ $ms = Measure-Command { & $test.Value -iterations $_ -props $properties }
765+
766+ [pscustomobject]@{
767+ Iterations = $_
768+ Test = $test.Key
769+ TotalMilliseconds = [math]::Round($ms.TotalMilliseconds, 2)
770+ }
771+
772+ [GC]::Collect()
773+ [GC]::WaitForPendingFinalizers()
774+ }
775+
776+ $groupResult = $groupResult | Sort-Object TotalMilliseconds
777+ $groupResult | Select-Object *, @{
778+ Name = 'RelativeSpeed'
779+ Expression = {
780+ $relativeSpeed = $_.TotalMilliseconds / $groupResult[0].TotalMilliseconds
781+ [math]::Round($relativeSpeed, 2).ToString() + 'x'
782+ }
783+ }
784+ }
785+ ```
786+
787+ And these are the results:
788+
789+ ``` output
790+ Iterations Test TotalMilliseconds RelativeSpeed
791+ ---------- ---- ----------------- -------------
792+ 1024 [ordered] into [pscustomobject] cast 22.00 1x
793+ 1024 PSObject.Properties.Add 153.17 6.96x
794+ 1024 Add-Member 261.96 11.91x
795+ 10240 [ordered] into [pscustomobject] cast 65.24 1x
796+ 10240 PSObject.Properties.Add 1293.07 19.82x
797+ 10240 Add-Member 2203.03 33.77x
798+ 102400 [ordered] into [pscustomobject] cast 639.83 1x
799+ 102400 PSObject.Properties.Add 13914.67 21.75x
800+ 102400 Add-Member 23496.08 36.72x
801+ ```
802+
630803## Related links
631804
632805- [ ` $null ` ] [ 03 ]
@@ -643,6 +816,7 @@ the **Append** parameter, which isn't required for the later implementation.
643816- [ ` [StreamReader] ` ] [ 15 ]
644817- [ ` [File]::ReadLines() ` method] [ 16 ]
645818- [ Write-Host] [ 17 ]
819+ - [ Add-Member] [ 18 ]
646820
647821<!-- Link reference definitions -->
648822[ 02 ] : ../../learn/deep-dives/everything-about-hashtable.md
@@ -661,3 +835,5 @@ the **Append** parameter, which isn't required for the later implementation.
661835[ 15 ] : xref:System.IO.StreamReader
662836[ 16 ] : xref:System.IO.File.ReadLines*
663837[ 17 ] : xref:Microsoft.PowerShell.Utility.Write-Host
838+ [ 18 ] : xref:Microsoft.PowerShell.Utility.Add-Member
839+ [ 19 ] : /powershell/module/microsoft.powershell.core/about/about_hash_tables#creating-ordered-dictionaries
0 commit comments