PSCustomObject mystery

I’m writing a function that returns several PSCustomObjects, but the results are unexpected (to me). Below is a mock function that exhibits the same behavior.

Function New-CoolObject
{
    $CoolObject     = [PSCustomObject] @{
        Name        = "CoolObject"
        CoolString  = $Null
    }

    $CoolObject.CoolString = "Cool"
    $CoolObject

    $CoolObject.CoolString = "Uncool"
    $CoolObject
}

What I want to do is to return two separate objects that are similar, but with some different values. The real function returns a large amount of properties that I was hoping I wouldn’t have to define in multiple places. Running the function seems to do what I want:

PS C:\Cool> New-CoolObject

Name       CoolString
----       ----------
CoolObject Cool
CoolObject Uncool

However, when using the same function to populate a variable, the results are different:

PS C:\Cool> $Result = New-CoolObject
PS C:\Cool> $Result

Name       CoolString
----       ----------
CoolObject Uncool
CoolObject Uncool

I get the feeling that I’m actually not returning two separate objects, but I don’t understand why. Can anyone explain?

Very interesting. I’ve been able to repro it.
Which PowerShell version are you using ? 5.1.14393.953 in my case.

My investigation,

  1. Changing the assignment (using Set-Variable instead of X=), and inserting a quick-and-dirty Write-Host in the pipeline,
    shows the objects are created and passed properly, but the assignment gets wrong results :
    New-CoolObject | % {Write-host $; $} | set-variable X

  2. Adding a Trace-Command might provide some extra info : (I’ll check later, but let’s try in parallel)
    trace-command { New-CoolObject | % {Write-host $; $} | set-variable X } -pshost -name *

Hello, and thank you for the reply!

I’m using the same version of PowerShell: 5.1.14393.953

I’m not experienced with interpreting the results from Trace-Command, but it looks to me like Set-Variable is at least getting the right values:

...
DEBUG: ParameterBinding Information: 0 : BIND PIPELINE object to parameters: [Set-Variable]
DEBUG: ParameterBinding Information: 0 :     PIPELINE object TYPE = [System.Management.Automation.PSCustomObject]
DEBUG: ParameterBinding Information: 0 :     RESTORING pipeline parameter's original values
DEBUG: ParameterBinding Information: 0 :     Parameter [Value] PIPELINE INPUT ValueFromPipeline NO COERCION
DEBUG: MemberResolution Information: 0 :     Enumeration Start
DEBUG: MemberResolution Information: 0 :         Generating the total list of members
DEBUG: MemberResolution Information: 0 :             Type table members: 0.
DEBUG: MemberResolution Information: 0 :             Adapted members: 0.
DEBUG: MemberResolution Information: 0 :         Enumerating PSObject with type "System.Management.Automation.PSCustomObject".
DEBUG: MemberResolution Information: 0 :         PSObject instance members: 2
DEBUG: ParameterBinderController Information: 0 :  WriteLine       Adding PipelineParameter name=Value; value=@{Name=CoolObject; CoolString=Cool}
...
DEBUG: ParameterBinding Information: 0 : BIND PIPELINE object to parameters: [Set-Variable]
DEBUG: ParameterBinding Information: 0 :     PIPELINE object TYPE = [System.Management.Automation.PSCustomObject]
DEBUG: ParameterBinding Information: 0 :     RESTORING pipeline parameter's original values
DEBUG: ParameterBinding Information: 0 :     Parameter [Value] PIPELINE INPUT ValueFromPipeline NO COERCION
DEBUG: MemberResolution Information: 0 :     Enumeration Start
DEBUG: MemberResolution Information: 0 :         Generating the total list of members
DEBUG: MemberResolution Information: 0 :             Type table members: 0.
DEBUG: MemberResolution Information: 0 :             Adapted members: 0.
DEBUG: MemberResolution Information: 0 :         Enumerating PSObject with type "System.Management.Automation.PSCustomObject".
DEBUG: MemberResolution Information: 0 :         PSObject instance members: 2
DEBUG: ParameterBinderController Information: 0 :  WriteLine       Adding PipelineParameter name=Value; value=@{Name=CoolObject; CoolString=Uncool}
...

Try this:

Function New-CoolObject {

    $CoolObject     = [PSCustomObject] @{
        Name        = 'CoolObject'
        CoolString  = $Null
    }

    $CoolObject.CoolString = 'Cool'
    $CoolObject

    Remove-Variable CoolObject
    $CoolObject.CoolString = 'Uncool'
    $CoolObject
}

The second assignment $CoolObject.CoolString = ‘Uncool’ reaches back and changes the previous assigned value since you’re manipulating the same $CoolObject. To fix this and prevent the reach back, get a new $CoolObject by adding Remove-Variable CoolObject before every $CoolObject assignment…

I could be wrong, but when you call the function on it’s own, it will display the result of CoolString each time it’s referenced but when you call it to be sent to a variable, only the last call of CoolString gets stored. I may not be explaining that correctly.

that happen because you have only one object
when your function return $CoolObject it return the same object twice because object is reference type
you can try this:

 C:\> $Result = New-CoolObject
 C:\> $Result

Name       CoolString
----       ----------
CoolObject Uncool
CoolObject Uncool

 C:\> $Result[0].CoolString='Very Cool'
 C:\> $Result

Name       CoolString
----       ----------
CoolObject Very Cool
CoolObject Very Cool

Invoking New-CoolObject without assigning to variable in fact just display object current state on the screen (host)
and you see two different view of the same object because of “snapshot” effect
If you want to get two different objects you can clone first object

Function New-CoolObject
{
    $CoolObject     = [PSCustomObject] @{
        Name        = "CoolObject"
        CoolString  = $Null
    }

    $CoolObject.CoolString = "Cool"
    $CoolObject

    # clone can be saved into the same or different variable. I use the same
    $CoolObject = $CoolObject.PSObject.Copy()

    $CoolObject.CoolString = "Uncool"
    $CoolObject
}
$Result = New-CoolObject
$Result

Name       CoolString
----       ----------
CoolObject Cool
CoolObject Uncool
 C:\>

No mystery… you are creating one object.

Function New-CoolObjects{

 param($objects)

foreach($i in $objects){

    [PScustomobject]@{
        Name = "CoolObject"
        CoolString = $i
    }


}
    

}


new-coolobjects 1,2

Thanks everyone! I guess the mystery is solved. :wink:

Looks like the PSObject.Copy() method is the way to go in my case.