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();
+ }
+ }
+ }
+ }
+}