powershellselect-object

Powershell ExpandProperty changes source object - why?


Lets say I have an array of objects with some properties, one of which is itself an array of objects. I want to 'flatten' this structure & report each of the embedded objects alongside selected attributes from its parent object.

ExpandProperty seems ideal, but why is the expanded object updated on the source object? and how to prevent this?

Here's a simple test setup:

$json = '[{"Name":"James","Age":42,"Books":[{"Title":"Consider Phlebas","Author":"Iain M. Banks"},{"Title":"The Player of Games","Author":"Iain M. Banks"}]},{"Name":"Susan","Age":36, "Books":[{"Title":"The Three Body Problem","Author":"Cixin Liu"},{"Title":"The Dark Forest","Author":"Cixin Liu"}]}]'
$obj = $json | ConvertFrom-Json
$obj |FT

Name  Age Books                                                                                                
----  --- -----                                                                                                
James  42 {@{Title=Consider Phlebas; Author=Iain M. Banks}, @{Title=The Player of Games; Author=Iain M. Banks}}
Susan  36 {@{Title=The Three Body Problem; Author=Cixin Liu}, @{Title=The Dark Forest; Author=Cixin Liu}}

This statement produces exactly the output I am after

$obj | Select-Object -Property Name -ExpandProperty books

Title                  Author        Name 
-----                  ------        ---- 
Consider Phlebas       Iain M. Banks James
The Player of Games    Iain M. Banks James
The Three Body Problem Cixin Liu     Susan
The Dark Forest        Cixin Liu     Susan

But the sting in the tail is my original object is modified (the Name property from the parent object has been added to each embedded book object) & if I run the last command again I get the error Select-Object : The property cannot be processed because the property "Name" already exists.

$obj | FT

Name  Age Books                                                                                                                        
----  --- -----                                                                                                                        
James  42 {@{Title=Consider Phlebas; Author=Iain M. Banks; Name=James}, @{Title=The Player of Games; Author=Iain M. Banks; Name=James}}
Susan  36 {@{Title=The Three Body Problem; Author=Cixin Liu; Name=Susan}, @{Title=The Dark Forest; Author=Cixin Liu; Name=Susan}}      

This just seems weird. I thought select-object created a new object as output from each input object, but ExpandProperty appears to update the source object. How can I get the output I want without modifying the input object, and such that I can run the statement again without error


Solution

  • The issue is explained in the -ExpandProperty description, in Note and Warning. Basically, when -Property and -ExpandProperty are used together, the cmdlet will attach a NoteProperty to the input objects, this is why a consecutive Select-Object call with the same arguments throws an error, the properties are already added.

    If you want to avoid this issue you could first:

    $obj | Select-Object -Property Name -ExpandProperty books
    

    And then, in consecutive calls refer to just:

    $obj.Books
    

    However, if you do not want to touch the original objects then the extremely cumbersome solution can be to use a calculated property with help of loop:

    $obj | ForEach-Object { $i = $_; $_.Books } | Select-Object *, @{ N='Name'; E={ $i.Name }}
    

    A little function that can overcome the issue with Select-Object:

    function expand {
        param(
            [Parameter(ValueFromPipeline)]
            [object] $InputObject,
    
            [Parameter(Position = 0)]
            [string] $ExpandProperty,
    
            [Parameter(Position = 1)]
            [ValidateNotNull()]
            [SupportsWildcards()]
            [string[]] $Property
        )
    
        begin {
            $dict = [ordered]@{}
            $Property = [System.Collections.Generic.HashSet[string]]::new(
                $Property, [System.StringComparer]::OrdinalIgnoreCase)
        }
        process {
            $propertyInfo = $InputObject.PSObject.Properties
            foreach ($object in $InputObject.$ExpandProperty) {
                foreach ($prop in $object.PSObject.Properties) {
                    $dict[$prop.Name] = $prop.Value
                }
    
                foreach ($prop in $Property) {
                    foreach ($match in $propertyInfo.Match($prop)) {
                        if ($match.Name -ne $ExpandProperty) {
                            $dict[$match.Name] = $match.Value
                        }
                    }
                }
    
                [pscustomobject] $dict
            }
    
            $dict.Clear()
        }
    }
    

    Then you can use it like:

    PS ..\> $obj | expand books *
    
    # Title                  Author        Name  Age
    # -----                  ------        ----  ---
    # Consider Phlebas       Iain M. Banks James  42
    # The Player of Games    Iain M. Banks James  42
    # The Three Body Problem Cixin Liu     Susan  36
    # The Dark Forest        Cixin Liu     Susan  36