diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/JsonObject.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/JsonObject.cs
index ff576c7e68..3113dc40d8 100644
--- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/JsonObject.cs
+++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/JsonObject.cs
@@ -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
diff --git a/src/System.Management.Automation/engine/OrderedHashtable.cs b/src/System.Management.Automation/engine/OrderedHashtable.cs
new file mode 100644
index 0000000000..d14d3dc244
--- /dev/null
+++ b/src/System.Management.Automation/engine/OrderedHashtable.cs
@@ -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
+{
+ ///
+ /// OrderedHashtable is a hashtable that preserves the order of the keys.
+ ///
+ public sealed class OrderedHashtable : Hashtable, IEnumerable
+ {
+ private readonly OrderedDictionary _orderedDictionary;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public OrderedHashtable()
+ {
+ _orderedDictionary = new OrderedDictionary();
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The capacity.
+ public OrderedHashtable(int capacity) : base(capacity)
+ {
+ _orderedDictionary = new OrderedDictionary(capacity);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The dictionary to use for initialization.
+ public OrderedHashtable(IDictionary dictionary)
+ {
+ _orderedDictionary = new OrderedDictionary(dictionary.Count);
+ foreach (DictionaryEntry entry in dictionary)
+ {
+ _orderedDictionary.Add(entry.Key, entry.Value);
+ }
+ }
+
+ ///
+ /// Get the number of items in the hashtable.
+ ///
+ public override int Count
+ {
+ get
+ {
+ return _orderedDictionary.Count;
+ }
+ }
+
+ ///
+ /// Get if the hashtable is a fixed size.
+ ///
+ public override bool IsFixedSize
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Get if the hashtable is read-only.
+ ///
+ public override bool IsReadOnly
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Get if the hashtable is synchronized.
+ ///
+ public override bool IsSynchronized
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Gets the keys in the hashtable.
+ ///
+ public override ICollection Keys
+ {
+ get
+ {
+ return _orderedDictionary.Keys;
+ }
+ }
+
+ ///
+ /// Gets the values in the hashtable.
+ ///
+ public override ICollection Values
+ {
+ get
+ {
+ return _orderedDictionary.Values;
+ }
+ }
+
+ ///
+ /// Gets or sets the value associated with the specified key.
+ ///
+ /// The key.
+ /// The value associated with the key.
+ public override object? this[object key]
+ {
+ get
+ {
+ return _orderedDictionary[key];
+ }
+
+ set
+ {
+ _orderedDictionary[key] = value;
+ }
+ }
+
+ ///
+ /// Adds the specified key and value to the hashtable.
+ ///
+ /// The key.
+ /// The value.
+ public override void Add(object key, object? value)
+ {
+ _orderedDictionary.Add(key, value);
+ }
+
+ ///
+ /// Removes all keys and values from the hashtable.
+ ///
+ public override void Clear()
+ {
+ _orderedDictionary.Clear();
+ }
+
+ ///
+ /// Get a shallow clone of the hashtable.
+ ///
+ /// A shallow clone of the hashtable.
+ public override object Clone()
+ {
+ return new OrderedHashtable(_orderedDictionary);
+ }
+
+ ///
+ /// Determines whether the hashtable contains a specific key.
+ ///
+ /// The key to locate in the hashtable.
+ /// true if the hashtable contains an element with the specified key; otherwise, false.
+ public override bool Contains(object key)
+ {
+ return _orderedDictionary.Contains(key);
+ }
+
+ ///
+ /// Determines whether the hashtable contains a specific key.
+ ///
+ /// The key to locate in the hashtable.
+ /// true if the hashtable contains an element with the specified key; otherwise, false.
+ public override bool ContainsKey(object key)
+ {
+ return _orderedDictionary.Contains(key);
+ }
+
+ ///
+ /// Determines whether the hashtable contains a specific value.
+ ///
+ /// The value to locate in the hashtable.
+ /// true if the hashtable contains an element with the specified value; otherwise, false.
+ public override bool ContainsValue(object? value)
+ {
+ foreach (DictionaryEntry entry in _orderedDictionary)
+ {
+ if (Equals(entry.Value, value))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// Copies the elements of the hashtable to an array of type object, starting at the specified array index.
+ ///
+ /// The one-dimensional array that is the destination of the elements copied from the hashtable. The array must have zero-based indexing.
+ /// The zero-based index in array at which copying begins.
+ public override void CopyTo(Array array, int arrayIndex)
+ {
+ _orderedDictionary.CopyTo(array, arrayIndex);
+ }
+
+ ///
+ /// Get the enumerator.
+ ///
+ /// The enumerator.
+ public override IDictionaryEnumerator GetEnumerator()
+ {
+ return _orderedDictionary.GetEnumerator();
+ }
+
+ ///
+ /// Get the enumerator.
+ ///
+ /// The enumerator.
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ ///
+ /// Returns the data needed to seralize the Hashtable.
+ ///
+ /// The serialization info.
+ /// The serialization context.
+ public override void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ _orderedDictionary.GetObjectData(info, context);
+ }
+
+ ///
+ /// Removes the specified key from the hashtable.
+ ///
+ /// The key to remove.
+ public override void Remove(object key)
+ {
+ _orderedDictionary.Remove(key);
+ }
+ }
+}
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertFrom-Json.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertFrom-Json.Tests.ps1
index 029e7a7003..135625db98 100644
--- a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertFrom-Json.Tests.ps1
+++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertFrom-Json.Tests.ps1
@@ -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" {
diff --git a/test/powershell/engine/OrderedHashtable.Tests.ps1 b/test/powershell/engine/OrderedHashtable.Tests.ps1
new file mode 100644
index 0000000000..4df61c49ed
--- /dev/null
+++ b/test/powershell/engine/OrderedHashtable.Tests.ps1
@@ -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)
+ }
+}