File: Routing\RouteValueDictionary.cs
Web Access
Project: src\src\Http\Http.Abstractions\src\Microsoft.AspNetCore.Http.Abstractions.csproj (Microsoft.AspNetCore.Http.Abstractions)
// 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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Shared;
 
#if !COMPONENTS
using System.Collections.Concurrent;
using System.Reflection.Metadata;
using Microsoft.AspNetCore.Http.Abstractions;
using Microsoft.Extensions.Internal;
using Microsoft.AspNetCore.Routing;
#endif
 
#if !COMPONENTS
[assembly: MetadataUpdateHandler(typeof(RouteValueDictionary.MetadataUpdateHandler))]
#endif
 
#if COMPONENTS
namespace Microsoft.AspNetCore.Components.Routing;
#else
namespace Microsoft.AspNetCore.Routing;
#endif
 
#if !COMPONENTS
/// <summary>
/// An <see cref="IDictionary{String, Object}"/> type for route values.
/// </summary>
#endif
[DebuggerTypeProxy(typeof(DictionaryDebugView<string, object?>))]
[DebuggerDisplay("Count = {Count}")]
#if !COMPONENTS
public class RouteValueDictionary : IDictionary<string, object?>, IReadOnlyDictionary<string, object?>
#else
internal class RouteValueDictionary : IDictionary<string, object?>, IReadOnlyDictionary<string, object?>
#endif
{
    // 4 is a good default capacity here because that leaves enough space for area/controller/action/id
    private const int DefaultCapacity = 4;
#if !COMPONENTS
    private static readonly ConcurrentDictionary<Type, PropertyHelper[]> _propertyCache = new ConcurrentDictionary<Type, PropertyHelper[]>();
#endif
 
    internal KeyValuePair<string, object?>[] _arrayStorage;
#if !COMPONENTS
    internal PropertyStorage? _propertyStorage;
#endif
    private int _count;
 
#if !COMPONENTS
    /// <summary>
    /// Creates a new <see cref="RouteValueDictionary"/> from the provided array.
    /// The new instance will take ownership of the array, and may mutate it.
    /// </summary>
    /// <param name="items">The items array.</param>
    /// <returns>A new <see cref="RouteValueDictionary"/>.</returns>
    public static RouteValueDictionary FromArray(KeyValuePair<string, object?>[] items)
    {
        ArgumentNullException.ThrowIfNull(items);
 
        // We need to compress the array by removing non-contiguous items. We
        // typically have a very small number of items to process. We don't need
        // to preserve order.
        var start = 0;
        var end = items.Length - 1;
 
        // We walk forwards from the beginning of the array and fill in 'null' slots.
        // We walk backwards from the end of the array end move items in non-null' slots
        // into whatever start is pointing to. O(n)
        while (start <= end)
        {
            if (items[start].Key != null)
            {
                start++;
            }
            else if (items[end].Key != null)
            {
                // Swap this item into start and advance
                items[start] = items[end];
                items[end] = default;
                start++;
                end--;
            }
            else
            {
                // Both null, we need to hold on 'start' since we
                // still need to fill it with something.
                end--;
            }
        }
 
        return new RouteValueDictionary()
        {
            _arrayStorage = items!,
            _count = start,
        };
    }
#endif
 
    /// <summary>
    /// Creates an empty <see cref="RouteValueDictionary"/>.
    /// </summary>
    public RouteValueDictionary()
    {
        _arrayStorage = Array.Empty<KeyValuePair<string, object?>>();
    }
 
#if !COMPONENTS
    /// <summary>
    /// Creates a <see cref="RouteValueDictionary"/> initialized with the specified <paramref name="values"/>.
    /// </summary>
    /// <param name="values">An object to initialize the dictionary. The value can be of type
    /// <see cref="IDictionary{TKey, TValue}"/> or <see cref="IReadOnlyDictionary{TKey, TValue}"/>
    /// or an object with public properties as key-value pairs.
    /// </param>
    /// <remarks>
    /// If the value is a dictionary or other <see cref="IEnumerable{T}"/> of <see cref="KeyValuePair{String, Object}"/>,
    /// then its entries are copied. Otherwise the object is interpreted as a set of key-value pairs where the
    /// property names are keys, and property values are the values, and copied into the dictionary.
    /// Only public instance non-index properties are considered.
    /// </remarks>
    [RequiresUnreferencedCode("This constructor may perform reflection on the specificed value which may be trimmed if not referenced directly. Consider using a different overload to avoid this issue.")]
    public RouteValueDictionary(object? values)
    {
        if (values is RouteValueDictionary dictionary)
        {
            Initialize(dictionary);
 
            return;
        }
 
        if (values is IEnumerable<KeyValuePair<string, object?>> keyValueEnumerable)
        {
            Initialize(keyValueEnumerable);
 
            return;
        }
 
        if (values is IEnumerable<KeyValuePair<string, string?>> stringValueEnumerable)
        {
            Initialize(stringValueEnumerable);
 
            return;
        }
 
        if (values != null)
        {
            var storage = new PropertyStorage(values);
            _propertyStorage = storage;
            _count = storage.Properties.Length;
            _arrayStorage = Array.Empty<KeyValuePair<string, object?>>();
        }
        else
        {
            _arrayStorage = Array.Empty<KeyValuePair<string, object?>>();
        }
    }
#endif
 
    /// <summary>
    /// Creates a <see cref="RouteValueDictionary"/> initialized with the specified <paramref name="values"/>.
    /// </summary>
    /// <param name="values">A sequence of values to add to the dictionary..</param>
    public RouteValueDictionary(IEnumerable<KeyValuePair<string, object?>>? values)
    {
        if (values is not null)
        {
            Initialize(values);
        }
        else
        {
            _arrayStorage = Array.Empty<KeyValuePair<string, object?>>();
        }
    }
 
#if !COMPONENTS
    /// <summary>
    /// Creates a <see cref="RouteValueDictionary"/> initialized with the specified <paramref name="values"/>.
    /// </summary>
    /// <param name="values">A sequence of values to add to the dictionary..</param>
    public RouteValueDictionary(IEnumerable<KeyValuePair<string, string?>>? values)
    {
        if (values is not null)
        {
            Initialize(values);
        }
        else
        {
            _arrayStorage = Array.Empty<KeyValuePair<string, object?>>();
        }
    }
 
    /// <summary>
    /// Creates a <see cref="RouteValueDictionary"/> initialized with the specified <paramref name="dictionary"/>.
    /// </summary>
    /// <param name="dictionary">A <see cref="RouteValueDictionary"/> to initialize the dictionary.</param>
    public RouteValueDictionary(RouteValueDictionary? dictionary)
    {
        if (dictionary is not null)
        {
            Initialize(dictionary);
        }
        else
        {
            _arrayStorage = Array.Empty<KeyValuePair<string, object?>>();
        }
    }
#endif
 
    [MemberNotNull(nameof(_arrayStorage))]
    private void Initialize(IEnumerable<KeyValuePair<string, string?>> stringValueEnumerable)
    {
        _arrayStorage = Array.Empty<KeyValuePair<string, object?>>();
 
        foreach (var kvp in stringValueEnumerable)
        {
            Add(kvp.Key, kvp.Value);
        }
    }
 
    [MemberNotNull(nameof(_arrayStorage))]
    private void Initialize(IEnumerable<KeyValuePair<string, object?>> keyValueEnumerable)
    {
        _arrayStorage = Array.Empty<KeyValuePair<string, object?>>();
 
        foreach (var kvp in keyValueEnumerable)
        {
            Add(kvp.Key, kvp.Value);
        }
    }
 
    [MemberNotNull(nameof(_arrayStorage))]
    private void Initialize(RouteValueDictionary dictionary)
    {
#if !COMPONENTS
        if (dictionary._propertyStorage != null)
        {
            // PropertyStorage is immutable so we can just copy it.
            _propertyStorage = dictionary._propertyStorage;
            _count = dictionary._count;
            _arrayStorage = Array.Empty<KeyValuePair<string, object?>>();
            return;
        }
#endif
 
        var count = dictionary._count;
        if (count > 0)
        {
            var other = dictionary._arrayStorage;
            var storage = new KeyValuePair<string, object?>[count];
            Array.Copy(other, 0, storage, 0, count);
            _arrayStorage = storage;
            _count = count;
        }
        else
        {
            _arrayStorage = Array.Empty<KeyValuePair<string, object?>>();
        }
    }
 
    /// <inheritdoc />
    public object? this[string key]
    {
        get
        {
            if (key == null)
            {
                ThrowArgumentNullExceptionForKey();
            }
 
            TryGetValue(key, out var value);
            return value;
        }
 
        set
        {
            if (key == null)
            {
                ThrowArgumentNullExceptionForKey();
            }
 
            // We're calling this here for the side-effect of converting from properties
            // to array. We need to create the array even if we just set an existing value since
            // property storage is immutable.
            EnsureCapacity(_count);
 
            var index = FindIndex(key);
            if (index < 0)
            {
                EnsureCapacity(_count + 1);
                _arrayStorage[_count++] = new KeyValuePair<string, object?>(key, value);
            }
            else
            {
                _arrayStorage[index] = new KeyValuePair<string, object?>(key, value);
            }
        }
    }
 
#if !COMPONENTS
    /// <summary>
    /// Gets the comparer for this dictionary.
    /// </summary>
    /// <remarks>
    /// This will always be a reference to <see cref="StringComparer.OrdinalIgnoreCase"/>
    /// </remarks>
    public IEqualityComparer<string> Comparer => StringComparer.OrdinalIgnoreCase;
#endif
 
    /// <inheritdoc />
    public int Count => _count;
 
    /// <inheritdoc />
    bool ICollection<KeyValuePair<string, object?>>.IsReadOnly => false;
 
    /// <inheritdoc />
    public ICollection<string> Keys
    {
        get
        {
            EnsureCapacity(_count);
 
            var array = _arrayStorage;
            var keys = new string[_count];
            for (var i = 0; i < keys.Length; i++)
            {
                keys[i] = array[i].Key;
            }
 
            return keys;
        }
    }
 
    IEnumerable<string> IReadOnlyDictionary<string, object?>.Keys => Keys;
 
    /// <inheritdoc />
    public ICollection<object?> Values
    {
        get
        {
            EnsureCapacity(_count);
 
            var array = _arrayStorage;
            var values = new object?[_count];
            for (var i = 0; i < values.Length; i++)
            {
                values[i] = array[i].Value;
            }
 
            return values;
        }
    }
 
    IEnumerable<object?> IReadOnlyDictionary<string, object?>.Values => Values;
 
    /// <inheritdoc />
    void ICollection<KeyValuePair<string, object?>>.Add(KeyValuePair<string, object?> item)
    {
        Add(item.Key, item.Value);
    }
 
    /// <inheritdoc />
    public void Add(string key, object? value)
    {
        if (key == null)
        {
            ThrowArgumentNullExceptionForKey();
        }
 
        EnsureCapacity(_count + 1);
 
        if (ContainsKeyArray(key))
        {
#if !COMPONENTS
            var message = Resources.FormatRouteValueDictionary_DuplicateKey(key, nameof(RouteValueDictionary));
            throw new ArgumentException(message, nameof(key));
#else
            throw new ArgumentException($"An element with the key '{key}' already exists in the {nameof(RouteValueDictionary)}.");
#endif
        }
 
        _arrayStorage[_count] = new KeyValuePair<string, object?>(key, value);
        _count++;
    }
 
    /// <inheritdoc />
    public void Clear()
    {
        if (_count == 0)
        {
            return;
        }
 
#if !COMPONENTS
        if (_propertyStorage != null)
        {
            _arrayStorage = Array.Empty<KeyValuePair<string, object?>>();
            _propertyStorage = null;
            _count = 0;
            return;
        }
#endif
        Array.Clear(_arrayStorage, 0, _count);
        _count = 0;
    }
 
    /// <inheritdoc />
    bool ICollection<KeyValuePair<string, object?>>.Contains(KeyValuePair<string, object?> item)
    {
        return TryGetValue(item.Key, out var value) && EqualityComparer<object>.Default.Equals(value, item.Value);
    }
 
    /// <inheritdoc />
    public bool ContainsKey(string key)
    {
        if (key == null)
        {
            ThrowArgumentNullExceptionForKey();
        }
 
        return ContainsKeyCore(key);
    }
 
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private bool ContainsKeyCore(string key)
    {
#if !COMPONENTS
        if (_propertyStorage == null)
        {
            return ContainsKeyArray(key);
        }
 
        return ContainsKeyProperties(key);
#else
        return ContainsKeyArray(key);
#endif
    }
 
    /// <inheritdoc />
    void ICollection<KeyValuePair<string, object?>>.CopyTo(
        KeyValuePair<string, object?>[] array,
        int arrayIndex)
    {
        ArgumentNullException.ThrowIfNull(array);
 
        if (arrayIndex < 0 || arrayIndex > array.Length || array.Length - arrayIndex < Count)
        {
            throw new ArgumentOutOfRangeException(nameof(arrayIndex));
        }
 
        if (Count == 0)
        {
            return;
        }
 
        EnsureCapacity(Count);
 
        var storage = _arrayStorage;
        Array.Copy(storage, 0, array, arrayIndex, _count);
    }
 
    /// <inheritdoc />
    public Enumerator GetEnumerator()
    {
        return new Enumerator(this);
    }
 
    /// <inheritdoc />
    IEnumerator<KeyValuePair<string, object?>> IEnumerable<KeyValuePair<string, object?>>.GetEnumerator()
    {
        return GetEnumerator();
    }
 
    /// <inheritdoc />
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
 
    /// <inheritdoc />
    bool ICollection<KeyValuePair<string, object?>>.Remove(KeyValuePair<string, object?> item)
    {
        if (Count == 0)
        {
            return false;
        }
 
        Debug.Assert(_arrayStorage != null);
 
        EnsureCapacity(Count);
 
        var index = FindIndex(item.Key);
        var array = _arrayStorage;
        if (index >= 0 && EqualityComparer<object>.Default.Equals(array[index].Value, item.Value))
        {
            Array.Copy(array, index + 1, array, index, _count - index);
            _count--;
            array[_count] = default;
            return true;
        }
 
        return false;
    }
 
    /// <inheritdoc />
    public bool Remove(string key)
    {
        if (key == null)
        {
            ThrowArgumentNullExceptionForKey();
        }
 
        if (Count == 0)
        {
            return false;
        }
 
        // Ensure property storage is converted to array storage as we'll be
        // applying the lookup and removal on the array
        EnsureCapacity(_count);
 
        var index = FindIndex(key);
        if (index >= 0)
        {
            _count--;
            var array = _arrayStorage;
            Array.Copy(array, index + 1, array, index, _count - index);
            array[_count] = default;
 
            return true;
        }
 
        return false;
    }
 
    /// <summary>
    /// Attempts to remove and return the value that has the specified key from the <see cref="RouteValueDictionary"/>.
    /// </summary>
    /// <param name="key">The key of the element to remove and return.</param>
    /// <param name="value">When this method returns, contains the object removed from the <see cref="RouteValueDictionary"/>, or <c>null</c> if key does not exist.</param>
    /// <returns>
    /// <c>true</c> if the object was removed successfully; otherwise, <c>false</c>.
    /// </returns>
    public bool Remove(string key, out object? value)
    {
        if (key == null)
        {
            ThrowArgumentNullExceptionForKey();
        }
 
        if (_count == 0)
        {
            value = default;
            return false;
        }
 
        // Ensure property storage is converted to array storage as we'll be
        // applying the lookup and removal on the array
        EnsureCapacity(_count);
 
        var index = FindIndex(key);
        if (index >= 0)
        {
            _count--;
            var array = _arrayStorage;
            value = array[index].Value;
            Array.Copy(array, index + 1, array, index, _count - index);
            array[_count] = default;
 
            return true;
        }
 
        value = default;
        return false;
    }
 
    /// <summary>
    /// Attempts to the add the provided <paramref name="key"/> and <paramref name="value"/> to the dictionary.
    /// </summary>
    /// <param name="key">The key.</param>
    /// <param name="value">The value.</param>
    /// <returns>Returns <c>true</c> if the value was added. Returns <c>false</c> if the key was already present.</returns>
    public bool TryAdd(string key, object? value)
    {
        if (key == null)
        {
            ThrowArgumentNullExceptionForKey();
        }
 
        if (ContainsKeyCore(key))
        {
            return false;
        }
 
        EnsureCapacity(Count + 1);
        _arrayStorage[Count] = new KeyValuePair<string, object?>(key, value);
        _count++;
        return true;
    }
 
    /// <inheritdoc />
    public bool TryGetValue(string key, out object? value)
    {
        if (key == null)
        {
            ThrowArgumentNullExceptionForKey();
        }
 
#if COMPONENTS
        return TryFindItem(key, out value);
#else
        if (_propertyStorage == null)
        {
            return TryFindItem(key, out value);
        }
 
        return TryGetValueSlow(key, out value);
    }
 
    [UnconditionalSuppressMessage("Trimming", "IL2026",
        Justification = "The constructor that would result in _propertyStorage being non-null is annotated with RequiresUnreferencedCodeAttribute. " +
        "We do not need to additionally produce an error in this method since it is shared by trimmer friendly code paths.")]
    private bool TryGetValueSlow(string key, out object? value)
    {
        if (_propertyStorage != null)
        {
            var storage = _propertyStorage;
            for (var i = 0; i < storage.Properties.Length; i++)
            {
                if (string.Equals(storage.Properties[i].Name, key, StringComparison.OrdinalIgnoreCase))
                {
                    value = storage.Properties[i].GetValue(storage.Value);
                    return true;
                }
            }
        }
 
        value = default;
        return false;
#endif
    }
 
    [DoesNotReturn]
    private static void ThrowArgumentNullExceptionForKey()
    {
        throw new ArgumentNullException("key");
    }
 
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void EnsureCapacity(int capacity)
    {
#if !COMPONENTS
        if (_propertyStorage != null || _arrayStorage.Length < capacity)
        {
            EnsureCapacitySlow(capacity);
        }
#else
        if (_arrayStorage.Length < capacity)
        {
            EnsureCapacitySlow(capacity);
        }
#endif
    }
 
    [UnconditionalSuppressMessage("Trimming", "IL2026",
        Justification = "The constructor that would result in _propertyStorage being non-null is annotated with RequiresUnreferencedCodeAttribute. " +
        "We do not need to additionally produce an error in this method since it is shared by trimmer friendly code paths.")]
    private void EnsureCapacitySlow(int capacity)
    {
#if !COMPONENTS
        if (_propertyStorage != null)
        {
            var storage = _propertyStorage;
 
            // If we're converting from properties, it's likely due to an 'add' to make sure we have at least
            // the default amount of space.
            capacity = Math.Max(DefaultCapacity, Math.Max(storage.Properties.Length, capacity));
            var array = new KeyValuePair<string, object?>[capacity];
 
            for (var i = 0; i < storage.Properties.Length; i++)
            {
                var property = storage.Properties[i];
                array[i] = new KeyValuePair<string, object?>(property.Name, property.GetValue(storage.Value));
            }
 
            _arrayStorage = array;
            _propertyStorage = null;
            return;
        }
#endif
        if (_arrayStorage.Length < capacity)
        {
            capacity = _arrayStorage.Length == 0 ? DefaultCapacity : _arrayStorage.Length * 2;
            var array = new KeyValuePair<string, object?>[capacity];
            if (_count > 0)
            {
                Array.Copy(_arrayStorage, 0, array, 0, _count);
            }
 
            _arrayStorage = array;
        }
    }
 
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private int FindIndex(string key)
    {
        // Generally the bounds checking here will be elided by the JIT because this will be called
        // on the same code path as EnsureCapacity.
        var array = _arrayStorage;
        var count = _count;
 
        for (var i = 0; i < count; i++)
        {
            if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase))
            {
                return i;
            }
        }
 
        return -1;
    }
 
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private bool TryFindItem(string key, out object? value)
    {
        var array = _arrayStorage;
        var count = _count;
 
        // Elide bounds check for indexing.
        if ((uint)count <= (uint)array.Length)
        {
            for (var i = 0; i < count; i++)
            {
                if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase))
                {
                    value = array[i].Value;
                    return true;
                }
            }
        }
 
        value = null;
        return false;
    }
 
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private bool ContainsKeyArray(string key)
    {
        var array = _arrayStorage;
        var count = _count;
 
        // Elide bounds check for indexing.
        if ((uint)count <= (uint)array.Length)
        {
            for (var i = 0; i < count; i++)
            {
                if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase))
                {
                    return true;
                }
            }
        }
 
        return false;
    }
 
#if !COMPONENTS
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private bool ContainsKeyProperties(string key)
    {
        Debug.Assert(_propertyStorage != null);
 
        var properties = _propertyStorage.Properties;
        for (var i = 0; i < properties.Length; i++)
        {
            if (string.Equals(properties[i].Name, key, StringComparison.OrdinalIgnoreCase))
            {
                return true;
            }
        }
 
        return false;
    }
#endif
 
    /// <inheritdoc />
    public struct Enumerator : IEnumerator<KeyValuePair<string, object?>>
    {
        private readonly RouteValueDictionary _dictionary;
        private int _index;
 
        /// <summary>
        /// Instantiates a new enumerator with the values provided in <paramref name="dictionary"/>.
        /// </summary>
        /// <param name="dictionary">A <see cref="RouteValueDictionary"/>.</param>
        public Enumerator(RouteValueDictionary dictionary)
        {
            ArgumentNullException.ThrowIfNull(dictionary);
 
            _dictionary = dictionary;
 
            Current = default;
            _index = 0;
        }
 
        /// <inheritdoc />
        public KeyValuePair<string, object?> Current { get; private set; }
 
        object IEnumerator.Current => Current;
 
        /// <summary>
        /// Releases resources used by the <see cref="Enumerator"/>.
        /// </summary>
        public void Dispose()
        {
        }
 
        // Similar to the design of List<T>.Enumerator - Split into fast path and slow path for inlining friendliness
        /// <inheritdoc />
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public bool MoveNext()
        {
            var dictionary = _dictionary;
 
#if !COMPONENTS
            // The uncommon case is that the propertyStorage is in use
            if (dictionary._propertyStorage == null && ((uint)_index < (uint)dictionary._count))
            {
                Current = dictionary._arrayStorage[_index];
                _index++;
                return true;
            }
#else
            if (((uint)_index < (uint)dictionary._count))
            {
                Current = dictionary._arrayStorage[_index];
                _index++;
                return true;
            }
#endif
 
            return MoveNextRare();
        }
 
        [UnconditionalSuppressMessage("Trimming", "IL2026",
            Justification = "The constructor that would result in _propertyStorage being non-null is annotated with RequiresUnreferencedCodeAttribute. " +
            "We do not need to additionally produce an error in this method since it is shared by trimmer friendly code paths.")]
        private bool MoveNextRare()
        {
            var dictionary = _dictionary;
#if !COMPONENTS
            if (dictionary._propertyStorage != null && ((uint)_index < (uint)dictionary._count))
            {
                var storage = dictionary._propertyStorage;
                var property = storage.Properties[_index];
                Current = new KeyValuePair<string, object?>(property.Name, property.GetValue(storage.Value));
                _index++;
                return true;
            }
#endif
 
            _index = dictionary._count;
            Current = default;
            return false;
        }
 
        /// <inheritdoc />
        public void Reset()
        {
            Current = default;
            _index = 0;
        }
    }
 
#if !COMPONENTS
    [RequiresUnreferencedCode("This API is not trim safe - from PropertyHelper")]
    internal sealed class PropertyStorage
    {
        public readonly object Value;
        public readonly PropertyHelper[] Properties;
 
        public PropertyStorage(object value)
        {
            Debug.Assert(value != null);
            Value = value;
 
            // Cache the properties so we can know if we've already validated them for duplicates.
            var type = Value.GetType();
            if (!_propertyCache.TryGetValue(type, out Properties!))
            {
                Properties = PropertyHelper.GetVisibleProperties(type, allPropertiesCache: null, visiblePropertiesCache: null);
                ValidatePropertyNames(type, Properties);
                _propertyCache.TryAdd(type, Properties);
            }
        }
 
        private static void ValidatePropertyNames(Type type, PropertyHelper[] properties)
        {
            var names = new Dictionary<string, PropertyHelper>(StringComparer.OrdinalIgnoreCase);
            for (var i = 0; i < properties.Length; i++)
            {
                var property = properties[i];
 
                if (names.TryGetValue(property.Name, out var duplicate))
                {
                    var message = Resources.FormatRouteValueDictionary_DuplicatePropertyName(
                        type.FullName,
                        property.Name,
                        duplicate.Name,
                        nameof(RouteValueDictionary));
                    throw new InvalidOperationException(message);
                }
 
                names.Add(property.Name, property);
            }
        }
    }
 
    internal static class MetadataUpdateHandler
    {
        /// <summary>
        /// Invoked as part of <see cref="MetadataUpdateHandlerAttribute" /> contract for hot reload.
        /// </summary>
        internal static void ClearCache(Type[]? _)
        {
            _propertyCache.Clear();
        }
    }
#endif
}