From 6185b16795dc4e6c1dcadf69f4eabd3097a4eb3e Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Mon, 28 Sep 2015 22:14:32 -0700 Subject: [PATCH] Custom dictionary type for attributes --- .../Properties/Resources.Designer.cs | 16 + .../Rendering/TagBuilder.cs | 47 +- .../Resources.resx | 3 + .../ViewFeatures/AttributeDictionary.cs | 681 ++++++++++++++++++ .../Rendering/TagBuilderTest.cs | 4 +- .../ViewFeatures/AttributeDictionaryTest.cs | 464 ++++++++++++ 6 files changed, 1198 insertions(+), 17 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/AttributeDictionary.cs create mode 100644 test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/AttributeDictionaryTest.cs diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/Properties/Resources.Designer.cs index bbfaf4292c..f1dcaa3908 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/Properties/Resources.Designer.cs @@ -842,6 +842,22 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures return string.Format(CultureInfo.CurrentCulture, GetString("TempData_CannotSerializeToSession"), p0, p1); } + /// + /// The collection already contains an entry with key '{0}'. + /// + internal static string Dictionary_DuplicateKey + { + get { return GetString("Dictionary_DuplicateKey"); } + } + + /// + /// The collection already contains an entry with key '{0}'. + /// + internal static string FormatDictionary_DuplicateKey(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Dictionary_DuplicateKey"), p0); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/TagBuilder.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/TagBuilder.cs index a329153a6c..0dadb9ea8f 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/TagBuilder.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/Rendering/TagBuilder.cs @@ -17,6 +17,8 @@ namespace Microsoft.AspNet.Mvc.Rendering [DebuggerDisplay("{DebuggerToString()}")] public class TagBuilder : IHtmlContent { + private AttributeDictionary _attributes; + public TagBuilder(string tagName) { if (string.IsNullOrEmpty(tagName)) @@ -25,15 +27,25 @@ namespace Microsoft.AspNet.Mvc.Rendering } TagName = tagName; - Attributes = new SortedDictionary(StringComparer.OrdinalIgnoreCase); - InnerHtml = new BufferedHtmlContent(); } /// /// Gets the set of attributes that will be written to the tag. /// - public IDictionary Attributes { get; } + public AttributeDictionary Attributes + { + get + { + // Perf: Avoid allocating `_attributes` if possible + if (_attributes == null) + { + _attributes = new AttributeDictionary(); + } + + return _attributes; + } + } public IHtmlContentBuilder InnerHtml { get; } @@ -151,20 +163,24 @@ namespace Microsoft.AspNet.Mvc.Rendering private void AppendAttributes(TextWriter writer, IHtmlEncoder encoder) { - foreach (var attribute in Attributes) + // Perf: Avoid allocating enumerator for `_attributes` if possible + if (_attributes != null && _attributes.Count > 0) { - var key = attribute.Key; - if (string.Equals(key, "id", StringComparison.OrdinalIgnoreCase) && - string.IsNullOrEmpty(attribute.Value)) + foreach (var attribute in Attributes) { - continue; - } + var key = attribute.Key; + if (string.Equals(key, "id", StringComparison.OrdinalIgnoreCase) && + string.IsNullOrEmpty(attribute.Value)) + { + continue; + } - writer.Write(" "); - writer.Write(key); - writer.Write("=\""); - encoder.HtmlEncode(attribute.Value, writer); - writer.Write("\""); + writer.Write(" "); + writer.Write(key); + writer.Write("=\""); + encoder.HtmlEncode(attribute.Value, writer); + writer.Write("\""); + } } } @@ -193,7 +209,8 @@ namespace Microsoft.AspNet.Mvc.Rendering public void MergeAttributes(IDictionary attributes, bool replaceExisting) { - if (attributes != null) + // Perf: Avoid allocating enumerator for `attributes` if possible + if (attributes != null && attributes.Count > 0) { foreach (var entry in attributes) { diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/Resources.resx b/src/Microsoft.AspNet.Mvc.ViewFeatures/Resources.resx index 3abf07efde..eddad01662 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/Resources.resx @@ -274,4 +274,7 @@ The '{0}' cannot serialize an object of type '{1}' to session state. + + The collection already contains an entry with key '{0}'. + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/AttributeDictionary.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/AttributeDictionary.cs new file mode 100644 index 0000000000..5a4a4de110 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/AttributeDictionary.cs @@ -0,0 +1,681 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Mvc.ViewFeatures +{ + /// + /// A dictionary for HTML attributes. + /// + public class AttributeDictionary : IDictionary, IReadOnlyDictionary + { + private List> _items; + + /// + public string this[string key] + { + get + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + var index = Find(key); + if (index < 0) + { + throw new KeyNotFoundException(); + } + else + { + return Get(index).Value; + } + } + + set + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + var item = new KeyValuePair(key, value); + var index = Find(key); + if (index < 0) + { + Insert(~index, item); + } + else + { + Set(index, item); + } + } + } + + /// + public int Count => _items == null ? 0 : _items.Count; + + /// + public bool IsReadOnly + { + get + { + return false; + } + } + + /// + public ICollection Keys + { + get + { + return new KeyCollection(this); + } + } + + /// + public ICollection Values + { + get + { + return new ValueCollection(this); + } + } + + /// + IEnumerable IReadOnlyDictionary.Keys + { + get + { + return new KeyCollection(this); + } + } + + /// + IEnumerable IReadOnlyDictionary.Values + { + get + { + return new ValueCollection(this); + } + } + + private KeyValuePair Get(int index) + { + if (index >= Count) + { + throw new ArgumentOutOfRangeException(); + } + + return _items[index]; + } + + private void Set(int index, KeyValuePair value) + { + if (index > Count) + { + throw new ArgumentOutOfRangeException(); + } + + if (_items == null) + { + _items = new List>(); + } + + _items[index] = value; + } + + private void Insert(int index, KeyValuePair value) + { + if (index > Count) + { + throw new ArgumentOutOfRangeException(); + } + + if (_items == null) + { + _items = new List>(); + } + + _items.Insert(index, value); + } + + private void Remove(int index) + { + if (index >= Count) + { + throw new ArgumentOutOfRangeException(); + } + + + _items.RemoveAt(index); + } + + // This API is a lot like List.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) + { + 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; + } + + /// + public void Clear() + { + if (_items != null) + { + _items.Clear(); + } + } + + /// + public void Add(KeyValuePair item) + { + if (item.Key == null) + { + throw new ArgumentException( + Resources.FormatPropertyOfTypeCannotBeNull( + nameof(KeyValuePair.Key), + nameof(KeyValuePair)), + nameof(item)); + } + + var index = Find(item.Key); + if (index < 0) + { + Insert(~index, item); + } + else + { + throw new InvalidOperationException(Resources.FormatDictionary_DuplicateKey(item.Key)); + } + } + + /// + public void Add(string key, string value) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + Add(new KeyValuePair(key, value)); + } + + /// + public bool Contains(KeyValuePair item) + { + if (item.Key == null) + { + throw new ArgumentException( + Resources.FormatPropertyOfTypeCannotBeNull( + nameof(KeyValuePair.Key), + nameof(KeyValuePair)), + nameof(item)); + } + + var index = Find(item.Key); + if (index < 0) + { + return false; + } + else + { + return string.Equals(item.Value, Get(index).Value, StringComparison.OrdinalIgnoreCase); + } + } + + /// + public bool ContainsKey(string key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (Count == 0) + { + return false; + } + + return Find(key) >= 0; + } + + /// + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + if (arrayIndex < 0 || arrayIndex >= array.Length) + { + throw new IndexOutOfRangeException(); + } + + for (var i = 0; i < Count; i++) + { + array[arrayIndex + i] = Get(i); + } + } + + /// + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + /// + public bool Remove(KeyValuePair item) + { + if (item.Key == null) + { + throw new ArgumentException( + Resources.FormatPropertyOfTypeCannotBeNull( + nameof(KeyValuePair.Key), + nameof(KeyValuePair)), + 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; + } + } + + /// + public bool Remove(string key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + var index = Find(key); + if (index < 0) + { + return false; + } + else + { + Remove(index); + return true; + } + } + + /// + public bool TryGetValue(string key, out string value) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + var index = Find(key); + if (index < 0) + { + value = null; + return false; + } + else + { + value = Get(index).Value; + return true; + } + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + IEnumerator> IEnumerable>.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// An enumerator for . + /// + public struct Enumerator : IEnumerator> + { + private readonly AttributeDictionary _attributes; + + private int _index; + + /// + /// Creates a new . + /// + /// The . + public Enumerator(AttributeDictionary attributes) + { + _attributes = attributes; + + _index = -1; + } + + /// + public KeyValuePair Current + { + get + { + return _attributes.Get(_index); + } + } + + /// + object IEnumerator.Current + { + get + { + return Current; + } + } + + /// + public void Dispose() + { + } + + /// + public bool MoveNext() + { + _index++; + return _index < _attributes.Count; + } + + /// + public void Reset() + { + _index = -1; + } + } + + private class KeyCollection : ICollection + { + 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) + { + if (item == null) + { + throw new ArgumentNullException(nameof(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) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + if (arrayIndex < 0 || arrayIndex >= array.Length) + { + throw new IndexOutOfRangeException(); + } + + for (var i = 0; i < _attributes.Count; i++) + { + array[arrayIndex + i] = _attributes.Get(i).Key; + } + } + + public Enumerator GetEnumerator() + { + return new Enumerator(this._attributes); + } + + public bool Remove(string item) + { + throw new NotSupportedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public struct Enumerator : IEnumerator + { + private readonly AttributeDictionary _attributes; + + private int _index; + + public Enumerator(AttributeDictionary attributes) + { + _attributes = attributes; + + _index = -1; + } + + public string Current + { + get + { + return _attributes.Get(_index).Key; + } + } + + object IEnumerator.Current + { + get + { + return Current; + } + } + + public void Dispose() + { + } + + public bool MoveNext() + { + _index++; + return _index < _attributes.Count; + } + + public void Reset() + { + _index = -1; + } + } + } + + private class ValueCollection : ICollection + { + 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) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + if (arrayIndex < 0 || arrayIndex >= array.Length) + { + throw new IndexOutOfRangeException(); + } + + for (var i = 0; i < _attributes.Count; i++) + { + array[arrayIndex + i] = _attributes.Get(i).Value; + } + } + + public Enumerator GetEnumerator() + { + return new Enumerator(this._attributes); + } + + public bool Remove(string item) + { + throw new NotSupportedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public struct Enumerator : IEnumerator + { + private readonly AttributeDictionary _attributes; + + private int _index; + + public Enumerator(AttributeDictionary attributes) + { + _attributes = attributes; + + _index = -1; + } + + public string Current + { + get + { + return _attributes.Get(_index).Key; + } + } + + object IEnumerator.Current + { + get + { + return Current; + } + } + + public void Dispose() + { + } + + public bool MoveNext() + { + _index++; + return _index < _attributes.Count; + } + + public void Reset() + { + _index = -1; + } + } + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/TagBuilderTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/TagBuilderTest.cs index fa83cf6cfe..948012918f 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/TagBuilderTest.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/TagBuilderTest.cs @@ -27,7 +27,7 @@ namespace Microsoft.AspNet.Mvc.Core.Rendering [Theory] [InlineData(false, "Hello", "World")] - [InlineData(true, "Hello", "something else")] + [InlineData(true, "hello", "something else")] public void MergeAttribute_IgnoresCase(bool replaceExisting, string expectedKey, string expectedValue) { // Arrange @@ -54,7 +54,7 @@ namespace Microsoft.AspNet.Mvc.Core.Rendering // Assert var attribute = Assert.Single(tagBuilder.Attributes); - Assert.Equal(new KeyValuePair("ClaSs", "success btn"), attribute); + Assert.Equal(new KeyValuePair("class", "success btn"), attribute); } [Fact] diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/AttributeDictionaryTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/AttributeDictionaryTest.cs new file mode 100644 index 0000000000..64125efcf6 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/AttributeDictionaryTest.cs @@ -0,0 +1,464 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNet.Mvc.ViewFeatures +{ + public class AttributeDictionaryTest + { + [Fact] + public void AttributeDictionary_AddItems() + { + // Arrange + var attributes = new AttributeDictionary(); + + // Act + attributes.Add("zero", "0"); + attributes.Add("one", "1"); + attributes.Add("two", "2"); + + // Assert + Assert.Equal(3, attributes.Count); + Assert.Collection( + attributes, + kvp => Assert.Equal(new KeyValuePair("one", "1"), kvp), + kvp => Assert.Equal(new KeyValuePair("two", "2"), kvp), + kvp => Assert.Equal(new KeyValuePair("zero", "0"), kvp)); + } + + [Fact] + public void AttributeDictionary_AddItems_AsKeyValuePairs() + { + // Arrange + var attributes = new AttributeDictionary(); + + // Act + attributes.Add(new KeyValuePair("zero", "0")); + attributes.Add(new KeyValuePair("one", "1")); + attributes.Add(new KeyValuePair("two", "2")); + + // Assert + Assert.Equal(3, attributes.Count); + Assert.Collection( + attributes, + kvp => Assert.Equal(new KeyValuePair("one", "1"), kvp), + kvp => Assert.Equal(new KeyValuePair("two", "2"), kvp), + kvp => Assert.Equal(new KeyValuePair("zero", "0"), kvp)); + } + + [Fact] + public void AttributeDictionary_Add_DuplicateKey() + { + // Arrange + var attributes = new AttributeDictionary(); + + attributes.Add("zero", "0"); + attributes.Add("one", "1"); + attributes.Add("two", "2"); + + // Act & Assert + Assert.Throws(() => attributes.Add("one", "15")); + } + + [Fact] + public void AttributeDictionary_Clear() + { + // Arrange + var attributes = new AttributeDictionary(); + + attributes.Add(new KeyValuePair("zero", "0")); + attributes.Add(new KeyValuePair("one", "1")); + attributes.Add(new KeyValuePair("two", "2")); + + // Act + attributes.Clear(); + + // Assert + Assert.Equal(0, attributes.Count); + Assert.Empty(attributes); + } + + [Fact] + public void AttributeDictionary_Contains_Success() + { + // Arrange + var attributes = new AttributeDictionary(); + + attributes.Add(new KeyValuePair("zero", "0")); + attributes.Add(new KeyValuePair("one", "1")); + attributes.Add(new KeyValuePair("two", "2")); + + // Act + var result = attributes.Contains(new KeyValuePair("zero", "0")); + + // Assert + Assert.True(result); + } + + [Fact] + public void AttributeDictionary_Contains_Failure() + { + // Arrange + var attributes = new AttributeDictionary(); + + attributes.Add(new KeyValuePair("zero", "0")); + attributes.Add(new KeyValuePair("one", "1")); + attributes.Add(new KeyValuePair("two", "2")); + + // Act + var result = attributes.Contains(new KeyValuePair("zero", "nada")); + + // Assert + Assert.False(result); + } + + [Fact] + public void AttributeDictionary_ContainsKey_Success() + { + // Arrange + var attributes = new AttributeDictionary(); + + attributes.Add(new KeyValuePair("zero", "0")); + attributes.Add(new KeyValuePair("one", "1")); + attributes.Add(new KeyValuePair("two", "2")); + + // Act + var result = attributes.ContainsKey("one"); + + // Assert + Assert.True(result); + } + + [Fact] + public void AttributeDictionary_ContainsKey_Failure() + { + // Arrange + var attributes = new AttributeDictionary(); + + attributes.Add(new KeyValuePair("zero", "0")); + attributes.Add(new KeyValuePair("one", "1")); + attributes.Add(new KeyValuePair("two", "2")); + + // Act + var result = attributes.ContainsKey("one!"); + + // Assert + Assert.False(result); + } + + [Fact] + public void AttributeDictionary_CopyTo() + { + // Arrange + var attributes = new AttributeDictionary(); + + attributes.Add(new KeyValuePair("zero", "0")); + attributes.Add(new KeyValuePair("one", "1")); + attributes.Add(new KeyValuePair("two", "2")); + + var array = new KeyValuePair[attributes.Count + 1]; + + // Act + attributes.CopyTo(array, 1); + + // Assert + Assert.Collection( + array, + kvp => Assert.Equal(default(KeyValuePair), kvp), + kvp => Assert.Equal(new KeyValuePair("one", "1"), kvp), + kvp => Assert.Equal(new KeyValuePair("two", "2"), kvp), + kvp => Assert.Equal(new KeyValuePair("zero", "0"), kvp)); + } + + [Fact] + public void AttributeDictionary_IsReadOnly() + { + // Arrange + var attributes = new AttributeDictionary(); + + // Act + var result = attributes.IsReadOnly; + + // Assert + Assert.False(result); + } + + [Fact] + public void AttributeDictionary_Keys() + { + // Arrange + var attributes = new AttributeDictionary(); + + attributes.Add(new KeyValuePair("zero", "0")); + attributes.Add(new KeyValuePair("one", "1")); + attributes.Add(new KeyValuePair("two", "2")); + + // Act + var keys = attributes.Keys; + + // Assert + Assert.Equal(3, keys.Count); + Assert.Collection( + keys, + key => Assert.Equal("one", key), + key => Assert.Equal("two", key), + key => Assert.Equal("zero", key)); + } + + [Fact] + public void AttributeDictionary_Remove_Success() + { + // Arrange + var attributes = new AttributeDictionary(); + + attributes.Add(new KeyValuePair("zero", "0")); + attributes.Add(new KeyValuePair("one", "1")); + attributes.Add(new KeyValuePair("two", "2")); + + // Act + var result = attributes.Remove("one"); + + // Assert + Assert.True(result); + Assert.Equal(2, attributes.Count); + Assert.Collection( + attributes, + kvp => Assert.Equal(new KeyValuePair("two", "2"), kvp), + kvp => Assert.Equal(new KeyValuePair("zero", "0"), kvp)); + } + + [Fact] + public void AttributeDictionary_Remove_Failure() + { + // Arrange + var attributes = new AttributeDictionary(); + + attributes.Add(new KeyValuePair("zero", "0")); + attributes.Add(new KeyValuePair("one", "1")); + attributes.Add(new KeyValuePair("two", "2")); + + // Act + var result = attributes.Remove("nada"); + + // Assert + Assert.False(result); + Assert.Equal(3, attributes.Count); + Assert.Collection( + attributes, + kvp => Assert.Equal(new KeyValuePair("one", "1"), kvp), + kvp => Assert.Equal(new KeyValuePair("two", "2"), kvp), + kvp => Assert.Equal(new KeyValuePair("zero", "0"), kvp)); + } + + [Fact] + public void AttributeDictionary_Remove_KeyValuePair_Success() + { + // Arrange + var attributes = new AttributeDictionary(); + + attributes.Add(new KeyValuePair("zero", "0")); + attributes.Add(new KeyValuePair("one", "1")); + attributes.Add(new KeyValuePair("two", "2")); + + // Act + var result = attributes.Remove(new KeyValuePair("one", "1")); + + // Assert + Assert.True(result); + Assert.Equal(2, attributes.Count); + Assert.Collection( + attributes, + kvp => Assert.Equal(new KeyValuePair("two", "2"), kvp), + kvp => Assert.Equal(new KeyValuePair("zero", "0"), kvp)); + } + + [Fact] + public void AttributeDictionary_Remove_KeyValuePair_Failure() + { + // Arrange + var attributes = new AttributeDictionary(); + + attributes.Add(new KeyValuePair("zero", "0")); + attributes.Add(new KeyValuePair("one", "1")); + attributes.Add(new KeyValuePair("two", "2")); + + // Act + var result = attributes.Remove(new KeyValuePair("one", "0")); + + // Assert + Assert.False(result); + Assert.Equal(3, attributes.Count); + Assert.Collection( + attributes, + kvp => Assert.Equal(new KeyValuePair("one", "1"), kvp), + kvp => Assert.Equal(new KeyValuePair("two", "2"), kvp), + kvp => Assert.Equal(new KeyValuePair("zero", "0"), kvp)); + } + + [Fact] + public void AttributeDictionary_TryGetValue_Success() + { + // Arrange + var attributes = new AttributeDictionary(); + + attributes.Add(new KeyValuePair("zero", "0")); + attributes.Add(new KeyValuePair("one", "1")); + attributes.Add(new KeyValuePair("two", "2")); + + string value; + + // Act + var result = attributes.TryGetValue("two", out value); + + // Assert + Assert.True(result); + Assert.Equal("2", value); + } + + [Fact] + public void AttributeDictionary_TryGetValue_Failure() + { + // Arrange + var attributes = new AttributeDictionary(); + + attributes.Add(new KeyValuePair("zero", "0")); + attributes.Add(new KeyValuePair("one", "1")); + attributes.Add(new KeyValuePair("two", "2")); + + string value; + + // Act + var result = attributes.TryGetValue("nada", out value); + + + // Assert + Assert.False(result); + Assert.Null(value); + } + + [Fact] + public void AttributeDictionary_Values() + { + // Arrange + var attributes = new AttributeDictionary(); + + attributes.Add(new KeyValuePair("zero", "0")); + attributes.Add(new KeyValuePair("one", "1")); + attributes.Add(new KeyValuePair("two", "2")); + + // Act + var values = attributes.Values; + + // Assert + Assert.Equal(3, values.Count); + Assert.Collection( + values, + key => Assert.Equal("1", key), + key => Assert.Equal("2", key), + key => Assert.Equal("0", key)); + } + + [Fact] + public void AttributeDictionary_Indexer_Success() + { + // Arrange + var attributes = new AttributeDictionary(); + + attributes.Add(new KeyValuePair("zero", "0")); + attributes.Add(new KeyValuePair("one", "1")); + attributes.Add(new KeyValuePair("two", "2")); + + // Act + var value = attributes["two"]; + + // Assert + Assert.Equal("2", value); + } + + [Fact] + public void AttributeDictionary_Indexer_Fails() + { + // Arrange + var attributes = new AttributeDictionary(); + + attributes.Add(new KeyValuePair("zero", "0")); + attributes.Add(new KeyValuePair("one", "1")); + attributes.Add(new KeyValuePair("two", "2")); + + // Act & Assert + Assert.Throws(() => attributes["nada"]); + } + + [Fact] + public void AttributeDictionary_Indexer_SetValue() + { + // Arrange + var attributes = new AttributeDictionary(); + + attributes.Add(new KeyValuePair("zero", "0")); + attributes.Add(new KeyValuePair("one", "1")); + attributes.Add(new KeyValuePair("two", "2")); + + // Act + attributes["one"] = "1!"; + + // Assert + Assert.Equal(3, attributes.Count); + Assert.Collection( + attributes, + kvp => Assert.Equal(new KeyValuePair("one", "1!"), kvp), + kvp => Assert.Equal(new KeyValuePair("two", "2"), kvp), + kvp => Assert.Equal(new KeyValuePair("zero", "0"), kvp)); + } + + [Fact] + public void AttributeDictionary_Indexer_Insert() + { + // Arrange + var attributes = new AttributeDictionary(); + + attributes.Add(new KeyValuePair("zero", "0")); + attributes.Add(new KeyValuePair("one", "1")); + attributes.Add(new KeyValuePair("two", "2")); + + // Act + attributes["exciting!"] = "1!"; + + // Assert + Assert.Equal(4, attributes.Count); + Assert.Collection( + attributes, + kvp => Assert.Equal(new KeyValuePair("exciting!", "1!"), kvp), + kvp => Assert.Equal(new KeyValuePair("one", "1"), kvp), + kvp => Assert.Equal(new KeyValuePair("two", "2"), kvp), + kvp => Assert.Equal(new KeyValuePair("zero", "0"), kvp)); + } + + [Fact] + public void AttributeDictionary_CaseInsensitive() + { + // Arrange + var attributes = new AttributeDictionary(); + + attributes.Add(new KeyValuePair("zero", "0")); + attributes.Add(new KeyValuePair("one", "1")); + attributes.Add(new KeyValuePair("two", "2")); + + // Act + attributes["oNe"] = "1!"; + + // Assert + Assert.Equal(3, attributes.Count); + Assert.Collection( + attributes, + kvp => Assert.Equal(new KeyValuePair("oNe", "1!"), kvp), + kvp => Assert.Equal(new KeyValuePair("two", "2"), kvp), + kvp => Assert.Equal(new KeyValuePair("zero", "0"), kvp)); + } + } +}