// 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 Microsoft.AspNetCore.Routing.Abstractions; using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Routing { /// /// An type for route values. /// public class RouteValueDictionary : IDictionary, IReadOnlyDictionary { internal Storage _storage; /// /// Creates an empty . /// public RouteValueDictionary() { _storage = EmptyStorage.Instance; } /// /// 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) { var dictionary = values as RouteValueDictionary; if (dictionary != null) { var listStorage = dictionary._storage as ListStorage; if (listStorage != null) { _storage = new ListStorage(listStorage); return; } var propertyStorage = dictionary._storage as PropertyStorage; if (propertyStorage != null) { // PropertyStorage is immutable so we can just copy it. _storage = dictionary._storage; return; } // If we get here, it's an EmptyStorage. _storage = EmptyStorage.Instance; return; } var keyValueEnumerable = values as IEnumerable>; if (keyValueEnumerable != null) { var listStorage = new ListStorage(); _storage = listStorage; foreach (var kvp in keyValueEnumerable) { if (listStorage.ContainsKey(kvp.Key)) { var message = Resources.FormatRouteValueDictionary_DuplicateKey(kvp.Key, nameof(RouteValueDictionary)); throw new ArgumentException(message, nameof(values)); } listStorage._inner.Add(kvp); } return; } if (values != null) { _storage = new PropertyStorage(values); return; } _storage = EmptyStorage.Instance; } /// 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)); } if (!_storage.TrySetValue(key, value)) { Upgrade(); _storage.TrySetValue(key, value); } } } /// /// Gets the comparer for this dictionary. /// /// /// This will always be a reference to /// public IEqualityComparer Comparer => StringComparer.OrdinalIgnoreCase; /// public int Count => _storage.Count; /// bool ICollection>.IsReadOnly => false; /// public ICollection Keys { get { Upgrade(); var list = ((ListStorage)_storage)._inner; var keys = new string[list.Count]; for (var i = 0; i < keys.Length; i++) { keys[i] = list[i].Key; } return keys; } } IEnumerable IReadOnlyDictionary.Keys { get { return Keys; } } /// public ICollection Values { get { Upgrade(); var list = ((ListStorage)_storage)._inner; var values = new object[list.Count]; for (var i = 0; i < values.Length; i++) { values[i] = list[i].Value; } return values; } } IEnumerable IReadOnlyDictionary.Values { get { return 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)); } Upgrade(); var list = ((ListStorage)_storage)._inner; for (var i = 0; i < list.Count; i++) { if (string.Equals(list[i].Key, key, StringComparison.OrdinalIgnoreCase)) { var message = Resources.FormatRouteValueDictionary_DuplicateKey(key, nameof(RouteValueDictionary)); throw new ArgumentException(message, nameof(key)); } } list.Add(new KeyValuePair(key, value)); } /// public void Clear() { if (_storage.Count == 0) { return; } Upgrade(); var list = ((ListStorage)_storage)._inner; list.Clear(); } /// bool ICollection>.Contains(KeyValuePair item) { if (_storage.Count == 0) { return false; } Upgrade(); var list = ((ListStorage)_storage)._inner; for (var i = 0; i < list.Count; i++) { if (string.Equals(list[i].Key, item.Key, StringComparison.OrdinalIgnoreCase)) { return EqualityComparer.Default.Equals(list[i].Value, item.Value); } } return false; } /// public bool ContainsKey(string key) { if (key == null) { throw new ArgumentNullException(nameof(key)); } return _storage.ContainsKey(key); } /// 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 (_storage.Count == 0) { return; } Upgrade(); var list = ((ListStorage)_storage)._inner; list.CopyTo(array, arrayIndex); } /// public Enumerator GetEnumerator() { return new Enumerator(this); } /// IEnumerator> IEnumerable>.GetEnumerator() { return GetEnumerator(); } /// IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } /// bool ICollection>.Remove(KeyValuePair item) { if (_storage.Count == 0) { return false; } Upgrade(); var list = ((ListStorage)_storage)._inner; for (var i = 0; i < list.Count; i++) { if (string.Equals(list[i].Key, item.Key, StringComparison.OrdinalIgnoreCase) && EqualityComparer.Default.Equals(list[i].Value, item.Value)) { list.RemoveAt(i); return true; } } return false; } /// public bool Remove(string key) { if (key == null) { throw new ArgumentNullException(nameof(key)); } if (_storage.Count == 0) { return false; } Upgrade(); var list = ((ListStorage)_storage)._inner; for (var i = 0; i < list.Count; i++) { if (string.Equals(list[i].Key, key, StringComparison.OrdinalIgnoreCase)) { list.RemoveAt(i); return true; } } return false; } /// public bool TryGetValue(string key, out object value) { if (key == null) { throw new ArgumentNullException(nameof(key)); } return _storage.TryGetValue(key, out value); } private void Upgrade() { _storage.Upgrade(ref _storage); } public struct Enumerator : IEnumerator> { private readonly Storage _storage; private int _index; public Enumerator(RouteValueDictionary dictionary) { if (dictionary == null) { throw new ArgumentNullException(); } _storage = dictionary._storage; Current = default(KeyValuePair); _index = -1; } public KeyValuePair Current { get; private set; } object IEnumerator.Current => Current; public void Dispose() { } public bool MoveNext() { if (++_index < _storage.Count) { Current = _storage[_index]; return true; } Current = default(KeyValuePair); return false; } public void Reset() { Current = default(KeyValuePair); _index = -1; } } // Storage and its subclasses are internal for testing. internal abstract class Storage { public abstract int Count { get; } public abstract KeyValuePair this[int index] { get; } public abstract void Upgrade(ref Storage storage); public abstract bool TryGetValue(string key, out object value); public abstract bool ContainsKey(string key); public abstract bool TrySetValue(string key, object value); } internal class ListStorage : Storage { internal readonly List> _inner; public ListStorage() { _inner = new List>(); } public ListStorage(int capacity) { _inner = new List>(capacity); } public ListStorage(ListStorage other) { // Perf: Don't call the copy constructor, that would box the enumerator. _inner = new List>(other._inner.Capacity); for (var i = 0; i < other._inner.Count; i++) { _inner.Add(other._inner[i]); } } public override int Count => _inner.Count; public override KeyValuePair this[int index] => _inner[index]; public void Clear() { _inner.Clear(); } public override bool ContainsKey(string key) { for (var i = 0; i < _inner.Count; i++) { var kvp = _inner[i]; if (string.Equals(key, kvp.Key, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } public override bool TrySetValue(string key, object value) { for (var i = 0; i < _inner.Count; i++) { var kvp = _inner[i]; if (string.Equals(key, kvp.Key, StringComparison.OrdinalIgnoreCase)) { _inner[i] = new KeyValuePair(key, value); return true; } } _inner.Add(new KeyValuePair(key, value)); return true; } public override bool TryGetValue(string key, out object value) { for (var i = 0; i < _inner.Count; i++) { var kvp = _inner[i]; if (string.Equals(key, kvp.Key, StringComparison.OrdinalIgnoreCase)) { value = kvp.Value; return true; } } value = null; return false; } public override void Upgrade(ref Storage storage) { // Do nothing. } } internal class PropertyStorage : Storage { private static readonly PropertyCache _propertyCache = new PropertyCache(); internal readonly object _value; internal 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); } } public PropertyStorage(PropertyStorage propertyStorage) { _value = propertyStorage._value; _properties = propertyStorage._properties; } public override int Count => _properties.Length; public override KeyValuePair this[int index] { get { var property = _properties[index]; return new KeyValuePair(property.Name, property.GetValue(_value)); } } public override bool TryGetValue(string key, out object value) { for (var i = 0; i < _properties.Length; i++) { var property = _properties[i]; if (string.Equals(key, property.Name, StringComparison.OrdinalIgnoreCase)) { value = property.GetValue(_value); return true; } } value = null; return false; } public override bool ContainsKey(string key) { for (var i = 0; i < _properties.Length; i++) { var property = _properties[i]; if (string.Equals(key, property.Name, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } public override bool TrySetValue(string key, object value) { // PropertyStorage never sets a value. return false; } public override void Upgrade(ref Storage storage) { storage = new ListStorage(Count); for (var i = 0; i < _properties.Length; i++) { var property = _properties[i]; storage.TrySetValue(property.Name, property.GetValue(_value)); } } 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]; PropertyHelper duplicate; if (names.TryGetValue(property.Name, out duplicate)) { var message = Resources.FormatRouteValueDictionary_DuplicatePropertyName( type.FullName, property.Name, duplicate.Name, nameof(RouteValueDictionary)); throw new InvalidOperationException(message); } names.Add(property.Name, property); } } } internal class EmptyStorage : Storage { public static readonly EmptyStorage Instance = new EmptyStorage(); private EmptyStorage() { } public override int Count => 0; public override KeyValuePair this[int index] { get { throw new NotImplementedException(); } } public override bool ContainsKey(string key) { return false; } public override bool TryGetValue(string key, out object value) { value = null; return false; } public override bool TrySetValue(string key, object value) { return false; } public override void Upgrade(ref Storage storage) { storage = new ListStorage(); } } private class PropertyCache : ConcurrentDictionary { } } }