From 05a581a1324fc8a86869b1e28c6197b32e5e7e90 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 29 Aug 2018 17:07:16 +1200 Subject: [PATCH] Copy core endpoint routing types to HttpAbstractions (#1030) --- build/dependencies.props | 1 + ...rosoft.AspNetCore.Http.Abstractions.csproj | 1 + .../Properties/Resources.Designer.cs | 28 + .../Resources.resx | 6 + .../Routing/Endpoint.cs | 49 + .../Routing/EndpointMetadataCollection.cs | 201 ++ .../Routing/IEndpointFeature.cs | 18 + .../Routing/IRouteValuesFeature.cs | 20 + .../Routing/RouteValueDictionary.cs | 598 ++++++ .../EndpointMetadataCollectionTests.cs | 49 + .../RouteValueDictionaryTests.cs | 1621 +++++++++++++++++ 11 files changed, 2592 insertions(+) create mode 100644 src/Microsoft.AspNetCore.Http.Abstractions/Routing/Endpoint.cs create mode 100644 src/Microsoft.AspNetCore.Http.Abstractions/Routing/EndpointMetadataCollection.cs create mode 100644 src/Microsoft.AspNetCore.Http.Abstractions/Routing/IEndpointFeature.cs create mode 100644 src/Microsoft.AspNetCore.Http.Abstractions/Routing/IRouteValuesFeature.cs create mode 100644 src/Microsoft.AspNetCore.Http.Abstractions/Routing/RouteValueDictionary.cs create mode 100644 test/Microsoft.AspNetCore.Http.Abstractions.Tests/EndpointMetadataCollectionTests.cs create mode 100644 test/Microsoft.AspNetCore.Http.Abstractions.Tests/RouteValueDictionaryTests.cs diff --git a/build/dependencies.props b/build/dependencies.props index db34bfd06f..772f34b57d 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -13,6 +13,7 @@ 3.0.0-alpha1-10352 3.0.0-alpha1-10352 3.0.0-alpha1-10352 + 3.0.0-alpha1-10352 3.0.0-alpha1-10352 2.0.9 2.1.2 diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Microsoft.AspNetCore.Http.Abstractions.csproj b/src/Microsoft.AspNetCore.Http.Abstractions/Microsoft.AspNetCore.Http.Abstractions.csproj index d4a9eff0e6..eb17ae4e2f 100644 --- a/src/Microsoft.AspNetCore.Http.Abstractions/Microsoft.AspNetCore.Http.Abstractions.csproj +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Microsoft.AspNetCore.Http.Abstractions.csproj @@ -20,6 +20,7 @@ Microsoft.AspNetCore.Http.HttpResponse + diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Properties/Resources.Designer.cs index 6af7d138be..02985d630b 100644 --- a/src/Microsoft.AspNetCore.Http.Abstractions/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Properties/Resources.Designer.cs @@ -192,6 +192,34 @@ namespace Microsoft.AspNetCore.Http.Abstractions internal static string FormatArgumentCannotBeNullOrEmpty() => GetString("ArgumentCannotBeNullOrEmpty"); + /// + /// An element with the key '{0}' already exists in the {1}. + /// + internal static string RouteValueDictionary_DuplicateKey + { + get => GetString("RouteValueDictionary_DuplicateKey"); + } + + /// + /// An element with the key '{0}' already exists in the {1}. + /// + internal static string FormatRouteValueDictionary_DuplicateKey(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("RouteValueDictionary_DuplicateKey"), p0, p1); + + /// + /// The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons. + /// + internal static string RouteValueDictionary_DuplicatePropertyName + { + get => GetString("RouteValueDictionary_DuplicatePropertyName"); + } + + /// + /// The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons. + /// + internal static string FormatRouteValueDictionary_DuplicatePropertyName(object p0, object p1, object p2, object p3) + => string.Format(CultureInfo.CurrentCulture, GetString("RouteValueDictionary_DuplicatePropertyName"), p0, p1, p2, p3); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Resources.resx b/src/Microsoft.AspNetCore.Http.Abstractions/Resources.resx index dfdfeaf7d1..4ce2d8bd7a 100644 --- a/src/Microsoft.AspNetCore.Http.Abstractions/Resources.resx +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Resources.resx @@ -156,4 +156,10 @@ Argument cannot be null or empty. + + An element with the key '{0}' already exists in the {1}. + + + The type '{0}' defines properties '{1}' and '{2}' which differ only by casing. This is not supported by {3} which uses case-insensitive comparisons. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Routing/Endpoint.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/Endpoint.cs new file mode 100644 index 0000000000..16202b45f5 --- /dev/null +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/Endpoint.cs @@ -0,0 +1,49 @@ +// 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. + +namespace Microsoft.AspNetCore.Http +{ + /// + /// Respresents a logical endpoint in an application. + /// + public class Endpoint + { + /// + /// Creates a new instance of . + /// + /// The delegate used to process requests for the endpoint. + /// + /// The endpoint . May be null. + /// + /// + /// The informational display name of the endpoint. May be null. + /// + public Endpoint( + RequestDelegate requestDelegate, + EndpointMetadataCollection metadata, + string displayName) + { + // All are allowed to be null + RequestDelegate = requestDelegate; + Metadata = metadata ?? EndpointMetadataCollection.Empty; + DisplayName = displayName; + } + + /// + /// Gets the informational display name of this endpoint. + /// + public string DisplayName { get; } + + /// + /// Gets the collection of metadata associated with this endpoint. + /// + public EndpointMetadataCollection Metadata { get; } + + /// + /// Gets the delegate used to process requests for the endpoint. + /// + public RequestDelegate RequestDelegate { get; } + + public override string ToString() => DisplayName ?? base.ToString(); + } +} diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Routing/EndpointMetadataCollection.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/EndpointMetadataCollection.cs new file mode 100644 index 0000000000..a792fff295 --- /dev/null +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/EndpointMetadataCollection.cs @@ -0,0 +1,201 @@ +// 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.Linq; +using System.Runtime.CompilerServices; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// A collection of arbitrary metadata associated with an endpoint. + /// + /// + /// instances contain a list of metadata items + /// of arbitrary types. The metadata items are stored as an ordered collection with + /// items arranged in ascending order of precedence. + /// + public sealed class EndpointMetadataCollection : IReadOnlyList + { + /// + /// An empty . + /// + public static readonly EndpointMetadataCollection Empty = new EndpointMetadataCollection(Array.Empty()); + + private readonly object[] _items; + private readonly ConcurrentDictionary _cache; + + /// + /// Creates a new . + /// + /// The metadata items. + public EndpointMetadataCollection(IEnumerable items) + { + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + _items = items.ToArray(); + _cache = new ConcurrentDictionary(); + } + + /// + /// Creates a new . + /// + /// The metadata items. + public EndpointMetadataCollection(params object[] items) + : this((IEnumerable)items) + { + } + + /// + /// Gets the item at . + /// + /// The index of the item to retrieve. + /// The item at . + public object this[int index] => _items[index]; + + /// + /// Gets the count of metadata items. + /// + public int Count => _items.Length; + + /// + /// Gets the most significant metadata item of type . + /// + /// The type of metadata to retrieve. + /// + /// The most significant metadata of type or null. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T GetMetadata() where T : class + { + if (_cache.TryGetValue(typeof(T), out var result)) + { + var length = result.Length; + return length > 0 ? (T)result[length - 1] : default; + } + + return GetMetadataSlow(); + } + + private T GetMetadataSlow() where T : class + { + var array = GetOrderedMetadataSlow(); + var length = array.Length; + return length > 0 ? array[length - 1] : default; + } + + /// + /// Gets the metadata items of type in ascending + /// order of precedence. + /// + /// The type of metadata. + /// A sequence of metadata items of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IEnumerable GetOrderedMetadata() where T : class + { + if (_cache.TryGetValue(typeof(T), out var result)) + { + return (T[])result; + } + + return GetOrderedMetadataSlow(); + } + + private T[] GetOrderedMetadataSlow() where T : class + { + var items = new List(); + for (var i = 0; i < _items.Length; i++) + { + if (_items[i] is T item) + { + items.Add(item); + } + } + + var array = items.ToArray(); + _cache.TryAdd(typeof(T), array); + return array; + } + + /// + /// Gets an of all metadata items. + /// + /// An of all metadata items. + public Enumerator GetEnumerator() => new Enumerator(this); + + /// + /// Gets an of all metadata items. + /// + /// An of all metadata items. + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Gets an of all metadata items. + /// + /// An of all metadata items. + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Enumerates the elements of an . + /// + public struct Enumerator : IEnumerator + { + // Intentionally not readonly to prevent defensive struct copies + private object[] _items; + private int _index; + + internal Enumerator(EndpointMetadataCollection collection) + { + _items = collection._items; + _index = 0; + Current = null; + } + + /// + /// Gets the element at the current position of the enumerator + /// + public object Current { get; private set; } + + /// + /// Releases all resources used by the . + /// + public void Dispose() + { + } + + /// + /// Advances the enumerator to the next element of the . + /// + /// + /// true if the enumerator was successfully advanced to the next element; + /// false if the enumerator has passed the end of the collection. + /// + public bool MoveNext() + { + if (_index < _items.Length) + { + Current = _items[_index++]; + return true; + } + + Current = null; + return false; + } + + /// + /// Sets the enumerator to its initial position, which is before the first element in the collection. + /// + public void Reset() + { + _index = 0; + Current = null; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Routing/IEndpointFeature.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/IEndpointFeature.cs new file mode 100644 index 0000000000..ff0762bb20 --- /dev/null +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/IEndpointFeature.cs @@ -0,0 +1,18 @@ +// 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. + +namespace Microsoft.AspNetCore.Http.Features +{ + /// + /// A feature interface for endpoint routing. Use + /// to access an instance associated with the current request. + /// + public interface IEndpointFeature + { + /// + /// Gets or sets the selected for the current + /// request. + /// + Endpoint Endpoint { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Routing/IRouteValuesFeature.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/IRouteValuesFeature.cs new file mode 100644 index 0000000000..d1c91577fe --- /dev/null +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/IRouteValuesFeature.cs @@ -0,0 +1,20 @@ +// 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 Microsoft.AspNetCore.Routing; + +namespace Microsoft.AspNetCore.Http.Features +{ + /// + /// A feature interface for routing values. Use + /// to access the values associated with the current request. + /// + public interface IRouteValuesFeature + { + /// + /// Gets or sets the associated with the currrent + /// request. + /// + RouteValueDictionary RouteValues { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Http.Abstractions/Routing/RouteValueDictionary.cs b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/RouteValueDictionary.cs new file mode 100644 index 0000000000..f4153881b3 --- /dev/null +++ b/src/Microsoft.AspNetCore.Http.Abstractions/Routing/RouteValueDictionary.cs @@ -0,0 +1,598 @@ +// 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); + } + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Http.Abstractions.Tests/EndpointMetadataCollectionTests.cs b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/EndpointMetadataCollectionTests.cs new file mode 100644 index 0000000000..62432b7b2b --- /dev/null +++ b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/EndpointMetadataCollectionTests.cs @@ -0,0 +1,49 @@ +// 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.Generic; +using System.Text; +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Routing +{ + public class EndpointMetadataCollectionTests + { + [Fact] + public void Constructor_Enumeration_ContainsValues() + { + // Arrange & Act + var metadata = new EndpointMetadataCollection(new List + { + 1, + 2, + 3, + }); + + // Assert + Assert.Equal(3, metadata.Count); + + Assert.Collection(metadata, + value => Assert.Equal(1, value), + value => Assert.Equal(2, value), + value => Assert.Equal(3, value)); + } + + [Fact] + public void Constructor_ParamsArray_ContainsValues() + { + // Arrange & Act + var metadata = new EndpointMetadataCollection(1, 2, 3); + + // Assert + Assert.Equal(3, metadata.Count); + + Assert.Collection(metadata, + value => Assert.Equal(1, value), + value => Assert.Equal(2, value), + value => Assert.Equal(3, value)); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Http.Abstractions.Tests/RouteValueDictionaryTests.cs b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/RouteValueDictionaryTests.cs new file mode 100644 index 0000000000..524966dc05 --- /dev/null +++ b/test/Microsoft.AspNetCore.Http.Abstractions.Tests/RouteValueDictionaryTests.cs @@ -0,0 +1,1621 @@ +// 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.Generic; +using System.Linq; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Routing.Tests +{ + public class RouteValueDictionaryTests + { + [Fact] + public void DefaultCtor_UsesEmptyStorage() + { + // Arrange + // Act + var dict = new RouteValueDictionary(); + + // Assert + Assert.Empty(dict); + Assert.Empty(dict._arrayStorage); + Assert.Null(dict._propertyStorage); + } + + [Fact] + public void CreateFromNull_UsesEmptyStorage() + { + // Arrange + // Act + var dict = new RouteValueDictionary(null); + + // Assert + Assert.Empty(dict); + Assert.Empty(dict._arrayStorage); + Assert.Null(dict._propertyStorage); + } + + [Fact] + public void CreateFromRouteValueDictionary_WithArrayStorage_CopiesStorage() + { + // Arrange + var other = new RouteValueDictionary() + { + { "1", 1 } + }; + + // Act + var dict = new RouteValueDictionary(other); + + // Assert + Assert.Equal(other, dict); + + var storage = Assert.IsType[]>(dict._arrayStorage); + var otherStorage = Assert.IsType[]>(other._arrayStorage); + Assert.NotSame(otherStorage, storage); + } + + [Fact] + public void CreateFromRouteValueDictionary_WithPropertyStorage_CopiesStorage() + { + // Arrange + var other = new RouteValueDictionary(new { key = "value" }); + + // Act + var dict = new RouteValueDictionary(other); + + // Assert + Assert.Equal(other, dict); + + var storage = dict._propertyStorage; + var otherStorage = other._propertyStorage; + Assert.Same(otherStorage, storage); + } + + public static IEnumerable IEnumerableKeyValuePairData + { + get + { + var routeValues = new[] + { + new KeyValuePair("Name", "James"), + new KeyValuePair("Age", 30), + new KeyValuePair("Address", new Address() { City = "Redmond", State = "WA" }) + }; + + yield return new object[] { routeValues.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) }; + + yield return new object[] { routeValues.ToList() }; + + yield return new object[] { routeValues }; + } + } + + public static IEnumerable IEnumerableStringValuePairData + { + get + { + var routeValues = new[] + { + new KeyValuePair("First Name", "James"), + new KeyValuePair("Last Name", "Henrik"), + new KeyValuePair("Middle Name", "Bob") + }; + + yield return new object[] { routeValues.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) }; + + yield return new object[] { routeValues.ToList() }; + + yield return new object[] { routeValues }; + } + } + + [Theory] + [MemberData(nameof(IEnumerableKeyValuePairData))] + public void CreateFromIEnumerableKeyValuePair_CopiesValues(object values) + { + // Arrange & Act + var dict = new RouteValueDictionary(values); + + // Assert + Assert.IsType[]>(dict._arrayStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("Address", kvp.Key); + var address = Assert.IsType
(kvp.Value); + Assert.Equal("Redmond", address.City); + Assert.Equal("WA", address.State); + }, + kvp => { Assert.Equal("Age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("Name", kvp.Key); Assert.Equal("James", kvp.Value); }); + } + + [Theory] + [MemberData(nameof(IEnumerableStringValuePairData))] + public void CreateFromIEnumerableStringValuePair_CopiesValues(object values) + { + // Arrange & Act + var dict = new RouteValueDictionary(values); + + // Assert + Assert.IsType[]>(dict._arrayStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("First Name", kvp.Key); Assert.Equal("James", kvp.Value); }, + kvp => { Assert.Equal("Last Name", kvp.Key); Assert.Equal("Henrik", kvp.Value); }, + kvp => { Assert.Equal("Middle Name", kvp.Key); Assert.Equal("Bob", kvp.Value); }); + } + + [Fact] + public void CreateFromIEnumerableKeyValuePair_ThrowsExceptionForDuplicateKey() + { + // Arrange + var values = new List>() + { + new KeyValuePair("name", "Billy"), + new KeyValuePair("Name", "Joey"), + }; + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => new RouteValueDictionary(values), + "key", + $"An element with the key 'Name' already exists in the {nameof(RouteValueDictionary)}."); + } + + [Fact] + public void CreateFromIEnumerableStringValuePair_ThrowsExceptionForDuplicateKey() + { + // Arrange + var values = new List>() + { + new KeyValuePair("name", "Billy"), + new KeyValuePair("Name", "Joey"), + }; + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => new RouteValueDictionary(values), + "key", + $"An element with the key 'Name' already exists in the {nameof(RouteValueDictionary)}."); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromAnonymousType() + { + // Arrange + var obj = new { cool = "beans", awesome = 123 }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("awesome", kvp.Key); Assert.Equal(123, kvp.Value); }, + kvp => { Assert.Equal("cool", kvp.Key); Assert.Equal("beans", kvp.Value); }); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType() + { + // Arrange + var obj = new RegularType() { CoolnessFactor = 73 }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("CoolnessFactor", kvp.Key); + Assert.Equal(73, kvp.Value); + }, + kvp => + { + Assert.Equal("IsAwesome", kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.False(value); + }); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_PublicOnly() + { + // Arrange + var obj = new Visibility() { IsPublic = true, ItsInternalDealWithIt = 5 }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("IsPublic", kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.True(value); + }); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_IgnoresStatic() + { + // Arrange + var obj = new StaticProperty(); + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Empty(dict); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_IgnoresSetOnly() + { + // Arrange + var obj = new SetterOnly() { CoolSetOnly = false }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Empty(dict); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_IncludesInherited() + { + // Arrange + var obj = new Derived() { TotallySweetProperty = true, DerivedProperty = false }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("DerivedProperty", kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.False(value); + }, + kvp => + { + Assert.Equal("TotallySweetProperty", kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.True(value); + }); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_WithHiddenProperty() + { + // Arrange + var obj = new DerivedHiddenProperty() { DerivedProperty = 5 }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("DerivedProperty", kvp.Key); Assert.Equal(5, kvp.Value); }); + } + + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_WithIndexerProperty() + { + // Arrange + var obj = new IndexerProperty(); + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Empty(dict); + } + + [Fact] + public void CreateFromObject_MixedCaseThrows() + { + // Arrange + var obj = new { controller = "Home", Controller = "Home" }; + + var message = + $"The type '{obj.GetType().FullName}' defines properties 'controller' and 'Controller' which differ " + + $"only by casing. This is not supported by {nameof(RouteValueDictionary)} which uses " + + $"case-insensitive comparisons."; + + // Act & Assert + var exception = Assert.Throws(() => + { + var dictionary = new RouteValueDictionary(obj); + }); + + // Ignoring case to make sure we're not testing reflection's ordering. + Assert.Equal(message, exception.Message, ignoreCase: true); + } + + // Our comparer is hardcoded to be OrdinalIgnoreCase no matter what. + [Fact] + public void Comparer_IsOrdinalIgnoreCase() + { + // Arrange + // Act + var dict = new RouteValueDictionary(); + + // Assert + Assert.Same(StringComparer.OrdinalIgnoreCase, dict.Comparer); + } + + // Our comparer is hardcoded to be IsReadOnly==false no matter what. + [Fact] + public void IsReadOnly_False() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = ((ICollection>)dict).IsReadOnly; + + // Assert + Assert.False(result); + } + + [Fact] + public void IndexGet_EmptyStorage_ReturnsNull() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var value = dict["key"]; + + // Assert + Assert.Null(value); + } + + [Fact] + public void IndexGet_PropertyStorage_NoMatch_ReturnsNull() + { + // Arrange + var dict = new RouteValueDictionary(new { age = 30 }); + + // Act + var value = dict["key"]; + + // Assert + Assert.Null(value); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void IndexGet_PropertyStorage_Match_ReturnsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var value = dict["key"]; + + // Assert + Assert.Equal("value", value); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void IndexGet_PropertyStorage_MatchIgnoreCase_ReturnsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var value = dict["kEy"]; + + // Assert + Assert.Equal("value", value); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void IndexGet_ArrayStorage_NoMatch_ReturnsNull() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "age", 30 }, + }; + + // Act + var value = dict["key"]; + + // Assert + Assert.Null(value); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexGet_ListStorage_Match_ReturnsValue() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var value = dict["key"]; + + // Assert + Assert.Equal("value", value); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexGet_ListStorage_MatchIgnoreCase_ReturnsValue() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var value = dict["kEy"]; + + // Assert + Assert.Equal("value", value); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexSet_EmptyStorage_UpgradesToList() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexSet_PropertyStorage_NoMatch_AddsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { age = 30 }); + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexSet_PropertyStorage_Match_SetsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexSet_PropertyStorage_MatchIgnoreCase_SetsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + dict["kEy"] = "value"; + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("kEy", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexSet_ListStorage_NoMatch_AddsValue() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "age", 30 }, + }; + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexSet_ListStorage_Match_SetsValue() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexSet_ListStorage_MatchIgnoreCase_SetsValue() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Count_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var count = dict.Count; + + // Assert + Assert.Equal(0, count); + } + + [Fact] + public void Count_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value", }); + + // Act + var count = dict.Count; + + // Assert + Assert.Equal(1, count); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void Count_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var count = dict.Count; + + // Assert + Assert.Equal(1, count); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Keys_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var keys = dict.Keys; + + // Assert + Assert.Empty(keys); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Keys_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value", }); + + // Act + var keys = dict.Keys; + + // Assert + Assert.Equal(new[] { "key" }, keys); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Keys_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var keys = dict.Keys; + + // Assert + Assert.Equal(new[] { "key" }, keys); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Values_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var values = dict.Values; + + // Assert + Assert.Empty(values); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Values_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value", }); + + // Act + var values = dict.Values; + + // Assert + Assert.Equal(new object[] { "value" }, values); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Values_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var values = dict.Values; + + // Assert + Assert.Equal(new object[] { "value" }, values); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Add_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + dict.Add("key", "value"); + + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Add_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { age = 30 }); + + // Act + dict.Add("key", "value"); + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Add_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "age", 30 }, + }; + + // Act + dict.Add("key", "value"); + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Add_DuplicateKey() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var message = $"An element with the key 'key' already exists in the {nameof(RouteValueDictionary)}"; + + // Act & Assert + ExceptionAssert.ThrowsArgument(() => dict.Add("key", "value2"), "key", message); + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Add_DuplicateKey_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var message = $"An element with the key 'kEy' already exists in the {nameof(RouteValueDictionary)}"; + + // Act & Assert + ExceptionAssert.ThrowsArgument(() => dict.Add("kEy", "value2"), "key", message); + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Add_KeyValuePair() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "age", 30 }, + }; + + // Act + ((ICollection>)dict).Add(new KeyValuePair("key", "value")); + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Clear_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + dict.Clear(); + + // Assert + Assert.Empty(dict); + } + + [Fact] + public void Clear_PropertyStorage_AlreadyEmpty() + { + // Arrange + var dict = new RouteValueDictionary(new { }); + + // Act + dict.Clear(); + + // Assert + Assert.Empty(dict); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void Clear_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + dict.Clear(); + + // Assert + Assert.Empty(dict); + Assert.Null(dict._propertyStorage); + } + + [Fact] + public void Clear_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + dict.Clear(); + + // Assert + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Contains_KeyValuePair_True() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("key", "value"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.True(result); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Contains_KeyValuePair_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("KEY", "value"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.True(result); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Contains_KeyValuePair_False() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("other", "value"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.False(result); + Assert.IsType[]>(dict._arrayStorage); + } + + // Value comparisons use the default equality comparer. + [Fact] + public void Contains_KeyValuePair_False_ValueComparisonIsDefault() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("key", "valUE"); + + // Act + var result = ((ICollection>)dict).Contains(input); + + // Assert + Assert.False(result); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void ContainsKey_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = dict.ContainsKey("key"); + + // Assert + Assert.False(result); + } + + [Fact] + public void ContainsKey_PropertyStorage_False() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.ContainsKey("other"); + + // Assert + Assert.False(result); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void ContainsKey_PropertyStorage_True() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.ContainsKey("key"); + + // Assert + Assert.True(result); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void ContainsKey_PropertyStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.ContainsKey("kEy"); + + // Assert + Assert.True(result); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void ContainsKey_ListStorage_False() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.ContainsKey("other"); + + // Assert + Assert.False(result); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void ContainsKey_ListStorage_True() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.ContainsKey("key"); + + // Assert + Assert.True(result); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void ContainsKey_ListStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.ContainsKey("kEy"); + + // Assert + Assert.True(result); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void CopyTo() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var array = new KeyValuePair[2]; + + // Act + ((ICollection>)dict).CopyTo(array, 1); + + // Assert + Assert.Equal( + new KeyValuePair[] + { + default(KeyValuePair), + new KeyValuePair("key", "value") + }, + array); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyValuePair_True() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("key", "value"); + + // Act + var result = ((ICollection>)dict).Remove(input); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyValuePair_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("KEY", "value"); + + // Act + var result = ((ICollection>)dict).Remove(input); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyValuePair_False() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("other", "value"); + + // Act + var result = ((ICollection>)dict).Remove(input); + + // Assert + Assert.False(result); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + // Value comparisons use the default equality comparer. + [Fact] + public void Remove_KeyValuePair_False_ValueComparisonIsDefault() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + var input = new KeyValuePair("key", "valUE"); + + // Act + var result = ((ICollection>)dict).Remove(input); + + // Assert + Assert.False(result); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = dict.Remove("key"); + + // Assert + Assert.False(result); + } + + [Fact] + public void Remove_PropertyStorage_Empty() + { + // Arrange + var dict = new RouteValueDictionary(new { }); + + // Act + var result = dict.Remove("other"); + + // Assert + Assert.False(result); + Assert.Empty(dict); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void Remove_PropertyStorage_False() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.Remove("other"); + + // Assert + Assert.False(result); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_PropertyStorage_True() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.Remove("key"); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_PropertyStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var result = dict.Remove("kEy"); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_ListStorage_False() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.Remove("other"); + + // Assert + Assert.False(result); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_ListStorage_True() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.Remove("key"); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_ListStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.Remove("kEy"); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void TryGetValue_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + object value; + var result = dict.TryGetValue("key", out value); + + // Assert + Assert.False(result); + Assert.Null(value); + } + + [Fact] + public void TryGetValue_PropertyStorage_False() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + object value; + var result = dict.TryGetValue("other", out value); + + // Assert + Assert.False(result); + Assert.Null(value); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void TryGetValue_PropertyStorage_True() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + object value; + var result = dict.TryGetValue("key", out value); + + // Assert + Assert.True(result); + Assert.Equal("value", value); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void TryGetValue_PropertyStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + object value; + var result = dict.TryGetValue("kEy", out value); + + // Assert + Assert.True(result); + Assert.Equal("value", value); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void TryGetValue_ListStorage_False() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + object value; + var result = dict.TryGetValue("other", out value); + + // Assert + Assert.False(result); + Assert.Null(value); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void TryGetValue_ListStorage_True() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + object value; + var result = dict.TryGetValue("key", out value); + + // Assert + Assert.True(result); + Assert.Equal("value", value); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void TryGetValue_ListStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() + { + { "key", "value" }, + }; + + // Act + object value; + var result = dict.TryGetValue("kEy", out value); + + // Assert + Assert.True(result); + Assert.Equal("value", value); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void ListStorage_DynamicallyAdjustsCapacity() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act 1 + dict.Add("key", "value"); + + // Assert 1 + var storage = Assert.IsType[]>(dict._arrayStorage); + Assert.Equal(4, storage.Length); + + // Act 2 + dict.Add("key2", "value2"); + dict.Add("key3", "value3"); + dict.Add("key4", "value4"); + dict.Add("key5", "value5"); + + // Assert 2 + storage = Assert.IsType[]>(dict._arrayStorage); + Assert.Equal(8, storage.Length); + } + + [Fact] + public void ListStorage_RemoveAt_RearrangesInnerArray() + { + // Arrange + var dict = new RouteValueDictionary(); + dict.Add("key", "value"); + dict.Add("key2", "value2"); + dict.Add("key3", "value3"); + + // Assert 1 + var storage = Assert.IsType[]>(dict._arrayStorage); + Assert.Equal(3, dict.Count); + + // Act + dict.Remove("key2"); + + // Assert 2 + storage = Assert.IsType[]>(dict._arrayStorage); + Assert.Equal(2, dict.Count); + Assert.Equal("key", storage[0].Key); + Assert.Equal("value", storage[0].Value); + Assert.Equal("key3", storage[1].Key); + Assert.Equal("value3", storage[1].Value); + } + + [Fact] + public void FromArray_TakesOwnershipOfArray() + { + // Arrange + var array = new KeyValuePair[] + { + new KeyValuePair("a", 0), + new KeyValuePair("b", 1), + new KeyValuePair("c", 2), + }; + + var dictionary = RouteValueDictionary.FromArray(array); + + // Act - modifying the array should modify the dictionary + array[0] = new KeyValuePair("aa", 10); + + // Assert + Assert.Equal(3, dictionary.Count); + Assert.Equal(10, dictionary["aa"]); + } + + [Fact] + public void FromArray_EmptyArray() + { + // Arrange + var array = Array.Empty>(); + + // Act + var dictionary = RouteValueDictionary.FromArray(array); + + // Assert + Assert.Empty(dictionary); + } + + [Fact] + public void FromArray_RemovesGapsInArray() + { + // Arrange + var array = new KeyValuePair[] + { + new KeyValuePair(null, null), + new KeyValuePair("a", 0), + new KeyValuePair(null, null), + new KeyValuePair(null, null), + new KeyValuePair("b", 1), + new KeyValuePair("c", 2), + new KeyValuePair("d", 3), + new KeyValuePair(null, null), + }; + + // Act - calling From should modify the array + var dictionary = RouteValueDictionary.FromArray(array); + + // Assert + Assert.Equal(4, dictionary.Count); + Assert.Equal( + new KeyValuePair[] + { + new KeyValuePair("d", 3), + new KeyValuePair("a", 0), + new KeyValuePair("c", 2), + new KeyValuePair("b", 1), + new KeyValuePair(null, null), + new KeyValuePair(null, null), + new KeyValuePair(null, null), + new KeyValuePair(null, null), + }, + array); + } + + private class RegularType + { + public bool IsAwesome { get; set; } + + public int CoolnessFactor { get; set; } + } + + private class Visibility + { + private string PrivateYo { get; set; } + + internal int ItsInternalDealWithIt { get; set; } + + public bool IsPublic { get; set; } + } + + private class StaticProperty + { + public static bool IsStatic { get; set; } + } + + private class SetterOnly + { + private bool _coolSetOnly; + + public bool CoolSetOnly { set { _coolSetOnly = value; } } + } + + private class Base + { + public bool DerivedProperty { get; set; } + } + + private class Derived : Base + { + public bool TotallySweetProperty { get; set; } + } + + private class DerivedHiddenProperty : Base + { + public new int DerivedProperty { get; set; } + } + + private class IndexerProperty + { + public bool this[string key] + { + get { return false; } + set { } + } + } + + private class Address + { + public string City { get; set; } + + public string State { get; set; } + } + } +}