From bc5f02444bc7291b295b6b5f41fb90fe6468528f Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 26 Jun 2018 13:41:49 -0700 Subject: [PATCH] Benchmarks and improvements to route value dictionary (#577) * Add benchmarks for RVD There are the scenarios that are critical for URL matching performance. * Reimplement RouteValueDictionary Improves the scenarios with benchmarks by about 30% * Fix benchmark * PR feedback * More feedback and tests --- .../RouteValueDictionaryBenchmark.cs | 139 ++++ .../RouteValueDictionary.cs | 626 ++++++------------ .../RouteValueDictionaryTests.cs | 197 +++--- 3 files changed, 427 insertions(+), 535 deletions(-) create mode 100644 benchmarks/Microsoft.AspNetCore.Routing.Performance/RouteValueDictionaryBenchmark.cs diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/RouteValueDictionaryBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/RouteValueDictionaryBenchmark.cs new file mode 100644 index 0000000000..656a742367 --- /dev/null +++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/RouteValueDictionaryBenchmark.cs @@ -0,0 +1,139 @@ +// 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 BenchmarkDotNet.Attributes; + +namespace Microsoft.AspNetCore.Routing +{ + public class RouteValueDictionaryBenchmark + { + // These dictionaries are used by a few tests over and over, so don't modify them destructively. + private RouteValueDictionary _arrayValues; + private RouteValueDictionary _propertyValues; + + [GlobalSetup] + public void Setup() + { + _arrayValues = new RouteValueDictionary() + { + { "action", "Index" }, + { "controller", "Home" }, + { "id", "17" }, + }; + _propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17" }); + } + + [Benchmark] + public RouteValueDictionary AddSingleItem() + { + var dictionary = new RouteValueDictionary(); + dictionary.Add("action", "Index"); + return dictionary; + } + + [Benchmark] + public RouteValueDictionary AddThreeItems() + { + var dictionary = new RouteValueDictionary(); + dictionary.Add("action", "Index"); + dictionary.Add("controller", "Home"); + dictionary.Add("id", "15"); + return dictionary; + } + + [Benchmark] + public RouteValueDictionary ForEachThreeItems_Array() + { + var dictionary = _arrayValues; + foreach (var kvp in dictionary) + { + GC.KeepAlive(kvp.Value); + } + return dictionary; + } + + [Benchmark] + public RouteValueDictionary ForEachThreeItems_Properties() + { + var dictionary = _arrayValues; + foreach (var kvp in dictionary) + { + GC.KeepAlive(kvp.Value); + } + return dictionary; + } + + [Benchmark] + public RouteValueDictionary GetThreeItems_Array() + { + var dictionary = _arrayValues; + GC.KeepAlive(dictionary["action"]); + GC.KeepAlive(dictionary["controller"]); + GC.KeepAlive(dictionary["id"]); + return dictionary; + } + + [Benchmark] + public RouteValueDictionary GetThreeItems_Properties() + { + var dictionary = _propertyValues; + GC.KeepAlive(dictionary["action"]); + GC.KeepAlive(dictionary["controller"]); + GC.KeepAlive(dictionary["id"]); + return dictionary; + } + + [Benchmark] + public RouteValueDictionary SetSingleItem() + { + var dictionary = new RouteValueDictionary(); + dictionary["action"] = "Index"; + return dictionary; + } + + [Benchmark] + public RouteValueDictionary SetExistingItem() + { + var dictionary = _arrayValues; + dictionary["action"] = "About"; + return dictionary; + } + + [Benchmark] + public RouteValueDictionary SetThreeItems() + { + var dictionary = new RouteValueDictionary(); + dictionary["action"] = "Index"; + dictionary["controller"] = "Home"; + dictionary["id"] = "15"; + return dictionary; + } + + [Benchmark] + public RouteValueDictionary TryGetValueThreeItems_Array() + { + var dictionary = _arrayValues; + dictionary.TryGetValue("action", out var action); + dictionary.TryGetValue("controller", out var controller); + dictionary.TryGetValue("id", out var id); + GC.KeepAlive(action); + GC.KeepAlive(controller); + GC.KeepAlive(id); + return dictionary; + } + + [Benchmark] + public RouteValueDictionary TryGetValueThreeItems_Properties() + { + var dictionary = _propertyValues; + dictionary.TryGetValue("action", out var action); + dictionary.TryGetValue("controller", out var controller); + dictionary.TryGetValue("id", out var id); + GC.KeepAlive(action); + GC.KeepAlive(controller); + GC.KeepAlive(id); + return dictionary; + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs b/src/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs index d0bc8e9ecf..ad3da79e3e 100644 --- a/src/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs +++ b/src/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs @@ -6,6 +6,7 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; +using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Routing.Abstractions; using Microsoft.Extensions.Internal; @@ -16,14 +17,19 @@ namespace Microsoft.AspNetCore.Routing /// public class RouteValueDictionary : IDictionary, IReadOnlyDictionary { - internal Storage _storage; + // 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 an empty . /// public RouteValueDictionary() { - _storage = EmptyStorage.Instance; + _arrayStorage = Array.Empty>(); } /// @@ -40,63 +46,45 @@ namespace Microsoft.AspNetCore.Routing /// Only public instance non-index properties are considered. /// public RouteValueDictionary(object values) + : this() { - var dictionary = values as RouteValueDictionary; - if (dictionary != null) + if (values is RouteValueDictionary dictionary) { - var listStorage = dictionary._storage as ListStorage; - if (listStorage != null) - { - _storage = new ListStorage(listStorage); - return; - } - - var propertyStorage = dictionary._storage as PropertyStorage; - if (propertyStorage != null) + if (dictionary._propertyStorage != null) { // PropertyStorage is immutable so we can just copy it. - _storage = dictionary._storage; + _propertyStorage = dictionary._propertyStorage; + _count = dictionary._count; return; } - // If we get here, it's an EmptyStorage. - _storage = EmptyStorage.Instance; + 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; } - var keyValueEnumerable = values as IEnumerable>; - if (keyValueEnumerable != null) + if (values is IEnumerable> keyValueEnumerable) { - 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.Add(kvp); + Add(kvp.Key, kvp.Value); } return; } - var stringValueEnumerable = values as IEnumerable>; - if (stringValueEnumerable != null) + if (values is IEnumerable> stringValueEnumerable) { - var listStorage = new ListStorage(); - _storage = listStorage; foreach (var kvp in stringValueEnumerable) { - if (listStorage.ContainsKey(kvp.Key)) - { - var message = Resources.FormatRouteValueDictionary_DuplicateKey(kvp.Key, nameof(RouteValueDictionary)); - throw new ArgumentException(message, nameof(values)); - } - - listStorage.Add(new KeyValuePair(kvp.Key, kvp.Value)); + Add(kvp.Key, kvp.Value); } return; @@ -104,11 +92,11 @@ namespace Microsoft.AspNetCore.Routing if (values != null) { - _storage = new PropertyStorage(values); + var storage = new PropertyStorage(values); + _propertyStorage = storage; + _count = storage.Properties.Length; return; } - - _storage = EmptyStorage.Instance; } /// @@ -133,10 +121,21 @@ namespace Microsoft.AspNetCore.Routing throw new ArgumentNullException(nameof(key)); } - if (!_storage.TrySetValue(key, value)) + // 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) { - Upgrade(); - _storage.TrySetValue(key, value); + EnsureCapacity(_count + 1); + _arrayStorage[_count++] = new KeyValuePair(key, value); + } + else + { + + _arrayStorage[index] = new KeyValuePair(key, value); } } } @@ -150,7 +149,7 @@ namespace Microsoft.AspNetCore.Routing public IEqualityComparer Comparer => StringComparer.OrdinalIgnoreCase; /// - public int Count => _storage.Count; + public int Count => _count; /// bool ICollection>.IsReadOnly => false; @@ -160,13 +159,13 @@ namespace Microsoft.AspNetCore.Routing { get { - Upgrade(); + EnsureCapacity(_count); - var list = (ListStorage)_storage; - var keys = new string[list.Count]; + var array = _arrayStorage; + var keys = new string[_count]; for (var i = 0; i < keys.Length; i++) { - keys[i] = list[i].Key; + keys[i] = array[i].Key; } return keys; @@ -186,13 +185,13 @@ namespace Microsoft.AspNetCore.Routing { get { - Upgrade(); + EnsureCapacity(_count); - var list = (ListStorage)_storage; - var values = new object[list.Count]; + var array = _arrayStorage; + var values = new object[_count]; for (var i = 0; i < values.Length; i++) { - values[i] = list[i].Value; + values[i] = array[i].Value; } return values; @@ -221,55 +220,43 @@ namespace Microsoft.AspNetCore.Routing throw new ArgumentNullException(nameof(key)); } - Upgrade(); + EnsureCapacity(_count + 1); - var list = (ListStorage)_storage; - for (var i = 0; i < list.Count; i++) + var index = FindInArray(key); + if (index >= 0) { - if (string.Equals(list[i].Key, key, StringComparison.OrdinalIgnoreCase)) - { - var message = Resources.FormatRouteValueDictionary_DuplicateKey(key, nameof(RouteValueDictionary)); - throw new ArgumentException(message, nameof(key)); - } + var message = Resources.FormatRouteValueDictionary_DuplicateKey(key, nameof(RouteValueDictionary)); + throw new ArgumentException(message, nameof(key)); } - list.Add(new KeyValuePair(key, value)); + _arrayStorage[_count] = new KeyValuePair(key, value); + _count++; } /// public void Clear() { - if (_storage.Count == 0) + if (_count == 0) { return; } - Upgrade(); + if (_propertyStorage != null) + { + _arrayStorage = Array.Empty>(); + _propertyStorage = null; + _count = 0; + return; + } - var list = (ListStorage)_storage; - list.Clear(); + Array.Clear(_arrayStorage, 0, _count); + _count = 0; } /// bool ICollection>.Contains(KeyValuePair item) { - if (_storage.Count == 0) - { - return false; - } - - Upgrade(); - - var list = (ListStorage)_storage; - 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; + return TryGetValue(item.Key, out var value) && EqualityComparer.Default.Equals(value, item.Value); } /// @@ -280,7 +267,7 @@ namespace Microsoft.AspNetCore.Routing throw new ArgumentNullException(nameof(key)); } - return _storage.ContainsKey(key); + return TryGetValue(key, out var _); } /// @@ -298,15 +285,15 @@ namespace Microsoft.AspNetCore.Routing throw new ArgumentOutOfRangeException(nameof(arrayIndex)); } - if (_storage.Count == 0) + if (Count == 0) { return; } - Upgrade(); + EnsureCapacity(Count); - var list = (ListStorage)_storage; - list.CopyTo(array, arrayIndex); + var storage = _arrayStorage; + Array.Copy(storage, 0, array, arrayIndex, _count); } /// @@ -330,22 +317,21 @@ namespace Microsoft.AspNetCore.Routing /// bool ICollection>.Remove(KeyValuePair item) { - if (_storage.Count == 0) + if (Count == 0) { return false; } - Upgrade(); + EnsureCapacity(Count); - var list = (ListStorage)_storage; - for (var i = 0; i < list.Count; i++) + var index = FindInArray(item.Key); + var array = _arrayStorage; + if (index >= 0 && EqualityComparer.Default.Equals(array[index].Value, item.Value)) { - if (string.Equals(list[i].Key, item.Key, StringComparison.OrdinalIgnoreCase) && - EqualityComparer.Default.Equals(list[i].Value, item.Value)) - { - list.RemoveAt(i); - return true; - } + Array.Copy(array, index + 1, array, index, _count - index); + _count--; + array[_count] = default; + return true; } return false; @@ -359,21 +345,22 @@ namespace Microsoft.AspNetCore.Routing throw new ArgumentNullException(nameof(key)); } - if (_storage.Count == 0) + if (Count == 0) { return false; } - Upgrade(); + EnsureCapacity(Count); - var list = (ListStorage)_storage; - for (var i = 0; i < list.Count; i++) + var index = FindInArray(key); + if (index >= 0) { - if (string.Equals(list[i].Key, key, StringComparison.OrdinalIgnoreCase)) - { - list.RemoveAt(i); - return true; - } + _count--; + var array = _arrayStorage; + Array.Copy(array, index + 1, array, index, _count - index); + array[_count] = default; + + return true; } return false; @@ -387,17 +374,95 @@ namespace Microsoft.AspNetCore.Routing throw new ArgumentNullException(nameof(key)); } - return _storage.TryGetValue(key, out 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; + } + + 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; } - private void Upgrade() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureCapacity(int capacity) { - _storage.Upgrade(ref _storage); + 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 readonly Storage _storage; + private RouteValueDictionary _dictionary; private int _index; public Enumerator(RouteValueDictionary dictionary) @@ -407,9 +472,9 @@ namespace Microsoft.AspNetCore.Routing throw new ArgumentNullException(); } - _storage = dictionary._storage; + _dictionary = dictionary; - Current = default(KeyValuePair); + Current = default; _index = -1; } @@ -423,296 +488,50 @@ namespace Microsoft.AspNetCore.Routing public bool MoveNext() { - if (++_index < _storage.Count) + if (++_index < _dictionary.Count) { - Current = _storage[_index]; + 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(KeyValuePair); + Current = default; return false; } public void Reset() { - Current = default(KeyValuePair); + Current = default; _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; set; } - - 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 - { - private KeyValuePair[] _items; - private int _count; - - private static readonly KeyValuePair[] _emptyArray = new KeyValuePair[0]; - - public ListStorage() - { - _items = _emptyArray; - } - - public ListStorage(int capacity) - { - if (capacity == 0) - { - _items = _emptyArray; - } - else - { - _items = new KeyValuePair[capacity]; - } - } - - public ListStorage(ListStorage other) - { - if (other.Count == 0) - { - _items = _emptyArray; - } - else - { - _items = new KeyValuePair[other.Count]; - for (var i = 0; i < other.Count; i++) - { - this.Add(other[i]); - } - } - } - - public int Capacity => _items.Length; - - public override int Count => _count; - - public override KeyValuePair this[int index] - { - get - { - if (index < 0 || index >= _count) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - return _items[index]; - } - set - { - if (index < 0 || index >= _count) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - _items[index] = value; - } - } - - public void Add(KeyValuePair item) - { - if (_count == _items.Length) - { - EnsureCapacity(_count + 1); - } - - _items[_count++] = item; - } - - public void RemoveAt(int index) - { - _count--; - - for (var i = index; i < _count; i++) - { - _items[i] = _items[i + 1]; - } - - _items[_count] = default(KeyValuePair); - } - - public void Clear() - { - for (var i = 0; i < _count; i++) - { - _items[i] = default(KeyValuePair); - } - - _count = 0; - } - - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - for (var i = 0; i < _count; i++) - { - array[arrayIndex++] = _items[i]; - } - } - - public override bool ContainsKey(string key) - { - for (var i = 0; i < Count; i++) - { - var kvp = _items[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 < Count; i++) - { - var kvp = _items[i]; - if (string.Equals(key, kvp.Key, StringComparison.OrdinalIgnoreCase)) - { - _items[i] = new KeyValuePair(key, value); - return true; - } - } - - Add(new KeyValuePair(key, value)); - return true; - } - - public override bool TryGetValue(string key, out object value) - { - for (var i = 0; i < Count; i++) - { - var kvp = _items[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. - } - - private void EnsureCapacity(int min) - { - var newLength = _items.Length == 0 ? 4 : _items.Length * 2; - var newItems = new KeyValuePair[newLength]; - for (var i = 0; i < _count; i++) - { - newItems[i] = _items[i]; - } - - _items = newItems; - } - } - - internal class PropertyStorage : Storage + internal class PropertyStorage { private static readonly PropertyCache _propertyCache = new PropertyCache(); - internal readonly object _value; - internal readonly PropertyHelper[] _properties; + public readonly object Value; + public readonly PropertyHelper[] Properties; public PropertyStorage(object value) { Debug.Assert(value != null); - _value = value; + 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)) + 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)); - } - set - { - // PropertyStorage never sets a value. - throw new NotImplementedException(); - } - } - - 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)); + Properties = PropertyHelper.GetVisibleProperties(type); + ValidatePropertyNames(type, Properties); + _propertyCache.TryAdd(type, Properties); } } @@ -723,8 +542,7 @@ namespace Microsoft.AspNetCore.Routing { var property = properties[i]; - PropertyHelper duplicate; - if (names.TryGetValue(property.Name, out duplicate)) + if (names.TryGetValue(property.Name, out var duplicate)) { var message = Resources.FormatRouteValueDictionary_DuplicatePropertyName( type.FullName, @@ -739,50 +557,6 @@ namespace Microsoft.AspNetCore.Routing } } - 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(); - } - set - { - 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 { } diff --git a/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteValueDictionaryTests.cs b/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteValueDictionaryTests.cs index 764f773e16..294e034131 100644 --- a/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteValueDictionaryTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Routing.Abstractions.Tests/RouteValueDictionaryTests.cs @@ -20,7 +20,8 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Empty(dict); - Assert.IsType(dict._storage); + Assert.Empty(dict._arrayStorage); + Assert.Null(dict._propertyStorage); } [Fact] @@ -32,11 +33,12 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Empty(dict); - Assert.IsType(dict._storage); + Assert.Empty(dict._arrayStorage); + Assert.Null(dict._propertyStorage); } [Fact] - public void CreateFromRouteValueDictionary_WithListStorage_CopiesStorage() + public void CreateFromRouteValueDictionary_WithArrayStorage_CopiesStorage() { // Arrange var other = new RouteValueDictionary() @@ -50,8 +52,8 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Equal(other, dict); - var storage = Assert.IsType(dict._storage); - var otherStorage = Assert.IsType(other._storage); + var storage = Assert.IsType[]>(dict._arrayStorage); + var otherStorage = Assert.IsType[]>(other._arrayStorage); Assert.NotSame(otherStorage, storage); } @@ -67,25 +69,8 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Equal(other, dict); - var storage = Assert.IsType(dict._storage); - var otherStorage = Assert.IsType(other._storage); - Assert.Same(otherStorage, storage); - } - - [Fact] - public void CreateFromRouteValueDictionary_WithEmptyStorage_SharedInstance() - { - // Arrange - var other = new RouteValueDictionary(); - - // Act - var dict = new RouteValueDictionary(other); - - // Assert - Assert.Equal(other, dict); - - var storage = Assert.IsType(dict._storage); - var otherStorage = Assert.IsType(other._storage); + var storage = dict._propertyStorage; + var otherStorage = other._propertyStorage; Assert.Same(otherStorage, storage); } @@ -135,7 +120,7 @@ namespace Microsoft.AspNetCore.Routing.Tests var dict = new RouteValueDictionary(values); // Assert - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); Assert.Collection( dict.OrderBy(kvp => kvp.Key), kvp => @@ -157,7 +142,7 @@ namespace Microsoft.AspNetCore.Routing.Tests var dict = new RouteValueDictionary(values); // Assert - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); Assert.Collection( dict.OrderBy(kvp => kvp.Key), kvp => { Assert.Equal("First Name", kvp.Key); Assert.Equal("James", kvp.Value); }, @@ -178,7 +163,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Act & Assert ExceptionAssert.ThrowsArgument( () => new RouteValueDictionary(values), - "values", + "key", $"An element with the key 'Name' already exists in the {nameof(RouteValueDictionary)}."); } @@ -195,7 +180,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Act & Assert ExceptionAssert.ThrowsArgument( () => new RouteValueDictionary(values), - "values", + "key", $"An element with the key 'Name' already exists in the {nameof(RouteValueDictionary)}."); } @@ -209,7 +194,7 @@ namespace Microsoft.AspNetCore.Routing.Tests var dict = new RouteValueDictionary(obj); // Assert - Assert.IsType(dict._storage); + Assert.NotNull(dict._propertyStorage); Assert.Collection( dict.OrderBy(kvp => kvp.Key), kvp => { Assert.Equal("awesome", kvp.Key); Assert.Equal(123, kvp.Value); }, @@ -226,7 +211,7 @@ namespace Microsoft.AspNetCore.Routing.Tests var dict = new RouteValueDictionary(obj); // Assert - Assert.IsType(dict._storage); + Assert.NotNull(dict._propertyStorage); Assert.Collection( dict.OrderBy(kvp => kvp.Key), kvp => @@ -252,7 +237,7 @@ namespace Microsoft.AspNetCore.Routing.Tests var dict = new RouteValueDictionary(obj); // Assert - Assert.IsType(dict._storage); + Assert.NotNull(dict._propertyStorage); Assert.Collection( dict.OrderBy(kvp => kvp.Key), kvp => @@ -273,7 +258,7 @@ namespace Microsoft.AspNetCore.Routing.Tests var dict = new RouteValueDictionary(obj); // Assert - Assert.IsType(dict._storage); + Assert.NotNull(dict._propertyStorage); Assert.Empty(dict); } @@ -287,7 +272,7 @@ namespace Microsoft.AspNetCore.Routing.Tests var dict = new RouteValueDictionary(obj); // Assert - Assert.IsType(dict._storage); + Assert.NotNull(dict._propertyStorage); Assert.Empty(dict); } @@ -301,7 +286,7 @@ namespace Microsoft.AspNetCore.Routing.Tests var dict = new RouteValueDictionary(obj); // Assert - Assert.IsType(dict._storage); + Assert.NotNull(dict._propertyStorage); Assert.Collection( dict.OrderBy(kvp => kvp.Key), kvp => @@ -328,7 +313,7 @@ namespace Microsoft.AspNetCore.Routing.Tests var dict = new RouteValueDictionary(obj); // Assert - Assert.IsType(dict._storage); + Assert.NotNull(dict._propertyStorage); Assert.Collection( dict.OrderBy(kvp => kvp.Key), kvp => { Assert.Equal("DerivedProperty", kvp.Key); Assert.Equal(5, kvp.Value); }); @@ -344,7 +329,7 @@ namespace Microsoft.AspNetCore.Routing.Tests var dict = new RouteValueDictionary(obj); // Assert - Assert.IsType(dict._storage); + Assert.NotNull(dict._propertyStorage); Assert.Empty(dict); } @@ -406,7 +391,6 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Null(value); - Assert.IsType(dict._storage); } [Fact] @@ -420,7 +404,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Null(value); - Assert.IsType(dict._storage); + Assert.NotNull(dict._propertyStorage); } [Fact] @@ -434,7 +418,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Equal("value", value); - Assert.IsType(dict._storage); + Assert.NotNull(dict._propertyStorage); } [Fact] @@ -448,11 +432,11 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Equal("value", value); - Assert.IsType(dict._storage); + Assert.NotNull(dict._propertyStorage); } [Fact] - public void IndexGet_ListStorage_NoMatch_ReturnsNull() + public void IndexGet_ArrayStorage_NoMatch_ReturnsNull() { // Arrange var dict = new RouteValueDictionary() @@ -465,7 +449,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Null(value); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -482,7 +466,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Equal("value", value); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -499,7 +483,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Equal("value", value); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -513,7 +497,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -530,7 +514,7 @@ namespace Microsoft.AspNetCore.Routing.Tests 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._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -544,7 +528,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -558,7 +542,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Collection(dict, kvp => { Assert.Equal("kEy", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -578,7 +562,7 @@ namespace Microsoft.AspNetCore.Routing.Tests 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._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -595,7 +579,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -612,7 +596,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -626,7 +610,6 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Equal(0, count); - Assert.IsType(dict._storage); } [Fact] @@ -640,7 +623,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Equal(1, count); - Assert.IsType(dict._storage); + Assert.NotNull(dict._propertyStorage); } [Fact] @@ -657,7 +640,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Equal(1, count); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -671,7 +654,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Empty(keys); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -685,7 +668,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Equal(new[] { "key" }, keys); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -702,7 +685,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Equal(new[] { "key" }, keys); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -716,7 +699,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Empty(values); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -730,7 +713,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Equal(new object[] { "value" }, values); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -747,7 +730,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Equal(new object[] { "value" }, values); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -761,7 +744,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -778,7 +761,7 @@ namespace Microsoft.AspNetCore.Routing.Tests 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._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -798,7 +781,7 @@ namespace Microsoft.AspNetCore.Routing.Tests 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._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -819,7 +802,7 @@ namespace Microsoft.AspNetCore.Routing.Tests Assert.Collection( dict.OrderBy(kvp => kvp.Key), kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -840,7 +823,7 @@ namespace Microsoft.AspNetCore.Routing.Tests Assert.Collection( dict.OrderBy(kvp => kvp.Key), kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -860,7 +843,7 @@ namespace Microsoft.AspNetCore.Routing.Tests 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._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -874,7 +857,6 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Empty(dict); - Assert.IsType(dict._storage); } [Fact] @@ -888,7 +870,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Empty(dict); - Assert.IsType(dict._storage); + Assert.NotNull(dict._propertyStorage); } [Fact] @@ -902,7 +884,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Empty(dict); - Assert.IsType(dict._storage); + Assert.Null(dict._propertyStorage); } [Fact] @@ -919,7 +901,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.Empty(dict); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -938,7 +920,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.True(result); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -957,7 +939,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.True(result); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -976,7 +958,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.False(result); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } // Value comparisons use the default equality comparer. @@ -996,7 +978,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.False(result); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -1010,7 +992,6 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.False(result); - Assert.IsType(dict._storage); } [Fact] @@ -1024,7 +1005,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.False(result); - Assert.IsType(dict._storage); + Assert.NotNull(dict._propertyStorage); } [Fact] @@ -1038,7 +1019,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.True(result); - Assert.IsType(dict._storage); + Assert.NotNull(dict._propertyStorage); } [Fact] @@ -1052,7 +1033,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.True(result); - Assert.IsType(dict._storage); + Assert.NotNull(dict._propertyStorage); } [Fact] @@ -1069,7 +1050,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.False(result); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -1086,7 +1067,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.True(result); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -1103,7 +1084,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.True(result); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -1128,7 +1109,7 @@ namespace Microsoft.AspNetCore.Routing.Tests new KeyValuePair("key", "value") }, array); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -1148,7 +1129,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.True(result); Assert.Empty(dict); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -1168,7 +1149,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.True(result); Assert.Empty(dict); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -1188,7 +1169,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.False(result); Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } // Value comparisons use the default equality comparer. @@ -1209,7 +1190,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.False(result); Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -1223,7 +1204,6 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.False(result); - Assert.IsType(dict._storage); } [Fact] @@ -1238,7 +1218,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.False(result); Assert.Empty(dict); - Assert.IsType(dict._storage); + Assert.NotNull(dict._propertyStorage); } [Fact] @@ -1253,7 +1233,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.False(result); Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -1268,7 +1248,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.True(result); Assert.Empty(dict); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -1283,7 +1263,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.True(result); Assert.Empty(dict); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -1301,7 +1281,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.False(result); Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -1319,7 +1299,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.True(result); Assert.Empty(dict); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -1337,7 +1317,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.True(result); Assert.Empty(dict); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -1353,7 +1333,6 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.False(result); Assert.Null(value); - Assert.IsType(dict._storage); } [Fact] @@ -1369,7 +1348,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.False(result); Assert.Null(value); - Assert.IsType(dict._storage); + Assert.NotNull(dict._propertyStorage); } [Fact] @@ -1385,7 +1364,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.True(result); Assert.Equal("value", value); - Assert.IsType(dict._storage); + Assert.NotNull(dict._propertyStorage); } [Fact] @@ -1401,7 +1380,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.True(result); Assert.Equal("value", value); - Assert.IsType(dict._storage); + Assert.NotNull(dict._propertyStorage); } [Fact] @@ -1420,7 +1399,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.False(result); Assert.Null(value); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -1439,7 +1418,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.True(result); Assert.Equal("value", value); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -1458,7 +1437,7 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.True(result); Assert.Equal("value", value); - Assert.IsType(dict._storage); + Assert.IsType[]>(dict._arrayStorage); } [Fact] @@ -1471,8 +1450,8 @@ namespace Microsoft.AspNetCore.Routing.Tests dict.Add("key", "value"); // Assert 1 - var storage = Assert.IsType(dict._storage); - Assert.Equal(4, storage.Capacity); + var storage = Assert.IsType[]>(dict._arrayStorage); + Assert.Equal(4, storage.Length); // Act 2 dict.Add("key2", "value2"); @@ -1481,7 +1460,8 @@ namespace Microsoft.AspNetCore.Routing.Tests dict.Add("key5", "value5"); // Assert 2 - Assert.Equal(8, storage.Capacity); + storage = Assert.IsType[]>(dict._arrayStorage); + Assert.Equal(8, storage.Length); } [Fact] @@ -1494,20 +1474,19 @@ namespace Microsoft.AspNetCore.Routing.Tests dict.Add("key3", "value3"); // Assert 1 - var storage = Assert.IsType(dict._storage); - Assert.Equal(3, storage.Count); + var storage = Assert.IsType[]>(dict._arrayStorage); + Assert.Equal(3, dict.Count); // Act dict.Remove("key2"); // Assert 2 - Assert.Equal(2, storage.Count); + 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); - - Assert.Throws(() => storage[2]); } private class RegularType