diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index 6905b8d1e5dd5..313a13c07a92f 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -706,11 +706,11 @@ internal JsonNode() { } public System.Text.Json.Nodes.JsonValue AsValue() { throw null; } public System.Text.Json.Nodes.JsonNode DeepClone() { throw null; } public static bool DeepEquals(System.Text.Json.Nodes.JsonNode? node1, System.Text.Json.Nodes.JsonNode? node2) { throw null; } - public string GetPropertyName() { throw null; } + public int GetElementIndex() { throw null; } public string GetPath() { throw null; } + public string GetPropertyName() { throw null; } + public System.Text.Json.JsonValueKind GetValueKind() { throw null; } public virtual T GetValue() { throw null; } - public JsonValueKind GetValueKind() { throw null; } - public int GetElementIndex() { throw null; } public static explicit operator bool (System.Text.Json.Nodes.JsonNode value) { throw null; } public static explicit operator byte (System.Text.Json.Nodes.JsonNode value) { throw null; } public static explicit operator char (System.Text.Json.Nodes.JsonNode value) { throw null; } @@ -801,7 +801,7 @@ internal JsonNode() { } public static System.Threading.Tasks.Task ParseAsync(System.IO.Stream utf8Json, System.Text.Json.Nodes.JsonNodeOptions? nodeOptions = default(System.Text.Json.Nodes.JsonNodeOptions?), System.Text.Json.JsonDocumentOptions documentOptions = default(System.Text.Json.JsonDocumentOptions), System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } [System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute("Creating JsonValue instances with non-primitive types requires generating code at runtime.")] [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("Creating JsonValue instances with non-primitive types is not compatible with trimming. It can result in non-primitive types being serialized, which may have their members trimmed.")] - public void ReplaceWith(T value) { throw null; } + public void ReplaceWith(T value) { } public string ToJsonString(System.Text.Json.JsonSerializerOptions? options = null) { throw null; } public override string ToString() { throw null; } public abstract void WriteTo(System.Text.Json.Utf8JsonWriter writer, System.Text.Json.JsonSerializerOptions? options = null); @@ -811,7 +811,7 @@ public partial struct JsonNodeOptions private int _dummyPrimitive; public bool PropertyNameCaseInsensitive { readonly get { throw null; } set { } } } - public sealed partial class JsonObject : System.Text.Json.Nodes.JsonNode, System.Collections.Generic.ICollection>, System.Collections.Generic.IDictionary, System.Collections.Generic.IEnumerable>, System.Collections.IEnumerable + public sealed partial class JsonObject : System.Text.Json.Nodes.JsonNode, System.Collections.Generic.ICollection>, System.Collections.Generic.IDictionary, System.Collections.Generic.IEnumerable>, System.Collections.Generic.IList>, System.Collections.IEnumerable { public JsonObject(System.Collections.Generic.IEnumerable> properties, System.Text.Json.Nodes.JsonNodeOptions? options = default(System.Text.Json.Nodes.JsonNodeOptions?)) { } public JsonObject(System.Text.Json.Nodes.JsonNodeOptions? options = default(System.Text.Json.Nodes.JsonNodeOptions?)) { } @@ -819,17 +819,27 @@ public sealed partial class JsonObject : System.Text.Json.Nodes.JsonNode, System bool System.Collections.Generic.ICollection>.IsReadOnly { get { throw null; } } System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Keys { get { throw null; } } System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Values { get { throw null; } } + System.Collections.Generic.KeyValuePair System.Collections.Generic.IList>.this[int index] { get { throw null; } set { } } public void Add(System.Collections.Generic.KeyValuePair property) { } public void Add(string propertyName, System.Text.Json.Nodes.JsonNode? value) { } public void Clear() { } public bool ContainsKey(string propertyName) { throw null; } public static System.Text.Json.Nodes.JsonObject? Create(System.Text.Json.JsonElement element, System.Text.Json.Nodes.JsonNodeOptions? options = default(System.Text.Json.Nodes.JsonNodeOptions?)) { throw null; } + public System.Collections.Generic.KeyValuePair GetAt(int index) { throw null; } public System.Collections.Generic.IEnumerator> GetEnumerator() { throw null; } + public int IndexOf(string propertyName) { throw null; } + public void Insert(int index, string propertyName, System.Text.Json.Nodes.JsonNode? value) { } public bool Remove(string propertyName) { throw null; } + public void RemoveAt(int index) { } + public void SetAt(int index, string propertyName, System.Text.Json.Nodes.JsonNode? value) { } + public void SetAt(int index, System.Text.Json.Nodes.JsonNode? value) { } bool System.Collections.Generic.ICollection>.Contains(System.Collections.Generic.KeyValuePair item) { throw null; } void System.Collections.Generic.ICollection>.CopyTo(System.Collections.Generic.KeyValuePair[] array, int index) { } bool System.Collections.Generic.ICollection>.Remove(System.Collections.Generic.KeyValuePair item) { throw null; } - bool System.Collections.Generic.IDictionary.TryGetValue(string propertyName, out System.Text.Json.Nodes.JsonNode? jsonNode) { throw null; } + bool System.Collections.Generic.IDictionary.TryGetValue(string propertyName, out System.Text.Json.Nodes.JsonNode jsonNode) { throw null; } + int System.Collections.Generic.IList>.IndexOf(System.Collections.Generic.KeyValuePair item) { throw null; } + void System.Collections.Generic.IList>.Insert(int index, System.Collections.Generic.KeyValuePair item) { } + void System.Collections.Generic.IList>.RemoveAt(int index) { } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } public bool TryGetPropertyValue(string propertyName, out System.Text.Json.Nodes.JsonNode? jsonNode) { throw null; } public override void WriteTo(System.Text.Json.Utf8JsonWriter writer, System.Text.Json.JsonSerializerOptions? options = null) { } diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index ac93199558164..aee388056c8cc 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -72,9 +72,6 @@ The System.Text.Json library is built-in as part of the shared framework in .NET - - - @@ -85,6 +82,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET + @@ -354,6 +352,12 @@ The System.Text.Json library is built-in as part of the shared framework in .NET + + + + + + @@ -376,6 +380,12 @@ The System.Text.Json library is built-in as part of the shared framework in .NET + + + + + + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyDictionary.ValueCollection.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyDictionary.ValueCollection.cs deleted file mode 100644 index 06b5e355bc8f7..0000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyDictionary.ValueCollection.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections; -using System.Collections.Generic; - -namespace System.Text.Json -{ - internal sealed partial class JsonPropertyDictionary - { - private ValueCollection? _valueCollection; - - public IList GetValueCollection() - { - return _valueCollection ??= new ValueCollection(this); - } - - private sealed class ValueCollection : IList - { - private readonly JsonPropertyDictionary _parent; - - public ValueCollection(JsonPropertyDictionary jsonObject) - { - _parent = jsonObject; - } - - public int Count => _parent.Count; - - public bool IsReadOnly => true; - - public T this[int index] - { - get => _parent.List[index].Value; - set => throw ThrowHelper.GetNotSupportedException_CollectionIsReadOnly(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - foreach (KeyValuePair item in _parent) - { - yield return item.Value; - } - } - - public void Add(T jsonNode) => ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); - - public void Clear() => ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); - - public bool Contains(T jsonNode) => _parent.ContainsValue(jsonNode); - - public void CopyTo(T[] nodeArray, int index) - { - if (index < 0) - { - ThrowHelper.ThrowArgumentOutOfRangeException_ArrayIndexNegative(nameof(index)); - } - - foreach (KeyValuePair item in _parent) - { - if (index >= nodeArray.Length) - { - ThrowHelper.ThrowArgumentException_ArrayTooSmall(nameof(nodeArray)); - } - - nodeArray[index++] = item.Value; - } - } - - public IEnumerator GetEnumerator() - { - foreach (KeyValuePair item in _parent) - { - yield return item.Value; - } - } - - bool ICollection.Remove(T node) => throw ThrowHelper.GetNotSupportedException_CollectionIsReadOnly(); - public int IndexOf(T item) => throw ThrowHelper.GetNotSupportedException_CollectionIsReadOnly(); - public void Insert(int index, T item) => throw ThrowHelper.GetNotSupportedException_CollectionIsReadOnly(); - public void RemoveAt(int index) => throw ThrowHelper.GetNotSupportedException_CollectionIsReadOnly(); - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyDictionary.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyDictionary.cs deleted file mode 100644 index d812bffca3c59..0000000000000 --- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyDictionary.cs +++ /dev/null @@ -1,418 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; - -namespace System.Text.Json -{ - /// - /// Keeps both a List and Dictionary in sync to enable deterministic enumeration ordering of List - /// and performance benefits of Dictionary once a threshold is hit. - /// - internal sealed partial class JsonPropertyDictionary where T : class? - { - private const int ListToDictionaryThreshold = 9; - - private Dictionary? _propertyDictionary; - private readonly List> _propertyList; - - private readonly StringComparer _stringComparer; - - public JsonPropertyDictionary(bool caseInsensitive) - { - _stringComparer = caseInsensitive ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; - _propertyList = new List>(); - } - - public JsonPropertyDictionary(bool caseInsensitive, int capacity) - { - _stringComparer = caseInsensitive ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; - - if (capacity > ListToDictionaryThreshold) - { - _propertyDictionary = new(capacity, _stringComparer); - } - - _propertyList = new(capacity); - } - - // Enable direct access to the List for performance reasons. - public List> List => _propertyList; - - public void Add(string propertyName, T value) - { - if (IsReadOnly) - { - ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); - } - - if (propertyName == null) - { - ThrowHelper.ThrowArgumentNullException(nameof(propertyName)); - } - - AddValue(propertyName, value); - } - - public void Add(KeyValuePair property) - { - if (IsReadOnly) - { - ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); - } - - Add(property.Key, property.Value); - } - - public bool TryAdd(string propertyName, T value) - { - if (IsReadOnly) - { - ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); - } - - // A check for a null propertyName is not required since this method is only called by internal code. - Debug.Assert(propertyName != null); - - return TryAddValue(propertyName, value); - } - - public void Clear() - { - if (IsReadOnly) - { - ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); - } - - _propertyList.Clear(); - _propertyDictionary?.Clear(); - } - - public bool ContainsKey(string propertyName) - { - if (propertyName is null) - { - ThrowHelper.ThrowArgumentNullException(nameof(propertyName)); - } - - return ContainsProperty(propertyName); - } - - public int Count - { - get - { - return _propertyList.Count; - } - } - - public bool Remove(string propertyName) - { - if (IsReadOnly) - { - ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); - } - - if (propertyName == null) - { - ThrowHelper.ThrowArgumentNullException(nameof(propertyName)); - } - - return TryRemoveProperty(propertyName, out _); - } - - public bool Contains(KeyValuePair item) - { - foreach (KeyValuePair existing in this) - { - if (ReferenceEquals(item.Value, existing.Value) && _stringComparer.Equals(item.Key, existing.Key)) - { - return true; - } - } - - return false; - } - - public void CopyTo(KeyValuePair[] array, int index) - { - if (index < 0) - { - ThrowHelper.ThrowArgumentOutOfRangeException_ArrayIndexNegative(nameof(index)); - } - - foreach (KeyValuePair item in _propertyList) - { - if (index >= array.Length) - { - ThrowHelper.ThrowArgumentException_ArrayTooSmall(nameof(array)); - } - - array[index++] = item; - } - } - - public List>.Enumerator GetEnumerator() => _propertyList.GetEnumerator(); - - public IList Keys => GetKeyCollection(); - - public IList Values => GetValueCollection(); - - public bool TryGetValue(string propertyName, [MaybeNullWhen(false)] out T value) - { - if (propertyName is null) - { - ThrowHelper.ThrowArgumentNullException(nameof(propertyName)); - } - - if (_propertyDictionary != null) - { - return _propertyDictionary.TryGetValue(propertyName, out value); - } - else - { - foreach (KeyValuePair item in _propertyList) - { - if (_stringComparer.Equals(propertyName, item.Key)) - { - value = item.Value; - return true; - } - } - } - - value = null; - return false; - } - - public bool IsReadOnly { get; set; } - - [DisallowNull] - public T? this[string propertyName] - { - get - { - if (TryGetPropertyValue(propertyName, out T? value)) - { - return value; - } - - // Return null for missing properties. - return null; - } - - set - { - SetValue(propertyName, value, out bool _); - } - } - - public T? SetValue(string propertyName, T value, out bool valueAlreadyInDictionary) - { - if (IsReadOnly) - { - ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); - } - - if (propertyName == null) - { - ThrowHelper.ThrowArgumentNullException(nameof(propertyName)); - } - - CreateDictionaryIfThresholdMet(); - - valueAlreadyInDictionary = false; - T? existing = null; - - if (_propertyDictionary != null) - { - // Fast path if item doesn't exist in dictionary. - if (_propertyDictionary.TryAdd(propertyName, value)) - { - _propertyList.Add(new KeyValuePair(propertyName, value)); - return null; - } - - existing = _propertyDictionary[propertyName]; - if (ReferenceEquals(existing, value)) - { - // Ignore if the same value. - valueAlreadyInDictionary = true; - return null; - } - } - - int i = FindValueIndex(propertyName); - if (i >= 0) - { - if (_propertyDictionary != null) - { - _propertyDictionary[propertyName] = value; - } - else - { - KeyValuePair current = _propertyList[i]; - if (ReferenceEquals(current.Value, value)) - { - // Ignore if the same value. - valueAlreadyInDictionary = true; - return null; - } - - existing = current.Value; - } - - _propertyList[i] = new KeyValuePair(propertyName, value); - } - else - { - _propertyDictionary?.Add(propertyName, value); - _propertyList.Add(new KeyValuePair(propertyName, value)); - Debug.Assert(existing == null); - } - - return existing; - } - - private void AddValue(string propertyName, T value) - { - if (!TryAddValue(propertyName, value)) - { - ThrowHelper.ThrowArgumentException_DuplicateKey(nameof(propertyName), propertyName); - } - } - - internal bool TryAddValue(string propertyName, T value) - { - if (IsReadOnly) - { - ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); - } - - CreateDictionaryIfThresholdMet(); - - if (_propertyDictionary == null) - { - // Verify there are no duplicates before adding. - if (ContainsProperty(propertyName)) - { - return false; - } - } - else - { - if (!_propertyDictionary.TryAdd(propertyName, value)) - { - return false; - } - } - - _propertyList.Add(new KeyValuePair(propertyName, value)); - return true; - } - - private void CreateDictionaryIfThresholdMet() - { - if (_propertyDictionary == null && _propertyList.Count > ListToDictionaryThreshold) - { - _propertyDictionary = JsonHelpers.CreateDictionaryFromCollection(_propertyList, _stringComparer); - } - } - - internal bool ContainsValue(T value) - { - foreach (T item in GetValueCollection()) - { - if (ReferenceEquals(item, value)) - { - return true; - } - } - - return false; - } - - public KeyValuePair? FindValue(T value) - { - foreach (KeyValuePair item in this) - { - if (ReferenceEquals(item.Value, value)) - { - return item; - } - } - - return null; - } - - private bool ContainsProperty(string propertyName) - { - if (_propertyDictionary != null) - { - return _propertyDictionary.ContainsKey(propertyName); - } - - foreach (KeyValuePair item in _propertyList) - { - if (_stringComparer.Equals(propertyName, item.Key)) - { - return true; - } - } - - return false; - } - - private int FindValueIndex(string propertyName) - { - for (int i = 0; i < _propertyList.Count; i++) - { - KeyValuePair current = _propertyList[i]; - if (_stringComparer.Equals(propertyName, current.Key)) - { - return i; - } - } - - return -1; - } - - public bool TryGetPropertyValue(string propertyName, [MaybeNullWhen(false)] out T value) => TryGetValue(propertyName, out value); - - public bool TryRemoveProperty(string propertyName, [MaybeNullWhen(false)] out T existing) - { - if (IsReadOnly) - { - ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); - } - - if (_propertyDictionary != null) - { - if (!_propertyDictionary.TryGetValue(propertyName, out existing)) - { - return false; - } - - bool success = _propertyDictionary.Remove(propertyName); - Debug.Assert(success); - } - - for (int i = 0; i < _propertyList.Count; i++) - { - KeyValuePair current = _propertyList[i]; - - if (_stringComparer.Equals(current.Key, propertyName)) - { - _propertyList.RemoveAt(i); - existing = current.Value; - return true; - } - } - - existing = null; - return false; - } - } -} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonArray.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonArray.cs index 8dd4b6f9d98e7..0c95ee94fe896 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonArray.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonArray.cs @@ -67,7 +67,7 @@ public JsonArray(params ReadOnlySpan items) : base() InitializeFromSpan(items); } - internal override JsonValueKind GetValueKindCore() => JsonValueKind.Array; + private protected override JsonValueKind GetValueKindCore() => JsonValueKind.Array; internal override JsonNode DeepCloneCore() { @@ -222,14 +222,14 @@ public void Add(T? value) /// /// Gets or creates the underlying list containing the element nodes of the array. /// - internal List List => _list is { } list ? list : InitializeList(); + private List List => _list ?? InitializeList(); - internal JsonNode? GetItem(int index) + private protected override JsonNode? GetItem(int index) { return List[index]; } - internal void SetItem(int index, JsonNode? value) + private protected override void SetItem(int index, JsonNode? value) { value?.AssignParent(this); DetachParent(List[index]); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.cs index fed66cb08738b..70b80505b7a7d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.cs @@ -197,20 +197,23 @@ public virtual T GetValue() => /// is less than 0 or is greater than the number of properties. /// /// - /// The current is not a . + /// The current is not a or . /// public JsonNode? this[int index] { - get - { - return AsArray().GetItem(index); - } - set - { - AsArray().SetItem(index, value); - } + get => GetItem(index); + set => SetItem(index, value); } + private protected virtual JsonNode? GetItem(int index) + { + ThrowHelper.ThrowInvalidOperationException_NodeWrongType(nameof(JsonArray), nameof(JsonObject)); + return null; + } + + private protected virtual void SetItem(int index, JsonNode? node) => + ThrowHelper.ThrowInvalidOperationException_NodeWrongType(nameof(JsonArray), nameof(JsonObject)); + /// /// Gets or sets the element with the specified property name. /// If the property is not found, is returned. @@ -247,7 +250,7 @@ public JsonNode? this[string propertyName] /// public JsonValueKind GetValueKind() => GetValueKindCore(); - internal abstract JsonValueKind GetValueKindCore(); + private protected abstract JsonValueKind GetValueKindCore(); /// /// Returns property name of the current node from the parent object. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.IDictionary.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.IDictionary.cs index 7ff014fe1ef86..8756b93366f90 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.IDictionary.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.IDictionary.cs @@ -10,7 +10,7 @@ namespace System.Text.Json.Nodes { public partial class JsonObject : IDictionary { - private JsonPropertyDictionary? _dictionary; + private OrderedDictionary? _dictionary; /// /// Adds an element with the provided property name and value to the . @@ -48,7 +48,7 @@ public void Add(string propertyName, JsonNode? value) /// public void Clear() { - JsonPropertyDictionary? dictionary = _dictionary; + OrderedDictionary? dictionary = _dictionary; if (dictionary is null) { @@ -56,7 +56,7 @@ public void Clear() return; } - foreach (JsonNode? node in dictionary.GetValueCollection()) + foreach (JsonNode? node in dictionary.Values) { DetachParent(node); } @@ -98,7 +98,7 @@ public bool Remove(string propertyName) ThrowHelper.ThrowArgumentNullException(nameof(propertyName)); } - bool success = Dictionary.TryRemoveProperty(propertyName, out JsonNode? removedNode); + bool success = Dictionary.Remove(propertyName, out JsonNode? removedNode); if (success) { DetachParent(removedNode); @@ -114,7 +114,8 @@ public bool Remove(string propertyName) /// /// if the contains an element with the property name; otherwise, . /// - bool ICollection>.Contains(KeyValuePair item) => Dictionary.Contains(item); + bool ICollection>.Contains(KeyValuePair item) => + ((IDictionary)Dictionary).Contains(item); /// /// Copies the elements of the to an array of type KeyValuePair starting at the specified array index. @@ -133,7 +134,8 @@ public bool Remove(string propertyName) /// The number of elements in the source ICollection is greater than the available space from /// to the end of the destination . /// - void ICollection>.CopyTo(KeyValuePair[] array, int index) => Dictionary.CopyTo(array, index); + void ICollection>.CopyTo(KeyValuePair[] array, int index) => + ((IDictionary)Dictionary).CopyTo(array, index); /// /// Returns an enumerator that iterates through the . @@ -193,13 +195,13 @@ public bool Remove(string propertyName) /// IEnumerator IEnumerable.GetEnumerator() => Dictionary.GetEnumerator(); - private JsonPropertyDictionary InitializeDictionary() + private OrderedDictionary InitializeDictionary() { - GetUnderlyingRepresentation(out JsonPropertyDictionary? dictionary, out JsonElement? jsonElement); + GetUnderlyingRepresentation(out OrderedDictionary? dictionary, out JsonElement? jsonElement); if (dictionary is null) { - dictionary = new JsonPropertyDictionary(IsCaseInsensitive(Options)); + dictionary = CreateDictionary(Options); if (jsonElement.HasValue) { @@ -211,7 +213,7 @@ public bool Remove(string propertyName) node.Parent = this; } - dictionary.Add(new KeyValuePair(jElementProperty.Name, node)); + dictionary.Add(jElementProperty.Name, node); } } @@ -224,14 +226,20 @@ public bool Remove(string propertyName) return dictionary; } - private static bool IsCaseInsensitive(JsonNodeOptions? options) => - options?.PropertyNameCaseInsensitive ?? false; + private static OrderedDictionary CreateDictionary(JsonNodeOptions? options, int capacity = 0) + { + StringComparer comparer = options?.PropertyNameCaseInsensitive ?? false + ? StringComparer.OrdinalIgnoreCase + : StringComparer.Ordinal; + + return new(capacity, comparer); + } /// /// Provides a coherent view of the underlying representation of the current node. /// The jsonElement value should be consumed if and only if dictionary value is null. /// - private void GetUnderlyingRepresentation(out JsonPropertyDictionary? dictionary, out JsonElement? jsonElement) + private void GetUnderlyingRepresentation(out OrderedDictionary? dictionary, out JsonElement? jsonElement) { // Because JsonElement cannot be read atomically there might be torn reads, // however the order of read/write operations guarantees that that's only diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.IList.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.IList.cs new file mode 100644 index 0000000000000..d7032fd5125ed --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.IList.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Collections.Generic; + +namespace System.Text.Json.Nodes +{ + public partial class JsonObject : IList> + { + /// Gets the property the specified index. + /// The zero-based index of the pair to get. + /// The property at the specified index as a key/value pair. + /// is less than 0 or greater than or equal to . + public KeyValuePair GetAt(int index) => Dictionary.GetAt(index); + + /// Sets a new property at the specified index. + /// The zero-based index of the property to set. + /// The property name to store at the specified index. + /// The JSON value to store at the specified index. + /// is less than 0 or greater than or equal to . + /// is already specified in a different index. + /// already has a parent. + public void SetAt(int index, string propertyName, JsonNode? value) + { + OrderedDictionary dictionary = Dictionary; + KeyValuePair existing = dictionary.GetAt(index); + dictionary.SetAt(index, propertyName, value); + DetachParent(existing.Value); + value?.AssignParent(this); + } + + /// Sets a new property value at the specified index. + /// The zero-based index of the property to set. + /// The JSON value to store at the specified index. + /// is less than 0 or greater than or equal to . + /// already has a parent. + public void SetAt(int index, JsonNode? value) + { + OrderedDictionary dictionary = Dictionary; + KeyValuePair existing = dictionary.GetAt(index); + dictionary.SetAt(index, value); + DetachParent(existing.Value); + value?.AssignParent(this); + } + + /// Determines the index of a specific property name in the object. + /// The property name to locate. + /// The index of if found; otherwise, -1. + /// is null. + public int IndexOf(string propertyName) => Dictionary.IndexOf(propertyName); + + /// Inserts a property into the object at the specified index. + /// The zero-based index at which the property should be inserted. + /// The property name to insert. + /// The JSON value to insert. + /// is null. + /// An element with the same key already exists in the . + /// is less than 0 or greater than . + public void Insert(int index, string propertyName, JsonNode? value) + { + Dictionary.Insert(index, propertyName, value); + value?.AssignParent(this); + } + + /// Removes the property at the specified index. + /// The zero-based index of the item to remove. + /// is less than 0 or greater than or equal to . + public void RemoveAt(int index) + { + KeyValuePair existing = Dictionary.GetAt(index); + Dictionary.RemoveAt(index); + DetachParent(existing.Value); + } + + /// + KeyValuePair IList>.this[int index] + { + get => GetAt(index); + set => SetAt(index, value.Key, value.Value); + } + + /// + int IList>.IndexOf(KeyValuePair item) => ((IList>)Dictionary).IndexOf(item); + + /// + void IList>.Insert(int index, KeyValuePair item) => Insert(index, item.Key, item.Value); + + /// + void IList>.RemoveAt(int index) => RemoveAt(index); + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.cs index 24f8653053af7..8b32efee897e2 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonObject.cs @@ -33,11 +33,8 @@ public JsonObject(JsonNodeOptions? options = null) : base(options) { } /// Options to control the behavior. public JsonObject(IEnumerable> properties, JsonNodeOptions? options = null) : this(options) { - bool isCaseInsensitive = IsCaseInsensitive(options); - - JsonPropertyDictionary dictionary = properties is ICollection> propertiesCollection - ? new(isCaseInsensitive, propertiesCollection.Count) - : new(isCaseInsensitive); + int capacity = properties is ICollection> propertiesCollection ? propertiesCollection.Count : 0; + OrderedDictionary dictionary = CreateDictionary(options, capacity); foreach (KeyValuePair node in properties) { @@ -76,11 +73,14 @@ internal JsonObject(JsonElement element, JsonNodeOptions? options = null) : this /// /// Gets or creates the underlying dictionary containing the properties of the object. /// - internal JsonPropertyDictionary Dictionary => _dictionary ?? InitializeDictionary(); + private OrderedDictionary Dictionary => _dictionary ?? InitializeDictionary(); + + private protected override JsonNode? GetItem(int index) => GetAt(index).Value; + private protected override void SetItem(int index, JsonNode? value) => SetAt(index, value); internal override JsonNode DeepCloneCore() { - GetUnderlyingRepresentation(out JsonPropertyDictionary? dictionary, out JsonElement? jsonElement); + GetUnderlyingRepresentation(out OrderedDictionary? dictionary, out JsonElement? jsonElement); if (dictionary is null) { @@ -89,10 +89,9 @@ internal override JsonNode DeepCloneCore() : new JsonObject(Options); } - bool caseInsensitive = IsCaseInsensitive(Options); var jObject = new JsonObject(Options) { - _dictionary = new JsonPropertyDictionary(caseInsensitive, dictionary.Count) + _dictionary = CreateDictionary(Options, Count) }; foreach (KeyValuePair item in dictionary) @@ -105,7 +104,7 @@ internal override JsonNode DeepCloneCore() internal string GetPropertyName(JsonNode? node) { - KeyValuePair? item = Dictionary.FindValue(node); + KeyValuePair? item = FindValue(node); return item.HasValue ? item.Value.Key : string.Empty; } @@ -118,7 +117,7 @@ internal string GetPropertyName(JsonNode? node) /// if a property with the specified name was found; otherwise, . /// public bool TryGetPropertyValue(string propertyName, out JsonNode? jsonNode) => - ((IDictionary)this).TryGetValue(propertyName, out jsonNode); + Dictionary.TryGetValue(propertyName, out jsonNode); /// public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? options = null) @@ -128,7 +127,7 @@ public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? optio ThrowHelper.ThrowArgumentNullException(nameof(writer)); } - GetUnderlyingRepresentation(out JsonPropertyDictionary? dictionary, out JsonElement? jsonElement); + GetUnderlyingRepresentation(out OrderedDictionary? dictionary, out JsonElement? jsonElement); if (dictionary is null && jsonElement.HasValue) { @@ -157,7 +156,7 @@ public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? optio } } - internal override JsonValueKind GetValueKindCore() => JsonValueKind.Object; + private protected override JsonValueKind GetValueKindCore() => JsonValueKind.Object; internal override bool DeepEqualsCore(JsonNode? node) { @@ -169,8 +168,8 @@ internal override bool DeepEqualsCore(JsonNode? node) // JsonValue instances have special comparison semantics, dispatch to their implementation. return value.DeepEqualsCore(this); case JsonObject jsonObject: - JsonPropertyDictionary currentDict = Dictionary; - JsonPropertyDictionary otherDict = jsonObject.Dictionary; + OrderedDictionary currentDict = Dictionary; + OrderedDictionary otherDict = jsonObject.Dictionary; if (currentDict.Count != otherDict.Count) { @@ -179,7 +178,7 @@ internal override bool DeepEqualsCore(JsonNode? node) foreach (KeyValuePair item in currentDict) { - JsonNode? jsonNode = otherDict[item.Key]; + otherDict.TryGetValue(item.Key, out JsonNode? jsonNode); if (!DeepEquals(item.Value, jsonNode)) { @@ -211,7 +210,7 @@ internal override void GetPath(ref ValueStringBuilder path, JsonNode? child) if (child != null) { - string propertyName = Dictionary.FindValue(child)!.Value.Key; + string propertyName = FindValue(child)!.Value.Key; if (propertyName.AsSpan().ContainsSpecialCharacters()) { path.Append("['"); @@ -228,14 +227,20 @@ internal override void GetPath(ref ValueStringBuilder path, JsonNode? child) internal void SetItem(string propertyName, JsonNode? value) { - JsonNode? replacedValue = Dictionary.SetValue(propertyName, value, out bool valueAlreadyInDictionary); + OrderedDictionary dict = Dictionary; - if (!valueAlreadyInDictionary) + if (dict.TryGetValue(propertyName, out JsonNode? replacedValue)) { - value?.AssignParent(this); + if (ReferenceEquals(value, replacedValue)) + { + return; + } + + DetachParent(replacedValue); } - DetachParent(replacedValue); + dict[propertyName] = value; + value?.AssignParent(this); } private void DetachParent(JsonNode? item) @@ -248,6 +253,19 @@ private void DetachParent(JsonNode? item) } } + private KeyValuePair? FindValue(JsonNode? value) + { + foreach (KeyValuePair item in Dictionary) + { + if (ReferenceEquals(item.Value, value)) + { + return item; + } + } + + return null; + } + [ExcludeFromCodeCoverage] // Justification = "Design-time" private sealed class DebugView { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfT.cs index 86db69d82cd74..8067c321e0db4 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfT.cs @@ -65,7 +65,7 @@ public override bool TryGetValue([NotNullWhen(true)] out T value) return false; } - internal sealed override JsonValueKind GetValueKindCore() + private protected sealed override JsonValueKind GetValueKindCore() { if (Value is JsonElement element) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyDictionary.KeyCollection.cs b/src/libraries/System.Text.Json/src/System/Text/Json/OrderedDictionary.KeyCollection.cs similarity index 50% rename from src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyDictionary.KeyCollection.cs rename to src/libraries/System.Text.Json/src/System/Text/Json/OrderedDictionary.KeyCollection.cs index 133af83869a4c..e366c4a577477 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonPropertyDictionary.KeyCollection.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/OrderedDictionary.KeyCollection.cs @@ -6,56 +6,49 @@ namespace System.Text.Json { - internal sealed partial class JsonPropertyDictionary + internal sealed partial class OrderedDictionary { - private KeyCollection? _keyCollection; - - public IList GetKeyCollection() - { - return _keyCollection ??= new KeyCollection(this); - } - - private sealed class KeyCollection : IList + private sealed class KeyCollection : IList { - private readonly JsonPropertyDictionary _parent; + private readonly OrderedDictionary _parent; - public KeyCollection(JsonPropertyDictionary jsonObject) + public KeyCollection(OrderedDictionary parent) { - _parent = jsonObject; + _parent = parent; } public int Count => _parent.Count; public bool IsReadOnly => true; - public string this[int index] + public TKey this[int index] { - get => _parent.List[index].Key; + get => _parent.GetAt(index).Key; set => throw ThrowHelper.GetNotSupportedException_CollectionIsReadOnly(); } IEnumerator IEnumerable.GetEnumerator() { - foreach (KeyValuePair item in _parent) + foreach (KeyValuePair item in _parent) { yield return item.Key; } } - public void Add(string propertyName) => ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); + public void Add(TKey propertyName) => ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); public void Clear() => ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); - public bool Contains(string propertyName) => _parent.ContainsProperty(propertyName); + public bool Contains(TKey propertyName) => _parent.ContainsKey(propertyName); - public void CopyTo(string[] propertyNameArray, int index) + public void CopyTo(TKey[] propertyNameArray, int index) { if (index < 0) { ThrowHelper.ThrowArgumentOutOfRangeException_ArrayIndexNegative(nameof(index)); } - foreach (KeyValuePair item in _parent) + foreach (KeyValuePair item in _parent) { if (index >= propertyNameArray.Length) { @@ -66,17 +59,17 @@ public void CopyTo(string[] propertyNameArray, int index) } } - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() { - foreach (KeyValuePair item in _parent) + foreach (KeyValuePair item in _parent) { yield return item.Key; } } - bool ICollection.Remove(string propertyName) => throw ThrowHelper.GetNotSupportedException_CollectionIsReadOnly(); - public int IndexOf(string item) => throw ThrowHelper.GetNotSupportedException_CollectionIsReadOnly(); - public void Insert(int index, string item) => throw ThrowHelper.GetNotSupportedException_CollectionIsReadOnly(); + bool ICollection.Remove(TKey propertyName) => throw ThrowHelper.GetNotSupportedException_CollectionIsReadOnly(); + public int IndexOf(TKey item) => throw ThrowHelper.GetNotSupportedException_CollectionIsReadOnly(); + public void Insert(int index, TKey item) => throw ThrowHelper.GetNotSupportedException_CollectionIsReadOnly(); public void RemoveAt(int index) => throw ThrowHelper.GetNotSupportedException_CollectionIsReadOnly(); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/OrderedDictionary.ValueCollection.cs b/src/libraries/System.Text.Json/src/System/Text/Json/OrderedDictionary.ValueCollection.cs new file mode 100644 index 0000000000000..ae9993afa9143 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/OrderedDictionary.ValueCollection.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Collections.Generic; + +namespace System.Text.Json +{ + internal sealed partial class OrderedDictionary + { + private sealed class ValueCollection : IList + { + private readonly OrderedDictionary _parent; + + public ValueCollection(OrderedDictionary parent) + { + _parent = parent; + } + + public int Count => _parent.Count; + + public bool IsReadOnly => true; + + public TValue this[int index] + { + get => _parent.GetAt(index).Value; + set => throw ThrowHelper.GetNotSupportedException_CollectionIsReadOnly(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + foreach (KeyValuePair item in _parent) + { + yield return item.Value; + } + } + + public void Add(TValue value) => ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); + + public void Clear() => ThrowHelper.ThrowNotSupportedException_CollectionIsReadOnly(); + + public bool Contains(TValue value) + { + EqualityComparer comparer = _parent._valueComparer; + foreach (KeyValuePair item in _parent._propertyList) + { + if (comparer.Equals(item.Value, value)) + { + return true; + } + } + + return false; + } + + public void CopyTo(TValue[] destination, int index) + { + if (index < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException_ArrayIndexNegative(nameof(index)); + } + + foreach (KeyValuePair item in _parent) + { + if (index >= destination.Length) + { + ThrowHelper.ThrowArgumentException_ArrayTooSmall(nameof(destination)); + } + + destination[index++] = item.Value; + } + } + + public IEnumerator GetEnumerator() + { + foreach (KeyValuePair item in _parent) + { + yield return item.Value; + } + } + + bool ICollection.Remove(TValue value) => throw ThrowHelper.GetNotSupportedException_CollectionIsReadOnly(); + public int IndexOf(TValue item) => throw ThrowHelper.GetNotSupportedException_CollectionIsReadOnly(); + public void Insert(int index, TValue value) => throw ThrowHelper.GetNotSupportedException_CollectionIsReadOnly(); + public void RemoveAt(int index) => throw ThrowHelper.GetNotSupportedException_CollectionIsReadOnly(); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/OrderedDictionary.cs b/src/libraries/System.Text.Json/src/System/Text/Json/OrderedDictionary.cs new file mode 100644 index 0000000000000..2ca4750eb2adf --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/OrderedDictionary.cs @@ -0,0 +1,321 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace System.Text.Json +{ + /// + /// Polyfill for System.Collections.Generic.OrderedDictionary added in .NET 9. + /// + internal sealed partial class OrderedDictionary : IDictionary, IList> + where TKey : notnull + { + private const int ListToDictionaryThreshold = 9; + + private Dictionary? _propertyDictionary; + private readonly List> _propertyList; + private readonly IEqualityComparer _keyComparer; + private readonly EqualityComparer _valueComparer = EqualityComparer.Default; + + public OrderedDictionary(int capacity, IEqualityComparer? keyComparer = null) + { + _keyComparer = keyComparer ?? EqualityComparer.Default; + _propertyList = new(capacity); + if (capacity > ListToDictionaryThreshold) + { + _propertyDictionary = new(capacity, _keyComparer); + } + } + + public void Add(TKey key, TValue value) + { + if (!TryAdd(key, value)) + { + ThrowHelper.ThrowArgumentException_DuplicateKey(nameof(key), key); + } + } + + public void Clear() + { + _propertyList.Clear(); + _propertyDictionary?.Clear(); + } + + public bool ContainsKey(TKey key) + { + return _propertyDictionary is { } dict + ? dict.ContainsKey(key) + : IndexOf(key) >= 0; + } + + public int Count => _propertyList.Count; + public List>.Enumerator GetEnumerator() => _propertyList.GetEnumerator(); + + public ICollection Keys => _keys ??= new(this); + private KeyCollection? _keys; + + public ICollection Values => _values ??= new(this); + private ValueCollection? _values; + + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + if (key is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(key)); + } + + if (_propertyDictionary is { } dict) + { + return dict.TryGetValue(key, out value); + } + else + { + IEqualityComparer comparer = _keyComparer; + foreach (KeyValuePair item in _propertyList) + { + if (comparer.Equals(key, item.Key)) + { + value = item.Value; + return true; + } + } + } + + value = default; + return false; + } + + public TValue this[TKey key] + { + get + { + if (!TryGetValue(key, out TValue? value)) + { + ThrowHelper.ThrowKeyNotFoundException(); + } + + return value; + } + + set + { + if (_propertyDictionary is { } dict) + { + dict[key] = value; + } + + KeyValuePair item = new(key, value); + int i = IndexOf(key); + if (i < 0) + { + _propertyList.Add(item); + } + else + { + _propertyList[i] = item; + } + } + } + + public bool TryAdd(TKey key, TValue value) + { + if (key is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(key)); + } + + CreateDictionaryIfThresholdMet(); + + if (_propertyDictionary is { } dict) + { + if (!dict.TryAdd(key, value)) + { + return false; + } + } + else if (IndexOf(key) >= 0) + { + return false; + } + + _propertyList.Add(new(key, value)); + return true; + } + + private void CreateDictionaryIfThresholdMet() + { + if (_propertyDictionary == null && _propertyList.Count > ListToDictionaryThreshold) + { + _propertyDictionary = JsonHelpers.CreateDictionaryFromCollection(_propertyList, _keyComparer); + } + } + + public int IndexOf(TKey key) + { + if (key is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(key)); + } + + List> propertyList = _propertyList; + IEqualityComparer keyComparer = _keyComparer; + + for (int i = 0; i < propertyList.Count; i++) + { + if (keyComparer.Equals(key, propertyList[i].Key)) + { + return i; + } + } + + return -1; + } + + public bool Remove(TKey key, [MaybeNullWhen(false)] out TValue existing) + { + if (_propertyDictionary != null) + { + if (!_propertyDictionary.TryGetValue(key, out existing)) + { + return false; + } + + bool success = _propertyDictionary.Remove(key); + Debug.Assert(success); + } + + for (int i = 0; i < _propertyList.Count; i++) + { + KeyValuePair current = _propertyList[i]; + + if (_keyComparer.Equals(current.Key, key)) + { + _propertyList.RemoveAt(i); + existing = current.Value; + return true; + } + } + + existing = default; + return false; + } + + public KeyValuePair GetAt(int index) => _propertyList[index]; + + public void SetAt(int index, TKey key, TValue value) + { + TKey existingKey = _propertyList[index].Key; + if (!_keyComparer.Equals(existingKey, key)) + { + if (ContainsKey(key)) + { + // The key already exists in a different position, throw an exception. + ThrowHelper.ThrowArgumentException_DuplicateKey(nameof(key), key); + } + + _propertyDictionary?.Remove(existingKey); + } + + if (_propertyDictionary != null) + { + _propertyDictionary[key] = value; + } + + _propertyList[index] = new(key, value); + } + + public void SetAt(int index, TValue value) + { + TKey key = _propertyList[index].Key; + if (_propertyDictionary != null) + { + _propertyDictionary[key] = value; + } + + _propertyList[index] = new(key, value); + } + + public void Insert(int index, TKey key, TValue value) + { + if (key is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(key)); + } + + if (ContainsKey(key)) + { + ThrowHelper.ThrowArgumentException_DuplicateKey(nameof(key), key); + } + + _propertyList.Insert(index, new(key, value)); + _propertyDictionary?.Add(key, value); + } + + public void RemoveAt(int index) + { + KeyValuePair item = _propertyList[index]; + _propertyList.RemoveAt(index); + _propertyDictionary?.Remove(item.Key); + } + + IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + bool ICollection>.IsReadOnly => false; + void ICollection>.Add(KeyValuePair item) => Add(item.Key, item.Value); + bool ICollection>.Contains(KeyValuePair item) => TryGetValue(item.Key, out TValue? existingValue) && _valueComparer.Equals(item.Value, existingValue); + bool ICollection>.Remove(KeyValuePair item) + { + return TryGetValue(item.Key, out TValue? existingValue) && _valueComparer.Equals(existingValue, item.Value) + ? Remove(item.Key, out _) + : false; + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + if (arrayIndex < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException_ArrayIndexNegative(nameof(arrayIndex)); + } + + foreach (KeyValuePair item in _propertyList) + { + if (arrayIndex >= array.Length) + { + ThrowHelper.ThrowArgumentException_ArrayTooSmall(nameof(array)); + } + + array[arrayIndex++] = item; + } + } + + bool IDictionary.Remove(TKey key) => Remove(key, out _); + void IList>.Insert(int index, KeyValuePair item) => Insert(index, item.Key, item.Value); + int IList>.IndexOf(KeyValuePair item) + { + List> propertyList = _propertyList; + IEqualityComparer keyComparer = _keyComparer; + EqualityComparer valueComparer = _valueComparer; + + for (int i = 0; i < propertyList.Count; i++) + { + KeyValuePair entry = propertyList[i]; + if (keyComparer.Equals(entry.Key, item.Key) && valueComparer.Equals(item.Value, entry.Value)) + { + return i; + } + } + + return -1; + } + + KeyValuePair IList>.this[int index] + { + get => GetAt(index); + set => SetAt(index, value.Key, value.Value); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs index b8935e1f2b478..c235fa4533937 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs @@ -335,10 +335,8 @@ internal sealed override bool OnTryWrite( jsonTypeInfo.OnSerializing?.Invoke(obj); - List> properties = jsonTypeInfo.PropertyCache!.List; - for (int i = 0; i < properties.Count; i++) + foreach (JsonPropertyInfo jsonPropertyInfo in jsonTypeInfo.PropertyCache) { - JsonPropertyInfo jsonPropertyInfo = properties[i].Value; if (jsonPropertyInfo.CanSerialize) { // Remember the current property for JsonPath support if an exception is thrown. @@ -385,10 +383,10 @@ internal sealed override bool OnTryWrite( state.Current.ProcessedStartToken = true; } - List> propertyList = jsonTypeInfo.PropertyCache!.List; - while (state.Current.EnumeratorIndex < propertyList.Count) + ReadOnlySpan propertyCache = jsonTypeInfo.PropertyCache; + while (state.Current.EnumeratorIndex < propertyCache.Length) { - JsonPropertyInfo jsonPropertyInfo = propertyList[state.Current.EnumeratorIndex].Value; + JsonPropertyInfo jsonPropertyInfo = propertyCache[state.Current.EnumeratorIndex]; if (jsonPropertyInfo.CanSerialize) { state.Current.JsonPropertyInfo = jsonPropertyInfo; @@ -415,7 +413,7 @@ internal sealed override bool OnTryWrite( } // Write extension data after the normal properties. - if (state.Current.EnumeratorIndex == propertyList.Count) + if (state.Current.EnumeratorIndex == propertyCache.Length) { JsonPropertyInfo? extensionDataProperty = jsonTypeInfo.ExtensionDataProperty; if (extensionDataProperty?.CanSerialize == true) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs index 7a16b52e87ea7..6aa1117eefac8 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Large.cs @@ -56,9 +56,7 @@ protected sealed override void InitializeConstructorArgumentCaches(ref ReadStack { JsonTypeInfo typeInfo = state.Current.JsonTypeInfo; - Debug.Assert(typeInfo.ParameterCache != null); - - object?[] arguments = ArrayPool.Shared.Rent(typeInfo.ParameterCache.Count); + object?[] arguments = ArrayPool.Shared.Rent(typeInfo.ParameterCache.Length); foreach (JsonParameterInfo parameterInfo in typeInfo.ParameterCache) { arguments[parameterInfo.Position] = parameterInfo.EffectiveDefaultValue; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs index 9176c2847697b..b6ad9089c4810 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.Small.cs @@ -93,7 +93,6 @@ protected override void InitializeConstructorArgumentCaches(ref ReadStack state, JsonTypeInfo typeInfo = state.Current.JsonTypeInfo; Debug.Assert(typeInfo.CreateObjectWithArgs != null); - Debug.Assert(typeInfo.ParameterCache != null); var arguments = new Arguments(); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs index 34cf9809cc087..d0d357903fc4a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs @@ -341,7 +341,7 @@ private void ReadConstructorArguments(scoped ref ReadStack state, ref Utf8JsonRe if (argumentState.FoundProperties == null) { argumentState.FoundProperties = - ArrayPool.Shared.Rent(Math.Max(1, state.Current.JsonTypeInfo.PropertyCache!.Count)); + ArrayPool.Shared.Rent(Math.Max(1, state.Current.JsonTypeInfo.PropertyCache.Length)); } else if (argumentState.FoundPropertyCount == argumentState.FoundProperties.Length) { @@ -537,7 +537,7 @@ private static bool HandlePropertyWithContinuation( if (argumentState.FoundPropertiesAsync == null) { - argumentState.FoundPropertiesAsync = ArrayPool.Shared.Rent(Math.Max(1, state.Current.JsonTypeInfo.PropertyCache!.Count)); + argumentState.FoundPropertiesAsync = ArrayPool.Shared.Rent(Math.Max(1, state.Current.JsonTypeInfo.PropertyCache.Length)); } else if (argumentState.FoundPropertyCount == argumentState.FoundPropertiesAsync!.Length) { @@ -570,7 +570,7 @@ private void BeginRead(scoped ref ReadStack state, JsonSerializerOptions options jsonTypeInfo.ValidateCanBeUsedForPropertyMetadataSerialization(); - if (jsonTypeInfo.ParameterCount != jsonTypeInfo.ParameterCache!.Count) + if (jsonTypeInfo.ParameterCount != jsonTypeInfo.ParameterCache.Length) { ThrowHelper.ThrowInvalidOperationException_ConstructorParameterIncompleteBinding(Type); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs index 0a50aba7ad0ce..cf26e662c3370 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs @@ -26,14 +26,6 @@ internal static JsonPropertyInfo LookupProperty( bool createExtensionProperty = true) { JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo; -#if DEBUG - if (jsonTypeInfo.Kind != JsonTypeInfoKind.Object) - { - string objTypeName = obj?.GetType().FullName ?? ""; - Debug.Fail($"obj.GetType() => {objTypeName}; {jsonTypeInfo.GetPropertyDebugInfo(unescapedPropertyName)}"); - } -#endif - useExtensionProperty = false; JsonPropertyInfo jsonPropertyInfo = jsonTypeInfo.GetProperty( diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs index 73f6633ff7b63..0adea37c3c6f4 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs @@ -732,25 +732,6 @@ private bool NumberHandingIsApplicable() internal abstract object? GetValueAsObject(object obj); -#if DEBUG - internal string GetDebugInfo(int indent = 0) - { - string ind = new string(' ', indent); - StringBuilder sb = new(); - - sb.AppendLine($"{ind}{{"); - sb.AppendLine($"{ind} Name: {Name},"); - sb.AppendLine($"{ind} NameAsUtf8.Length: {(NameAsUtf8Bytes?.Length ?? -1)},"); - sb.AppendLine($"{ind} IsConfigured: {IsConfigured},"); - sb.AppendLine($"{ind} IsIgnored: {IsIgnored},"); - sb.AppendLine($"{ind} CanSerialize: {CanSerialize},"); - sb.AppendLine($"{ind} CanDeserialize: {CanDeserialize},"); - sb.AppendLine($"{ind}}}"); - - return sb.ToString(); - } -#endif - internal bool HasGetter => _untypedGet is not null; internal bool HasSetter => _untypedSet is not null; internal bool IgnoreNullTokensOnRead { get; private protected set; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs index 34860fdfefb8d..ef778611fbd70 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs @@ -29,21 +29,50 @@ public abstract partial class JsonTypeInfo // that not all parameters are bound to object properties, and an exception will be thrown if deserialization is attempted. internal int ParameterCount { get; private protected set; } - // All of the serializable parameters on a POCO constructor keyed on parameter name. - // Only parameters which bind to properties are cached. - internal List? ParameterCache { get; private set; } + // All of the serializable parameters on a POCO constructor + internal ReadOnlySpan ParameterCache + { + get + { + Debug.Assert(IsConfigured && _parameterCache is not null); + return _parameterCache; + } + } internal bool UsesParameterizedConstructor { get { Debug.Assert(IsConfigured); - return ParameterCache != null; + return _parameterCache != null; } } - // All of the serializable properties on a POCO (except the optional extension property) keyed on property name. - internal JsonPropertyDictionary? PropertyCache { get; private set; } + private JsonParameterInfo[]? _parameterCache; + + // All of the serializable properties on a POCO (minus the extension property). + internal ReadOnlySpan PropertyCache + { + get + { + Debug.Assert(IsConfigured && _propertyCache is not null); + return _propertyCache; + } + } + + private JsonPropertyInfo[]? _propertyCache; + + // All of the serializable properties on a POCO (minus the extension property) keyed on property name. + internal Dictionary PropertyIndex + { + get + { + Debug.Assert(IsConfigured && _propertyIndex is not null); + return _propertyIndex; + } + } + + private Dictionary? _propertyIndex; // Fast cache of properties by first JSON ordering; may not contain all properties. Accessed before PropertyCache. // Use an array (instead of List) for highest performance. @@ -151,14 +180,7 @@ internal JsonPropertyInfo GetProperty( } // No cached item was found. Try the main dictionary which has all of the properties. -#if DEBUG - if (PropertyCache == null) - { - Debug.Fail($"Property cache is null. {GetPropertyDebugInfo(propertyName)}"); - } -#endif - - if (PropertyCache!.TryGetValue(JsonHelpers.Utf8GetString(propertyName), out JsonPropertyInfo? info)) + if (PropertyIndex.TryGetValue(JsonHelpers.Utf8GetString(propertyName), out JsonPropertyInfo? info)) { Debug.Assert(info != null, "PropertyCache contains null JsonPropertyInfo"); @@ -166,15 +188,6 @@ internal JsonPropertyInfo GetProperty( { if (propertyName.SequenceEqual(info.NameAsUtf8Bytes)) { -#if DEBUG - ulong recomputedKey = GetKey(info.NameAsUtf8Bytes.AsSpan()); - if (key != recomputedKey) - { - string propertyNameStr = JsonHelpers.Utf8GetString(propertyName); - Debug.Fail($"key {key} [propertyName={propertyNameStr}] does not match re-computed value {recomputedKey} for the same sequence (case-insensitive). {info.GetDebugInfo()}"); - } -#endif - // Use the existing byte[] reference instead of creating another one. utf8PropertyName = info.NameAsUtf8Bytes!; } @@ -186,14 +199,6 @@ internal JsonPropertyInfo GetProperty( } else { -#if DEBUG - ulong recomputedKey = GetKey(info.NameAsUtf8Bytes.AsSpan()); - if (key != recomputedKey) - { - string propertyNameStr = JsonHelpers.Utf8GetString(propertyName); - Debug.Fail($"key {key} [propertyName={propertyNameStr}] does not match re-computed value {recomputedKey} for the same sequence (case-sensitive). {info.GetDebugInfo()}"); - } -#endif utf8PropertyName = info.NameAsUtf8Bytes; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs index 147de20f3c5a6..29f7dcdcca323 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs @@ -878,45 +878,6 @@ bool IsCurrentNodeCompatible() internal bool DetermineUsesParameterizedConstructor() => Converter.ConstructorIsParameterized && CreateObject is null; -#if DEBUG - internal string GetPropertyDebugInfo(ReadOnlySpan unescapedPropertyName) - { - string propertyName = JsonHelpers.Utf8GetString(unescapedPropertyName); - return $"propertyName = {propertyName}; DebugInfo={GetDebugInfo()}"; - } - - internal string GetDebugInfo() - { - ConverterStrategy converterStrategy = Converter.ConverterStrategy; - string jtiTypeName = GetType().Name; - string typeName = Type.FullName!; - bool propCacheInitialized = PropertyCache != null; - - StringBuilder sb = new(); - sb.AppendLine("{"); - sb.AppendLine($" GetType: {jtiTypeName},"); - sb.AppendLine($" Type: {typeName},"); - sb.AppendLine($" ConverterStrategy: {converterStrategy},"); - sb.AppendLine($" IsConfigured: {IsConfigured},"); - sb.AppendLine($" HasPropertyCache: {propCacheInitialized},"); - - if (propCacheInitialized) - { - sb.AppendLine(" Properties: {"); - foreach (JsonPropertyInfo pi in PropertyCache!.Values) - { - sb.AppendLine($" {pi.Name}:"); - sb.AppendLine($"{pi.GetDebugInfo(indent: 6)},"); - } - - sb.AppendLine(" },"); - } - - sb.AppendLine("}"); - return sb.ToString(); - } -#endif - /// /// Creates a blank instance. /// @@ -1080,11 +1041,14 @@ private protected readonly struct ParameterLookupKey(Type type, string name) : I internal void ConfigureProperties() { Debug.Assert(Kind == JsonTypeInfoKind.Object); - Debug.Assert(PropertyCache is null); + Debug.Assert(_propertyCache is null); + Debug.Assert(_propertyIndex is null); Debug.Assert(ExtensionDataProperty is null); JsonPropertyInfoList properties = PropertyList; - JsonPropertyDictionary propertyCache = CreatePropertyCache(capacity: properties.Count); + StringComparer comparer = Options.PropertyNameCaseInsensitive ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; + Dictionary propertyIndex = new(properties.Count, comparer); + List propertyCache = new(properties.Count); int numberOfRequiredProperties = 0; bool arePropertiesSorted = true; @@ -1121,10 +1085,12 @@ internal void ConfigureProperties() previousPropertyOrder = property.Order; } - if (!propertyCache.TryAddValue(property.Name, property)) + if (!propertyIndex.TryAdd(property.Name, property)) { ThrowHelper.ThrowInvalidOperationException_SerializerPropertyNameConflict(Type, property.Name); } + + propertyCache.Add(property); } property.Configure(); @@ -1134,11 +1100,12 @@ internal void ConfigureProperties() { // Properties have been configured by the user and require sorting. properties.SortProperties(); - propertyCache.List.StableSortByKey(static propInfo => propInfo.Value.Order); + propertyCache.StableSortByKey(static propInfo => propInfo.Order); } NumberOfRequiredProperties = numberOfRequiredProperties; - PropertyCache = propertyCache; + _propertyCache = propertyCache.ToArray(); + _propertyIndex = propertyIndex; // Override global UnmappedMemberHandling configuration // if type specifies an extension data property. @@ -1189,15 +1156,14 @@ internal void ConfigureConstructorParameters() { Debug.Assert(Kind == JsonTypeInfoKind.Object); Debug.Assert(DetermineUsesParameterizedConstructor()); - Debug.Assert(PropertyCache is not null); - Debug.Assert(ParameterCache is null); + Debug.Assert(_propertyCache is not null); + Debug.Assert(_parameterCache is null); List parameterCache = new(ParameterCount); Dictionary parameterIndex = new(ParameterCount); - foreach (KeyValuePair kvp in PropertyCache.List) + foreach (JsonPropertyInfo propertyInfo in _propertyCache) { - JsonPropertyInfo propertyInfo = kvp.Value; JsonParameterInfo? parameterInfo = propertyInfo.AssociatedParameter; if (parameterInfo is null) { @@ -1224,7 +1190,7 @@ internal void ConfigureConstructorParameters() ThrowHelper.ThrowInvalidOperationException_ExtensionDataCannotBindToCtorParam(ExtensionDataProperty.MemberName, ExtensionDataProperty); } - ParameterCache = parameterCache; + _parameterCache = parameterCache.ToArray(); _parameterInfoValuesIndex = null; } @@ -1337,11 +1303,6 @@ internal static bool IsValidExtensionDataProperty(Type propertyType) (propertyType.FullName == JsonObjectTypeName && ReferenceEquals(propertyType.Assembly, typeof(JsonTypeInfo).Assembly)); } - internal JsonPropertyDictionary CreatePropertyCache(int capacity) - { - return new JsonPropertyDictionary(Options.PropertyNameCaseInsensitive, capacity); - } - private static JsonTypeInfoKind GetTypeInfoKind(Type type, JsonConverter converter) { if (type == typeof(object) && converter.CanBePolymorphic) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Node.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Node.cs index 4391ef8d2dbff..9fe72311c7cf5 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Node.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Node.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -15,11 +17,17 @@ public static void ThrowArgumentException_NodeValueNotAllowed(string paramName) } [DoesNotReturn] - public static void ThrowArgumentException_DuplicateKey(string paramName, string propertyName) + public static void ThrowArgumentException_DuplicateKey(string paramName, object? propertyName) { throw new ArgumentException(SR.Format(SR.NodeDuplicateKey, propertyName), paramName); } + [DoesNotReturn] + public static void ThrowKeyNotFoundException() + { + throw new KeyNotFoundException(); + } + [DoesNotReturn] public static void ThrowInvalidOperationException_NodeAlreadyHasParent() { @@ -45,9 +53,11 @@ public static void ThrowNotSupportedException_CollectionIsReadOnly() } [DoesNotReturn] - public static void ThrowInvalidOperationException_NodeWrongType(string typeName) + public static void ThrowInvalidOperationException_NodeWrongType(params ReadOnlySpan supportedTypeNames) { - throw new InvalidOperationException(SR.Format(SR.NodeWrongType, typeName)); + Debug.Assert(supportedTypeNames.Length > 0); + string concatenatedNames = supportedTypeNames.Length == 1 ? supportedTypeNames[0] : string.Join(", ", supportedTypeNames.ToArray()); + throw new InvalidOperationException(SR.Format(SR.NodeWrongType, concatenatedNames)); } [DoesNotReturn] diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs index b5e2f99947e32..4a2759a57c398 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs @@ -293,15 +293,11 @@ public static void ThrowJsonException_JsonRequiredPropertyMissing(JsonTypeInfo p StringBuilder listOfMissingPropertiesBuilder = new(); bool first = true; - Debug.Assert(parent.PropertyCache != null); - // Soft cut-off length - once message becomes longer than that we won't be adding more elements const int CutOffLength = 60; - foreach (KeyValuePair kvp in parent.PropertyCache.List) + foreach (JsonPropertyInfo property in parent.PropertyCache) { - JsonPropertyInfo property = kvp.Value; - if (!property.IsRequired || requiredPropertiesSet[property.RequiredPropertyIndex]) { continue; diff --git a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.TestTypes.cs b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.TestTypes.cs index 13de2b5a0f85a..d3aa23ea94851 100644 --- a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.TestTypes.cs +++ b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.TestTypes.cs @@ -349,7 +349,7 @@ public static IEnumerable GetTestDataCore() } else { - schemaObj["$anchor"] = anchorName; + schemaObj.Insert(0, "$anchor", anchorName); } return schemaObj; @@ -398,7 +398,7 @@ public static IEnumerable GetTestDataCore() } else { - schemaObj["$id"] = idUrl; + schemaObj.Insert(0, "$id", idUrl); } return schemaObj; @@ -495,7 +495,7 @@ public static IEnumerable GetTestDataCore() if (descriptionAttribute != null) { - schemaObj["description"] = (JsonNode)descriptionAttribute.Description; + schemaObj.Insert(0, "description", (JsonNode)descriptionAttribute.Description); } return schemaObj; diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonObjectTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonObjectTests.cs index 8ae552714bca9..c6df4e23b7b6c 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonObjectTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonObjectTests.cs @@ -811,7 +811,7 @@ public static void ChangeCollectionWhileEnumeratingFails(JsonObject jObject, int // Exception string sample: "Collection was modified; enumeration operation may not execute" Assert.Throws(() => { - foreach(KeyValuePair node in jObject) + foreach (KeyValuePair node in jObject) { index++; jObject.Add("New_A", index); @@ -925,10 +925,10 @@ public static void ChangeCollectionWhileEnumeratingFails(JsonObject jObject, int [Fact] public static void TestJsonNodeOptionsSet() { - var options = new JsonNodeOptions() - { - PropertyNameCaseInsensitive = true - }; + var options = new JsonNodeOptions() + { + PropertyNameCaseInsensitive = true + }; // Ctor that takes just options var obj1 = new JsonObject(options); @@ -939,7 +939,7 @@ public static void TestJsonNodeOptionsSet() { new KeyValuePair("Hello", "World") }; - var obj2 = new JsonObject(props, options); + var obj2 = new JsonObject(props, options); // Create method using JsonDocument doc = JsonDocument.Parse(@"{""Hello"":""World""}"); @@ -1189,5 +1189,418 @@ public static void ReplaceWith() Assert.Null(jValue.Parent); Assert.Equal("{\"value\":5}", jObject.ToJsonString()); } + + // List-based APIs + + [Fact] + public static void IntIndexer_Getter_ReturnsExpectedProperty() + { + JsonObject jObject = new() + { + ["One"] = 1, + ["Two"] = "str", + ["Three"] = null, + }; + + JsonNode? value; + value = jObject[0]; + Assert.Equal(1, value.GetValue()); + + value = jObject[1]; + Assert.Equal("str", value.GetValue()); + + value = jObject[2]; + Assert.Null(value); + } + + [Fact] + public static void IntIndexer_Setter_UpdatesProperty() + { + JsonObject jObject = new() + { + ["One"] = 1, + ["Two"] = "str", + ["Three"] = null, + }; + + Assert.Null(jObject["Three"]); + Assert.Equal(3, jObject.Count); + + jObject[2] = -5; + + Assert.Equal(-5, jObject["Three"].GetValue()); + Assert.Equal(3, jObject.Count); + } + + [Fact] + public static void IntIndexer_ArgumentOutOfRange_ThrowsException() + { + JsonObject jObject = new() + { + ["One"] = 1, + ["Two"] = "str", + ["Three"] = null, + }; + + Assert.Throws(() => jObject[-1]); + Assert.Throws(() => jObject[jObject.Count]); + Assert.Throws(() => jObject[-1] = 42); + Assert.Throws(() => jObject[jObject.Count] = 42); + } + + [Fact] + public static void GetAt_ReturnsExpectedProperty() + { + JsonObject jObject = new() + { + ["One"] = 1, + ["Two"] = "str", + ["Three"] = null, + }; + + KeyValuePair kvp; + kvp = jObject.GetAt(0); + Assert.Equal("One", kvp.Key); + Assert.Equal(1, kvp.Value.GetValue()); + + kvp = jObject.GetAt(1); + Assert.Equal("Two", kvp.Key); + Assert.Equal("str", kvp.Value.GetValue()); + + kvp = jObject.GetAt(2); + Assert.Equal("Three", kvp.Key); + Assert.Null(kvp.Value); + } + + [Fact] + public static void GetAt_InvalidArgument_ThrowsArgumentOutOfRangeException() + { + JsonObject jObject = new() + { + ["One"] = 1, + ["Two"] = "str", + ["Three"] = null, + }; + + Assert.Throws(() => jObject.GetAt(-1)); + Assert.Throws(() => jObject.GetAt(jObject.Count)); + } + + [Fact] + public static void SetAt_UpdatesProperty() + { + JsonObject jObject = new() + { + ["One"] = 1, + ["Two"] = "str", + ["Three"] = null, + }; + + Assert.Null(jObject["Three"]); + Assert.Equal(3, jObject.Count); + + jObject.SetAt(2, -5); + + Assert.Equal(-5, jObject["Three"].GetValue()); + Assert.Equal(3, jObject.Count); + + jObject.SetAt(2, "Three", -33); + + Assert.Equal(-33, jObject["Three"].GetValue()); + Assert.Equal(3, jObject.Count); + + jObject.SetAt(2, "Four", "str"); + + Assert.Equal(3, jObject.Count); + Assert.DoesNotContain("Three", jObject); + Assert.Contains("Four", jObject); + Assert.Equal("str", jObject["Four"].GetValue()); + } + + [Fact] + public static void SetAt_CaseInsensitive_UpdatesProperty() + { + JsonObject jObject = new(new() { PropertyNameCaseInsensitive = true }) + { + ["One"] = 1, + ["Two"] = "str", + ["Three"] = null, + }; + + Assert.Null(jObject["three"]); + Assert.Equal(3, jObject.Count); + + jObject.SetAt(2, -5); + + Assert.Equal(-5, jObject["three"].GetValue()); + Assert.Equal(3, jObject.Count); + + jObject.SetAt(2, "THREE", -33); + + Assert.Equal(-33, jObject["ThRee"].GetValue()); + Assert.Equal(3, jObject.Count); + + jObject.SetAt(2, "Four", "str"); + + Assert.Equal(3, jObject.Count); + Assert.DoesNotContain("Three", jObject); + Assert.Contains("FOUR", jObject); + Assert.Equal("str", jObject["FoUR"].GetValue()); + } + + [Fact] + public static void SetAt_InvalidArgument_ThrowsArgumentOutOfRangeException() + { + JsonObject jObject = new() + { + ["One"] = 1, + ["Two"] = "str", + ["Three"] = null, + }; + + // Throws exception if the property name is null. + Assert.Throws(() => jObject.SetAt(2, propertyName: null, 5)); + + // Throws exception if the index is out of range. + Assert.Throws(() => jObject.SetAt(-1, "Four", 5)); + Assert.Throws(() => jObject.SetAt(jObject.Count, "Four", 5)); + Assert.Throws(() => jObject.SetAt(-1, 5)); + Assert.Throws(() => jObject.SetAt(jObject.Count, 5)); + + // Throws exception if the key exists at a different position. + Assert.Throws(() => jObject.SetAt(2, "One", "str")); + } + + [Fact] + public static void IndexOf_ReturnsExpectedIndex() + { + JsonObject jObject = new() + { + ["One"] = 1, + ["Two"] = "str", + ["Three"] = null, + }; + + Assert.Equal(0, jObject.IndexOf("One")); + Assert.Equal(1, jObject.IndexOf("Two")); + Assert.Equal(2, jObject.IndexOf("Three")); + + Assert.Equal(-1, jObject.IndexOf("Four")); + Assert.Equal(-1, jObject.IndexOf("three")); + } + + [Fact] + public static void IndexOf_CaseInsensitive_ReturnsExpectedIndex() + { + JsonObject jObject = new(new() { PropertyNameCaseInsensitive = true }) + { + ["One"] = 1, + ["Two"] = "str", + ["Three"] = null, + }; + + Assert.Equal(0, jObject.IndexOf("One")); + Assert.Equal(1, jObject.IndexOf("Two")); + Assert.Equal(2, jObject.IndexOf("Three")); + + Assert.Equal(0, jObject.IndexOf("onE")); + Assert.Equal(1, jObject.IndexOf("tWo")); + Assert.Equal(2, jObject.IndexOf("THREE")); + + Assert.Equal(-1, jObject.IndexOf("Four")); + } + + [Fact] + public static void IndexOf_NullPropertyName_ThrowsArgumentNullException() + { + JsonObject jObject = new() + { + ["One"] = 1, + ["Two"] = "str", + ["Three"] = null, + }; + + Assert.Throws(() => jObject.IndexOf(propertyName: null)); + } + + [Fact] + public static void Insert_InsertsProperty() + { + JsonObject jObject = new() + { + ["Two"] = "str", + }; + + Assert.Equal(1, jObject.Count); + + jObject.Insert(0, "One", 1); + + Assert.Equal(2, jObject.Count); + Assert.Equal(1, jObject["One"].GetValue()); + Assert.Equal("str", jObject["Two"].GetValue()); + Assert.Equal(0, jObject.IndexOf("One")); + Assert.Equal(1, jObject.IndexOf("Two")); + + jObject.Insert(1, "Three", null); + + Assert.Equal(3, jObject.Count); + Assert.Equal(1, jObject["One"].GetValue()); + Assert.Null(jObject["Three"]); + Assert.Equal("str", jObject["Two"].GetValue()); + Assert.Equal(0, jObject.IndexOf("One")); + Assert.Equal(1, jObject.IndexOf("Three")); + Assert.Equal(2, jObject.IndexOf("Two")); + + jObject.Insert(jObject.Count, "Four", 4); + + Assert.Equal(4, jObject.Count); + Assert.Equal(1, jObject["One"].GetValue()); + Assert.Null(jObject["Three"]); + Assert.Equal("str", jObject["Two"].GetValue()); + Assert.Equal(4, jObject["Four"].GetValue()); + Assert.Equal(0, jObject.IndexOf("One")); + Assert.Equal(1, jObject.IndexOf("Three")); + Assert.Equal(2, jObject.IndexOf("Two")); + Assert.Equal(3, jObject.IndexOf("Four")); + } + + [Fact] + public static void Insert_ExistingKey_ThrowsArgumentException() + { + JsonObject jObject = new() + { + ["One"] = 1, + ["Two"] = "str", + ["Three"] = null, + }; + + Assert.Equal(3, jObject.Count); + + Assert.Throws(() => jObject.Insert(1, "Three", 3)); + + Assert.Equal(3, jObject.Count); + } + + [Fact] + public static void Insert_NullKey_ThrowsArgumentNullException() + { + JsonObject jObject = new() + { + ["One"] = 1, + ["Two"] = "str", + ["Three"] = null, + }; + + Assert.Equal(3, jObject.Count); + + Assert.Throws(() => jObject.Insert(1, propertyName: null, 3)); + + Assert.Equal(3, jObject.Count); + } + + [Fact] + public static void Insert_InvalidIndex_ThrowsArgumentOutOfRangeException() + { + JsonObject jObject = new() + { + ["One"] = 1, + ["Two"] = "str", + ["Three"] = null, + }; + + Assert.Equal(3, jObject.Count); + + Assert.Throws(() => jObject.Insert(-1, "Four", 4)); + Assert.Throws(() => jObject.Insert(jObject.Count + 1, "Four", 4)); + + Assert.Equal(3, jObject.Count); + } + + [Fact] + public static void RemoveAt_RemovesProperty() + { + JsonObject jObject = new() + { + ["One"] = 1, + ["Two"] = "str", + ["Three"] = null, + }; + + jObject.RemoveAt(1); + + Assert.Equal(2, jObject.Count); + Assert.Contains("One", jObject); + Assert.DoesNotContain("Two", jObject); + Assert.Contains("Three", jObject); + + jObject.RemoveAt(1); + + Assert.Equal(1, jObject.Count); + Assert.Contains("One", jObject); + Assert.DoesNotContain("Two", jObject); + Assert.DoesNotContain("Three", jObject); + + jObject.RemoveAt(0); + + Assert.Empty(jObject); + Assert.DoesNotContain("One", jObject); + Assert.DoesNotContain("Two", jObject); + Assert.DoesNotContain("Three", jObject); + } + + [Fact] + public static void RemoveAt_InvalidIndex_ThrowsArgumentOutOfRangeException() + { + JsonObject jObject = new() + { + ["One"] = 1, + ["Two"] = "str", + ["Three"] = null, + }; + + Assert.Throws(() => jObject.RemoveAt(-1)); + Assert.Throws(() => jObject.RemoveAt(jObject.Count)); + } + + [Fact] + public static void JsonObject_IsIList() + { + JsonObject jObject = new() + { + ["One"] = 1, + ["Two"] = "str", + ["Three"] = null, + }; + + IList> ilist = Assert.IsAssignableFrom>>(jObject); + + // Indexer getter + KeyValuePair kvp = ilist[1]; + Assert.Equal("Two", kvp.Key); + Assert.Equal("str", kvp.Value.GetValue()); + + // Indexer setter + kvp = new("Four", 4); + ilist[1] = kvp; + Assert.Contains(kvp, ilist); + Assert.DoesNotContain("Two", jObject); + Assert.Contains("Four", jObject); + + // IndexOf + Assert.Equal(1, ilist.IndexOf(kvp)); + Assert.Equal(-1, ilist.IndexOf(new("Four", 4))); // Different JsonNode instance + + // Insert + ilist.Insert(1, new("Two", "str")); + Assert.Equal(4, ilist.Count); + Assert.Contains("Two", jObject); + Assert.Equal(1, jObject.IndexOf("Two")); + + // RemoveAt + ilist.RemoveAt(1); + Assert.Equal(3, ilist.Count); + Assert.DoesNotContain("Two", jObject); + Assert.Equal(-1, jObject.IndexOf("Two")); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/ParseTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/ParseTests.cs index b8594d8c89c6c..721ee7bfeffcf 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/ParseTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/ParseTests.cs @@ -159,16 +159,18 @@ public static async Task InternalValueFields() FieldInfo jsonDictionaryField = typeof(JsonObject).GetField("_dictionary", BindingFlags.Instance | BindingFlags.NonPublic); Assert.NotNull(jsonDictionaryField); - Type jsonPropertyDictionaryType = typeof(JsonObject).Assembly.GetType("System.Text.Json.JsonPropertyDictionary`1"); +#if !NET9_0_OR_GREATER // Bespoke implementation replaced with OrderedDictionary + Type jsonPropertyDictionaryType = typeof(JsonObject).Assembly.GetType("System.Text.Json.OrderedDictionary`2"); Assert.NotNull(jsonPropertyDictionaryType); - jsonPropertyDictionaryType = jsonPropertyDictionaryType.MakeGenericType(new Type[] { typeof(JsonNode) }); + jsonPropertyDictionaryType = jsonPropertyDictionaryType.MakeGenericType(new Type[] { typeof(string), typeof(JsonNode) }); FieldInfo listField = jsonPropertyDictionaryType.GetField("_propertyList", BindingFlags.Instance | BindingFlags.NonPublic); Assert.NotNull(listField); FieldInfo dictionaryField = jsonPropertyDictionaryType.GetField("_propertyDictionary", BindingFlags.Instance | BindingFlags.NonPublic); Assert.NotNull(dictionaryField); +#endif using (MemoryStream stream = new MemoryStream(SimpleTestClass.s_data)) { @@ -186,8 +188,10 @@ public static async Task InternalValueFields() jsonDictionary = jsonDictionaryField.GetValue(node); Assert.NotNull(jsonDictionary); +#if !NET9_0_OR_GREATER // Bespoke implementation replaced with OrderedDictionary Assert.NotNull(listField.GetValue(jsonDictionary)); Assert.NotNull(dictionaryField.GetValue(jsonDictionary)); // The dictionary threshold was reached. +#endif Test(); void Test() @@ -217,8 +221,11 @@ void Test() jsonDictionary = jsonDictionaryField.GetValue(node); Assert.NotNull(jsonDictionary); +#if !NET9_0_OR_GREATER // Bespoke implementation replaced with OrderedDictionary Assert.NotNull(listField.GetValue(jsonDictionary)); Assert.NotNull(dictionaryField.GetValue(jsonDictionary)); // The dictionary threshold was reached. +#endif + Test(); void Test()