This adds an HttpHeaders.NonValidated property, which returns a type that provides a non-validating / non-parsing / non-allocating view of headers in the collection. Querying the resulting collection does not force parsing or validation on the contents of the headers, handing back exactly the raw data that it contains; if a header doesn't contain a raw value but instead contains an already parsed value, a string representation of that header value(s) is returned. When using the strongly-typed members, querying and enumeration is allocation-free, unless strings need to be created to represent already parsed values.
// Changes to this file must follow the https://aka.ms/api-review process.
// ------------------------------------------------------------------------------
+using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace System.Net.Http
public override string ToString() { throw null; }
public static bool TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? input, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Net.Http.Headers.EntityTagHeaderValue? parsedValue) { throw null; }
}
+ public readonly partial struct HeaderStringValues : System.Collections.Generic.IEnumerable<string>, System.Collections.Generic.IReadOnlyCollection<string>, System.Collections.IEnumerable
+ {
+ private readonly object _dummy;
+ private readonly int _dummyPrimitive;
+ public int Count { get { throw null; } }
+ public System.Net.Http.Headers.HeaderStringValues.Enumerator GetEnumerator() { throw null; }
+ System.Collections.Generic.IEnumerator<string> System.Collections.Generic.IEnumerable<string>.GetEnumerator() { throw null; }
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }
+ public override string ToString() { throw null; }
+ public partial struct Enumerator : System.Collections.Generic.IEnumerator<string>, System.Collections.IEnumerator, System.IDisposable
+ {
+ private object _dummy;
+ private int _dummyPrimitive;
+ public string Current { get { throw null; } }
+ object System.Collections.IEnumerator.Current { get { throw null; } }
+ public void Dispose() { }
+ public bool MoveNext() { throw null; }
+ void System.Collections.IEnumerator.Reset() { }
+ }
+ }
public sealed partial class HttpContentHeaders : System.Net.Http.Headers.HttpHeaders
{
internal HttpContentHeaders() { }
public abstract partial class HttpHeaders : System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string, System.Collections.Generic.IEnumerable<string>>>, System.Collections.IEnumerable
{
protected HttpHeaders() { }
+ public System.Net.Http.Headers.HttpHeadersNonValidated NonValidated { get { throw null; } }
public void Add(string name, System.Collections.Generic.IEnumerable<string?> values) { }
public void Add(string name, string? value) { }
public void Clear() { }
public bool TryAddWithoutValidation(string name, string? value) { throw null; }
public bool TryGetValues(string name, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Collections.Generic.IEnumerable<string>? values) { throw null; }
}
+ public readonly partial struct HttpHeadersNonValidated : System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string, System.Net.Http.Headers.HeaderStringValues>>, System.Collections.Generic.IReadOnlyCollection<System.Collections.Generic.KeyValuePair<string, System.Net.Http.Headers.HeaderStringValues>>, System.Collections.Generic.IReadOnlyDictionary<string, System.Net.Http.Headers.HeaderStringValues>, System.Collections.IEnumerable
+ {
+ private readonly object _dummy;
+ private readonly int _dummyPrimitive;
+ public int Count { get { throw null; } }
+ public System.Net.Http.Headers.HeaderStringValues this[string headerName] { get { throw null; } }
+ System.Collections.Generic.IEnumerable<string> System.Collections.Generic.IReadOnlyDictionary<string, System.Net.Http.Headers.HeaderStringValues>.Keys { get { throw null; } }
+ System.Collections.Generic.IEnumerable<System.Net.Http.Headers.HeaderStringValues> System.Collections.Generic.IReadOnlyDictionary<string, System.Net.Http.Headers.HeaderStringValues>.Values { get { throw null; } }
+ public bool Contains(string headerName) { throw null; }
+ bool System.Collections.Generic.IReadOnlyDictionary<string, System.Net.Http.Headers.HeaderStringValues>.ContainsKey(string key) { throw null; }
+ public System.Net.Http.Headers.HttpHeadersNonValidated.Enumerator GetEnumerator() { throw null; }
+ System.Collections.Generic.IEnumerator<System.Collections.Generic.KeyValuePair<string, System.Net.Http.Headers.HeaderStringValues>> System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string, System.Net.Http.Headers.HeaderStringValues>>.GetEnumerator() { throw null; }
+ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; }
+ public bool TryGetValues(string headerName, out System.Net.Http.Headers.HeaderStringValues values) { throw null; }
+ bool System.Collections.Generic.IReadOnlyDictionary<string, System.Net.Http.Headers.HeaderStringValues>.TryGetValue(string key, out System.Net.Http.Headers.HeaderStringValues value) { throw null; }
+ public partial struct Enumerator : System.Collections.Generic.IEnumerator<System.Collections.Generic.KeyValuePair<string, System.Net.Http.Headers.HeaderStringValues>>, System.Collections.IEnumerator, System.IDisposable
+ {
+ private object _dummy;
+ private int _dummyPrimitive;
+ public System.Collections.Generic.KeyValuePair<string, System.Net.Http.Headers.HeaderStringValues> Current { get { throw null; } }
+ object System.Collections.IEnumerator.Current { get { throw null; } }
+ public void Dispose() { }
+ public bool MoveNext() { throw null; }
+ void System.Collections.IEnumerator.Reset() { }
+ }
+ }
public sealed partial class HttpHeaderValueCollection<T> : System.Collections.Generic.ICollection<T>, System.Collections.Generic.IEnumerable<T>, System.Collections.IEnumerable where T : class
{
internal HttpHeaderValueCollection() { }
<Compile Include="System\Net\Http\Headers\EntityTagHeaderValue.cs" />
<Compile Include="System\Net\Http\Headers\GenericHeaderParser.cs" />
<Compile Include="System\Net\Http\Headers\HeaderDescriptor.cs" />
+ <Compile Include="System\Net\Http\Headers\HeaderStringValues.cs" />
<Compile Include="System\Net\Http\Headers\HeaderUtilities.cs" />
<Compile Include="System\Net\Http\Headers\HttpContentHeaders.cs" />
<Compile Include="System\Net\Http\Headers\HttpGeneralHeaders.cs" />
<Compile Include="System\Net\Http\Headers\HttpHeaderParser.cs" />
<Compile Include="System\Net\Http\Headers\HttpHeaders.cs" />
+ <Compile Include="System\Net\Http\Headers\HttpHeadersNonValidated.cs" />
<Compile Include="System\Net\Http\Headers\HttpHeaderValueCollection.cs" />
<Compile Include="System\Net\Http\Headers\HttpRequestHeaders.cs" />
<Compile Include="System\Net\Http\Headers\HttpResponseHeaders.cs" />
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections;
+using System.Collections.Generic;
+
+namespace System.Net.Http.Headers
+{
+ /// <summary>Provides a collection of header string values.</summary>
+ public readonly struct HeaderStringValues : IReadOnlyCollection<string>
+ {
+ /// <summary>The associated header. This is used only for producing a string from <see cref="_value"/> when it's an array.</summary>
+ private readonly HeaderDescriptor _header;
+ /// <summary>A string or string array (or null if the instance is default).</summary>
+ private readonly object _value;
+
+ /// <summary>Initializes the instance.</summary>
+ /// <param name="descriptor">The header descriptor associated with the header value.</param>
+ /// <param name="value">The header value.</param>
+ internal HeaderStringValues(HeaderDescriptor descriptor, string value)
+ {
+ _header = descriptor;
+ _value = value;
+ }
+
+ /// <summary>Initializes the instance.</summary>
+ /// <param name="descriptor">The header descriptor associated with the header values.</param>
+ /// <param name="values">The header values.</param>
+ internal HeaderStringValues(HeaderDescriptor descriptor, string[] values)
+ {
+ _header = descriptor;
+ _value = values;
+ }
+
+ /// <summary>Gets the number of header values in the collection.</summary>
+ public int Count => _value switch
+ {
+ string => 1,
+ string[] values => values.Length,
+ _ => 0
+ };
+
+ /// <summary>Gets a string containing all the headers in the collection.</summary>
+ /// <returns></returns>
+ public override string ToString() => _value switch
+ {
+ string value => value,
+ string[] values => string.Join(_header.Parser is HttpHeaderParser parser && parser.SupportsMultipleValues ? parser.Separator : HttpHeaderParser.DefaultSeparator, values),
+ _ => string.Empty,
+ };
+
+ /// <summary>Gets an enumerator for all of the strings in the collection.</summary>
+ /// <returns></returns>
+ public Enumerator GetEnumerator() => new Enumerator(_value);
+
+ /// <inheritdoc/>
+ IEnumerator<string> IEnumerable<string>.GetEnumerator() => GetEnumerator();
+
+ /// <inheritdoc/>
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ /// <summary>Enumerates the elements of a <see cref="HeaderStringValues"/>.</summary>
+ public struct Enumerator : IEnumerator<string>
+ {
+ /// <summary>If this wraps a string[], that array. Otherwise, null.</summary>
+ private readonly string[]? _values;
+ /// <summary>The current string header value. If this wraps a single string, that string.</summary>
+ private string? _current;
+ /// <summary>Current state of the iteration.</summary>
+ private int _index;
+
+ /// <summary>Initializes the enumerator with a string or string[].</summary>
+ /// <param name="value">The string or string[] value, or null if this collection is empty.</param>
+ internal Enumerator(object value)
+ {
+ if (value is string s)
+ {
+ _values = null;
+ _current = s;
+ }
+ else
+ {
+ _values = value as string[];
+ _current = null;
+ }
+
+ _index = 0;
+ }
+
+ /// <inheritdoc/>
+ public bool MoveNext()
+ {
+ int index = _index;
+ if (index < 0)
+ {
+ return false;
+ }
+
+ string[]? values = _values;
+ if (values != null)
+ {
+ if ((uint)index < (uint)values.Length)
+ {
+ _index = index + 1;
+ _current = values[index];
+ return true;
+ }
+
+ _index = -1;
+ return false;
+ }
+
+ _index = -1;
+ return _current != null;
+ }
+
+ /// <inheritdoc/>
+ public string Current => _current!;
+
+ /// <inheritdoc/>
+ object IEnumerator.Current => Current;
+
+ /// <inheritdoc/>
+ public void Dispose() { }
+
+ /// <inheritdoc/>
+ void IEnumerator.Reset() => throw new NotSupportedException();
+ }
+ }
+}
{
if (headers[i] is HttpHeaders hh)
{
- foreach (KeyValuePair<string, string[]> header in hh.EnumerateWithoutValidation())
+ foreach (KeyValuePair<string, HeaderStringValues> header in hh.NonValidated)
{
foreach (string headerValue in header.Value)
{
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
using System.Text;
namespace System.Net.Http.Headers
internal Dictionary<HeaderDescriptor, object>? HeaderStore => _headerStore;
+ /// <summary>Gets a view of the contents of this headers collection that does not parse nor validate the data upon access.</summary>
+ public HttpHeadersNonValidated NonValidated => new HttpHeadersNonValidated(this);
+
public void Add(string name, string? value) => Add(GetHeaderDescriptor(name), value);
internal void Add(HeaderDescriptor descriptor, string? value)
{
if (_headerStore != null && TryGetAndParseHeaderInfo(descriptor, out HeaderStoreItemInfo? info))
{
- values = GetValuesAsStrings(descriptor, info);
+ values = GetStoreValuesAsStringArray(descriptor, info);
return true;
}
public override string ToString()
{
- if (_headerStore == null || _headerStore.Count == 0)
- {
- return string.Empty;
- }
-
// Return all headers as string similar to:
// HeaderName1: Value1, Value2
// HeaderName2: Value1
// ...
- var sb = new StringBuilder();
- foreach (KeyValuePair<string, string> header in GetHeaderStrings())
- {
- sb.Append(header.Key).Append(": ").AppendLine(header.Value);
- }
- return sb.ToString();
- }
+ var vsb = new ValueStringBuilder(stackalloc char[512]);
- internal IEnumerable<KeyValuePair<string, string>> GetHeaderStrings()
- {
- if (_headerStore == null)
+ if (_headerStore is Dictionary<HeaderDescriptor, object> headerStore)
{
- yield break;
- }
+ foreach (KeyValuePair<HeaderDescriptor, object> header in headerStore)
+ {
+ vsb.Append(header.Key.Name);
+ vsb.Append(": ");
- foreach (KeyValuePair<HeaderDescriptor, object> header in _headerStore)
- {
- string stringValue = GetHeaderString(header.Key, header.Value);
+ GetStoreValuesAsStringOrStringArray(header.Key, header.Value, out string? singleValue, out string[]? multiValue);
+ Debug.Assert(singleValue is not null ^ multiValue is not null);
+
+ if (singleValue is not null)
+ {
+ vsb.Append(singleValue);
+ }
+ else
+ {
+ // Note that if we get multiple values for a header that doesn't support multiple values, we'll
+ // just separate the values using a comma (default separator).
+ string? separator = header.Key.Parser is HttpHeaderParser parser && parser.SupportsMultipleValues ? parser.Separator : HttpHeaderParser.DefaultSeparator;
- yield return new KeyValuePair<string, string>(header.Key.Name, stringValue);
+ for (int i = 0; i < multiValue!.Length; i++)
+ {
+ if (i != 0) vsb.Append(separator);
+ vsb.Append(multiValue[i]);
+ }
+ }
+
+ vsb.Append(Environment.NewLine);
+ }
}
+
+ return vsb.ToString();
}
- internal string GetHeaderString(HeaderDescriptor descriptor, object? exclude = null)
+ internal string GetHeaderString(HeaderDescriptor descriptor)
{
if (TryGetHeaderValue(descriptor, out object? info))
{
- string[] values = GetValuesAsStrings(descriptor, info, exclude);
+ GetStoreValuesAsStringOrStringArray(descriptor, info, out string? singleValue, out string[]? multiValue);
+ Debug.Assert(singleValue is not null ^ multiValue is not null);
- if (values.Length == 1)
+ if (singleValue is not null)
{
- return values[0];
+ return singleValue;
}
// Note that if we get multiple values for a header that doesn't support multiple values, we'll
// just separate the values using a comma (default separator).
- string? separator = HttpHeaderParser.DefaultSeparator;
- if (descriptor.Parser != null && descriptor.Parser.SupportsMultipleValues)
- {
- separator = descriptor.Parser.Separator;
- }
-
- return string.Join(separator, values);
+ string? separator = descriptor.Parser != null && descriptor.Parser.SupportsMultipleValues ? descriptor.Parser.Separator : HttpHeaderParser.DefaultSeparator;
+ return string.Join(separator, multiValue!);
}
return string.Empty;
}
else
{
- string[] values = GetValuesAsStrings(descriptor, info);
+ string[] values = GetStoreValuesAsStringArray(descriptor, info);
yield return new KeyValuePair<string, IEnumerable<string>>(descriptor.Name, values);
}
}
}
- internal IEnumerable<KeyValuePair<string, string[]>> EnumerateWithoutValidation()
- {
- if (_headerStore == null)
- {
- yield break;
- }
-
- foreach (KeyValuePair<HeaderDescriptor, object> header in _headerStore)
- {
- string[] values = TryGetHeaderValue(header.Key, out object? info) ?
- GetValuesAsStrings(header.Key, info) :
- Array.Empty<string>();
-
- yield return new KeyValuePair<string, string[]>(header.Key.Name, values);
- }
- }
-
#endregion
#region IEnumerable Members
(_headerStore ??= new Dictionary<HeaderDescriptor, object>()).Add(descriptor, value);
}
- private bool TryGetHeaderValue(HeaderDescriptor descriptor, [NotNullWhen(true)] out object? value)
+ internal bool TryGetHeaderValue(HeaderDescriptor descriptor, [NotNullWhen(true)] out object? value)
{
if (_headerStore == null)
{
return false;
}
- private static string[] GetValuesAsStrings(HeaderDescriptor descriptor, object value, object? exclude = null)
+ internal static string[] GetStoreValuesAsStringArray(HeaderDescriptor descriptor, HeaderStoreItemInfo info)
{
- HeaderStoreItemInfo? info = value as HeaderStoreItemInfo;
+ GetStoreValuesAsStringOrStringArray(descriptor, info, out string? singleValue, out string[]? multiValue);
+ Debug.Assert(singleValue is not null ^ multiValue is not null);
+ return multiValue ?? new[] { singleValue! };
+ }
+
+ internal static void GetStoreValuesAsStringOrStringArray(HeaderDescriptor descriptor, object sourceValues, out string? singleValue, out string[]? multiValue)
+ {
+ HeaderStoreItemInfo? info = sourceValues as HeaderStoreItemInfo;
if (info is null)
{
- Debug.Assert(value is string);
- return new string[1] { (string)value };
+ Debug.Assert(sourceValues is string);
+ singleValue = (string)sourceValues;
+ multiValue = null;
+ return;
}
int length = GetValueCount(info);
- string[] values;
- if (length > 0)
+ Span<string?> values;
+ singleValue = null;
+ if (length == 1)
{
- values = new string[length];
- int currentIndex = 0;
-
- ReadStoreValues<string?>(values, info.RawValue, null, null, ref currentIndex);
- ReadStoreValues<object?>(values, info.ParsedValue, descriptor.Parser, exclude, ref currentIndex);
-
- // Set parser parameter to 'null' for invalid values: The invalid values is always a string so we
- // don't need the parser to "serialize" the value to a string.
- ReadStoreValues<string?>(values, info.InvalidValue, null, null, ref currentIndex);
-
- // The values array may not be full because some values were excluded
- if (currentIndex < length)
- {
- values = values.AsSpan(0, currentIndex).ToArray();
- }
+ multiValue = null;
+ values = MemoryMarshal.CreateSpan(ref singleValue, 1);
}
else
{
- values = Array.Empty<string>();
+ values = multiValue = length != 0 ? new string[length] : Array.Empty<string>();
}
- Debug.Assert(values != null);
- return values;
+ int currentIndex = 0;
+ ReadStoreValues<string?>(values, info.RawValue, null, ref currentIndex);
+ ReadStoreValues<object?>(values, info.ParsedValue, descriptor.Parser, ref currentIndex);
+ ReadStoreValues<string?>(values, info.InvalidValue, null, ref currentIndex);
+ Debug.Assert(currentIndex == length);
}
- internal static int GetValuesAsStrings(HeaderDescriptor descriptor, object sourceValues, [NotNull] ref string[]? values)
+ internal static int GetStoreValuesIntoStringArray(HeaderDescriptor descriptor, object sourceValues, [NotNull] ref string[]? values)
{
values ??= Array.Empty<string>();
}
int currentIndex = 0;
- ReadStoreValues<string?>(values, info.RawValue, null, null, ref currentIndex);
- ReadStoreValues<object?>(values, info.ParsedValue, descriptor.Parser, null, ref currentIndex);
- ReadStoreValues<string?>(values, info.InvalidValue, null, null, ref currentIndex);
+ ReadStoreValues<string?>(values, info.RawValue, null, ref currentIndex);
+ ReadStoreValues<object?>(values, info.ParsedValue, descriptor.Parser, ref currentIndex);
+ ReadStoreValues<string?>(values, info.InvalidValue, null, ref currentIndex);
Debug.Assert(currentIndex == length);
}
1;
}
- private static void ReadStoreValues<T>(string?[] values, object? storeValue, HttpHeaderParser? parser,
- T exclude, ref int currentIndex)
+ private static void ReadStoreValues<T>(Span<string?> values, object? storeValue, HttpHeaderParser? parser, ref int currentIndex)
{
- Debug.Assert(values != null);
-
if (storeValue != null)
{
List<T>? storeValues = storeValue as List<T>;
if (storeValues == null)
{
- if (ShouldAdd<T>(storeValue, parser, exclude))
- {
- values[currentIndex] = parser == null ? storeValue.ToString() : parser.ToString(storeValue);
- currentIndex++;
- }
+ values[currentIndex] = parser == null ? storeValue.ToString() : parser.ToString(storeValue);
+ currentIndex++;
}
else
{
foreach (object? item in storeValues)
{
- if (ShouldAdd<T>(item, parser, exclude))
- {
- Debug.Assert(item != null);
- values[currentIndex] = parser == null ? item.ToString() : parser.ToString(item);
- currentIndex++;
- }
+ Debug.Assert(item != null);
+ values[currentIndex] = parser == null ? item.ToString() : parser.ToString(item);
+ currentIndex++;
}
}
}
}
- private static bool ShouldAdd<T>(object? storeValue, HttpHeaderParser? parser, T exclude)
- {
- bool add = true;
- if (parser != null && exclude != null)
- {
- if (parser.Comparer != null)
- {
- add = !parser.Comparer.Equals(exclude, storeValue);
- }
- else
- {
- add = !exclude.Equals(storeValue);
- }
- }
- return add;
- }
-
private bool AreEqual(object value, object? storeValue, IEqualityComparer? comparer)
{
Debug.Assert(value != null);
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace System.Net.Http.Headers
+{
+ /// <summary>Provides a view on top of a <see cref="HttpHeaders"/> collection that avoids forcing validation or parsing on its contents.</summary>
+ /// <remarks>
+ /// The view surfaces data as it's stored in the headers collection. Any header values that have not yet been parsed / validated won't be
+ /// as part of any accesses from this view, e.g. a raw header value of "one, two" that hasn't yet been parsed due to other operations
+ /// on the <see cref="HttpHeaders"/> will be surfaced as a single header value rather than two. For any header values that have already
+ /// been parsed and validated, that value will be converted to a string to be returned from operations on this view.
+ /// </remarks>
+ public readonly struct HttpHeadersNonValidated : IReadOnlyDictionary<string, HeaderStringValues>
+ {
+ /// <summary>The wrapped headers collection.</summary>
+ private readonly HttpHeaders? _headers;
+
+ /// <summary>Initializes the view.</summary>
+ /// <param name="headers">The wrapped headers collection.</param>
+ internal HttpHeadersNonValidated(HttpHeaders headers) => _headers = headers;
+
+ /// <summary>Gets the number of headers stored in the collection.</summary>
+ /// <remarks>Multiple header values associated with the same header name are considered to be one header as far as this count is concerned.</remarks>
+ public int Count => _headers?.HeaderStore?.Count ?? 0;
+
+ /// <summary>Gets whether the collection contains the specified header.</summary>
+ /// <param name="headerName">The name of the header.</param>
+ /// <returns>true if the collection contains the header; otherwise, false.</returns>
+ public bool Contains(string headerName) =>
+ _headers is HttpHeaders headers &&
+ HeaderDescriptor.TryGet(headerName, out HeaderDescriptor descriptor) &&
+ headers.TryGetHeaderValue(descriptor, out _);
+
+ /// <summary>Gets the values for the specified header name.</summary>
+ /// <param name="headerName">The name of the header.</param>
+ /// <returns>The values for the specified header.</returns>
+ /// <exception cref="KeyNotFoundException">The header was not contained in the collection.</exception>
+ public HeaderStringValues this[string headerName]
+ {
+ get
+ {
+ if (TryGetValues(headerName, out HeaderStringValues values))
+ {
+ return values;
+ }
+
+ throw new KeyNotFoundException(SR.net_http_headers_not_found);
+ }
+ }
+
+ /// <inheritdoc/>
+ bool IReadOnlyDictionary<string, HeaderStringValues>.ContainsKey(string key) => Contains(key);
+
+ /// <summary>Attempts to retrieve the values associated with the specified header name.</summary>
+ /// <param name="headerName">The name of the header.</param>
+ /// <param name="values">The retrieved header values.</param>
+ /// <returns>true if the collection contains the specified header; otherwise, false.</returns>
+ public bool TryGetValues(string headerName, out HeaderStringValues values)
+ {
+ if (_headers is HttpHeaders headers &&
+ HeaderDescriptor.TryGet(headerName, out HeaderDescriptor descriptor) &&
+ headers.TryGetHeaderValue(descriptor, out object? info))
+ {
+ HttpHeaders.GetStoreValuesAsStringOrStringArray(descriptor, info, out string? singleValue, out string[]? multiValue);
+ Debug.Assert(singleValue is not null ^ multiValue is not null);
+ values = singleValue is not null ?
+ new HeaderStringValues(descriptor, singleValue) :
+ new HeaderStringValues(descriptor, multiValue!);
+ return true;
+ }
+
+ values = default;
+ return false;
+ }
+
+ /// <inheritdoc/>
+ bool IReadOnlyDictionary<string, HeaderStringValues>.TryGetValue(string key, out HeaderStringValues value) => TryGetValues(key, out value);
+
+ /// <summary>Gets an enumerator that iterates through the <see cref="HttpHeadersNonValidated"/>.</summary>
+ /// <returns>An enumerator that iterates through the <see cref="HttpHeadersNonValidated"/>.</returns>
+ public Enumerator GetEnumerator() =>
+ _headers is HttpHeaders headers && headers.HeaderStore is Dictionary<HeaderDescriptor, object> store ?
+ new Enumerator(store.GetEnumerator()) :
+ default;
+
+ /// <inheritdoc/>
+ IEnumerator<KeyValuePair<string, HeaderStringValues>> IEnumerable<KeyValuePair<string, HeaderStringValues>>.GetEnumerator() => GetEnumerator();
+
+ /// <inheritdoc/>
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ /// <inheritdoc/>
+ IEnumerable<string> IReadOnlyDictionary<string, HeaderStringValues>.Keys
+ {
+ get
+ {
+ foreach (KeyValuePair<string, HeaderStringValues> header in this)
+ {
+ yield return header.Key;
+ }
+ }
+ }
+
+ /// <inheritdoc/>
+ IEnumerable<HeaderStringValues> IReadOnlyDictionary<string, HeaderStringValues>.Values
+ {
+ get
+ {
+ foreach (KeyValuePair<string, HeaderStringValues> header in this)
+ {
+ yield return header.Value;
+ }
+ }
+ }
+
+ /// <summary>Enumerates the elements of a <see cref="HttpHeadersNonValidated"/>.</summary>
+ public struct Enumerator : IEnumerator<KeyValuePair<string, HeaderStringValues>>
+ {
+ /// <summary>The wrapped enumerator for the underlying headers dictionary.</summary>
+ private Dictionary<HeaderDescriptor, object>.Enumerator _headerStoreEnumerator;
+ /// <summary>The current value.</summary>
+ private KeyValuePair<string, HeaderStringValues> _current;
+ /// <summary>true if the enumerator was constructed via the ctor; otherwise, false./</summary>
+ private bool _valid;
+
+ /// <summary>Initializes the enumerator.</summary>
+ /// <param name="headerStoreEnumerator">The underlying dictionary enumerator.</param>
+ internal Enumerator(Dictionary<HeaderDescriptor, object>.Enumerator headerStoreEnumerator)
+ {
+ _headerStoreEnumerator = headerStoreEnumerator;
+ _current = default;
+ _valid = true;
+ }
+
+ /// <inheritdoc/>
+ public bool MoveNext()
+ {
+ if (_valid && _headerStoreEnumerator.MoveNext())
+ {
+ KeyValuePair<HeaderDescriptor, object> current = _headerStoreEnumerator.Current;
+
+ HttpHeaders.GetStoreValuesAsStringOrStringArray(current.Key, current.Value, out string? singleValue, out string[]? multiValue);
+ Debug.Assert(singleValue is not null ^ multiValue is not null);
+
+ _current = new KeyValuePair<string, HeaderStringValues>(
+ current.Key.Name,
+ singleValue is not null ? new HeaderStringValues(current.Key, singleValue) : new HeaderStringValues(current.Key, multiValue!));
+ return true;
+ }
+
+ _current = default;
+ return false;
+ }
+
+ /// <inheritdoc/>
+ public KeyValuePair<string, HeaderStringValues> Current => _current;
+
+ /// <inheritdoc/>
+ object IEnumerator.Current => _current;
+
+ /// <inheritdoc/>
+ public void Dispose() { }
+
+ /// <inheritdoc/>
+ void IEnumerator.Reset() => throw new NotSupportedException();
+ }
+ }
+}
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net.Http.Headers;
using System.Text;
}
// Add headers.
- foreach (KeyValuePair<string, IEnumerable<string>> headerPair in content.Headers)
+ foreach (KeyValuePair<string, HeaderStringValues> headerPair in content.Headers.NonValidated)
{
Encoding headerValueEncoding = HeaderEncodingSelector?.Invoke(headerPair.Key, content) ?? HttpRuleParser.DefaultHttpEncoding;
foreach (HttpContent content in _nestedContent)
{
// Headers.
- foreach (KeyValuePair<string, IEnumerable<string>> headerPair in content.Headers)
+ foreach (KeyValuePair<string, HeaderStringValues> headerPair in content.Headers.NonValidated)
{
currentLength += headerPair.Key.Length + ColonSpaceLength;
ref string[]? tmpHeaderValuesArray = ref t_headerValues;
foreach (KeyValuePair<HeaderDescriptor, object> header in headers.HeaderStore)
{
- int headerValuesCount = HttpHeaders.GetValuesAsStrings(header.Key, header.Value, ref tmpHeaderValuesArray);
+ int headerValuesCount = HttpHeaders.GetStoreValuesIntoStringArray(header.Key, header.Value, ref tmpHeaderValuesArray);
Debug.Assert(headerValuesCount > 0, "No values for header??");
ReadOnlySpan<string> headerValues = tmpHeaderValuesArray.AsSpan(0, headerValuesCount);
foreach (KeyValuePair<HeaderDescriptor, object> header in headers.HeaderStore)
{
- int headerValuesCount = HttpHeaders.GetValuesAsStrings(header.Key, header.Value, ref _headerValues);
+ int headerValuesCount = HttpHeaders.GetStoreValuesIntoStringArray(header.Key, header.Value, ref _headerValues);
Debug.Assert(headerValuesCount > 0, "No values for header??");
ReadOnlySpan<string> headerValues = _headerValues.AsSpan(0, headerValuesCount);
await WriteTwoBytesAsync((byte)':', (byte)' ', async).ConfigureAwait(false);
}
- int headerValuesCount = HttpHeaders.GetValuesAsStrings(header.Key, header.Value, ref _headerValues);
+ int headerValuesCount = HttpHeaders.GetStoreValuesIntoStringArray(header.Key, header.Value, ref _headerValues);
Debug.Assert(headerValuesCount > 0, "No values for header??");
if (headerValuesCount > 0)
{
foreach (KeyValuePair<HeaderDescriptor, object> header in headers.HeaderStore)
{
- int headerValuesCount = HttpHeaders.GetValuesAsStrings(header.Key, header.Value, ref headerValues);
+ int headerValuesCount = HttpHeaders.GetStoreValuesIntoStringArray(header.Key, header.Value, ref headerValues);
Assert.InRange(headerValuesCount, 0, int.MaxValue);
ReadOnlySpan<string> headerValuesSpan = headerValues.AsSpan(0, headerValuesCount);
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System;
using System.Collections;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Net.Http.Headers;
-using System.Text;
+using System.Tests;
using Xunit;
}
[Fact]
- public void GetHeaderStrings_SetValidAndInvalidHeaderValues_AllHeaderValuesReturned()
+ public void NonValidated_Default_Empty()
+ {
+ HttpHeadersNonValidated v = default;
+ Assert.Equal(0, v.Count);
+ Assert.Empty(v);
+ Assert.False(v.TryGetValues("Host", out HeaderStringValues values));
+ Assert.Empty(values);
+ }
+
+ [Fact]
+ public void NonValidated_SetValidAndInvalidHeaderValues_AllHeaderValuesReturned()
{
MockHeaderParser parser = new MockHeaderParser("---");
MockHeaders headers = new MockHeaders(parser);
headers.TryAddWithoutValidation(headers.Descriptor, "value2,value3");
headers.TryAddWithoutValidation(headers.Descriptor, invalidHeaderValue);
- foreach (var header in headers.GetHeaderStrings())
+ string expectedValue = "value2,value3---" + invalidHeaderValue + "---" + parsedPrefix + "1";
+
+ Assert.Equal(1, headers.NonValidated.Count);
+
+ int iterations = 0;
+ foreach (KeyValuePair<string, HeaderStringValues> header in headers.NonValidated)
{
- Assert.Equal(headers.Descriptor.Name, header.Key);
// Note that raw values don't get parsed but just added to the result.
- Assert.Equal("value2,value3---" + invalidHeaderValue + "---" + parsedPrefix + "1", header.Value);
+ iterations++;
+ Assert.Equal(headers.Descriptor.Name, header.Key);
+ Assert.Equal(3, header.Value.Count);
+ Assert.Equal(expectedValue, header.Value.ToString());
}
+
+ Assert.Equal(1, iterations);
}
[Fact]
- public void GetHeaderStrings_SetMultipleHeaders_AllHeaderValuesReturned()
+ public void NonValidated_SetMultipleHeaders_AllHeaderValuesReturned()
{
MockHeaderParser parser = new MockHeaderParser(true);
MockHeaders headers = new MockHeaders(parser);
string[] expectedHeaderValues = { parsedPrefix + "1", "value2", "", "value41, value42" };
int i = 0;
- foreach (var header in headers.GetHeaderStrings())
+ foreach (KeyValuePair<string, HeaderStringValues> header in headers.NonValidated)
{
Assert.NotEqual(expectedHeaderNames.Length, i);
Assert.Equal(expectedHeaderNames[i], header.Key);
- Assert.Equal(expectedHeaderValues[i], header.Value);
+ Assert.Equal(expectedHeaderValues[i], header.Value.ToString());
i++;
}
}
[Fact]
- public void GetHeaderStrings_SetMultipleValuesOnSingleValueHeader_AllHeaderValuesReturned()
+ public void NonValidated_SetMultipleValuesOnSingleValueHeader_AllHeaderValuesReturned()
{
MockHeaderParser parser = new MockHeaderParser(false);
MockHeaders headers = new MockHeaders(parser);
headers.TryAddWithoutValidation(headers.Descriptor, "value1");
headers.TryAddWithoutValidation(headers.Descriptor, rawPrefix);
- foreach (var header in headers.GetHeaderStrings())
+ foreach (KeyValuePair<string, HeaderStringValues> header in headers.NonValidated)
{
Assert.Equal(headers.Descriptor.Name, header.Key);
// Note that the added rawPrefix did not get parsed
- Assert.Equal("value1, " + rawPrefix, header.Value);
+ Assert.Equal("value1, " + rawPrefix, header.Value.ToString());
+ }
+ }
+
+ [ActiveIssue("https://github.com/dotnet/runtime/issues/53647", TestPlatforms.Browser)]
+ [Fact]
+ public void NonValidated_ValidAndInvalidValues_DictionaryMembersWork()
+ {
+ var headers = new HttpResponseHeaders();
+ IReadOnlyDictionary<string, HeaderStringValues> nonValidated = headers.NonValidated;
+
+ Assert.True(headers.TryAddWithoutValidation("Location", "http:/invalidLocation"));
+ Assert.True(headers.TryAddWithoutValidation("Location", "http:/anotherLocation"));
+ Assert.True(headers.TryAddWithoutValidation("Date", "not a date"));
+
+ Assert.Equal(2, nonValidated.Count);
+
+ Assert.True(nonValidated.ContainsKey("Location"));
+ Assert.True(nonValidated.ContainsKey("Date"));
+
+ Assert.False(nonValidated.ContainsKey("Age"));
+ Assert.False(nonValidated.TryGetValue("Age", out _));
+ Assert.Throws<KeyNotFoundException>(() => nonValidated["Age"]);
+
+ Assert.True(nonValidated.TryGetValue("Location", out HeaderStringValues locations));
+ Assert.Equal(2, locations.Count);
+ Assert.Equal(new[] { "http:/invalidLocation", "http:/anotherLocation" }, locations.ToArray());
+ Assert.Equal("http:/invalidLocation, http:/anotherLocation", locations.ToString());
+
+ Assert.True(nonValidated.TryGetValue("Date", out HeaderStringValues dates));
+ Assert.Equal(1, dates.Count);
+ Assert.Equal(new[] { "not a date" }, dates.ToArray());
+ Assert.Equal("not a date", dates.ToString());
+
+ dates = nonValidated["Date"];
+ Assert.Equal(1, dates.Count);
+ Assert.Equal(new[] { "not a date" }, dates.ToArray());
+ Assert.Equal("not a date", dates.ToString());
+
+ Assert.Equal(new HashSet<string> { "Location", "Date" }, nonValidated.Keys.ToHashSet());
+ }
+
+ [ActiveIssue("https://github.com/dotnet/runtime/issues/53647", TestPlatforms.Browser)]
+ [Fact]
+ public void NonValidated_ValidInvalidAndRaw_AllReturned()
+ {
+ var headers = new HttpResponseHeaders();
+ IReadOnlyDictionary<string, HeaderStringValues> nonValidated = headers.NonValidated;
+
+ // Parsed value
+ headers.Date = new DateTimeOffset(1, 2, 3, 4, 5, 6, TimeSpan.Zero);
+
+ // Invalid value
+ headers.TryAddWithoutValidation("Date", "not a date");
+ foreach (KeyValuePair<string, IEnumerable<string>> _ in headers) { }
+
+ // Raw value
+ headers.TryAddWithoutValidation("Date", "another not a date");
+
+ // All three show up
+ Assert.Equal(1, nonValidated.Count);
+ Assert.Equal(3, nonValidated["Date"].Count);
+ using (new ThreadCultureChange(new CultureInfo("en-US")))
+ {
+ Assert.Equal(new HashSet<string> { "not a date", "another not a date", "Sat, 03 Feb 0001 04:05:06 GMT" }, nonValidated["Date"].ToHashSet());
}
}
{
MockHeaders headers = new MockHeaders();
- var enumerator = headers.GetEnumerator();
+ IEnumerator<KeyValuePair<string, IEnumerable<string>>> enumerator = headers.GetEnumerator();
Assert.False(enumerator.MoveNext());
}
// The value added with TryAddWithoutValidation() wasn't parsed yet.
Assert.Equal(1, headers.Parser.TryParseValueCallCount);
- var enumerator = headers.GetEnumerator();
+ IEnumerator<KeyValuePair<string, IEnumerable<string>>> enumerator = headers.GetEnumerator();
// Getting the enumerator doesn't trigger parsing.
Assert.Equal(1, headers.Parser.TryParseValueCallCount);
headers.Add(customHeaderName, string.Empty);
headers.Add(headers.Descriptor, string.Empty);
- var enumerator = headers.GetEnumerator();
+ IEnumerator<KeyValuePair<string, IEnumerable<string>>> enumerator = headers.GetEnumerator();
Assert.True(enumerator.MoveNext());
Assert.Equal(customHeaderName, enumerator.Current.Key);
System.Collections.IEnumerable headersAsIEnumerable = headers;
- var enumerator = headersAsIEnumerable.GetEnumerator();
+ IEnumerator enumerator = headersAsIEnumerable.GetEnumerator();
KeyValuePair<string, IEnumerable<string>> currentValue;
Assert.False(destination.Contains("custom"), "destination contains 'custom' header.");
}
+ [Fact]
+ public void HeaderStringValues_Default_Empty()
+ {
+ HeaderStringValues v = default;
+ Assert.Equal(0, v.Count);
+ Assert.Empty(v);
+ Assert.Equal(string.Empty, v.ToString());
+ }
+
+ [Fact]
+ public void HeaderStringValues_Constructed_ProducesExpectedResults()
+ {
+ // 0 strings
+ foreach (HeaderStringValues hsv in new[] { new HeaderStringValues(KnownHeaders.Accept.Descriptor, Array.Empty<string>()) })
+ {
+ Assert.Equal(0, hsv.Count);
+
+ HeaderStringValues.Enumerator e = hsv.GetEnumerator();
+
+ Assert.False(e.MoveNext());
+
+ Assert.Equal(string.Empty, hsv.ToString());
+ }
+
+ // 1 string
+ foreach (HeaderStringValues hsv in new[] { new HeaderStringValues(KnownHeaders.Accept.Descriptor, "hello"), new HeaderStringValues(KnownHeaders.Accept.Descriptor, new[] { "hello" }) })
+ {
+ Assert.Equal(1, hsv.Count);
+
+ HeaderStringValues.Enumerator e = hsv.GetEnumerator();
+
+ Assert.True(e.MoveNext());
+ Assert.Equal("hello", e.Current);
+
+ Assert.False(e.MoveNext());
+
+ Assert.Equal("hello", hsv.ToString());
+ }
+
+ // 2 strings
+ foreach (HeaderStringValues hsv in new[] { new HeaderStringValues(KnownHeaders.Accept.Descriptor, new[] { "hello", "world" }) })
+ {
+ Assert.Equal(2, hsv.Count);
+
+ HeaderStringValues.Enumerator e = hsv.GetEnumerator();
+
+ Assert.True(e.MoveNext());
+ Assert.Equal("hello", e.Current);
+
+ Assert.True(e.MoveNext());
+ Assert.Equal("world", e.Current);
+
+ Assert.False(e.MoveNext());
+
+ Assert.Equal("hello, world", hsv.ToString());
+ }
+ }
+
public static IEnumerable<object[]> GetInvalidHeaderNames()
{
yield return new object[] { "invalid header" };
headers.Add("Accept-Charset", "utf-8");
headers.AcceptCharset.Add(new StringWithQualityHeaderValue("iso-8859-5", 0.5));
- foreach (var header in headers.GetHeaderStrings())
+ foreach (var header in headers.NonValidated)
{
Assert.Equal("Accept-Charset", header.Key);
- Assert.Equal("utf-8, iso-8859-5; q=0.5, invalid value", header.Value);
+ Assert.Equal("utf-8, iso-8859-5; q=0.5, invalid value", header.Value.ToString());
}
}
headers.Add("User-Agent", "custom2/1.1");
headers.UserAgent.Add(new ProductInfoHeaderValue("(comment)"));
- foreach (var header in headers.GetHeaderStrings())
+ foreach (var header in headers.NonValidated)
{
Assert.Equal("User-Agent", header.Key);
- Assert.Equal("custom2/1.1 (comment) custom\u4F1A", header.Value);
+ Assert.Equal("custom2/1.1 (comment) custom\u4F1A", header.Value.ToString());
}
}
Link="ProductionCode\System\Net\Http\Headers\GenericHeaderParser.cs" />
<Compile Include="..\..\src\System\Net\Http\Headers\HeaderDescriptor.cs"
Link="ProductionCode\System\Net\Http\Headers\HeaderDescriptor.cs" />
+ <Compile Include="..\..\src\System\Net\Http\Headers\HeaderStringValues.cs"
+ Link="ProductionCode\System\Net\Http\Headers\HeaderStringValues.cs" />
<Compile Include="..\..\src\System\Net\Http\Headers\HeaderUtilities.cs"
Link="ProductionCode\System\Net\Http\Headers\HeaderUtilities.cs" />
<Compile Include="..\..\src\System\Net\Http\Headers\HttpContentHeaders.cs"
Link="ProductionCode\System\Net\Http\Headers\HttpHeaderParser.cs" />
<Compile Include="..\..\src\System\Net\Http\Headers\HttpHeaders.cs"
Link="ProductionCode\System\Net\Http\Headers\HttpHeaders.cs" />
+ <Compile Include="..\..\src\System\Net\Http\Headers\HttpHeadersNonValidated.cs"
+ Link="ProductionCode\System\Net\Http\Headers\HttpHeadersNonValidated.cs" />
<Compile Include="..\..\src\System\Net\Http\Headers\HttpHeaderType.cs"
Link="ProductionCode\System\Net\Http\Headers\HttpHeaderType.cs" />
<Compile Include="..\..\src\System\Net\Http\Headers\HttpHeaderValueCollection.cs"
private static void ValidateHeader(HttpHeaders headers, string name, string expectedValue)
{
- if (!headers.TryGetValues(name, out IEnumerable<string>? values))
+ if (headers.NonValidated.TryGetValues(name, out HeaderStringValues hsv))
{
- throw new WebSocketException(WebSocketError.Faulted, SR.Format(SR.net_WebSockets_MissingResponseHeader, name));
- }
+ if (hsv.Count == 1)
+ {
+ foreach (string value in hsv)
+ {
+ if (string.Equals(value, expectedValue, StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+ break;
+ }
+ }
- Debug.Assert(values is string[]);
- string[] array = (string[])values;
- if (array.Length != 1 || !string.Equals(array[0], expectedValue, StringComparison.OrdinalIgnoreCase))
- {
- throw new WebSocketException(WebSocketError.HeaderError, SR.Format(SR.net_WebSockets_InvalidResponseHeader, name, string.Join(", ", array)));
+ throw new WebSocketException(WebSocketError.HeaderError, SR.Format(SR.net_WebSockets_InvalidResponseHeader, name, hsv));
}
+
+ throw new WebSocketException(WebSocketError.Faulted, SR.Format(SR.net_WebSockets_MissingResponseHeader, name));
}
/// <summary>Used as a sentinel to indicate that ClientWebSocket should use the system's default proxy.</summary>