// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Http.Abstractions; using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Routing { /// /// An type for route values. /// public class RouteValueDictionary : IDictionary, IReadOnlyDictionary { // 4 is a good default capacity here because that leaves enough space for area/controller/action/id private const int DefaultCapacity = 4; internal KeyValuePair[] _arrayStorage; internal PropertyStorage _propertyStorage; private int _count; /// /// Creates a new from the provided array. /// The new instance will take ownership of the array, and may mutate it. /// /// The items array. /// A new . public static RouteValueDictionary FromArray(KeyValuePair[] items) { if (items == null) { throw new ArgumentNullException(nameof(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, }; } /// /// Creates an empty . /// public RouteValueDictionary() { _arrayStorage = Array.Empty>(); } /// /// Creates a initialized with the specified . /// /// An object to initialize the dictionary. The value can be of type /// or /// or an object with public properties as key-value pairs. /// /// /// If the value is a dictionary or other of , /// 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. /// public RouteValueDictionary(object values) : this() { if (values is RouteValueDictionary dictionary) { if (dictionary._propertyStorage != null) { // PropertyStorage is immutable so we can just copy it. _propertyStorage = dictionary._propertyStorage; _count = dictionary._count; return; } var other = dictionary._arrayStorage; var storage = new KeyValuePair[other.Length]; if (dictionary._count != 0) { Array.Copy(other, 0, storage, 0, dictionary._count); } _arrayStorage = storage; _count = dictionary._count; return; } if (values is IEnumerable> keyValueEnumerable) { foreach (var kvp in keyValueEnumerable) { Add(kvp.Key, kvp.Value); } return; } if (values is IEnumerable> stringValueEnumerable) { foreach (var kvp in stringValueEnumerable) { Add(kvp.Key, kvp.Value); } return; } if (values != null) { var storage = new PropertyStorage(values); _propertyStorage = storage; _count = storage.Properties.Length; return; } } /// public object this[string key] { get { if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException(nameof(key)); } object value; TryGetValue(key, out value); return value; } set { if (string.IsNullOrEmpty(key)) { throw new ArgumentNullException(nameof(key)); } // 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 = FindInArray(key); if (index < 0) { EnsureCapacity(_count + 1); _arrayStorage[_count++] = new KeyValuePair(key, value); } else { _arrayStorage[index] = new KeyValuePair(key, value); } } } /// /// Gets the comparer for this dictionary. /// /// /// This will always be a reference to /// public IEqualityComparer Comparer => StringComparer.OrdinalIgnoreCase; /// public int Count => _count; /// bool ICollection>.IsReadOnly => false; /// public ICollection 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 IReadOnlyDictionary.Keys => Keys; /// public ICollection 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 IReadOnlyDictionary.Values => Values; /// void ICollection>.Add(KeyValuePair item) { Add(item.Key, item.Value); } /// public void Add(string key, object value) { if (key == null) { throw new ArgumentNullException(nameof(key)); } EnsureCapacity(_count + 1); var index = FindInArray(key); if (index >= 0) { var message = Resources.FormatRouteValueDictionary_DuplicateKey(key, nameof(RouteValueDictionary)); throw new ArgumentException(message, nameof(key)); } _arrayStorage[_count] = new KeyValuePair(key, value); _count++; } /// public void Clear() { if (_count == 0) { return; } if (_propertyStorage != null) { _arrayStorage = Array.Empty>(); _propertyStorage = null; _count = 0; return; } Array.Clear(_arrayStorage, 0, _count); _count = 0; } /// bool ICollection>.Contains(KeyValuePair item) { return TryGetValue(item.Key, out var value) && EqualityComparer.Default.Equals(value, item.Value); } /// public bool ContainsKey(string key) { if (key == null) { throw new ArgumentNullException(nameof(key)); } return TryGetValue(key, out var _); } /// void ICollection>.CopyTo( KeyValuePair[] array, int arrayIndex) { if (array == null) { throw new ArgumentNullException(nameof(array)); } if (arrayIndex < 0 || arrayIndex > array.Length || array.Length - arrayIndex < this.Count) { throw new ArgumentOutOfRangeException(nameof(arrayIndex)); } if (Count == 0) { return; } EnsureCapacity(Count); var storage = _arrayStorage; Array.Copy(storage, 0, array, arrayIndex, _count); } /// public Enumerator GetEnumerator() { return new Enumerator(this); } /// IEnumerator> IEnumerable>.GetEnumerator() { return GetEnumerator(); } /// IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } /// bool ICollection>.Remove(KeyValuePair item) { if (Count == 0) { return false; } EnsureCapacity(Count); var index = FindInArray(item.Key); var array = _arrayStorage; if (index >= 0 && EqualityComparer.Default.Equals(array[index].Value, item.Value)) { Array.Copy(array, index + 1, array, index, _count - index); _count--; array[_count] = default; return true; } return false; } /// public bool Remove(string key) { if (key == null) { throw new ArgumentNullException(nameof(key)); } if (Count == 0) { return false; } EnsureCapacity(Count); var index = FindInArray(key); if (index >= 0) { _count--; var array = _arrayStorage; Array.Copy(array, index + 1, array, index, _count - index); array[_count] = default; return true; } return false; } /// public bool TryGetValue(string key, out object value) { if (key == null) { throw new ArgumentNullException(nameof(key)); } 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; } var array = _arrayStorage; for (var i = 0; i < _count; i++) { if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase)) { value = array[i].Value; return true; } } value = default; return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void EnsureCapacity(int capacity) { if (_propertyStorage != null || _arrayStorage.Length < capacity) { EnsureCapacitySlow(capacity); } } private void EnsureCapacitySlow(int capacity) { if (_propertyStorage != null) { var storage = _propertyStorage; capacity = Math.Max(storage.Properties.Length, capacity); var array = new KeyValuePair[capacity]; for (var i = 0; i < storage.Properties.Length; i++) { var property = storage.Properties[i]; array[i] = new KeyValuePair(property.Name, property.GetValue(storage.Value)); } _arrayStorage = array; _propertyStorage = null; return; } if (_arrayStorage.Length < capacity) { capacity = _arrayStorage.Length == 0 ? DefaultCapacity : _arrayStorage.Length * 2; var array = new KeyValuePair[capacity]; if (_count > 0) { Array.Copy(_arrayStorage, 0, array, 0, _count); } _arrayStorage = array; } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private int FindInArray(string key) { var array = _arrayStorage; for (var i = 0; i < _count; i++) { if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase)) { return i; } } return -1; } public struct Enumerator : IEnumerator> { private RouteValueDictionary _dictionary; private int _index; public Enumerator(RouteValueDictionary dictionary) { if (dictionary == null) { throw new ArgumentNullException(); } _dictionary = dictionary; Current = default; _index = -1; } public KeyValuePair Current { get; private set; } object IEnumerator.Current => Current; public void Dispose() { } public bool MoveNext() { if (++_index < _dictionary.Count) { if (_dictionary._propertyStorage != null) { var storage = _dictionary._propertyStorage; var property = storage.Properties[_index]; Current = new KeyValuePair(property.Name, property.GetValue(storage.Value)); return true; } Current = _dictionary._arrayStorage[_index]; return true; } Current = default; return false; } public void Reset() { Current = default; _index = -1; } } internal class PropertyStorage { private static readonly ConcurrentDictionary _propertyCache = new ConcurrentDictionary(); 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); ValidatePropertyNames(type, Properties); _propertyCache.TryAdd(type, Properties); } } private static void ValidatePropertyNames(Type type, PropertyHelper[] properties) { var names = new Dictionary(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); } } } } }