diff --git a/src/Http/Http.Abstractions/src/Routing/RouteValueDictionary.cs b/src/Http/Http.Abstractions/src/Routing/RouteValueDictionary.cs index 415e4d157154..33e9baab979b 100644 --- a/src/Http/Http.Abstractions/src/Routing/RouteValueDictionary.cs +++ b/src/Http/Http.Abstractions/src/Routing/RouteValueDictionary.cs @@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Routing 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; + private readonly int DefaultCapacity = 4; internal KeyValuePair[] _arrayStorage; internal PropertyStorage? _propertyStorage; diff --git a/src/Http/Http/perf/Microbenchmarks/AdaptiveCapacityDictionaryBenchmark.cs b/src/Http/Http/perf/Microbenchmarks/AdaptiveCapacityDictionaryBenchmark.cs new file mode 100644 index 000000000000..40684acda48f --- /dev/null +++ b/src/Http/Http/perf/Microbenchmarks/AdaptiveCapacityDictionaryBenchmark.cs @@ -0,0 +1,331 @@ +// 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 BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Internal; + +namespace Microsoft.AspNetCore.Http +{ + public class AdaptiveCapacityDictionaryBenchmark + { + private AdaptiveCapacityDictionary _smallCapDict; + private AdaptiveCapacityDictionary _smallCapDictTen; + private AdaptiveCapacityDictionary _filledSmallDictionary; + private Dictionary _dict; + private Dictionary _dictTen; + private Dictionary _filledDictTen; + private KeyValuePair _oneValue; + private List> _tenValues; + + [IterationSetup] + public void Setup() + { + _oneValue = new KeyValuePair("a", "b"); + + _tenValues = new List>() + { + new KeyValuePair("a", "b"), + new KeyValuePair("c", "d"), + new KeyValuePair("e", "f"), + new KeyValuePair("g", "h"), + new KeyValuePair("i", "j"), + new KeyValuePair("k", "l"), + new KeyValuePair("m", "n"), + new KeyValuePair("o", "p"), + new KeyValuePair("q", "r"), + new KeyValuePair("s", "t"), + }; + + _smallCapDict = new AdaptiveCapacityDictionary(capacity: 1, StringComparer.OrdinalIgnoreCase); + _smallCapDictTen = new AdaptiveCapacityDictionary(capacity: 10, StringComparer.OrdinalIgnoreCase); + _filledSmallDictionary = new AdaptiveCapacityDictionary(_tenValues, capacity: 10, StringComparer.OrdinalIgnoreCase); + + _dict = new Dictionary(1, StringComparer.OrdinalIgnoreCase); + _dictTen = new Dictionary(10, StringComparer.OrdinalIgnoreCase); + _filledDictTen = new Dictionary(10, StringComparer.OrdinalIgnoreCase); + + foreach (var a in _tenValues) + { + _filledDictTen[a.Key] = a.Value; + } + } + + [Benchmark] + public void OneValue_SmallDict() + { + _smallCapDict[_oneValue.Key] = _oneValue.Value; + _ = _smallCapDict[_oneValue.Key]; + } + + [Benchmark] + public void OneValue_Dict() + { + _dict[_oneValue.Key] = _oneValue.Value; + _ = _dict[_oneValue.Key]; + } + + [Benchmark] + public void OneValue_SmallDict_Set() + { + _smallCapDict[_oneValue.Key] = _oneValue.Value; + } + + [Benchmark] + public void OneValue_Dict_Set() + { + _dict[_oneValue.Key] = _oneValue.Value; + } + + + [Benchmark] + public void OneValue_SmallDict_Get() + { + _smallCapDict.TryGetValue("test", out var val); + } + + [Benchmark] + public void OneValue_Dict_Get() + { + _dict.TryGetValue("test", out var val); + } + + [Benchmark] + public void FourValues_SmallDict() + { + for (var i = 0; i < 4; i++) + { + var val = _tenValues[i]; + _smallCapDictTen[val.Key] = val.Value; + _ = _smallCapDictTen[val.Key]; + } + } + + [Benchmark] + public void FiveValues_SmallDict() + { + for (var i = 0; i < 5; i++) + { + var val = _tenValues[i]; + _smallCapDictTen[val.Key] = val.Value; + _ = _smallCapDictTen[val.Key]; + } + } + + [Benchmark] + public void SixValues_SmallDict() + { + for (var i = 0; i < 6; i++) + { + var val = _tenValues[i]; + _smallCapDictTen[val.Key] = val.Value; + _ = _smallCapDictTen[val.Key]; + } + } + + [Benchmark] + public void SevenValues_SmallDict() + { + for (var i = 0; i < 7; i++) + { + var val = _tenValues[i]; + _smallCapDictTen[val.Key] = val.Value; + _ = _smallCapDictTen[val.Key]; + } + } + + [Benchmark] + public void EightValues_SmallDict() + { + for (var i = 0; i < 8; i++) + { + var val = _tenValues[i]; + _smallCapDictTen[val.Key] = val.Value; + _ = _smallCapDictTen[val.Key]; + } + } + + [Benchmark] + public void NineValues_SmallDict() + { + for (var i = 0; i < 9; i++) + { + var val = _tenValues[i]; + _smallCapDictTen[val.Key] = val.Value; + _ = _smallCapDictTen[val.Key]; + } + } + + [Benchmark] + public void TenValues_SmallDict() + { + for (var i = 0; i < 10; i++) + { + var val = _tenValues[i]; + _smallCapDictTen[val.Key] = val.Value; + _ = _smallCapDictTen[val.Key]; + } + } + + + [Benchmark] + public void FourValues_Dict() + { + for (var i = 0; i < 4; i++) + { + var val = _tenValues[i]; + _dictTen[val.Key] = val.Value; + _ = _dictTen[val.Key]; + } + } + + [Benchmark] + public void FiveValues_Dict() + { + for (var i = 0; i < 5; i++) + { + var val = _tenValues[i]; + _dictTen[val.Key] = val.Value; + _ = _dictTen[val.Key]; + } + } + [Benchmark] + public void SixValues_Dict() + { + for (var i = 0; i < 6; i++) + { + var val = _tenValues[i]; + _dictTen[val.Key] = val.Value; + _ = _dictTen[val.Key]; + } + } + [Benchmark] + public void SevenValues_Dict() + { + for (var i = 0; i < 7; i++) + { + var val = _tenValues[i]; + _dictTen[val.Key] = val.Value; + _ = _dictTen[val.Key]; + } + } + [Benchmark] + public void EightValues_Dict() + { + for (var i = 0; i < 8; i++) + { + var val = _tenValues[i]; + _dictTen[val.Key] = val.Value; + _ = _dictTen[val.Key]; + } + } + [Benchmark] + public void NineValues_Dict() + { + for (var i = 0; i < 9; i++) + { + var val = _tenValues[i]; + _dictTen[val.Key] = val.Value; + _ = _dictTen[val.Key]; + } + } + + [Benchmark] + public void TenValues_Dict() + { + for (var i = 0; i < 10; i++) + { + var val = _tenValues[i]; + _dictTen[val.Key] = val.Value; + _ = _dictTen[val.Key]; + } + } + + [Benchmark] + public void FourValues_SmallDictGet() + { + _ = _filledSmallDictionary["g"]; + } + + [Benchmark] + public void FiveValues_SmallDictGet() + { + _ = _filledSmallDictionary["i"]; + } + + [Benchmark] + public void SixValues_SmallDictGetGet() + { + _ = _filledSmallDictionary["k"]; + + } + + [Benchmark] + public void SevenValues_SmallDictGetGet() + { + _ = _filledSmallDictionary["m"]; + } + + [Benchmark] + public void EightValues_SmallDictGet() + { + _ = _filledSmallDictionary["o"]; + } + + [Benchmark] + public void NineValues_SmallDictGet() + { + _ = _filledSmallDictionary["q"]; + } + + [Benchmark] + public void TenValues_SmallDictGet() + { + _ = _filledSmallDictionary["s"]; + } + + [Benchmark] + public void TenValues_DictGet() + { + _ = _filledDictTen["s"]; + } + + [Benchmark] + public void SmallDict() + { + _ = new AdaptiveCapacityDictionary(capacity: 1); + } + + [Benchmark] + public void Dict() + { + _ = new Dictionary(capacity: 1); + } + + + [Benchmark] + public void SmallDictFour() + { + _ = new AdaptiveCapacityDictionary(capacity: 4); + } + + [Benchmark] + public void DictFour() + { + _ = new Dictionary(capacity: 4); + } + + [Benchmark] + public void SmallDictTen() + { + _ = new AdaptiveCapacityDictionary(capacity: 10); + } + + [Benchmark] + public void DictTen() + { + _ = new Dictionary(capacity: 10); + } + } +} diff --git a/src/Http/Http/perf/Microbenchmarks/Microsoft.AspNetCore.Http.Microbenchmarks.csproj b/src/Http/Http/perf/Microbenchmarks/Microsoft.AspNetCore.Http.Microbenchmarks.csproj index 01539b81e19c..f5d277301b12 100644 --- a/src/Http/Http/perf/Microbenchmarks/Microsoft.AspNetCore.Http.Microbenchmarks.csproj +++ b/src/Http/Http/perf/Microbenchmarks/Microsoft.AspNetCore.Http.Microbenchmarks.csproj @@ -11,7 +11,6 @@ - diff --git a/src/Http/Http/src/Internal/RequestCookieCollection.cs b/src/Http/Http/src/Internal/RequestCookieCollection.cs index f7ae17212887..7a02b276b342 100644 --- a/src/Http/Http/src/Internal/RequestCookieCollection.cs +++ b/src/Http/Http/src/Internal/RequestCookieCollection.cs @@ -6,7 +6,9 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Primitives; +using Microsoft.AspNetCore.Internal; using Microsoft.Net.Http.Headers; +using System.Linq; namespace Microsoft.AspNetCore.Http { @@ -19,20 +21,22 @@ internal class RequestCookieCollection : IRequestCookieCollection private static readonly IEnumerator> EmptyIEnumeratorType = EmptyEnumerator; private static readonly IEnumerator EmptyIEnumerator = EmptyEnumerator; - private Dictionary? Store { get; set; } + private AdaptiveCapacityDictionary Store { get; set; } public RequestCookieCollection() { + Store = new AdaptiveCapacityDictionary(StringComparer.OrdinalIgnoreCase); } - public RequestCookieCollection(Dictionary store) + public RequestCookieCollection(int capacity) { - Store = store; + Store = new AdaptiveCapacityDictionary(capacity, StringComparer.OrdinalIgnoreCase); } - public RequestCookieCollection(int capacity) + // For tests + public RequestCookieCollection(Dictionary store) { - Store = new Dictionary(capacity, StringComparer.OrdinalIgnoreCase); + Store = new AdaptiveCapacityDictionary(store); } public string? this[string key] @@ -121,6 +125,7 @@ public bool TryGetValue(string key, [MaybeNullWhen(false)] out string? value) value = null; return false; } + return Store.TryGetValue(key, out value); } @@ -172,10 +177,10 @@ IEnumerator IEnumerable.GetEnumerator() public struct Enumerator : IEnumerator> { // Do NOT make this readonly, or MoveNext will not work - private Dictionary.Enumerator _dictionaryEnumerator; + private AdaptiveCapacityDictionary.Enumerator _dictionaryEnumerator; private bool _notEmpty; - internal Enumerator(Dictionary.Enumerator dictionaryEnumerator) + internal Enumerator(AdaptiveCapacityDictionary.Enumerator dictionaryEnumerator) { _dictionaryEnumerator = dictionaryEnumerator; _notEmpty = true; @@ -197,7 +202,7 @@ public KeyValuePair Current if (_notEmpty) { var current = _dictionaryEnumerator.Current; - return new KeyValuePair(current.Key, current.Value); + return new KeyValuePair(current.Key, (string)current.Value!); } return default(KeyValuePair); } diff --git a/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj b/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj index 18e06e184ca4..669a90b01f12 100644 --- a/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj +++ b/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj @@ -1,4 +1,4 @@ - + ASP.NET Core default HTTP feature implementations. @@ -19,6 +19,7 @@ + diff --git a/src/Http/Shared/CookieHeaderParserShared.cs b/src/Http/Shared/CookieHeaderParserShared.cs index 061fe0b52874..e72b2e45c1d5 100644 --- a/src/Http/Shared/CookieHeaderParserShared.cs +++ b/src/Http/Shared/CookieHeaderParserShared.cs @@ -11,7 +11,7 @@ namespace Microsoft.Net.Http.Headers { internal static class CookieHeaderParserShared { - public static bool TryParseValues(StringValues values, Dictionary store, bool enableCookieNameEncoding, bool supportsMultipleValues) + public static bool TryParseValues(StringValues values, IDictionary store, bool enableCookieNameEncoding, bool supportsMultipleValues) { // If a parser returns an empty list, it means there was no value, but that's valid (e.g. "Accept: "). The caller // can ignore the value. diff --git a/src/Shared/Dictionary/AdaptiveCapacityDictionary.cs b/src/Shared/Dictionary/AdaptiveCapacityDictionary.cs new file mode 100644 index 000000000000..d039c4ffbbbd --- /dev/null +++ b/src/Shared/Dictionary/AdaptiveCapacityDictionary.cs @@ -0,0 +1,711 @@ +// 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.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Microsoft.AspNetCore.Internal +{ + /// + /// An type to hold a small amount of items (10 or less in the common case). + /// + internal class AdaptiveCapacityDictionary : IDictionary, IReadOnlyDictionary where TKey : notnull + { + // Threshold for size of array to use. + private const int DefaultArrayThreshold = 10; + + internal KeyValuePair[]? _arrayStorage; + private int _count; + internal Dictionary? _dictionaryStorage; + private IEqualityComparer _comparer; + + /// + /// Creates an empty . + /// + public AdaptiveCapacityDictionary() + : this(0, EqualityComparer.Default) + { + } + + /// + /// Creates a . + /// + /// Equality comparison. + public AdaptiveCapacityDictionary(IEqualityComparer comparer) + : this(0, comparer) + { + } + + /// + /// Creates a . + /// + /// Initial capacity. + public AdaptiveCapacityDictionary(int capacity) + : this(capacity, EqualityComparer.Default) + { + } + + /// + /// Creates a . + /// + /// Initial capacity. + /// Equality comparison. + public AdaptiveCapacityDictionary(int capacity, IEqualityComparer comparer) + { + if (comparer is not null) + { + _comparer = comparer; + } + else + { + _comparer = EqualityComparer.Default; + } + + if (capacity == 0) + { + _arrayStorage = Array.Empty>(); + } + else if (capacity <= DefaultArrayThreshold) + { + _arrayStorage = new KeyValuePair[capacity]; + } + else + { + _dictionaryStorage = new Dictionary(capacity); + _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. + /// + /// This constructor is unoptimized and primarily used for tests. + /// Equality comparison. + /// Initial capacity. + internal AdaptiveCapacityDictionary(IEnumerable> values, int capacity, IEqualityComparer comparer) + { + _comparer = comparer ?? EqualityComparer.Default; + + _arrayStorage = new KeyValuePair[capacity]; + + foreach (var kvp in values) + { + Add(kvp.Key, kvp.Value); + } + } + + /// + /// Creates a initialized with the specified . + /// + /// A dictionary to use. + /// + internal AdaptiveCapacityDictionary(Dictionary dict) + { + _comparer = dict.Comparer; + _dictionaryStorage = dict; + } + + /// + public TValue this[TKey key] + { + get + { + if (key == null) + { + ThrowArgumentNullExceptionForKey(); + } + + TryGetValue(key, out var value); + + return value!; + } + + set + { + if (key == null) + { + ThrowArgumentNullExceptionForKey(); + } + + if (_arrayStorage != null) + { + var index = FindIndex(key); + if (index < 0) + { + EnsureCapacity(_count + 1); + if (_dictionaryStorage != null) + { + _dictionaryStorage[key] = value; + return; + } + _arrayStorage[_count++] = new KeyValuePair(key, value); + } + else + { + _arrayStorage[index] = new KeyValuePair(key, value); + } + return; + } + + _dictionaryStorage![key] = value; + } + } + + /// + public int Count => _dictionaryStorage != null ? _dictionaryStorage.Count : _count; + + /// + public IEqualityComparer Comparer => _comparer; + + /// + bool ICollection>.IsReadOnly => false; + + /// + public ICollection Keys + { + get + { + if (_arrayStorage != null) + { + // TODO if common operation, make keys and values + // in separate arrays to avoid copying. + var array = _arrayStorage; + var keys = new TKey[_count]; + for (var i = 0; i < keys.Length; i++) + { + keys[i] = array[i].Key; + } + + return keys; + } + + return _dictionaryStorage!.Keys; + } + } + + IEnumerable IReadOnlyDictionary.Keys => Keys; + + /// + public ICollection Values + { + get + { + if (_arrayStorage != null) + { + // TODO if common operation, make keys and values + // in separate arrays to avoid copying. + var array = _arrayStorage; + var values = new TValue[_count]; + for (var i = 0; i < values.Length; i++) + { + values[i] = array[i].Value; + } + + return values; + } + + return _dictionaryStorage!.Values; + } + } + + IEnumerable IReadOnlyDictionary.Values => Values; + + /// + void ICollection>.Add(KeyValuePair item) + { + if (_arrayStorage != null) + { + Add(item.Key, item.Value); + return; + } + + ((ICollection>)_dictionaryStorage!).Add(item); + return; + } + + /// + public void Add(TKey key, TValue value) + { + if (key == null) + { + ThrowArgumentNullExceptionForKey(); + } + + if (_arrayStorage != null) + { + EnsureCapacity(_count + 1); + + if (_dictionaryStorage != null) + { + Debug.Assert(_arrayStorage == null); + _dictionaryStorage.Add(key, value); + return; + } + + if (ContainsKeyArray(key)) + { + throw new ArgumentException($"An element with the key '{key}' already exists in the {nameof(AdaptiveCapacityDictionary)}.", nameof(key)); + } + + _arrayStorage[_count] = new KeyValuePair(key, value); + _count++; + return; + } + + _dictionaryStorage!.Add(key, value); + } + + /// + public void Clear() + { + if (_dictionaryStorage != null) + { + _dictionaryStorage.Clear(); + } + + if (_count == 0) + { + return; + } + if (_arrayStorage != null) + { + Array.Clear(_arrayStorage, 0, _count); + _count = 0; + } + } + + /// + bool ICollection>.Contains(KeyValuePair item) + { + if (_dictionaryStorage != null) + { + return ((ICollection>)_dictionaryStorage).Contains(item); + } + + return TryGetValue(item.Key, out var value) && EqualityComparer.Default.Equals(value, item.Value); + } + + /// + public bool ContainsKey(TKey key) + { + if (key == null) + { + ThrowArgumentNullExceptionForKey(); + } + + if (_dictionaryStorage is null) + { + return ContainsKeyArray(key); + } + + return _dictionaryStorage.ContainsKey(key); + } + + /// + void ICollection>.CopyTo( + KeyValuePair[] array, + int arrayIndex) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + if ((uint)arrayIndex > array.Length || array.Length - arrayIndex < this.Count) + { + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + } + + if (_arrayStorage != null) + { + if (Count == 0) + { + return; + } + + var storage = _arrayStorage; + Array.Copy(storage, 0, array, arrayIndex, _count); + return; + } + + ((ICollection>)_dictionaryStorage!).CopyTo(array, arrayIndex); + } + + /// + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + /// + IEnumerator> IEnumerable>.GetEnumerator() + { + if (_dictionaryStorage != null) + { + return _dictionaryStorage.GetEnumerator(); + } + + return GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + if (_dictionaryStorage != null) + { + return _dictionaryStorage.GetEnumerator(); + } + + return GetEnumerator(); + } + + /// + bool ICollection>.Remove(KeyValuePair item) + { + if (_arrayStorage != null) + { + if (Count == 0) + { + return false; + } + + var index = FindIndex(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; + } + + return ((ICollection>)_dictionaryStorage!).Remove(item); + } + + /// + public bool Remove(TKey key) + { + if (key == null) + { + ThrowArgumentNullExceptionForKey(); + } + + if (_arrayStorage != null) + { + if (Count == 0) + { + return false; + } + + var index = FindIndex(key); + if (index >= 0) + { + _count--; + var array = _arrayStorage; + Array.Copy(array, index + 1, array, index, _count - index); + array[_count] = default; + + return true; + } + + return false; + } + + return _dictionaryStorage!.Remove(key); + } + + /// + /// Attempts to remove and return the value that has the specified key from the . + /// + /// The key of the element to remove and return. + /// When this method returns, contains the object removed from the , or null if key does not exist. + /// + /// true if the object was removed successfully; otherwise, false. + /// + public bool Remove(TKey key, out TValue? value) + { + if (key == null) + { + ThrowArgumentNullExceptionForKey(); + } + + if (_arrayStorage != null) + { + if (_count == 0) + { + value = default; + return false; + } + + var index = FindIndex(key); + if (index >= 0) + { + _count--; + var array = _arrayStorage; + value = array[index].Value; + Array.Copy(array, index + 1, array, index, _count - index); + array[_count] = default; + + return true; + } + + value = default; + return false; + } + + return _dictionaryStorage!.Remove(key, out value); + } + + /// + /// Attempts to the add the provided and to the dictionary. + /// + /// The key. + /// The value. + /// Returns true if the value was added. Returns false if the key was already present. + public bool TryAdd(TKey key, TValue value) + { + if (key == null) + { + ThrowArgumentNullExceptionForKey(); + } + + if (_arrayStorage != null) + { + if (ContainsKey(key)) + { + return false; + } + + EnsureCapacity(Count + 1); + + if (_dictionaryStorage != null) + { + return _dictionaryStorage.TryAdd(key, value); + } + + _arrayStorage[Count] = new KeyValuePair(key, value); + _count++; + return true; + } + + return _dictionaryStorage!.TryAdd(key, value); + } + + /// + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + if (key == null) + { + ThrowArgumentNullExceptionForKey(); + } + + if (_arrayStorage != null) + { + return TryFindItem(key, out value); + } + + return _dictionaryStorage!.TryGetValue(key, out value); + } + + [DoesNotReturn] + private static void ThrowArgumentNullExceptionForKey() + { + throw new ArgumentNullException("key"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureCapacity(int capacity) + { + if (_arrayStorage!.Length >= capacity) + { + return; + } + + EnsureCapacitySlow(capacity); + } + + private void EnsureCapacitySlow(int capacity) + { + Debug.Assert(_arrayStorage != null); + + if (capacity > DefaultArrayThreshold) + { + _dictionaryStorage = new Dictionary(capacity); + foreach (var item in _arrayStorage) + { + _dictionaryStorage[item.Key] = item.Value; + } + + // Clear array storage. + _arrayStorage = null; + } + else + { + capacity = _arrayStorage.Length == 0 ? DefaultArrayThreshold : _arrayStorage.Length * 2; + var array = new KeyValuePair[capacity]; + if (_count > 0) + { + Array.Copy(_arrayStorage, 0, array, 0, _count); + } + + _arrayStorage = array; + } + } + private Span> ArrayStorageSpan + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + Debug.Assert(_arrayStorage is not null); + Debug.Assert(_count <= _arrayStorage.Length); + + ref var r = ref MemoryMarshal.GetArrayDataReference(_arrayStorage); + return MemoryMarshal.CreateSpan(ref r, _count); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int FindIndex(TKey key) + { + Debug.Assert(_dictionaryStorage == null); + Debug.Assert(_arrayStorage != null); + + if (_count > 0) + { + for (var i = 0; i < ArrayStorageSpan.Length; ++i) + { + if (_comparer.Equals(ArrayStorageSpan[i].Key, key)) + { + return i; + } + } + } + + return -1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryFindItem(TKey key, out TValue? value) + { + Debug.Assert(_dictionaryStorage == null); + Debug.Assert(_arrayStorage != null); + + if (_count > 0) + { + foreach (ref var item in ArrayStorageSpan) + { + if (_comparer.Equals(item.Key, key)) + { + value = item.Value; + return true; + } + } + } + + value = default; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool ContainsKeyArray(TKey key) => TryFindItem(key, out _); + + + /// + public struct Enumerator : IEnumerator> + { + private readonly AdaptiveCapacityDictionary _dictionary; + private int _index; + // Don't mark this as readonly + private Dictionary.Enumerator? _dictionaryEnumerator; + + /// + /// Instantiates a new enumerator with the values provided in . + /// + /// A . + public Enumerator(AdaptiveCapacityDictionary dictionary) + { + if (dictionary == null) + { + throw new ArgumentNullException(); + } + + _dictionary = dictionary; + + if (_dictionary._dictionaryStorage != null) + { + _dictionaryEnumerator = _dictionary._dictionaryStorage.GetEnumerator(); + } + else + { + _dictionaryEnumerator = null; + } + + Current = default; + _index = 0; + } + + /// + public KeyValuePair Current { get; private set; } + + object IEnumerator.Current => Current; + + /// + /// Releases resources used by the . + /// + public void Dispose() + { + } + + // Similar to the design of List.Enumerator - Split into fast path and slow path for inlining friendliness + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + var dictionary = _dictionary; + if (dictionary._arrayStorage != null) + { + if (dictionary._count <= _index) + { + return false; + } + + Current = dictionary._arrayStorage[_index]; + _index++; + return true; + } + else + { + var enumerator = _dictionaryEnumerator!.Value; + var hasNext = enumerator.MoveNext(); + if (hasNext) + { + Current = enumerator.Current; + } + + _dictionaryEnumerator = enumerator; + + return hasNext; + } + } + + /// + public void Reset() + { + Current = default; + _index = 0; + } + } + } +} diff --git a/src/Shared/test/Shared.Tests/AdaptiveCapacityDictionaryTests.cs b/src/Shared/test/Shared.Tests/AdaptiveCapacityDictionaryTests.cs new file mode 100644 index 000000000000..b31ae8e14928 --- /dev/null +++ b/src/Shared/test/Shared.Tests/AdaptiveCapacityDictionaryTests.cs @@ -0,0 +1,1405 @@ +// 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.Internal; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Internal.Tests +{ + public class AdaptiveCapacityDictionaryTests + { + [Fact] + public void DefaultCtor() + { + // Arrange + // Act + var dict = new AdaptiveCapacityDictionary(); + + // Assert + Assert.Empty(dict); + Assert.Empty(dict._arrayStorage); + Assert.Null(dict._dictionaryStorage); + } + + [Fact] + public void CreateFromNull() + { + // Arrange + // Act + var dict = new AdaptiveCapacityDictionary(); + + // Assert + Assert.Empty(dict); + Assert.Empty(dict._arrayStorage); + Assert.Null(dict._dictionaryStorage); + } + + public static KeyValuePair[] IEnumerableKeyValuePairData + { + get + { + return new[] + { + new KeyValuePair("Name", "James"), + new KeyValuePair("Age", 30), + new KeyValuePair("Address", new Address() { City = "Redmond", State = "WA" }) + }; + } + } + + public static KeyValuePair[] IEnumerableStringValuePairData + { + get + { + return new[] + { + new KeyValuePair("First Name", "James"), + new KeyValuePair("Last Name", "Henrik"), + new KeyValuePair("Middle Name", "Bob") + }; + } + } + + [Fact] + public void CreateFromIEnumerableKeyValuePair_CopiesValues() + { + // Arrange & Act + var dict = new AdaptiveCapacityDictionary(IEnumerableKeyValuePairData, capacity: IEnumerableKeyValuePairData.Length, EqualityComparer.Default); + + // 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); }); + } + + [Fact] + public void CreateFromIEnumerableStringValuePair_CopiesValues() + { + // Arrange & Act + var dict = new AdaptiveCapacityDictionary(IEnumerableStringValuePairData, capacity: 3, StringComparer.OrdinalIgnoreCase); + + // 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, Act & Assert + ExceptionAssert.ThrowsArgument( + () => new AdaptiveCapacityDictionary(StringComparer.OrdinalIgnoreCase) + { + { "name", "Billy" }, + { "Name", "Joey" } + }, + "key", + $"An element with the key 'Name' already exists in the {nameof(AdaptiveCapacityDictionary)}."); + } + + [Fact] + public void CreateFromIEnumerableStringValuePair_ThrowsExceptionForDuplicateKey() + { + // Arrange + var values = new List>() + { + new KeyValuePair("name", "Billy"), + new KeyValuePair("Name", "Joey"), + }; + + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => new AdaptiveCapacityDictionary(values, capacity: 3, StringComparer.OrdinalIgnoreCase), + "key", + $"An element with the key 'Name' already exists in the {nameof(AdaptiveCapacityDictionary)}."); + } + + [Fact] + public void Comparer_IsOrdinalIgnoreCase() + { + // Arrange + // Act + var dict = new AdaptiveCapacityDictionary(StringComparer.OrdinalIgnoreCase); + + // 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 AdaptiveCapacityDictionary(); + + // Act + var result = ((ICollection>)dict).IsReadOnly; + + // Assert + Assert.False(result); + } + + [Fact] + public void IndexGet_EmptyStringIsAllowed() + { + // Arrange + var dict = new AdaptiveCapacityDictionary(); + + // Act + var value = dict[""]; + + // Assert + Assert.Null(value); + } + + [Fact] + public void IndexGet_EmptyStorage_ReturnsNull() + { + // Arrange + var dict = new AdaptiveCapacityDictionary(); + + // Act + var value = dict["key"]; + + // Assert + Assert.Null(value); + } + + [Fact] + public void IndexGet_ArrayStorage_NoMatch_ReturnsNull() + { + // Arrange + var dict = new AdaptiveCapacityDictionary(); + dict.Add("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 AdaptiveCapacityDictionary() + { + { "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 AdaptiveCapacityDictionary(StringComparer.OrdinalIgnoreCase) + { + { "key", "value" }, + }; + + // Act + var value = dict["kEy"]; + + // Assert + Assert.Equal("value", value); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void IndexSet_EmptyStringIsAllowed() + { + // Arrange + var dict = new AdaptiveCapacityDictionary(); + + // Act + dict[""] = "foo"; + + // Assert + Assert.Equal("foo", dict[""]); + } + + [Fact] + public void IndexSet_EmptyStorage_UpgradesToList() + { + // Arrange + var dict = new AdaptiveCapacityDictionary(); + + // 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 AdaptiveCapacityDictionary() + { + { "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 AdaptiveCapacityDictionary() + { + { "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 AdaptiveCapacityDictionary() + { + { "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 AdaptiveCapacityDictionary(); + + // Act + var count = dict.Count; + + // Assert + Assert.Equal(0, count); + } + + [Fact] + public void Count_ListStorage() + { + // Arrange + var dict = new AdaptiveCapacityDictionary() + { + { "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 AdaptiveCapacityDictionary(); + + // Act + var keys = dict.Keys; + + // Assert + Assert.Empty(keys); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Keys_ListStorage() + { + // Arrange + var dict = new AdaptiveCapacityDictionary() + { + { "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 AdaptiveCapacityDictionary(); + + // Act + var values = dict.Values; + + // Assert + Assert.Empty(values); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Values_ListStorage() + { + // Arrange + var dict = new AdaptiveCapacityDictionary() + { + { "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 AdaptiveCapacityDictionary(); + + // 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_EmptyStringIsAllowed() + { + // Arrange + var dict = new AdaptiveCapacityDictionary(); + + // Act + dict.Add("", "foo"); + + // Assert + Assert.Equal("foo", dict[""]); + } + + [Fact] + public void Add_ListStorage() + { + // Arrange + var dict = new AdaptiveCapacityDictionary() + { + { "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 AdaptiveCapacityDictionary() + { + { "key", "value" }, + }; + + var message = $"An element with the key 'key' already exists in the {nameof(AdaptiveCapacityDictionary)}"; + + // 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 AdaptiveCapacityDictionary(StringComparer.OrdinalIgnoreCase) + { + { "key", "value" }, + }; + + var message = $"An element with the key 'kEy' already exists in the {nameof(AdaptiveCapacityDictionary)}"; + + // 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 AdaptiveCapacityDictionary() + { + { "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 AdaptiveCapacityDictionary(); + + // Act + dict.Clear(); + + // Assert + Assert.Empty(dict); + } + + [Fact] + public void Clear_ListStorage() + { + // Arrange + var dict = new AdaptiveCapacityDictionary() + { + { "key", "value" }, + }; + + // Act + dict.Clear(); + + // Assert + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + Assert.Null(dict._dictionaryStorage); + } + + [Fact] + public void Contains_ListStorage_KeyValuePair_True() + { + // Arrange + var dict = new AdaptiveCapacityDictionary() + { + { "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_ListStory_KeyValuePair_True_CaseInsensitive() + { + // Arrange + var dict = new AdaptiveCapacityDictionary(StringComparer.OrdinalIgnoreCase) + { + { "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_ListStorage_KeyValuePair_False() + { + // Arrange + var dict = new AdaptiveCapacityDictionary() + { + { "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_ListStorage_KeyValuePair_False_ValueComparisonIsDefault() + { + // Arrange + var dict = new AdaptiveCapacityDictionary() + { + { "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 AdaptiveCapacityDictionary(); + + // Act + var result = dict.ContainsKey("key"); + + // Assert + Assert.False(result); + } + + [Fact] + public void ContainsKey_EmptyStringIsAllowed() + { + // Arrange + var dict = new AdaptiveCapacityDictionary(); + + // Act + var result = dict.ContainsKey(""); + + // Assert + Assert.False(result); + } + + [Fact] + public void ContainsKey_ListStorage_False() + { + // Arrange + var dict = new AdaptiveCapacityDictionary() + { + { "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 AdaptiveCapacityDictionary() + { + { "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 AdaptiveCapacityDictionary(StringComparer.OrdinalIgnoreCase) + { + { "key", "value" }, + }; + + // Act + var result = dict.ContainsKey("kEy"); + + // Assert + Assert.True(result); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void CopyTo() + { + // Arrange + var dict = new AdaptiveCapacityDictionary() + { + { "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 AdaptiveCapacityDictionary() + { + { "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 AdaptiveCapacityDictionary(StringComparer.OrdinalIgnoreCase) + { + { "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 AdaptiveCapacityDictionary() + { + { "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 AdaptiveCapacityDictionary() + { + { "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 AdaptiveCapacityDictionary(); + + // Act + var result = dict.Remove("key"); + + // Assert + Assert.False(result); + } + + [Fact] + public void Remove_EmptyStringIsAllowed() + { + // Arrange + var dict = new AdaptiveCapacityDictionary(); + + // Act + var result = dict.Remove(""); + + // Assert + Assert.False(result); + } + + [Fact] + public void Remove_ListStorage_False() + { + // Arrange + var dict = new AdaptiveCapacityDictionary() + { + { "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 AdaptiveCapacityDictionary() + { + { "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 AdaptiveCapacityDictionary(StringComparer.OrdinalIgnoreCase) + { + { "key", "value" }, + }; + + // Act + var result = dict.Remove("kEy"); + + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + + [Fact] + public void Remove_KeyAndOutValue_EmptyStorage() + { + // Arrange + var dict = new AdaptiveCapacityDictionary(); + + // Act + var result = dict.Remove("key", out var removedValue); + + // Assert + Assert.False(result); + Assert.Null(removedValue); + } + + [Fact] + public void Remove_KeyAndOutValue_EmptyStringIsAllowed() + { + // Arrange + var dict = new AdaptiveCapacityDictionary(); + + // Act + var result = dict.Remove("", out var removedValue); + + // Assert + Assert.False(result); + Assert.Null(removedValue); + } + + [Fact] + public void Remove_KeyAndOutValue_ListStorage_False() + { + // Arrange + var dict = new AdaptiveCapacityDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.Remove("other", out var removedValue); + + // Assert + Assert.False(result); + Assert.Null(removedValue); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyAndOutValue_ListStorage_True() + { + // Arrange + object value = "value"; + var dict = new AdaptiveCapacityDictionary() + { + { "key", value } + }; + + // Act + var result = dict.Remove("key", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyAndOutValue_ListStorage_True_CaseInsensitive() + { + // Arrange + object value = "value"; + var dict = new AdaptiveCapacityDictionary(StringComparer.OrdinalIgnoreCase) + { + { "key", value } + }; + + // Act + var result = dict.Remove("kEy", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyAndOutValue_ListStorage_KeyExists_First() + { + // Arrange + object value = "value"; + var dict = new AdaptiveCapacityDictionary() + { + { "key", value }, + { "other", 5 }, + { "dotnet", "rocks" } + }; + + // Act + var result = dict.Remove("key", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Equal(2, dict.Count); + Assert.False(dict.ContainsKey("key")); + Assert.True(dict.ContainsKey("other")); + Assert.True(dict.ContainsKey("dotnet")); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyAndOutValue_ListStorage_KeyExists_Middle() + { + // Arrange + object value = "value"; + var dict = new AdaptiveCapacityDictionary() + { + { "other", 5 }, + { "key", value }, + { "dotnet", "rocks" } + }; + + // Act + var result = dict.Remove("key", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Equal(2, dict.Count); + Assert.False(dict.ContainsKey("key")); + Assert.True(dict.ContainsKey("other")); + Assert.True(dict.ContainsKey("dotnet")); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void Remove_KeyAndOutValue_ListStorage_KeyExists_Last() + { + // Arrange + object value = "value"; + var dict = new AdaptiveCapacityDictionary() + { + { "other", 5 }, + { "dotnet", "rocks" }, + { "key", value } + }; + + // Act + var result = dict.Remove("key", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Equal(2, dict.Count); + Assert.False(dict.ContainsKey("key")); + Assert.True(dict.ContainsKey("other")); + Assert.True(dict.ContainsKey("dotnet")); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void TryAdd_EmptyStringIsAllowed() + { + // Arrange + var dict = new AdaptiveCapacityDictionary(); + + // Act + var result = dict.TryAdd("", "foo"); + + // Assert + Assert.True(result); + } + + [Fact] + public void TryAdd_EmptyStorage_CanAdd() + { + // Arrange + var dict = new AdaptiveCapacityDictionary(); + + // Act + var result = dict.TryAdd("key", "value"); + + // Assert + Assert.True(result); + Assert.Collection( + dict._arrayStorage, + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp)); + } + + [Fact] + public void TryAdd_ArrayStorage_CanAdd() + { + // Arrange + var dict = new AdaptiveCapacityDictionary() + { + { "key0", "value0" }, + }; + + // Act + var result = dict.TryAdd("key1", "value1"); + + // Assert + Assert.True(result); + Assert.Collection( + dict._arrayStorage, + kvp => Assert.Equal(new KeyValuePair("key0", "value0"), kvp), + kvp => Assert.Equal(new KeyValuePair("key1", "value1"), kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp)); + } + + [Fact] + public void TryAdd_ArrayStorage_DoesNotAddWhenKeyIsPresent() + { + // Arrange + var dict = new AdaptiveCapacityDictionary() + { + { "key0", "value0" }, + }; + + // Act + var result = dict.TryAdd("key0", "value1"); + + // Assert + Assert.False(result); + Assert.Collection( + dict._arrayStorage, + kvp => Assert.Equal(new KeyValuePair("key0", "value0"), kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp)); + } + + [Fact] + public void TryGetValue_EmptyStorage() + { + // Arrange + var dict = new AdaptiveCapacityDictionary(); + + // Act + var result = dict.TryGetValue("key", out var value); + + // Assert + Assert.False(result); + Assert.Null(value); + } + + [Fact] + public void TryGetValue_EmptyStringIsAllowed() + { + // Arrange + var dict = new AdaptiveCapacityDictionary(); + + // Act + var result = dict.TryGetValue("", out var value); + + // Assert + Assert.False(result); + Assert.Null(value); + } + + [Fact] + public void TryGetValue_ListStorage_False() + { + // Arrange + var dict = new AdaptiveCapacityDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.TryGetValue("other", out var value); + + // Assert + Assert.False(result); + Assert.Null(value); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void TryGetValue_ListStorage_True() + { + // Arrange + var dict = new AdaptiveCapacityDictionary() + { + { "key", "value" }, + }; + + // Act + var result = dict.TryGetValue("key", out var value); + + // Assert + Assert.True(result); + Assert.Equal("value", value); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void TryGetValue_ListStorage_True_CaseInsensitive() + { + // Arrange + var dict = new AdaptiveCapacityDictionary(StringComparer.OrdinalIgnoreCase) + { + { "key", "value" }, + }; + + // Act + var result = dict.TryGetValue("kEy", out var value); + + // Assert + Assert.True(result); + Assert.Equal("value", value); + Assert.IsType[]>(dict._arrayStorage); + } + + [Fact] + public void ListStorage_SwitchesToDictionaryAfter10_Add() + { + // Arrange + var dict = new AdaptiveCapacityDictionary(); + + // Act 1 + dict.Add("key", "value"); + + // Assert 1 + var storage = Assert.IsType[]>(dict._arrayStorage); + Assert.Equal(10, storage.Length); + + // Act 2 + dict.Add("key2", "value2"); + dict.Add("key3", "value3"); + dict.Add("key4", "value4"); + dict.Add("key5", "value5"); + dict.Add("key6", "value2"); + dict.Add("key7", "value3"); + dict.Add("key8", "value4"); + dict.Add("key9", "value5"); + dict.Add("key10", "value2"); + dict.Add("key11", "value3"); + + // Assert 2 + Assert.Null(dict._arrayStorage); + Assert.Equal(11, dict.Count); + } + + [Fact] + public void ListStorage_SwitchesToDictionaryAfter10_TryAdd() + { + // Arrange + var dict = new AdaptiveCapacityDictionary(); + + // Act 1 + dict.TryAdd("key", "value"); + + // Assert 1 + var storage = Assert.IsType[]>(dict._arrayStorage); + Assert.Equal(10, storage.Length); + + // Act 2 + dict.TryAdd("key2", "value2"); + dict.TryAdd("key3", "value3"); + dict.TryAdd("key4", "value4"); + dict.TryAdd("key5", "value5"); + dict.TryAdd("key6", "value2"); + dict.TryAdd("key7", "value3"); + dict.TryAdd("key8", "value4"); + dict.TryAdd("key9", "value5"); + dict.TryAdd("key10", "value2"); + dict.TryAdd("key11", "value3"); + + // Assert 2 + Assert.Null(dict._arrayStorage); + Assert.Equal(11, dict.Count); + } + + [Fact] + public void ListStorage_SwitchesToDictionaryAfter10_Index() + { + // Arrange + var dict = new AdaptiveCapacityDictionary(); + + // Act 1 + dict["key"] = "value"; + + // Assert 1 + var storage = Assert.IsType[]>(dict._arrayStorage); + Assert.Equal(10, storage.Length); + + // Act 2 + dict["key1"] = "value"; + dict["key2"] = "value"; + dict["key3"] = "value"; + dict["key4"] = "value"; + dict["key5"] = "value"; + dict["key6"] = "value"; + dict["key7"] = "value"; + dict["key8"] = "value"; + dict["key9"] = "value"; + dict["key10"] = "value"; + + // Assert 2 + Assert.Null(dict._arrayStorage); + Assert.Equal(11, dict.Count); + } + + [Fact] + public void ListStorage_RemoveAt_RearrangesInnerArray() + { + // Arrange + var dict = new AdaptiveCapacityDictionary(); + 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); + } + + private void AssertEmptyArrayStorage(AdaptiveCapacityDictionary value) + { + Assert.Same(Array.Empty>(), value._arrayStorage); + } + + 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; } + } + } +} diff --git a/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj b/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj index b2ab2174f95e..c8e8888b6e2d 100644 --- a/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj +++ b/src/Shared/test/Shared.Tests/Microsoft.AspNetCore.Shared.Tests.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -27,6 +27,7 @@ +