Can anyone explain what appears to be odd behaviour when using Add-Member to add an attribute to a PSCustomObject object? For some reason, once you've added the member, the object is represented like a hashtable when displayed, even though it's still a PSCustomObject, e.g.:
Create a simple object:
[PSCustomObject] $test = New-Object -TypeName PSCustomObject -Property @{ a = 1; b = 2; c = 3; }
Check its type:
$test.GetType();
...which returns:
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False PSCustomObject System.Object
Then get its contents:
$test;
...which returns:
c b a
- - -
3 2 1
Add a property:
Add-Member -InputObject $test -MemberType NoteProperty -Name d -Value 4 -TypeName Int32;
Confirm its type hasn't changed:
$test.GetType();
...which returns:
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False PSCustomObject System.Object
Finally, get its contents again:
$test;
...which returns:
@{c=3; b=2; a=1; d=4}
Whereas I was hoping to get:
c d b a
- - - -
3 4 2 1
Any thoughts would be welcome, as I've been picking away at this for ages.
Many thanks
Omit the -TypeName Int32
argument in the Add-Member
call: it does not specify the -Value
argument's type.
# Do NOT use -TypeName, unless you want to assign the custom
# object stored in $test a specific ETS type identity.
Add-Member -InputObject $test -MemberType NoteProperty -Name d -Value 4
Note that Int32
([int]
) is implied for an unquoted argument that can be interpreted as a decimal number that fits into the [int]
range, such as 4
.
If you do need to specify the type explicitly, use a cast in an expression, e.g. ... -Value ([long] 4)
As for what you tried:
-TypeName Int32
assigns the full name of this type, System.Int32
, as the first entry in the list of ETS type names associated with the input object. (ETS is PowerShell's Extended Type System.)
You can see this list by accessing the intrinsic .pstypenames
property (and you also see its first entry in the header of Get-Member
's output):
PS> $test.pstypenames
System.Int32
System.Management.Automation.PSCustomObject
System.Object
As you can see, System.Int32
was inserted before the object's true .NET type identity (System.Management.Automation.PSCustomObject
); the remaining entries show the inheritance hierarchy.
Such ETS type names are normally used to associated custom behaviors with objects, irrespective of their true .NET type identities, notably with respect to extending the type (see about_Types.ps1xml) or associating custom display formatting with it (see about_Format.ps1xml).
Since PowerShell then thought that your [pscustomobject]
was of type [int]
(System.Int32
), it applied its usual output formatting for that type to it, which in essence means calling .ToString()
on the instance.
Calling .ToString()
on a [pscustomobject]
instance results in the hashtable-like representation you saw, as the following example demonstrates:
[pscustomobject]
instance ([pscustomobject] @{ ... }
), which is not only more efficient than a New-Object
call but also preserves the property order.PS> ([pscustomobject] @{ a = 'foo bar'; b = 2}).psobject.ToString()
@{a=foo bar; b=2}
Note the use of the intrinsic .psobject
property, which is necessary to work around a long-standing bug where direct .ToString()
calls on [pscustomobject]
instances return the empty string - see GitHub issue #6163.