Add HttpHeaders.NonValidated (#53555)
authorStephen Toub <stoub@microsoft.com>
Sun, 6 Jun 2021 21:23:38 +0000 (17:23 -0400)
committerGitHub <noreply@github.com>
Sun, 6 Jun 2021 21:23:38 +0000 (17:23 -0400)
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.

15 files changed:
src/libraries/System.Net.Http/ref/System.Net.Http.cs
src/libraries/System.Net.Http/src/System.Net.Http.csproj
src/libraries/System.Net.Http/src/System/Net/Http/Headers/HeaderStringValues.cs [new file with mode: 0644]
src/libraries/System.Net.Http/src/System/Net/Http/Headers/HeaderUtilities.cs
src/libraries/System.Net.Http/src/System/Net/Http/Headers/HttpHeaders.cs
src/libraries/System.Net.Http/src/System/Net/Http/Headers/HttpHeadersNonValidated.cs [new file with mode: 0644]
src/libraries/System.Net.Http/src/System/Net/Http/MultipartContent.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs
src/libraries/System.Net.Http/tests/UnitTests/HPack/HPackRoundtripTests.cs
src/libraries/System.Net.Http/tests/UnitTests/Headers/HttpHeadersTest.cs
src/libraries/System.Net.Http/tests/UnitTests/Headers/HttpRequestHeadersTest.cs
src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj
src/libraries/System.Net.WebSockets.Client/src/System/Net/WebSockets/WebSocketHandle.Managed.cs

index 9fa9bdc..02486ab 100644 (file)
@@ -4,6 +4,7 @@
 // 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
@@ -520,6 +521,26 @@ namespace System.Net.Http.Headers
         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() { }
@@ -538,6 +559,7 @@ namespace System.Net.Http.Headers
     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() { }
@@ -551,6 +573,32 @@ namespace System.Net.Http.Headers
         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() { }
index cf6c743..0ce5ec1 100644 (file)
     <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" />
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HeaderStringValues.cs b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HeaderStringValues.cs
new file mode 100644 (file)
index 0000000..a313a23
--- /dev/null
@@ -0,0 +1,130 @@
+// 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();
+        }
+    }
+}
index d3802a1..4bd5db9 100644 (file)
@@ -357,7 +357,7 @@ namespace System.Net.Http.Headers
             {
                 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)
                         {
index fcdee9f..6633329 100644 (file)
@@ -5,6 +5,7 @@ using System.Collections;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
 using System.Text;
 
 namespace System.Net.Http.Headers
@@ -75,6 +76,9 @@ 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)
@@ -226,7 +230,7 @@ namespace System.Net.Http.Headers
         {
             if (_headerStore != null && TryGetAndParseHeaderInfo(descriptor, out HeaderStoreItemInfo? info))
             {
-                values = GetValuesAsStrings(descriptor, info);
+                values = GetStoreValuesAsStringArray(descriptor, info);
                 return true;
             }
 
@@ -246,59 +250,63 @@ namespace System.Net.Http.Headers
 
         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;
@@ -343,29 +351,12 @@ namespace System.Net.Http.Headers
                 }
                 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
@@ -713,7 +704,7 @@ namespace System.Net.Http.Headers
             (_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)
             {
@@ -1162,46 +1153,46 @@ namespace System.Net.Http.Headers
             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>();
 
@@ -1229,9 +1220,9 @@ namespace System.Net.Http.Headers
                 }
 
                 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);
             }
 
@@ -1253,55 +1244,29 @@ namespace System.Net.Http.Headers
                 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);
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HttpHeadersNonValidated.cs b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HttpHeadersNonValidated.cs
new file mode 100644 (file)
index 0000000..ad1a185
--- /dev/null
@@ -0,0 +1,172 @@
+// 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();
+        }
+    }
+}
index 8227b4b..4ac7a34 100644 (file)
@@ -4,7 +4,6 @@
 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;
@@ -333,7 +332,7 @@ namespace System.Net.Http
             }
 
             // 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;
 
@@ -388,7 +387,7 @@ namespace System.Net.Http
             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;
 
index 703a340..c60370d 100644 (file)
@@ -1136,7 +1136,7 @@ namespace System.Net.Http
             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);
 
index 970890d..3013d49 100644 (file)
@@ -587,7 +587,7 @@ namespace System.Net.Http
 
             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);
 
index 01640d1..00e3f3b 100644 (file)
@@ -263,7 +263,7 @@ namespace System.Net.Http
                         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)
                     {
index 03f801e..7ddee86 100644 (file)
@@ -62,7 +62,7 @@ namespace System.Net.Http.Unit.Tests.HPack
 
             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);
 
index 58dd95c..2a25ea7 100644 (file)
@@ -1,12 +1,12 @@
 // 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;
 
@@ -1421,7 +1421,17 @@ namespace System.Net.Http.Tests
         }
 
         [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);
@@ -1431,16 +1441,25 @@ namespace System.Net.Http.Tests
             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);
@@ -1456,17 +1475,17 @@ namespace System.Net.Http.Tests
             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);
@@ -1474,11 +1493,75 @@ namespace System.Net.Http.Tests
             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());
             }
         }
 
@@ -1562,7 +1645,7 @@ namespace System.Net.Http.Tests
         {
             MockHeaders headers = new MockHeaders();
 
-            var enumerator = headers.GetEnumerator();
+            IEnumerator<KeyValuePair<string, IEnumerable<string>>> enumerator = headers.GetEnumerator();
             Assert.False(enumerator.MoveNext());
         }
 
@@ -1577,7 +1660,7 @@ namespace System.Net.Http.Tests
             // 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);
@@ -1610,7 +1693,7 @@ namespace System.Net.Http.Tests
             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);
@@ -1631,7 +1714,7 @@ namespace System.Net.Http.Tests
 
             System.Collections.IEnumerable headersAsIEnumerable = headers;
 
-            var enumerator = headersAsIEnumerable.GetEnumerator();
+            IEnumerator enumerator = headersAsIEnumerable.GetEnumerator();
 
             KeyValuePair<string, IEnumerable<string>> currentValue;
 
@@ -2048,6 +2131,64 @@ namespace System.Net.Http.Tests
             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" };
index b5df472..4ce88a8 100644 (file)
@@ -151,10 +151,10 @@ namespace System.Net.Http.Tests
             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());
             }
         }
 
@@ -656,10 +656,10 @@ namespace System.Net.Http.Tests
             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());
             }
         }
 
index 3b78905..186f1cb 100644 (file)
              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"
index e0c3902..0af0cd5 100644 (file)
@@ -411,17 +411,24 @@ namespace System.Net.WebSockets
 
         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>