mirror of
https://github.com/PowerShell/PowerShell
synced 2026-06-08 12:12:50 +00:00
Change ConvertFrom-Json -AsHashtable to use ordered hashtable (#17405)
JSON typically has ordering to make it easier to read. Currently, a Hashtable does not preserve ordering so for complex JSON, the members are not where you would expect in terms of order. This change wraps OrderedDictionary into a OrderedHashtable that derives from Hashtable. This preserves existing compatibility with scripts that expect a Hashtable to be returned, but preserves the order of elements.
This commit is contained in:
@@ -344,7 +344,7 @@ namespace Microsoft.PowerShell.Commands
|
||||
private static Hashtable PopulateHashTableFromJDictionary(JObject entries, out ErrorRecord error)
|
||||
{
|
||||
error = null;
|
||||
Hashtable result = new(entries.Count);
|
||||
OrderedHashtable result = new(entries.Count);
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
// Case sensitive duplicates should normally not occur since JsonConvert.DeserializeObject
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Specialized;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace System.Management.Automation
|
||||
{
|
||||
/// <summary>
|
||||
/// OrderedHashtable is a hashtable that preserves the order of the keys.
|
||||
/// </summary>
|
||||
public sealed class OrderedHashtable : Hashtable, IEnumerable
|
||||
{
|
||||
private readonly OrderedDictionary _orderedDictionary;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OrderedHashtable"/> class.
|
||||
/// </summary>
|
||||
public OrderedHashtable()
|
||||
{
|
||||
_orderedDictionary = new OrderedDictionary();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OrderedHashtable"/> class.
|
||||
/// </summary>
|
||||
/// <param name="capacity">The capacity.</param>
|
||||
public OrderedHashtable(int capacity) : base(capacity)
|
||||
{
|
||||
_orderedDictionary = new OrderedDictionary(capacity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OrderedHashtable"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dictionary">The dictionary to use for initialization.</param>
|
||||
public OrderedHashtable(IDictionary dictionary)
|
||||
{
|
||||
_orderedDictionary = new OrderedDictionary(dictionary.Count);
|
||||
foreach (DictionaryEntry entry in dictionary)
|
||||
{
|
||||
_orderedDictionary.Add(entry.Key, entry.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the number of items in the hashtable.
|
||||
/// </summary>
|
||||
public override int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
return _orderedDictionary.Count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get if the hashtable is a fixed size.
|
||||
/// </summary>
|
||||
public override bool IsFixedSize
|
||||
{
|
||||
get
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get if the hashtable is read-only.
|
||||
/// </summary>
|
||||
public override bool IsReadOnly
|
||||
{
|
||||
get
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get if the hashtable is synchronized.
|
||||
/// </summary>
|
||||
public override bool IsSynchronized
|
||||
{
|
||||
get
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the keys in the hashtable.
|
||||
/// </summary>
|
||||
public override ICollection Keys
|
||||
{
|
||||
get
|
||||
{
|
||||
return _orderedDictionary.Keys;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the values in the hashtable.
|
||||
/// </summary>
|
||||
public override ICollection Values
|
||||
{
|
||||
get
|
||||
{
|
||||
return _orderedDictionary.Values;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value associated with the specified key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>The value associated with the key.</returns>
|
||||
public override object? this[object key]
|
||||
{
|
||||
get
|
||||
{
|
||||
return _orderedDictionary[key];
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
_orderedDictionary[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the specified key and value to the hashtable.
|
||||
/// </summary>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
public override void Add(object key, object? value)
|
||||
{
|
||||
_orderedDictionary.Add(key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all keys and values from the hashtable.
|
||||
/// </summary>
|
||||
public override void Clear()
|
||||
{
|
||||
_orderedDictionary.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a shallow clone of the hashtable.
|
||||
/// </summary>
|
||||
/// <returns>A shallow clone of the hashtable.</returns>
|
||||
public override object Clone()
|
||||
{
|
||||
return new OrderedHashtable(_orderedDictionary);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the hashtable contains a specific key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to locate in the hashtable.</param>
|
||||
/// <returns>true if the hashtable contains an element with the specified key; otherwise, false.</returns>
|
||||
public override bool Contains(object key)
|
||||
{
|
||||
return _orderedDictionary.Contains(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the hashtable contains a specific key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to locate in the hashtable.</param>
|
||||
/// <returns>true if the hashtable contains an element with the specified key; otherwise, false.</returns>
|
||||
public override bool ContainsKey(object key)
|
||||
{
|
||||
return _orderedDictionary.Contains(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the hashtable contains a specific value.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to locate in the hashtable.</param>
|
||||
/// <returns>true if the hashtable contains an element with the specified value; otherwise, false.</returns>
|
||||
public override bool ContainsValue(object? value)
|
||||
{
|
||||
foreach (DictionaryEntry entry in _orderedDictionary)
|
||||
{
|
||||
if (Equals(entry.Value, value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the elements of the hashtable to an array of type object, starting at the specified array index.
|
||||
/// </summary>
|
||||
/// <param name="array">The one-dimensional array that is the destination of the elements copied from the hashtable. The array must have zero-based indexing.</param>
|
||||
/// <param name="arrayIndex">The zero-based index in array at which copying begins.</param>
|
||||
public override void CopyTo(Array array, int arrayIndex)
|
||||
{
|
||||
_orderedDictionary.CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the enumerator.
|
||||
/// </summary>
|
||||
/// <returns>The enumerator.</returns>
|
||||
public override IDictionaryEnumerator GetEnumerator()
|
||||
{
|
||||
return _orderedDictionary.GetEnumerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the enumerator.
|
||||
/// </summary>
|
||||
/// <returns>The enumerator.</returns>
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the data needed to seralize the Hashtable.
|
||||
/// </summary>
|
||||
/// <param name="info">The serialization info.</param>
|
||||
/// <param name="context">The serialization context.</param>
|
||||
public override void GetObjectData(SerializationInfo info, StreamingContext context)
|
||||
{
|
||||
_orderedDictionary.GetObjectData(info, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the specified key from the hashtable.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to remove.</param>
|
||||
public override void Remove(object key)
|
||||
{
|
||||
_orderedDictionary.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,6 +142,17 @@ Describe 'ConvertFrom-Json Unit Tests' -tags "CI" {
|
||||
$out[1] | Should -Be $null
|
||||
$out[2] | Should -Be 2
|
||||
}
|
||||
|
||||
It 'Order is preserved for a hashtable' {
|
||||
$json = '{"a":1,"b":2,"c":3}' | ConvertFrom-Json -AsHashtable
|
||||
($json | Out-String).Trim() | Should -BeExactly @"
|
||||
Name Value
|
||||
---- -----
|
||||
a 1
|
||||
b 2
|
||||
c 3
|
||||
"@
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'ConvertFrom-Json -Depth Tests' -tags "Feature" {
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT License.
|
||||
|
||||
Describe 'Tests for OrderedHashtable' {
|
||||
|
||||
It 'Can create an empty OrderedHashtable' {
|
||||
$oh =[System.Management.Automation.OrderedHashtable]::new()
|
||||
$oh.Count | Should -Be 0
|
||||
$oh.GetType().Name | Should -BeExactly 'OrderedHashtable'
|
||||
}
|
||||
|
||||
It 'Can create an empty OrderedHashtable with a capacity' {
|
||||
$oh = [System.Management.Automation.OrderedHashtable]::new(10)
|
||||
$oh.Count | Should -Be 0
|
||||
$oh.GetType().Name | Should -BeExactly 'OrderedHashtable'
|
||||
}
|
||||
|
||||
It 'Can create an OrderedHashtable with an initial dictionary' {
|
||||
$h = @{ a = 1; b = 2 }
|
||||
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
|
||||
$oh.Count | Should -Be 2
|
||||
$oh['a'] | Should -Be $h['a']
|
||||
$oh['b'] | Should -Be $h['b']
|
||||
}
|
||||
|
||||
It 'Can use the Add() method' {
|
||||
$oh = [System.Management.Automation.OrderedHashtable]::new()
|
||||
$oh.Add('a', 1)
|
||||
$oh.Add('b', 2)
|
||||
$oh.Count | Should -Be 2
|
||||
$oh['a'] | Should -Be 1
|
||||
$oh['b'] | Should -Be 2
|
||||
}
|
||||
|
||||
It 'Can use the Clear() method' {
|
||||
$h = @{ a = 1; b = 2 }
|
||||
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
|
||||
$oh.Count | Should -Be 2
|
||||
$oh.Clear()
|
||||
$oh.Count | Should -Be 0
|
||||
}
|
||||
|
||||
It 'Can use the Clone() method' {
|
||||
$h = @{ a = 1; b = 2 }
|
||||
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
|
||||
$oh2 = $oh.Clone()
|
||||
$oh2.Count | Should -Be 2
|
||||
$oh2['a'] | Should -Be $h['a']
|
||||
$oh2['b'] | Should -Be $h['b']
|
||||
}
|
||||
|
||||
It 'Can use the Contains() method' {
|
||||
$h = @{ a = 1; b = 2 }
|
||||
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
|
||||
$oh.Contains('a') | Should -BeTrue
|
||||
$oh.Contains('b') | Should -BeTrue
|
||||
$oh.Contains('c') | Should -BeFalse
|
||||
}
|
||||
|
||||
It 'Can use the ContainsKey() method' {
|
||||
$h = @{ a = 1; b = 2 }
|
||||
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
|
||||
$oh.ContainsKey('a') | Should -BeTrue
|
||||
$oh.ContainsKey('b') | Should -BeTrue
|
||||
$oh.ContainsKey('c') | Should -BeFalse
|
||||
}
|
||||
|
||||
It 'Can use the ContainsValue() method' {
|
||||
$h = @{ a = 1; b = 2 }
|
||||
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
|
||||
$oh.ContainsValue(1) | Should -BeTrue
|
||||
$oh.ContainsValue(2) | Should -BeTrue
|
||||
$oh.ContainsValue(3) | Should -BeFalse
|
||||
}
|
||||
|
||||
It 'Can use the CopyTo() method' {
|
||||
$oh = [System.Management.Automation.OrderedHashtable]::new()
|
||||
$oh.Add('a', 1)
|
||||
$oh.Add('b', 2)
|
||||
$oh.Add('c', 3)
|
||||
$a = (4, 5, 6, 7)
|
||||
$oh.CopyTo($a, 1)
|
||||
$a[0] | Should -Be 4
|
||||
# OrderedDictionary.CopyTo() doesn't guarantee to preserve order
|
||||
# so we can't easily test the values
|
||||
$a[1].GetType().Name | Should -BeExactly 'DictionaryEntry'
|
||||
$a[2].GetType().Name | Should -BeExactly 'DictionaryEntry'
|
||||
$a[3].GetType().Name | Should -BeExactly 'DictionaryEntry'
|
||||
}
|
||||
|
||||
It 'Can use the Equals() method' {
|
||||
$h = @{ a = 1; b = 2 }
|
||||
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
|
||||
$oh2 = $oh.Clone()
|
||||
$oh3 = $oh
|
||||
$oh.Equals($oh2) | Should -BeFalse
|
||||
$oh.Equals($oh3) | Should -BeTrue
|
||||
}
|
||||
|
||||
It 'Can use the GetEnumerator() method' {
|
||||
$h = @{ a = 1; b = 2 }
|
||||
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
|
||||
$oh.GetEnumerator().GetType().Name | Should -BeExactly 'OrderedDictionaryEnumerator'
|
||||
}
|
||||
|
||||
It 'Can use the GetHashCode() method' {
|
||||
$h = @{ a = 1; b = 2 }
|
||||
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
|
||||
$oh.GetHashCode() | Should -BeGreaterThan 0
|
||||
$oh2 = $oh.Clone()
|
||||
$oh.GetHashCode() | Should -Not -Be $oh2.GetHashCode()
|
||||
$oh3 = $oh
|
||||
$oh.GetHashCode() | Should -Be $oh3.GetHashCode()
|
||||
}
|
||||
|
||||
It 'Can use Remove() method' {
|
||||
$h = @{ a = 1; b = 2 }
|
||||
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
|
||||
$oh.Remove('a')
|
||||
$oh.Count | Should -Be 1
|
||||
$oh.Contains('a') | Should -BeFalse
|
||||
}
|
||||
|
||||
It 'Can use Item property' {
|
||||
$h = @{ a = 1; b = 2 }
|
||||
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
|
||||
$oh['a'] | Should -Be 1
|
||||
$oh['b'] | Should -Be 2
|
||||
}
|
||||
|
||||
It 'Can use IsFixedSize property' {
|
||||
$h = @{ a = 1; b = 2 }
|
||||
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
|
||||
$oh.IsFixedSize | Should -BeFalse
|
||||
}
|
||||
|
||||
It 'Can use IsReadOnly property' {
|
||||
$h = @{ a = 1; b = 2 }
|
||||
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
|
||||
$oh.IsReadOnly | Should -BeFalse
|
||||
}
|
||||
|
||||
It 'Can use IsSynchronized property' {
|
||||
$h = @{ a = 1; b = 2 }
|
||||
$oh = [System.Management.Automation.OrderedHashtable]::new($h)
|
||||
$oh.IsSynchronized | Should -BeFalse
|
||||
}
|
||||
|
||||
It 'Can use Keys property' {
|
||||
$oh = [System.Management.Automation.OrderedHashtable]::new()
|
||||
$oh['a'] = 1
|
||||
$oh['b'] = 2
|
||||
$oh.Keys | Should -Be ('a', 'b')
|
||||
}
|
||||
|
||||
It 'Can use Values property' {
|
||||
$oh = [System.Management.Automation.OrderedHashtable]::new()
|
||||
$oh['a'] = 1
|
||||
$oh['b'] = 2
|
||||
$oh.Values | Should -Be (1, 2)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user