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) + } +}