I have a workflow that is pretty picky with the PSCustomObjects it receives. I often use Select-Object
to get the properties from the objects I want to keep and sometimes I need to convert some of the values to more usable formats. The easiest way to do this is to use the @{Name='Name'; Expression = {'Expression'}}
technique.
BUT, that technique messes up the PSCustomObject in a way that blocks my further workflow.
The issue can be reproduced like this:
$object = [pscustomobject]@{
Text = "This is a string"
}
In the output below, the Definition for string is 'string Text=This is a string'
$object | Select-Object * | Get-Member
<# Outputs
TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
Text NoteProperty string Text=This is a string
#>
When adding a new NoteProperty with the Select-Object technique, the Definition is 'System.String Text2=This is a string'
This is what makes my next cmdlet throw.
$WhatHappensToText = $object | Select-Object @{Name='Text'; Expression={$_.Text}}
$WhatHappensToText | Get-Member
<# Outputs
TypeName: Selected.System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
Text NoteProperty System.String Text=This is a string
#>
When stripping the surplus like below, Definition is back to 'string Text2=This is a string'
Exporting to Clixml and re-importing does the same
$WhatHappensToText | ConvertTo-Json | ConvertFrom-Json | Get-Member
<# Outputs
TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
Text NoteProperty string Text=This is a string
#>
If I add the new NoteProperty like this, Definition is 'string Text2=This is a string' as I like it
$object2 = $object | Select-Object *
$object2 | foreach-object {$_ | Add-Member -MemberType NoteProperty -Name Text2 -Value $_.Text}
$object2 | Get-Member
<# Outputs
TypeName: Selected.System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
Text NoteProperty string Text=This is a string
Text2 NoteProperty string Text2=This is a string
#>
I have the following questions:
Why is the @{Name='Name'; Expression = {'Expression'}}
technique adding System.String to the Definition and not string like in the Add-Member scenario?
Is there a way to make the @{Name='Name'; Expression = {'Expression'}}
technique add a string and not a System.String?
Why is the
@{Name='Name'; Expression = {'Expression'}}
technique [i.e., using a calculated property] addingSystem.String
to the Definition and notstring
like in theAdd-Member
scenario?
This is a side effect of the unfortunate fact that the calculated-property technique creates a [psobject]
wrapper around the property value specified.
[psobject]
is meant to be a transparent helper type used behind the scenes, and while such wrappers are typically invisible, they can situationally result in different behavior, such as in the case at hand.
This problematic behavior, along with a list of scenarios where the behavior changes, is discussed in GitHub issue #5579.
Is there a way to make the
@{Name='Name'; Expression = {'Expression'}}
technique add astring
and not aSystem.String
?
This requires avoiding the [psobject]
wrapper, which isn't possible with the calculated-property technique.
Your options are:
Either: Use Add-Member
, as in your question; if you add the -PassThru
switch, the decorated object is passed through, allowing you to use the call as part of a pipeline:
[object]::new() | Add-Member -PassThru Text Hi! | Get-Member
Add-Member
in a pipeline doesn't allow you to determine the property value dynamically, based on each input object. While you can remedy that by wrapping the call in a ForEach-Object
call, the technique below is more efficient.Or: Use the intrinsic psobject
property to add a property to an existing object:
[object]::new() |
ForEach-Object {
# Add a property whose value is derived from the current input object.
$_.psobject.Properties.Add(
[psnoteproperty]::new('Text', "Hash code: $($_.GetHashCode())")
)
$_ # Pass the modified object through.
} |
Get-Member
ForEach-Object
anyway, the property value can be derived dynamically from the current pipeline input object, via the automatic $_
variable, as shown.