diff --git a/src/Http/Http/perf/Microbenchmarks/QueryCollectionBenchmarks.cs b/src/Http/Http/perf/Microbenchmarks/QueryCollectionBenchmarks.cs
new file mode 100644
index 000000000000..cc165ee1e430
--- /dev/null
+++ b/src/Http/Http/perf/Microbenchmarks/QueryCollectionBenchmarks.cs
@@ -0,0 +1,97 @@
+// 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 BenchmarkDotNet.Configs;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.WebUtilities;
+using static Microsoft.AspNetCore.Http.Features.QueryFeature;
+
+namespace Microsoft.AspNetCore.Http
+{
+ [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
+ [CategoriesColumn]
+ public class QueryCollectionBenchmarks
+ {
+ private string _queryString;
+ private string _singleValue;
+ private string _singleValueWithPlus;
+ private string _encoded;
+
+ [IterationSetup]
+ public void Setup()
+ {
+ _queryString = "?key1=value1&key2=value2&key3=value3&key4=&key5=";
+ _singleValue = "?key1=value1";
+ _singleValueWithPlus = "?key1=value1+value2+value3";
+ _encoded = "?key1=value%231";
+ }
+
+ [Benchmark(Description = "ParseNew")]
+ [BenchmarkCategory("QueryString")]
+ public void ParseNew()
+ {
+ _ = QueryFeature.ParseNullableQueryInternal(_queryString);
+ }
+
+ [Benchmark(Description = "ParseNew")]
+ [BenchmarkCategory("Single")]
+ public void ParseNewSingle()
+ {
+ _ = QueryFeature.ParseNullableQueryInternal(_singleValue);
+ }
+
+ [Benchmark(Description = "ParseNew")]
+ [BenchmarkCategory("SingleWithPlus")]
+ public void ParseNewSingleWithPlus()
+ {
+ _ = QueryFeature.ParseNullableQueryInternal(_singleValueWithPlus);
+ }
+
+ [Benchmark(Description = "ParseNew")]
+ [BenchmarkCategory("Encoded")]
+ public void ParseNewEncoded()
+ {
+ _ = QueryFeature.ParseNullableQueryInternal(_encoded);
+ }
+
+ [Benchmark(Description = "QueryHelpersParse")]
+ [BenchmarkCategory("QueryString")]
+ public void QueryHelpersParse()
+ {
+ _ = QueryHelpers.ParseNullableQuery(_queryString);
+ }
+
+ [Benchmark(Description = "QueryHelpersParse")]
+ [BenchmarkCategory("Single")]
+ public void QueryHelpersParseSingle()
+ {
+ _ = QueryHelpers.ParseNullableQuery(_singleValue);
+ }
+
+ [Benchmark(Description = "QueryHelpersParse")]
+ [BenchmarkCategory("SingleWithPlus")]
+ public void QueryHelpersParseSingleWithPlus()
+ {
+ _ = QueryHelpers.ParseNullableQuery(_singleValueWithPlus);
+ }
+
+ [Benchmark(Description = "QueryHelpersParse")]
+ [BenchmarkCategory("Encoded")]
+ public void QueryHelpersParseEncoded()
+ {
+ _ = QueryHelpers.ParseNullableQuery(_encoded);
+ }
+
+ [Benchmark]
+ [BenchmarkCategory("Constructor")]
+ public void Constructor()
+ {
+ var dict = new KvpAccumulator();
+ if (dict.HasValues)
+ {
+ return;
+ }
+ }
+ }
+}
diff --git a/src/Http/Http/perf/Microbenchmarks/RequestCookieCollectionBenchmarks.cs b/src/Http/Http/perf/Microbenchmarks/RequestCookieCollectionBenchmarks.cs
index 035a367b1324..41aea5de4e60 100644
--- a/src/Http/Http/perf/Microbenchmarks/RequestCookieCollectionBenchmarks.cs
+++ b/src/Http/Http/perf/Microbenchmarks/RequestCookieCollectionBenchmarks.cs
@@ -19,7 +19,7 @@ public void Setup()
[Benchmark]
public void Parse_TypicalCookie()
{
- RequestCookieCollection.Parse(_cookie);
+ _ = RequestCookieCollection.Parse(_cookie);
}
}
}
diff --git a/src/Http/Http/perf/Microbenchmarks/RouteValueDictionaryBenchmark.cs b/src/Http/Http/perf/Microbenchmarks/RouteValueDictionaryBenchmark.cs
index 76a8659c2a48..f1bc86c68a75 100644
--- a/src/Http/Http/perf/Microbenchmarks/RouteValueDictionaryBenchmark.cs
+++ b/src/Http/Http/perf/Microbenchmarks/RouteValueDictionaryBenchmark.cs
@@ -33,9 +33,9 @@ public void Ctor_Values_RouteValueDictionary_EmptyArray()
}
[Benchmark]
- public void Ctor_Values_RouteValueDictionary_Array()
+ public RouteValueDictionary Ctor_Values_RouteValueDictionary_Array()
{
- new RouteValueDictionary(_arrayValues);
+ return new RouteValueDictionary(_arrayValues);
}
[Benchmark]
diff --git a/src/Http/Http/src/Features/QueryFeature.cs b/src/Http/Http/src/Features/QueryFeature.cs
index 289f577ee339..bc7b1c7d2cf2 100644
--- a/src/Http/Http/src/Features/QueryFeature.cs
+++ b/src/Http/Http/src/Features/QueryFeature.cs
@@ -2,7 +2,14 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
-using Microsoft.AspNetCore.WebUtilities;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.Intrinsics;
+using System.Runtime.Intrinsics.X86;
+using Microsoft.AspNetCore.Internal;
+using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Http.Features
{
@@ -55,13 +62,9 @@ public IQueryCollection Query
{
get
{
- if (_features.Collection == null)
+ if (_features.Collection is null)
{
- if (_parsedValues == null)
- {
- _parsedValues = QueryCollection.Empty;
- }
- return _parsedValues;
+ return _parsedValues ?? QueryCollection.Empty;
}
var current = HttpRequestFeature.QueryString;
@@ -69,16 +72,11 @@ public IQueryCollection Query
{
_original = current;
- var result = QueryHelpers.ParseNullableQuery(current);
+ var result = ParseNullableQueryInternal(current);
- if (result == null)
- {
- _parsedValues = QueryCollection.Empty;
- }
- else
- {
- _parsedValues = new QueryCollection(result);
- }
+ _parsedValues = result is not null
+ ? new QueryCollectionInternal(result)
+ : QueryCollection.Empty;
}
return _parsedValues;
}
@@ -100,5 +98,221 @@ 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.
+ [SkipLocalsInit]
+ internal static AdaptiveCapacityDictionary? ParseNullableQueryInternal(string? queryString)
+ {
+ if (string.IsNullOrEmpty(queryString) || (queryString.Length == 1 && queryString[0] == '?'))
+ {
+ return null;
+ }
+
+ var accumulator = new KvpAccumulator();
+ var query = queryString.AsSpan();
+
+ if (query[0] == '?')
+ {
+ query = query[1..];
+ }
+
+ while (!query.IsEmpty)
+ {
+ var delimiterIndex = query.IndexOf('&');
+
+ var querySegment = delimiterIndex >= 0
+ ? query.Slice(0, delimiterIndex)
+ : query;
+
+ var equalIndex = querySegment.IndexOf('=');
+
+ if (equalIndex >= 0)
+ {
+ var name = SpanHelper.ReplacePlusWithSpace(querySegment.Slice(0, equalIndex));
+ var value = SpanHelper.ReplacePlusWithSpace(querySegment.Slice(equalIndex + 1));
+
+ accumulator.Append(
+ Uri.UnescapeDataString(name),
+ Uri.UnescapeDataString(value));
+ }
+ else
+ {
+ if (!querySegment.IsEmpty)
+ {
+ accumulator.Append(querySegment);
+ }
+ }
+
+ if (delimiterIndex < 0)
+ {
+ break;
+ }
+
+ query = query.Slice(delimiterIndex + 1);
+ }
+
+ return accumulator.HasValues
+ ? accumulator.GetResults()
+ : null;
+ }
+
+ 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;
+
+ public void Append(ReadOnlySpan key, ReadOnlySpan value = default)
+ => Append(key.ToString(), value.IsEmpty ? string.Empty : value.ToString());
+
+ ///
+ /// 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 is null)
+ {
+ _accumulator = new AdaptiveCapacityDictionary(StringComparer.OrdinalIgnoreCase);
+ }
+
+ if (!_accumulator.TryGetValue(key, out var values))
+ {
+ // First value for this key
+ _accumulator[key] = new StringValues(value);
+ }
+ else
+ {
+ AppendToExpandingAccumulator(key, value, values);
+ }
+
+ ValueCount++;
+ }
+
+ private void AppendToExpandingAccumulator(string key, string value, StringValues values)
+ {
+ // When there are some values for the same key, so switch to expanding accumulator, and
+ // add a zero count marker in the accumulator to indicate that switch.
+
+ if (values.Count != 0)
+ {
+ _accumulator[key] = default;
+
+ if (_expandingAccumulator is null)
+ {
+ _expandingAccumulator = new AdaptiveCapacityDictionary>(capacity: 5, StringComparer.OrdinalIgnoreCase);
+ }
+
+ // Already 2 (1 existing + the new one) entries so use List's expansion mechanism for more
+ var list = new List();
+
+ list.AddRange(values);
+ list.Add(value);
+
+ _expandingAccumulator[key] = list;
+ }
+ else
+ {
+ // The marker indicates we are in the expanding accumulator, so just append to the list.
+ _expandingAccumulator[key].Add(value);
+ }
+ }
+
+ ///
+ /// 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);
+ }
+ }
+
+ private static class SpanHelper
+ {
+ private static readonly SpanAction s_replacePlusWithSpace = ReplacePlusWithSpaceCore;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe string ReplacePlusWithSpace(ReadOnlySpan span)
+ {
+ fixed (char* ptr = &MemoryMarshal.GetReference(span))
+ {
+ return string.Create(span.Length, (IntPtr)ptr, s_replacePlusWithSpace);
+ }
+ }
+
+ private static unsafe void ReplacePlusWithSpaceCore(Span buffer, IntPtr state)
+ {
+ fixed (char* ptr = &MemoryMarshal.GetReference(buffer))
+ {
+ var input = (ushort*)state.ToPointer();
+ var output = (ushort*)ptr;
+
+ var i = (nint)0;
+ var n = (nint)(uint)buffer.Length;
+
+ if (Sse41.IsSupported && n >= Vector128.Count)
+ {
+ var vecPlus = Vector128.Create((ushort)'+');
+ var vecSpace = Vector128.Create((ushort)' ');
+
+ do
+ {
+ var vec = Sse2.LoadVector128(input + i);
+ var mask = Sse2.CompareEqual(vec, vecPlus);
+ var res = Sse41.BlendVariable(vec, vecSpace, mask);
+ Sse2.Store(output + i, res);
+ i += Vector128.Count;
+ } while (i <= n - Vector128.Count);
+ }
+
+ for (; i < n; ++i)
+ {
+ if (input[i] != '+')
+ {
+ output[i] = input[i];
+ }
+ else
+ {
+ output[i] = ' ';
+ }
+ }
+ }
+ }
+ }
}
}
diff --git a/src/Http/Http/src/QueryCollectionInternal.cs b/src/Http/Http/src/QueryCollectionInternal.cs
new file mode 100644
index 000000000000..45b40fe5feb1
--- /dev/null
+++ b/src/Http/Http/src/QueryCollectionInternal.cs
@@ -0,0 +1,129 @@
+// 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.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
+ {
+ private AdaptiveCapacityDictionary Store { get; }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The backing store.
+ internal QueryCollectionInternal(AdaptiveCapacityDictionary store)
+ {
+ Store = store;
+ }
+
+ ///
+ /// 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] => TryGetValue(key, out var value) ? value : StringValues.Empty;
+
+ ///
+ /// Gets the number of elements contained in the ;.
+ ///
+ /// The number of elements contained in the .
+ public int Count => Store.Count;
+
+ ///
+ /// Gets the collection of query names in this instance.
+ ///
+ public ICollection Keys => 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) => 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) => 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() => 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()
+ => Store.GetEnumerator();
+
+ ///
+ /// Returns an enumerator that iterates through a collection.
+ ///
+ /// An object that can be used to iterate through the collection.
+ IEnumerator IEnumerable.GetEnumerator() => 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 => _notEmpty ? _dictionaryEnumerator.Current : default;
+
+ ///
+ public void Dispose()
+ {
+ }
+
+ object IEnumerator.Current => Current;
+
+ void IEnumerator.Reset()
+ {
+ if (_notEmpty)
+ {
+ ((IEnumerator)_dictionaryEnumerator).Reset();
+ }
+ }
+ }
+ }
+}
diff --git a/src/Http/Http/test/Features/QueryFeatureTests.cs b/src/Http/Http/test/Features/QueryFeatureTests.cs
index e43e3ce7a976..04a0a7c8c462 100644
--- a/src/Http/Http/test/Features/QueryFeatureTests.cs
+++ b/src/Http/Http/test/Features/QueryFeatureTests.cs
@@ -1,6 +1,7 @@
// 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.Linq;
using Xunit;
namespace Microsoft.AspNetCore.Http.Features
@@ -12,9 +13,7 @@ public void QueryReturnsParsedQueryCollection()
{
// Arrange
var features = new FeatureCollection();
- var request = new HttpRequestFeature();
- request.QueryString = "foo=bar";
- features[typeof(IHttpRequestFeature)] = request;
+ features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "foo=bar" };
var provider = new QueryFeature(features);
@@ -25,6 +24,23 @@ public void QueryReturnsParsedQueryCollection()
Assert.Equal("bar", queryCollection["foo"]);
}
+ [Theory]
+ [InlineData("?key1=value1&key2=value2")]
+ [InlineData("key1=value1&key2=value2")]
+ public void ParseQueryWithUniqueKeysWorks(string queryString)
+ {
+ var features = new FeatureCollection();
+ features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = queryString };
+
+ var provider = new QueryFeature(features);
+
+ var queryCollection = provider.Query;
+
+ Assert.Equal(2, queryCollection.Count);
+ Assert.Equal("value1", queryCollection["key1"].FirstOrDefault());
+ Assert.Equal("value2", queryCollection["key2"].FirstOrDefault());
+ }
+
[Theory]
[InlineData("?q", "q")]
[InlineData("?q&", "q")]
@@ -34,9 +50,7 @@ public void QueryReturnsParsedQueryCollection()
public void KeyWithoutValuesAddedToQueryCollection(string queryString, string emptyParam)
{
var features = new FeatureCollection();
- var request = new HttpRequestFeature();
- request.QueryString = queryString;
- features[typeof(IHttpRequestFeature)] = request;
+ features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = queryString };
var provider = new QueryFeature(features);
@@ -53,9 +67,7 @@ public void KeyWithoutValuesAddedToQueryCollection(string queryString, string em
public void EmptyKeysNotAddedToQueryCollection(string queryString)
{
var features = new FeatureCollection();
- var request = new HttpRequestFeature();
- request.QueryString = queryString;
- features[typeof(IHttpRequestFeature)] = request;
+ features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = queryString };
var provider = new QueryFeature(features);
@@ -63,5 +75,80 @@ public void EmptyKeysNotAddedToQueryCollection(string queryString)
Assert.Equal(0, queryCollection.Count);
}
+
+ [Fact]
+ public void ParseQueryWithEmptyKeyWorks()
+ {
+ var features = new FeatureCollection();
+ features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?=value1&=" };
+
+ var provider = new QueryFeature(features);
+
+ var queryCollection = provider.Query;
+
+ Assert.Single(queryCollection);
+ Assert.Equal(new[] { "value1", "" }, queryCollection[""]);
+ }
+
+ [Fact]
+ public void ParseQueryWithDuplicateKeysGroups()
+ {
+ var features = new FeatureCollection();
+ features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?key1=valueA&key2=valueB&key1=valueC" };
+
+ var provider = new QueryFeature(features);
+
+ var queryCollection = provider.Query;
+
+ Assert.Equal(2, queryCollection.Count);
+ Assert.Equal(new[] { "valueA", "valueC" }, queryCollection["key1"]);
+ Assert.Equal("valueB", queryCollection["key2"].FirstOrDefault());
+ }
+
+ [Fact]
+ public void ParseQueryWithThreefoldKeysGroups()
+ {
+ var features = new FeatureCollection();
+ features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?key1=valueA&key2=valueB&key1=valueC&key1=valueD" };
+
+ var provider = new QueryFeature(features);
+
+ var queryCollection = provider.Query;
+
+ Assert.Equal(2, queryCollection.Count);
+ Assert.Equal(new[] { "valueA", "valueC", "valueD" }, queryCollection["key1"]);
+ Assert.Equal("valueB", queryCollection["key2"].FirstOrDefault());
+ }
+
+ [Fact]
+ public void ParseQueryWithEmptyValuesWorks()
+ {
+ var features = new FeatureCollection();
+ features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?key1=&key2=" };
+
+ var provider = new QueryFeature(features);
+
+ var queryCollection = provider.Query;
+
+ Assert.Equal(2, queryCollection.Count);
+ Assert.Equal(string.Empty, queryCollection["key1"].FirstOrDefault());
+ Assert.Equal(string.Empty, queryCollection["key2"].FirstOrDefault());
+ }
+
+ [Theory]
+ [InlineData("?")]
+ [InlineData("")]
+ [InlineData(null)]
+ public void ParseEmptyOrNullQueryWorks(string queryString)
+ {
+ var features = new FeatureCollection();
+ features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = queryString };
+
+ var provider = new QueryFeature(features);
+
+ var queryCollection = provider.Query;
+
+ Assert.Empty(queryCollection);
+ }
}
}
diff --git a/src/Shared/Dictionary/AdaptiveCapacityDictionary.cs b/src/Shared/Dictionary/AdaptiveCapacityDictionary.cs
index d039c4ffbbbd..3d2c208cd120 100644
--- a/src/Shared/Dictionary/AdaptiveCapacityDictionary.cs
+++ b/src/Shared/Dictionary/AdaptiveCapacityDictionary.cs
@@ -22,7 +22,7 @@ internal class AdaptiveCapacityDictionary : IDictionary[]? _arrayStorage;
private int _count;
internal Dictionary? _dictionaryStorage;
- private IEqualityComparer _comparer;
+ private readonly IEqualityComparer _comparer;
///
/// Creates an empty .
@@ -621,7 +621,6 @@ private bool TryFindItem(TKey key, out TValue? value)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool ContainsKeyArray(TKey key) => TryFindItem(key, out _);
-
///
public struct Enumerator : IEnumerator>
{