|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Internal;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Http.Features;
/// <summary>
/// Default implementation for <see cref="IQueryFeature"/>.
/// </summary>
public class QueryFeature : IQueryFeature
{
// Lambda hoisted to static readonly field to improve inlining https://github.com/dotnet/roslyn/issues/13624
private static readonly Func<IFeatureCollection, IHttpRequestFeature?> _nullRequestFeature = f => null;
private FeatureReferences<IHttpRequestFeature> _features;
private string? _original;
private IQueryCollection? _parsedValues;
/// <summary>
/// Initializes a new instance of <see cref="QueryFeature"/>.
/// </summary>
/// <param name="query">The <see cref="IQueryCollection"/> to use as a backing store.</param>
public QueryFeature(IQueryCollection query)
{
ArgumentNullException.ThrowIfNull(query);
_parsedValues = query;
}
/// <summary>
/// Initializes a new instance of <see cref="QueryFeature"/>.
/// </summary>
/// <param name="features">The <see cref="IFeatureCollection"/> to initialize.</param>
public QueryFeature(IFeatureCollection features)
{
ArgumentNullException.ThrowIfNull(features);
_features.Initalize(features);
}
private IHttpRequestFeature HttpRequestFeature =>
_features.Fetch(ref _features.Cache, _nullRequestFeature)!;
/// <inheritdoc />
public IQueryCollection Query
{
get
{
if (_features.Collection is null)
{
return _parsedValues ?? QueryCollection.Empty;
}
var current = HttpRequestFeature.QueryString;
if (_parsedValues == null || !string.Equals(_original, current, StringComparison.Ordinal))
{
_original = current;
var result = ParseNullableQueryInternal(current);
_parsedValues = result is not null
? new QueryCollectionInternal(result)
: QueryCollection.Empty;
}
return _parsedValues;
}
set
{
_parsedValues = value;
if (_features.Collection != null)
{
if (value == null)
{
_original = string.Empty;
HttpRequestFeature.QueryString = string.Empty;
}
else
{
_original = QueryString.Create(_parsedValues).ToString();
HttpRequestFeature.QueryString = _original;
}
}
}
}
/// <summary>
/// Parse a query string into its component key and value parts.
/// </summary>
/// <param name="queryString">The raw query string value, with or without the leading '?'.</param>
/// <returns>A collection of parsed keys and values, null if there are no entries.</returns>
[SkipLocalsInit]
internal static AdaptiveCapacityDictionary<string, StringValues>? ParseNullableQueryInternal(string? queryString)
{
if (string.IsNullOrEmpty(queryString) || (queryString.Length == 1 && queryString[0] == '?'))
{
return null;
}
var accumulator = new KvpAccumulator();
var enumerable = new QueryStringEnumerable(queryString);
foreach (var pair in enumerable)
{
accumulator.Append(pair.DecodeName().Span, pair.DecodeValue().Span);
}
return accumulator.HasValues
? accumulator.GetResults()
: null;
}
internal struct KvpAccumulator
{
/// <summary>
/// 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.
/// </summary>
private AdaptiveCapacityDictionary<string, StringValues> _accumulator;
private AdaptiveCapacityDictionary<string, List<string>> _expandingAccumulator;
public void Append(ReadOnlySpan<char> key, ReadOnlySpan<char> value)
=> Append(key.ToString(), value.ToString());
/// <summary>
/// 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.
/// </summary>
public void Append(string key, string value)
{
if (_accumulator is null)
{
_accumulator = new AdaptiveCapacityDictionary<string, StringValues>(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<string, List<string>>(capacity: 5, StringComparer.OrdinalIgnoreCase);
}
// Already 2 (1 existing + the new one) entries so use List's expansion mechanism for more
var list = new List<string>();
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);
}
}
/// <summary>
/// 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.
/// </summary>
public bool HasValues => ValueCount > 0;
/// <summary>
/// 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.
/// </summary>
public int KeyCount => _accumulator?.Count ?? 0;
/// <summary>
/// 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.
/// </summary>
public int ValueCount { get; private set; }
/// <summary>
/// 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.
/// </summary>
public AdaptiveCapacityDictionary<string, StringValues> 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<string, StringValues>(0, StringComparer.OrdinalIgnoreCase);
}
}
}
|