diff --git a/src/Http/Http/perf/Microbenchmarks/QueryCollectionBenchmarks.cs b/src/Http/Http/perf/Microbenchmarks/QueryCollectionBenchmarks.cs new file mode 100644 index 000000000000..757f517ca91c --- /dev/null +++ b/src/Http/Http/perf/Microbenchmarks/QueryCollectionBenchmarks.cs @@ -0,0 +1,52 @@ +// 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 BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Primitives; +using static Microsoft.AspNetCore.Http.Features.QueryFeature; + +namespace Microsoft.AspNetCore.Http +{ + public class QueryCollectionBenchmarks + { + private string _queryString; + private string _singleValue; + + [IterationSetup] + public void Setup() + { + _queryString = "?key1=value1&key2=value2&key3=value3&key4=&key5="; + _singleValue = "?key1=value1"; + } + + [Benchmark] + public void ParseNew() + { + var dict = QueryFeature.ParseNullableQueryInternal(_queryString); + } + + [Benchmark] + public void ParseNewSingle() + { + var dict = QueryFeature.ParseNullableQueryInternal(_singleValue); + } + + [Benchmark] + public void Parse() + { + var dict = QueryHelpers.ParseNullableQuery(_queryString); + } + + [Benchmark] + public void Constructor() + { + var dict = new KvpAccumulator(); + if (dict.HasValues) + { + return; + } + } + } +} diff --git a/src/Http/Http/src/Features/QueryFeature.cs b/src/Http/Http/src/Features/QueryFeature.cs index 289f577ee339..42f240ba1ba0 100644 --- a/src/Http/Http/src/Features/QueryFeature.cs +++ b/src/Http/Http/src/Features/QueryFeature.cs @@ -2,7 +2,10 @@ // 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 Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.Http.Features { @@ -69,7 +72,7 @@ public IQueryCollection Query { _original = current; - var result = QueryHelpers.ParseNullableQuery(current); + var result = ParseNullableQueryInternal(current); if (result == null) { @@ -77,7 +80,7 @@ public IQueryCollection Query } else { - _parsedValues = new QueryCollection(result); + _parsedValues = new QueryCollectionInternal(result); } } return _parsedValues; @@ -100,5 +103,170 @@ public IQueryCollection Query } } } + + /// + /// Parse a query string into its component key and value parts. + /// + /// The raw query string value, with or without the leading '?'. + /// A collection of parsed keys and values, null if there are no entries. + internal static AdaptiveCapacityDictionary? ParseNullableQueryInternal(string? queryString) + { + var accumulator = new KvpAccumulator(); + + if (string.IsNullOrEmpty(queryString) || queryString == "?") + { + return null; + } + + int scanIndex = 0; + if (queryString[0] == '?') + { + scanIndex = 1; + } + + int textLength = queryString.Length; + int equalIndex = queryString.IndexOf('='); + if (equalIndex == -1) + { + equalIndex = textLength; + } + while (scanIndex < textLength) + { + int delimiterIndex = queryString.IndexOf('&', scanIndex); + if (delimiterIndex == -1) + { + delimiterIndex = textLength; + } + if (equalIndex < delimiterIndex) + { + while (scanIndex != equalIndex && char.IsWhiteSpace(queryString[scanIndex])) + { + ++scanIndex; + } + string name = queryString.Substring(scanIndex, equalIndex - scanIndex); + string value = queryString.Substring(equalIndex + 1, delimiterIndex - equalIndex - 1); + accumulator.Append( + Uri.UnescapeDataString(name.Replace('+', ' ')), + Uri.UnescapeDataString(value.Replace('+', ' '))); + equalIndex = queryString.IndexOf('=', delimiterIndex); + if (equalIndex == -1) + { + equalIndex = textLength; + } + } + else + { + if (delimiterIndex > scanIndex) + { + accumulator.Append(queryString.Substring(scanIndex, delimiterIndex - scanIndex), string.Empty); + } + } + scanIndex = delimiterIndex + 1; + } + + if (!accumulator.HasValues) + { + return null; + } + + return accumulator.GetResults(); + } + + internal struct KvpAccumulator + { + /// + /// This API supports infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + private AdaptiveCapacityDictionary _accumulator; + private AdaptiveCapacityDictionary> _expandingAccumulator; + + /// + /// This API supports infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public void Append(string key, string value) + { + if (_accumulator == null) + { + _accumulator = new AdaptiveCapacityDictionary(StringComparer.OrdinalIgnoreCase); + } + + StringValues values; + if (_accumulator.TryGetValue(key, out values)) + { + if (values.Count == 0) + { + // Marker entry for this key to indicate entry already in expanding list dictionary + _expandingAccumulator[key].Add(value); + } + else if (values.Count == 1) + { + _accumulator[key] = StringValues.Concat(values, value); + } + else + { + // Add zero count entry and move to data to expanding list dictionary + _accumulator[key] = default(StringValues); + + if (_expandingAccumulator == null) + { + _expandingAccumulator = new AdaptiveCapacityDictionary>(5, StringComparer.OrdinalIgnoreCase); + } + + // Already 5 entries so use starting allocated as 10; then use List's expansion mechanism for more + var list = new List(10); + + list.AddRange(values); + list.Add(value); + + _expandingAccumulator[key] = list; + } + } + else + { + // First value for this key + _accumulator[key] = new StringValues(value); + } + + ValueCount++; + } + + /// + /// This API supports infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public bool HasValues => ValueCount > 0; + + /// + /// This API supports infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public int KeyCount => _accumulator?.Count ?? 0; + + /// + /// This API supports infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public int ValueCount { get; private set; } + + /// + /// This API supports infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public AdaptiveCapacityDictionary GetResults() + { + if (_expandingAccumulator != null) + { + // Coalesce count 3+ multi-value entries into _accumulator dictionary + foreach (var entry in _expandingAccumulator) + { + _accumulator[entry.Key] = new StringValues(entry.Value.ToArray()); + } + } + + return _accumulator ?? new AdaptiveCapacityDictionary(0, StringComparer.OrdinalIgnoreCase); + } + } } } diff --git a/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj b/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj index 669a90b01f12..8f9773316efd 100644 --- a/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj +++ b/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Http/Http/src/QueryCollectionInternal.cs b/src/Http/Http/src/QueryCollectionInternal.cs new file mode 100644 index 000000000000..db23b2edbc57 --- /dev/null +++ b/src/Http/Http/src/QueryCollectionInternal.cs @@ -0,0 +1,255 @@ +// 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 Microsoft.AspNetCore.Internal; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http +{ + /// + /// The HttpRequest query string collection + /// + internal class QueryCollectionInternal : IQueryCollection + { + /// + /// Gets an empty . + /// + public static readonly QueryCollectionInternal Empty = new QueryCollectionInternal(); + private static readonly string[] EmptyKeys = Array.Empty(); + private static readonly StringValues[] EmptyValues = Array.Empty(); + private static readonly Enumerator EmptyEnumerator = new Enumerator(); + // Pre-box + private static readonly IEnumerator> EmptyIEnumeratorType = EmptyEnumerator; + private static readonly IEnumerator EmptyIEnumerator = EmptyEnumerator; + + private AdaptiveCapacityDictionary? Store { get; } + + /// + /// Initializes a new instance of . + /// + public QueryCollectionInternal() + { + } + + /// + /// Initializes a new instance of . + /// + /// The backing store. + internal QueryCollectionInternal(AdaptiveCapacityDictionary store) + { + Store = store; + } + + /// + /// Creates a shallow copy of the specified . + /// + /// The to clone. + public QueryCollectionInternal(QueryCollectionInternal store) + { + Store = store.Store; + } + + /// + /// Initializes a new instance of . + /// + /// The initial number of query items that this instance can contain. + public QueryCollectionInternal(int capacity) + { + Store = new AdaptiveCapacityDictionary(capacity, StringComparer.OrdinalIgnoreCase); + } + + /// + /// Gets the associated set of values from the collection. + /// + /// The key name. + /// the associated value from the collection as a StringValues or StringValues.Empty if the key is not present. + public StringValues this[string key] + { + get + { + if (Store == null) + { + return StringValues.Empty; + } + + if (TryGetValue(key, out var value)) + { + return value; + } + return StringValues.Empty; + } + } + + /// + /// Gets the number of elements contained in the ;. + /// + /// The number of elements contained in the . + public int Count + { + get + { + if (Store == null) + { + return 0; + } + return Store.Count; + } + } + + /// + /// Gets the collection of query names in this instance. + /// + public ICollection Keys + { + get + { + if (Store == null) + { + return EmptyKeys; + } + return Store.Keys; + } + } + + /// + /// Determines whether the contains a specific key. + /// + /// The key. + /// true if the contains a specific key; otherwise, false. + public bool ContainsKey(string key) + { + if (Store == null) + { + return false; + } + return Store.ContainsKey(key); + } + + /// + /// Retrieves a value from the collection. + /// + /// The key. + /// The value. + /// true if the contains the key; otherwise, false. + public bool TryGetValue(string key, out StringValues value) + { + if (Store == null) + { + value = default(StringValues); + return false; + } + return Store.TryGetValue(key, out value); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + public Enumerator GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyEnumerator; + } + return new Enumerator(Store.GetEnumerator()); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator> IEnumerable>.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumeratorType; + } + return Store.GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumerator; + } + return Store.GetEnumerator(); + } + + /// + /// Enumerates a . + /// + public struct Enumerator : IEnumerator> + { + // Do NOT make this readonly, or MoveNext will not work + private AdaptiveCapacityDictionary.Enumerator _dictionaryEnumerator; + private bool _notEmpty; + + internal Enumerator(AdaptiveCapacityDictionary.Enumerator dictionaryEnumerator) + { + _dictionaryEnumerator = dictionaryEnumerator; + _notEmpty = true; + } + + /// + /// Advances the enumerator to the next element of the . + /// + /// if the enumerator was successfully advanced to the next element; + /// if the enumerator has passed the end of the collection. + public bool MoveNext() + { + if (_notEmpty) + { + return _dictionaryEnumerator.MoveNext(); + } + return false; + } + + /// + /// Gets the element at the current position of the enumerator. + /// + public KeyValuePair Current + { + get + { + if (_notEmpty) + { + return _dictionaryEnumerator.Current; + } + return default(KeyValuePair); + } + } + + /// + public void Dispose() + { + } + + object IEnumerator.Current + { + get + { + return Current; + } + } + + void IEnumerator.Reset() + { + if (_notEmpty) + { + ((IEnumerator)_dictionaryEnumerator).Reset(); + } + } + } + } +}