Lets say I have a hashtable:
$HashTable = @{}
Now, when I want to add something to the Hashtable I usually do it like this:
$HashTable.Something = 'Something'
When I test $hashTable it will show this output:
$HashTable
Name Value
---- -----
Something Something
A Colleague of mine used Add-Member in stead:
$HashTable | Add-Member -NotePropertyName 'SomethingAddMember' -NotePropertyValue 'SomethingAddMember'
It works, but when you look at $HashTable it will still only show this:
$HashTable
Name Value
---- -----
Something Something
When I run this $HashTable.SomethingAddMember it will show up:
$HashTable.SomethingAddMember
Why is it not showing up in the HashTable itself? When I convert it to Json it's there again! How confusing can this be, or am I missing something??
$HashTable | ConvertTo-Json
{
"Something": "Something",
"SomethingAddMember": "SomethingAddMember"
}
Is it because Add-Member adds a new property to the HashTable instead of the Keys with Values? How can this happen like this?
There's good information here already, but let me break it down conceptually:
The entries of a hashtable (key-value pairs) are distinct from its .NET type's members (properties, methods, events).
Hashtable entries are data, accessible via the System.Collections.Hashtable
type's members, notably the .Keys
and .Values
properties and the parameterized .Item
property, which PowerShell (like C#) surfaces syntactically via index syntax:
[...]
, as syntactic sugar for .Item(...)
; e.g.,@{ foo = 42 }['foo']
is a shortcut to @{ foo = 42 }.Item('foo')
Additionally, PowerShell allows you to access entry values with dot notation, using .
, the member-access operator - even though what you're accessing isn't a member, but a hashtable entry; e.g.
@{ foo = 42 }.foo
is equivalent to @{ foo = 42 }['foo']
.
Note that the inverse is not true: You can not use index notation to access an object's true properties: e.g., ([pscustomobject] @{ foo = 42 })['foo']
does not work as intended: by default, it quietly evaluates to $null
, and with Set-StrictMode
-Version 2
or higher in effect it causes an error - see this post.
In other words:
PowerShell's .
(dot notation) can access both:
[ordered]
hashtables - and, more generally, types that implement System.Collections.IDictionary
interface or its generic counterpart, System.Collections.Generic.IDictionary<TKey, TValue>
.[1]If there is a name conflict, the entry access takes precedence - e.g, @{ Count = 42 }.Count
returns 42
rather than the value of the type-native Count
property reflecting the number of entries; to access the latter, prefix the property name with get_
and invoke that as a method: @{ Count = 42 }.get_Count()
returns 1
, the entry count.
Note: The members that Add-Member
adds to an object aren't technically .NET type members (type-native properties and methods), but in PowerShell they act as such. (Such members, which are a feature of PowerShell's ETS (Extended Type System), are known to PowerShell only, and are of necessity maintained separately from the object they're associated with.)
In other words: Your Add-Member
call added a new property (NoteProperty
) rather than an entry to the hashtable instance, alongside the instance's type-native properties such as .Count
, .Keys
, and .Values
.
With respect to surfacing entries vs. members:
Since a hashtable's default output formatting show its entries only, any (ETS) members you've added won't show in the output - you'd have to access the .SomethingAddMember
property explicitly; you can also see it via reflection, using Get-Member
($Hashtable | Get-Member SomethingAddMember
)
While ETS properties generally rarely surface, they do in serialization contexts, such as when using ConvertTo-Json
, and in the case of hashtables (dictionaries) you get a mix of ETS properties and entries, which is what you saw:
In many contexts, PowerShell treat hashtables (dictionaries) as if they were regular objects, where the hashtable's entries - rather than the hashtable object's true type-native properties - are used in lieu of properties.
However, ETS properties are still included,[2] resulting in the aforementioned mix.
[1] Strangely, dot notation does not work if the key type happens to be [object]
, though that isn't typical, given that the very point of a generic type/interface is to use specific types - see GitHub issue #15997.
[2] Since PowerShell 7.2, ConvertTo-Json
no longer serializes ETS properties for [datetime]
and [string]
instances (see GitHub PR #15665), but other serialization cmdlets such as ConvertTo-Csv
still do.
On a related note, as of PowerShell 7.2, when ConvertTo-Json
does include ETS properties, it accidentally uses the type-native property's value if the ETS property happens to override the type-native one - see GitHub issue #13998.