1

I am creating a script to parse a CSV file, where I store the content of each indexed field in the CSV as a NoteProperty in a PSCustomObject.

As I parse the file line by line, I add the PSCustomObject to a list type. When I output my list, I want to be able to do something like:

 $list | Format-Table

and have a nice view of each row in the csv file, separated into columns with the heading up top.

Problem

When I add a PSCustomObject to the list, it changes the type of the list to a PSCustomObject. In practice, this has the apparent effect of applying any updates made to that PSCustomObject to every element in the list retroactively.

Here is a sample:

 $list  = [System.Collections.Generic.List[object]]::new()
 $PSCustomObject    = [PSCustomObject]@{ count  = 0}
 Foreach ($i in 1..5) {
    $PSCustomObject.count +=1
    $list.Add($PSCustomObject)
 }

Expected Output:

PS>$list
    count
    -----
        1
        2
        3
        4
        5

Actual Output:

PS>$list
    count
    -----
        5
        5
        5
        5
        5
Question

Is there any way to get the expected output?

Limitations / additional context if it helps

I'm trying to optimize performance, as I may parse very large CSV files. This is why I am stuck with a list. I understand the Add method in lists is faster than recreating an array with += for every row. I am also using a runspace pool to parse each field separately and update the object via $list.$field[$lineNumber] = <field value>, so this is why I need a way to dynamically update the PSCustomObject. A larger view of my code is:

    $out = [hashtable]::Synchronized(@{})
    $out.Add($key, @{'dataSets' = [List[object]]::new() } )    ### $key is the file name as I loop through each csv in a directory.
    $rowTemplate = [PSCustomObject]@{rowNum = 0}

    ### Additional steps to prepare the $out dictionary and some other variables
    ...
    ...
    try {
        ### Skip lines prior to the line with the headers
        $fileParser = [System.IO.StreamReader]$path
        Foreach ( $i in 1..$headerLineNumber ) {
            [void]$fileParser.ReadLine()
        }
        ### Load the file into a variable, and add empty PSCustomObjects for each line as a placeholder.
        while ($null -ne ($line = $fileParser.ReadLine())) { 
            [void]$fileContents.Add($line)
            $rowTemplate.RowNum += 1
            [void]$out.$key.dataSets.Add($rowTemplate)
        }
    }
    finally {$fileParser.close(); $fileParser.dispose()}
    ### Prepare the script block for each runspace
    $runspaceScript = {
        Param( $fileContents, $column, $columnIndex, $delimiter, $key, $out )
        $columnValues   = [System.Collections.ArrayList]::new()
        $linecount      = 0

        Foreach ( $line in $fileContents) {

            $entry = $line.split($delimiter)[$columnIndex]
            $out.$key.dataSets[$linecount].$column = $entry
            $linecount += 1
        }
    }
    ### Instantiate the runspace pool.

PS Version (5.1.19041)

Blaisem
  • 557
  • 8
  • 17
  • 2
    In short: Adding an instance of a .NET reference type such as `[pscustomobject]` to a collection adds a _reference_ to that instance, and if you attempt to re-use that instance and add it multiple times, all such collection entries point to the _very same instance_, which reflects the _latest_ modification performed to it. See the linked duplicate for details and solutions. – mklement0 Nov 03 '21 at 15:57
  • 1
    Thanks for the explanation behind the coding logic. I had no idea it was just a reference, but it makes sense. – Blaisem Nov 03 '21 at 16:14
  • Glad to hear it, Blaisem. As for keeping the order of entries (properties) in the template hashtable: see Mathias' update re use of an `[ordered]` hashtable. – mklement0 Nov 03 '21 at 16:19
  • I saw that with the hashtable. I was hoping to pipe it into format-table to display the keys (or note properties) horizontally, which doesn't seem to work with a hashtable. So I guess I lose the order. – Blaisem Nov 03 '21 at 16:23
  • 1
    If you don't mind paying the conversion cost, you can just cast your _ordered_ hashtable to `[pscustomobject]` (as in the answer) anytime to get the desired formatting; e.g. `$oht = [ordered] @{ one = 1; two = 2; three = 3 }; [pscustomobject] $oht` (you can additionally use `Format-Table` explicitly, but with up to 4 properties you'll get table formatting by default). – mklement0 Nov 03 '21 at 16:30

1 Answers1

2

You're (re-)adding the same object to the list, over and over.

You need to create a new object every time your loop runs, but you can still "template" the objects - just use a hashtable/dictionary instead of a custom object:

# this hashtable will be our object "template"
$scaffold = @{ Count = 0}

foreach($i in 1..5){
  $scaffold.Count += 1
  $newObject = [pscustomobject]$scaffold

  $list.Add($newObject)
}

As mklement0 suggests, if you're templating objects with multiple properties you might want to consider using an ordered dictionary to retain the order of the properties:

# this hashtable will be our object "template"
$scaffold = [ordered]@{ ID = 0; Count = 0}

foreach($i in 1..5){
  $scaffold['ID'] = Get-Random
  $scaffold['Count'] = $i
  $newObject = [pscustomobject]$scaffold

  $list.Add($newObject)
}
Mathias R. Jessen
  • 157,619
  • 12
  • 148
  • 206
  • @mklement0 Share your secret dupe target list :) – Mathias R. Jessen Nov 03 '21 at 15:55
  • :) It's in my private collection of notes, I'm afraid, and is based on the keywords that make sense to me. +1 for the hashtable-as-a-template idea, but I suggest using `[ordered]` to maintain property order. – mklement0 Nov 03 '21 at 16:01
  • Thanks for the lightning fast response. This does indeed fix the sample I posed. For some reason it's not working in my larger script, though. I added ``$newObject = [$PSCustomObject]$rowTemplate`` followed by ``$out.$key.dataSets.Add($newObject)``. Then I output $out.$key.datasets after the while loop, but it has the same erroneous behavior. – Blaisem Nov 03 '21 at 16:03
  • Okay, ``$rowTemplate = $rowTemplate.psobject.Copy()`` instead of ``$newObject = [PSCustomObject]$scaffold`` works. – Blaisem Nov 03 '21 at 16:09
  • @Blaisem Did you perhaps forget to change `$rowTemplate = [PSCustomObject]@{rowNum = 0}` to `$rowTemplate = @{rowNum = 0}`? – Mathias R. Jessen Nov 03 '21 at 16:09
  • 1
    @MathiasR.Jessen I did indeed forget to do that as well. Thanks for the help. Now I have 2 ways to go about it :) – Blaisem Nov 03 '21 at 16:10
  • The reason I didn't use a hashtable btw is because format-table doesn't list the values horizontally then. Although it's true I seem to have lost the order with the PSCustomObject. – Blaisem Nov 03 '21 at 16:13