diff --git a/src/Http/Http/src/Features/QueryFeature.cs b/src/Http/Http/src/Features/QueryFeature.cs index af1707aeeff7..91b77e63a682 100644 --- a/src/Http/Http/src/Features/QueryFeature.cs +++ b/src/Http/Http/src/Features/QueryFeature.cs @@ -110,10 +110,10 @@ public IQueryCollection Query } var accumulator = new KvpAccumulator(); - var enumerable = new QueryStringEnumerable(queryString.AsSpan()); + var enumerable = new QueryStringEnumerable(queryString); foreach (var pair in enumerable) { - accumulator.Append(pair.DecodeName(), pair.DecodeValue()); + accumulator.Append(pair.DecodeName().Span, pair.DecodeValue().Span); } return accumulator.HasValues diff --git a/src/Http/WebUtilities/src/PublicAPI.Unshipped.txt b/src/Http/WebUtilities/src/PublicAPI.Unshipped.txt index 85d85eece84f..36eedae54c67 100644 --- a/src/Http/WebUtilities/src/PublicAPI.Unshipped.txt +++ b/src/Http/WebUtilities/src/PublicAPI.Unshipped.txt @@ -4,15 +4,16 @@ Microsoft.AspNetCore.WebUtilities.FileBufferingReadStream.MemoryThreshold.get -> Microsoft.AspNetCore.WebUtilities.FileBufferingWriteStream.MemoryThreshold.get -> int Microsoft.AspNetCore.WebUtilities.QueryStringEnumerable Microsoft.AspNetCore.WebUtilities.QueryStringEnumerable.EncodedNameValuePair -Microsoft.AspNetCore.WebUtilities.QueryStringEnumerable.EncodedNameValuePair.DecodeName() -> System.ReadOnlySpan -Microsoft.AspNetCore.WebUtilities.QueryStringEnumerable.EncodedNameValuePair.DecodeValue() -> System.ReadOnlySpan -Microsoft.AspNetCore.WebUtilities.QueryStringEnumerable.EncodedNameValuePair.EncodedName.get -> System.ReadOnlySpan -Microsoft.AspNetCore.WebUtilities.QueryStringEnumerable.EncodedNameValuePair.EncodedValue.get -> System.ReadOnlySpan +Microsoft.AspNetCore.WebUtilities.QueryStringEnumerable.EncodedNameValuePair.DecodeName() -> System.ReadOnlyMemory +Microsoft.AspNetCore.WebUtilities.QueryStringEnumerable.EncodedNameValuePair.DecodeValue() -> System.ReadOnlyMemory +Microsoft.AspNetCore.WebUtilities.QueryStringEnumerable.EncodedNameValuePair.EncodedName.get -> System.ReadOnlyMemory +Microsoft.AspNetCore.WebUtilities.QueryStringEnumerable.EncodedNameValuePair.EncodedValue.get -> System.ReadOnlyMemory Microsoft.AspNetCore.WebUtilities.QueryStringEnumerable.Enumerator Microsoft.AspNetCore.WebUtilities.QueryStringEnumerable.Enumerator.Current.get -> Microsoft.AspNetCore.WebUtilities.QueryStringEnumerable.EncodedNameValuePair Microsoft.AspNetCore.WebUtilities.QueryStringEnumerable.Enumerator.MoveNext() -> bool Microsoft.AspNetCore.WebUtilities.QueryStringEnumerable.GetEnumerator() -> Microsoft.AspNetCore.WebUtilities.QueryStringEnumerable.Enumerator -Microsoft.AspNetCore.WebUtilities.QueryStringEnumerable.QueryStringEnumerable(System.ReadOnlySpan queryString) -> void +Microsoft.AspNetCore.WebUtilities.QueryStringEnumerable.QueryStringEnumerable(System.ReadOnlyMemory queryString) -> void +Microsoft.AspNetCore.WebUtilities.QueryStringEnumerable.QueryStringEnumerable(string? queryString) -> void override Microsoft.AspNetCore.WebUtilities.BufferedReadStream.ReadAsync(System.Memory buffer, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask override Microsoft.AspNetCore.WebUtilities.FileBufferingWriteStream.WriteAsync(System.ReadOnlyMemory buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask static Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseNullableQuery(string? queryString) -> System.Collections.Generic.Dictionary? diff --git a/src/Shared/QueryStringEnumerable.cs b/src/Shared/QueryStringEnumerable.cs index 0a239498de5e..51af48d347c1 100644 --- a/src/Shared/QueryStringEnumerable.cs +++ b/src/Shared/QueryStringEnumerable.cs @@ -22,15 +22,24 @@ namespace Microsoft.AspNetCore.Internal #else internal #endif - readonly ref struct QueryStringEnumerable + readonly struct QueryStringEnumerable { - private readonly ReadOnlySpan _queryString; + private readonly ReadOnlyMemory _queryString; /// /// Constructs an instance of . /// /// The query string. - public QueryStringEnumerable(ReadOnlySpan queryString) + public QueryStringEnumerable(string? queryString) + : this(queryString.AsMemory()) + { + } + + /// + /// Constructs an instance of . + /// + /// The query string. + public QueryStringEnumerable(ReadOnlyMemory queryString) { _queryString = queryString; } @@ -45,21 +54,21 @@ public Enumerator GetEnumerator() /// /// Represents a single name/value pair extracted from a query string during enumeration. /// - public readonly ref struct EncodedNameValuePair + public readonly struct EncodedNameValuePair { /// /// Gets the name from this name/value pair in its original encoded form. /// To get the decoded string, call . /// - public readonly ReadOnlySpan EncodedName { get; } + public readonly ReadOnlyMemory EncodedName { get; } /// /// Gets the value from this name/value pair in its original encoded form. /// To get the decoded string, call . /// - public readonly ReadOnlySpan EncodedValue { get; } + public readonly ReadOnlyMemory EncodedValue { get; } - internal EncodedNameValuePair(ReadOnlySpan encodedName, ReadOnlySpan encodedValue) + internal EncodedNameValuePair(ReadOnlyMemory encodedName, ReadOnlyMemory encodedValue) { EncodedName = encodedName; EncodedValue = encodedValue; @@ -69,37 +78,37 @@ internal EncodedNameValuePair(ReadOnlySpan encodedName, ReadOnlySpan /// Decodes the name from this name/value pair. /// /// Characters representing the decoded name. - public ReadOnlySpan DecodeName() + public ReadOnlyMemory DecodeName() => Decode(EncodedName); /// /// Decodes the value from this name/value pair. /// /// Characters representing the decoded value. - public ReadOnlySpan DecodeValue() + public ReadOnlyMemory DecodeValue() => Decode(EncodedValue); - private static ReadOnlySpan Decode(ReadOnlySpan chars) + private static ReadOnlyMemory Decode(ReadOnlyMemory chars) { // If the value is short, it's cheap to check up front if it really needs decoding. If it doesn't, // then we can save some allocations. - return chars.Length < 16 && chars.IndexOfAny('%', '+') < 0 + return chars.Length < 16 && chars.Span.IndexOfAny('%', '+') < 0 ? chars - : Uri.UnescapeDataString(SpanHelper.ReplacePlusWithSpace(chars)); + : Uri.UnescapeDataString(SpanHelper.ReplacePlusWithSpace(chars.Span)).AsMemory(); } } /// /// An enumerator that supplies the name/value pairs from a URI query string. /// - public ref struct Enumerator + public struct Enumerator { - private ReadOnlySpan _query; + private ReadOnlyMemory _query; - internal Enumerator(ReadOnlySpan query) + internal Enumerator(ReadOnlyMemory query) { Current = default; - _query = query.IsEmpty || query[0] != '?' + _query = query.IsEmpty || query.Span[0] != '?' ? query : query.Slice(1); } @@ -118,8 +127,8 @@ public bool MoveNext() while (!_query.IsEmpty) { // Chomp off the next segment - ReadOnlySpan segment; - var delimiterIndex = _query.IndexOf('&'); + ReadOnlyMemory segment; + var delimiterIndex = _query.Span.IndexOf('&'); if (delimiterIndex >= 0) { segment = _query.Slice(0, delimiterIndex); @@ -132,7 +141,7 @@ public bool MoveNext() } // If it's nonempty, emit it - var equalIndex = segment.IndexOf('='); + var equalIndex = segment.Span.IndexOf('='); if (equalIndex >= 0) { Current = new EncodedNameValuePair( diff --git a/src/Shared/test/Shared.Tests/QueryStringEnumerableTest.cs b/src/Shared/test/Shared.Tests/QueryStringEnumerableTest.cs index ad6ad0337c91..ab92109b5a97 100644 --- a/src/Shared/test/Shared.Tests/QueryStringEnumerableTest.cs +++ b/src/Shared/test/Shared.Tests/QueryStringEnumerableTest.cs @@ -97,14 +97,12 @@ public void DecodingWorks(string queryString, string expectedDecodedName, string } [Fact] - public void DecodingRetainsSpansIfDecodingNotNeeded() + public void DecodingReusesMemoryIfDecodingNotNeeded() { foreach (var kvp in new QueryStringEnumerable("?key=value")) { - Assert.True(MemoryExtensions.Overlaps(kvp.EncodedName, kvp.DecodeName(), out var nameOffset)); - Assert.True(MemoryExtensions.Overlaps(kvp.EncodedValue, kvp.DecodeValue(), out var valueOffset)); - Assert.Equal(0, nameOffset); - Assert.Equal(0, valueOffset); + Assert.True(kvp.EncodedName.Equals(kvp.DecodeName())); + Assert.True(kvp.EncodedValue.Equals(kvp.DecodeValue())); } }