File: System\Text\Json\JsonPropertyDictionary.cs
Web Access
Project: src\src\libraries\System.Text.Json\src\System.Text.Json.csproj (System.Text.Json)
// 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
{
    /// <summary>
    /// 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.
    /// </summary>
    internal sealed partial class JsonPropertyDictionary<T> where T : class?
    {
        private const int ListToDictionaryThreshold = 9;
 
        private Dictionary<string, T>? _propertyDictionary;
        private readonly List<KeyValuePair<string, T>> _propertyList;
 
        private readonly StringComparer _stringComparer;
 
        public JsonPropertyDictionary(bool caseInsensitive)
        {
            _stringComparer = caseInsensitive ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;
            _propertyList = new List<KeyValuePair<string, T>>();
        }
 
        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<KeyValuePair<string, T>> 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<string, T> 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<string, T> item)
        {
            foreach (KeyValuePair<string, T> existing in this)
            {
                if (ReferenceEquals(item.Value, existing.Value) && _stringComparer.Equals(item.Key, existing.Key))
                {
                    return true;
                }
            }
 
            return false;
        }
 
        public void CopyTo(KeyValuePair<string, T>[] array, int index)
        {
            if (index < 0)
            {
                ThrowHelper.ThrowArgumentOutOfRangeException_ArrayIndexNegative(nameof(index));
            }
 
            foreach (KeyValuePair<string, T> item in _propertyList)
            {
                if (index >= array.Length)
                {
                    ThrowHelper.ThrowArgumentException_ArrayTooSmall(nameof(array));
                }
 
                array[index++] = item;
            }
        }
 
        public List<KeyValuePair<string, T>>.Enumerator GetEnumerator() => _propertyList.GetEnumerator();
 
        public IList<string> Keys => GetKeyCollection();
 
        public IList<T> 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<string, T> 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<string, T>(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<string, T> current = _propertyList[i];
                    if (ReferenceEquals(current.Value, value))
                    {
                        // Ignore if the same value.
                        valueAlreadyInDictionary = true;
                        return null;
                    }
 
                    existing = current.Value;
                }
 
                _propertyList[i] = new KeyValuePair<string, T>(propertyName, value);
            }
            else
            {
                _propertyDictionary?.Add(propertyName, value);
                _propertyList.Add(new KeyValuePair<string, T>(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<string, T>(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<string, T>? FindValue(T value)
        {
            foreach (KeyValuePair<string, T> 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<string, T> 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<string, T> 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<string, T> current = _propertyList[i];
 
                if (_stringComparer.Equals(current.Key, propertyName))
                {
                    _propertyList.RemoveAt(i);
                    existing = current.Value;
                    return true;
                }
            }
 
            existing = null;
            return false;
        }
    }
}