File: System\Net\Http\Headers\HttpHeaderValueCollection.cs
Web Access
Project: src\src\libraries\System.Net.Http\src\System.Net.Http.csproj (System.Net.Http)
// 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.Generic;
using System.Diagnostics;
 
namespace System.Net.Http.Headers
{
    // This type is used for headers supporting a list of values. It essentially just forwards calls to
    // the actual header-store in HttpHeaders.
    //
    // This type can deal with a so called "special value": The RFC defines some headers which are collection of
    // values, but the RFC only defines 1 value, e.g. Transfer-Encoding: chunked, Connection: close,
    // Expect: 100-continue.
    // We expose strongly typed properties for these special values: TransferEncodingChunked, ConnectionClose,
    // ExpectContinue.
    // So we have 2 properties for each of these headers ('Transfer-Encoding' => TransferEncoding,
    // TransferEncodingChunked; 'Connection' => Connection, ConnectionClose; 'Expect' => Expect, ExpectContinue)
    //
    // The following solution was chosen:
    // - Keep HttpHeaders clean: HttpHeaders is unaware of these "special values"; it just stores the collection of
    //   headers.
    // - It is the responsibility of "higher level" components (HttpHeaderValueCollection, HttpRequestHeaders,
    //   HttpResponseHeaders) to deal with special values.
    // - HttpHeaderValueCollection can be configured with an IEqualityComparer and a "special value".
    //
    // Example: Server sends header "Transfer-Encoding: gzip, custom, chunked" to the client.
    // - HttpHeaders: HttpHeaders will have an entry in the header store for "Transfer-Encoding" with values
    //   "gzip", "custom", "chunked"
    // - HttpGeneralHeaders:
    //   - Property TransferEncoding: has three values "gzip", "custom", and "chunked"
    //   - Property TransferEncodingChunked: is set to "true".
    public sealed class HttpHeaderValueCollection<T> : ICollection<T> where T : class
    {
        private readonly HeaderDescriptor _descriptor;
        private readonly HttpHeaders _store;
 
        public int Count
        {
            get { return GetCount(); }
        }
 
        public bool IsReadOnly
        {
            get { return false; }
        }
 
        internal HttpHeaderValueCollection(HeaderDescriptor descriptor, HttpHeaders store)
        {
            _store = store;
            _descriptor = descriptor;
        }
 
        public void Add(T item)
        {
            CheckValue(item);
            _store.AddParsedValue(_descriptor, item);
        }
 
        public void ParseAdd(string? input)
        {
            _store.Add(_descriptor, input);
        }
 
        public bool TryParseAdd(string? input)
        {
            return _store.TryParseAndAddValue(_descriptor, input);
        }
 
        public void Clear()
        {
            _store.Remove(_descriptor);
        }
 
        public bool Contains(T item)
        {
            CheckValue(item);
            return _store.ContainsParsedValue(_descriptor, item);
        }
 
        public void CopyTo(T[] array, int arrayIndex)
        {
            ArgumentNullException.ThrowIfNull(array);
 
            // Allow arrayIndex == array.Length in case our own collection is empty
            ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex);
            ArgumentOutOfRangeException.ThrowIfGreaterThan(arrayIndex, array.Length);
 
            object? storeValue = _store.GetParsedAndInvalidValues(_descriptor);
 
            if (storeValue == null)
            {
                return;
            }
 
            List<object>? storeValues = storeValue as List<object>;
 
            if (storeValues == null)
            {
                if (storeValue is not HttpHeaders.InvalidValue)
                {
                    Debug.Assert(storeValue is T);
                    if (arrayIndex == array.Length)
                    {
                        throw new ArgumentException(SR.net_http_copyto_array_too_small);
                    }
                    array[arrayIndex] = (T)storeValue;
                }
            }
            else
            {
                foreach (object item in storeValues)
                {
                    if (item is not HttpHeaders.InvalidValue)
                    {
                        Debug.Assert(item is T);
                        if (arrayIndex == array.Length)
                        {
                            throw new ArgumentException(SR.net_http_copyto_array_too_small);
                        }
                        array[arrayIndex++] = (T)item;
                    }
                }
            }
        }
 
        public bool Remove(T item)
        {
            CheckValue(item);
            return _store.RemoveParsedValue(_descriptor, item);
        }
 
        #region IEnumerable<T> Members
 
        public IEnumerator<T> GetEnumerator()
        {
            object? storeValue = _store.GetParsedAndInvalidValues(_descriptor);
            return storeValue is null || storeValue is HttpHeaders.InvalidValue ?
                ((IEnumerable<T>)Array.Empty<T>()).GetEnumerator() : // use singleton empty array enumerator
                Iterate(storeValue);
 
            static IEnumerator<T> Iterate(object storeValue)
            {
                if (storeValue is List<object> storeValues)
                {
                    // We have multiple values. Iterate through the values and return them.
                    foreach (object item in storeValues)
                    {
                        if (item is HttpHeaders.InvalidValue)
                        {
                            continue;
                        }
                        Debug.Assert(item is T);
                        yield return (T)item;
                    }
                }
                else
                {
                    Debug.Assert(storeValue is T);
                    yield return (T)storeValue;
                }
            }
        }
 
        #endregion
 
        #region IEnumerable Members
 
        Collections.IEnumerator Collections.IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
 
        #endregion
 
        public override string ToString()
        {
            return _store.GetHeaderString(_descriptor);
        }
 
        private void CheckValue(T item)
        {
            ArgumentNullException.ThrowIfNull(item);
 
            if (_descriptor.Parser == GenericHeaderParser.TokenListParser)
            {
                // The collection expects valid HTTP tokens, which are typed as string.
                // Unlike other parsed values (which are always valid by construction),
                // we can't assume the provided string is a valid token. So validate it before we use it.
                Debug.Assert(typeof(T) == typeof(string));
                HeaderUtilities.CheckValidToken((string)(object)item, nameof(item));
            }
        }
 
        private int GetCount()
        {
            // This is an O(n) operation.
 
            object? storeValue = _store.GetParsedAndInvalidValues(_descriptor);
 
            if (storeValue == null)
            {
                return 0;
            }
 
            List<object>? storeValues = storeValue as List<object>;
 
            if (storeValues == null)
            {
                if (storeValue is not HttpHeaders.InvalidValue)
                {
                    return 1;
                }
                return 0;
            }
            else
            {
                int count = 0;
                foreach (object item in storeValues)
                {
                    if (item is not HttpHeaders.InvalidValue)
                    {
                        count++;
                    }
                }
                return count;
            }
        }
    }
}