File: AttributeDictionary.cs
Web Access
Project: src\src\Mvc\Mvc.ViewFeatures\src\Microsoft.AspNetCore.Mvc.ViewFeatures.csproj (Microsoft.AspNetCore.Mvc.ViewFeatures)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable enable
 
using System.Collections;
using System.Diagnostics;
using Microsoft.AspNetCore.Shared;
 
namespace Microsoft.AspNetCore.Mvc.ViewFeatures;
 
/// <summary>
/// A dictionary for HTML attributes.
/// </summary>
[DebuggerDisplay("Count = {Count}")]
[DebuggerTypeProxy(typeof(DictionaryDebugView<string, string?>))]
public class AttributeDictionary : IDictionary<string, string?>, IReadOnlyDictionary<string, string?>
{
    private List<KeyValuePair<string, string?>>? _items;
 
    /// <inheritdoc />
    public string? this[string key]
    {
        get
        {
            ArgumentNullException.ThrowIfNull(key);
 
            var index = Find(key);
            if (index < 0)
            {
                throw new KeyNotFoundException();
            }
            else
            {
                return Get(index).Value;
            }
        }
 
        set
        {
            ArgumentNullException.ThrowIfNull(key);
 
            var item = new KeyValuePair<string, string?>(key, value);
            var index = Find(key);
            if (index < 0)
            {
                Insert(~index, item);
            }
            else
            {
                Set(index, item);
            }
        }
    }
 
    /// <inheritdoc />
    public int Count => _items == null ? 0 : _items.Count;
 
    /// <inheritdoc />
    public bool IsReadOnly => false;
 
    /// <inheritdoc />
    public ICollection<string> Keys => new KeyCollection(this);
 
    /// <inheritdoc />
    public ICollection<string?> Values => new ValueCollection(this);
 
    /// <inheritdoc />
    IEnumerable<string> IReadOnlyDictionary<string, string?>.Keys => new KeyCollection(this);
 
    /// <inheritdoc />
    IEnumerable<string?> IReadOnlyDictionary<string, string?>.Values => new ValueCollection(this);
 
    private KeyValuePair<string, string?> Get(int index)
    {
        Debug.Assert(index >= 0 && index < Count && _items != null);
        return _items[index];
    }
 
    private void Set(int index, KeyValuePair<string, string?> value)
    {
        Debug.Assert(index >= 0 && index <= Count);
        Debug.Assert(value.Key != null);
 
        if (_items == null)
        {
            _items = new List<KeyValuePair<string, string?>>();
        }
 
        _items[index] = value;
    }
 
    private void Insert(int index, KeyValuePair<string, string?> value)
    {
        Debug.Assert(index >= 0 && index <= Count);
        Debug.Assert(value.Key != null);
 
        if (_items == null)
        {
            _items = new List<KeyValuePair<string, string?>>();
        }
 
        _items.Insert(index, value);
    }
 
    private void Remove(int index)
    {
        Debug.Assert(index >= 0 && index < Count);
 
        Debug.Assert(_items != null);
        _items.RemoveAt(index);
    }
 
    // This API is a lot like List<T>.BinarySearch https://msdn.microsoft.com/en-us/library/3f90y839(v=vs.110).aspx
    // If an item is not found, we return the compliment of where it belongs. Then we don't need to search again
    // to do something with it.
    private int Find(string key)
    {
        Debug.Assert(key != null);
 
        if (Count == 0)
        {
            return ~0;
        }
 
        var start = 0;
        var end = Count - 1;
 
        while (start <= end)
        {
            var pivot = start + (end - start >> 1);
 
            var compare = StringComparer.OrdinalIgnoreCase.Compare(Get(pivot).Key, key);
            if (compare == 0)
            {
                return pivot;
            }
            if (compare < 0)
            {
                start = pivot + 1;
            }
            else
            {
                end = pivot - 1;
            }
        }
 
        return ~start;
    }
 
    /// <inheritdoc />
    public void Clear()
    {
        _items?.Clear();
    }
 
    /// <inheritdoc />
    public void Add(KeyValuePair<string, string?> item)
    {
        if (item.Key == null)
        {
            throw new ArgumentException(
                Resources.FormatPropertyOfTypeCannotBeNull(
                    nameof(KeyValuePair<string, string?>.Key),
                    nameof(KeyValuePair<string, string?>)),
                nameof(item));
        }
 
        var index = Find(item.Key);
        if (index < 0)
        {
            Insert(~index, item);
        }
        else
        {
            throw new InvalidOperationException(Resources.FormatDictionary_DuplicateKey(item.Key));
        }
    }
 
    /// <inheritdoc />
    public void Add(string key, string? value)
    {
        ArgumentNullException.ThrowIfNull(key);
 
        Add(new KeyValuePair<string, string?>(key, value));
    }
 
    /// <inheritdoc />
    public bool Contains(KeyValuePair<string, string?> item)
    {
        if (item.Key == null)
        {
            throw new ArgumentException(
                Resources.FormatPropertyOfTypeCannotBeNull(
                    nameof(KeyValuePair<string, string?>.Key),
                    nameof(KeyValuePair<string, string?>)),
                nameof(item));
        }
 
        var index = Find(item.Key);
        if (index < 0)
        {
            return false;
        }
        else
        {
            return string.Equals(item.Value, Get(index).Value, StringComparison.OrdinalIgnoreCase);
        }
    }
 
    /// <inheritdoc />
    public bool ContainsKey(string key)
    {
        ArgumentNullException.ThrowIfNull(key);
 
        if (Count == 0)
        {
            return false;
        }
 
        return Find(key) >= 0;
    }
 
    /// <inheritdoc />
    public void CopyTo(KeyValuePair<string, string?>[] array, int arrayIndex)
    {
        ArgumentNullException.ThrowIfNull(array);
 
        if (arrayIndex < 0 || arrayIndex >= array.Length)
        {
            throw new ArgumentOutOfRangeException(nameof(arrayIndex));
        }
 
        for (var i = 0; i < Count; i++)
        {
            array[arrayIndex + i] = Get(i);
        }
    }
 
    /// <inheritdoc />
    public Enumerator GetEnumerator()
    {
        return new Enumerator(this);
    }
 
    /// <inheritdoc />
    public bool Remove(KeyValuePair<string, string?> item)
    {
        if (item.Key == null)
        {
            throw new ArgumentException(
                Resources.FormatPropertyOfTypeCannotBeNull(
                    nameof(KeyValuePair<string, string?>.Key),
                    nameof(KeyValuePair<string, string?>)),
                nameof(item));
        }
 
        var index = Find(item.Key);
        if (index < 0)
        {
            return false;
        }
        else if (string.Equals(item.Value, Get(index).Value, StringComparison.OrdinalIgnoreCase))
        {
            Remove(index);
            return true;
        }
        else
        {
            return false;
        }
    }
 
    /// <inheritdoc />
    public bool Remove(string key)
    {
        ArgumentNullException.ThrowIfNull(key);
 
        var index = Find(key);
        if (index < 0)
        {
            return false;
        }
        else
        {
            Remove(index);
            return true;
        }
    }
 
    /// <inheritdoc />
    public bool TryGetValue(string key, out string? value)
    {
        ArgumentNullException.ThrowIfNull(key);
 
        var index = Find(key);
        if (index < 0)
        {
            value = null;
            return false;
        }
        else
        {
            value = Get(index).Value;
            return true;
        }
    }
 
    /// <inheritdoc />
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
 
    /// <inheritdoc />
    IEnumerator<KeyValuePair<string, string?>> IEnumerable<KeyValuePair<string, string?>>.GetEnumerator()
    {
        return GetEnumerator();
    }
 
    /// <summary>
    /// An enumerator for <see cref="AttributeDictionary"/>.
    /// </summary>
    public struct Enumerator : IEnumerator<KeyValuePair<string, string?>>
    {
        private readonly AttributeDictionary _attributes;
 
        private int _index;
 
        /// <summary>
        /// Creates a new <see cref="Enumerator"/>.
        /// </summary>
        /// <param name="attributes">The <see cref="AttributeDictionary"/>.</param>
        public Enumerator(AttributeDictionary attributes)
        {
            _attributes = attributes;
 
            _index = -1;
        }
 
        /// <inheritdoc />
        public KeyValuePair<string, string?> Current => _attributes.Get(_index);
 
        /// <inheritdoc />
        object IEnumerator.Current => Current;
 
        /// <inheritdoc />
        public void Dispose()
        {
        }
 
        /// <inheritdoc />
        public bool MoveNext()
        {
            _index++;
            return _index < _attributes.Count;
        }
 
        /// <inheritdoc />
        public void Reset()
        {
            _index = -1;
        }
    }
 
    private sealed class KeyCollection : ICollection<string>
    {
        private readonly AttributeDictionary _attributes;
 
        public KeyCollection(AttributeDictionary attributes)
        {
            _attributes = attributes;
        }
 
        public int Count => _attributes.Count;
 
        public bool IsReadOnly => true;
 
        public void Add(string item)
        {
            throw new NotSupportedException();
        }
 
        public void Clear()
        {
            throw new NotSupportedException();
        }
 
        public bool Contains(string item)
        {
            ArgumentNullException.ThrowIfNull(item);
 
            for (var i = 0; i < _attributes.Count; i++)
            {
                if (string.Equals(item, _attributes.Get(i).Key, StringComparison.OrdinalIgnoreCase))
                {
                    return true;
                }
            }
 
            return false;
        }
 
        public void CopyTo(string[] array, int arrayIndex)
        {
            ArgumentNullException.ThrowIfNull(array);
 
            if (arrayIndex < 0 || arrayIndex >= array.Length)
            {
                throw new ArgumentOutOfRangeException(nameof(arrayIndex));
            }
 
            for (var i = 0; i < _attributes.Count; i++)
            {
                array[arrayIndex + i] = _attributes.Get(i).Key;
            }
        }
 
        public Enumerator GetEnumerator()
        {
            return new Enumerator(_attributes);
        }
 
        public bool Remove(string item)
        {
            throw new NotSupportedException();
        }
 
        IEnumerator<string> IEnumerable<string>.GetEnumerator()
        {
            return GetEnumerator();
        }
 
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
 
        public struct Enumerator : IEnumerator<string?>
        {
            private readonly AttributeDictionary _attributes;
 
            private int _index;
 
            public Enumerator(AttributeDictionary attributes)
            {
                _attributes = attributes;
 
                _index = -1;
            }
 
            public string Current => _attributes.Get(_index).Key;
 
            object IEnumerator.Current => Current;
 
            public void Dispose()
            {
            }
 
            public bool MoveNext()
            {
                _index++;
                return _index < _attributes.Count;
            }
 
            public void Reset()
            {
                _index = -1;
            }
        }
    }
 
    private sealed class ValueCollection : ICollection<string?>
    {
        private readonly AttributeDictionary _attributes;
 
        public ValueCollection(AttributeDictionary attributes)
        {
            _attributes = attributes;
        }
 
        public int Count => _attributes.Count;
 
        public bool IsReadOnly => true;
 
        public void Add(string? item)
        {
            throw new NotSupportedException();
        }
 
        public void Clear()
        {
            throw new NotSupportedException();
        }
 
        public bool Contains(string? item)
        {
            for (var i = 0; i < _attributes.Count; i++)
            {
                if (string.Equals(item, _attributes.Get(i).Value, StringComparison.OrdinalIgnoreCase))
                {
                    return true;
                }
            }
 
            return false;
        }
 
        public void CopyTo(string?[] array, int arrayIndex)
        {
            ArgumentNullException.ThrowIfNull(array);
 
            if (arrayIndex < 0 || arrayIndex >= array.Length)
            {
                throw new ArgumentOutOfRangeException(nameof(arrayIndex));
            }
 
            for (var i = 0; i < _attributes.Count; i++)
            {
                array[arrayIndex + i] = _attributes.Get(i).Value;
            }
        }
 
        public Enumerator GetEnumerator()
        {
            return new Enumerator(_attributes);
        }
 
        public bool Remove(string? item)
        {
            throw new NotSupportedException();
        }
 
        IEnumerator<string?> IEnumerable<string?>.GetEnumerator()
        {
            return GetEnumerator();
        }
 
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
 
        public struct Enumerator : IEnumerator<string?>
        {
            private readonly AttributeDictionary _attributes;
 
            private int _index;
 
            public Enumerator(AttributeDictionary attributes)
            {
                _attributes = attributes;
 
                _index = -1;
            }
 
            public string? Current => _attributes.Get(_index).Value;
 
            object? IEnumerator.Current => Current;
 
            public void Dispose()
            {
            }
 
            public bool MoveNext()
            {
                _index++;
                return _index < _attributes.Count;
            }
 
            public void Reset()
            {
                _index = -1;
            }
        }
    }
}