From 6cf49568b2bf4d622f7242d61e9ded15741c608c Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Fri, 1 Nov 2024 09:04:30 -0400 Subject: [PATCH] Improve AdditionalPropertiesDictionary - Add a strongly-typed Enumerator - Add a TryAdd method - Add a DebuggerDisplay for Count - Add a DebuggerTypeProxy for the collection of properties --- .../AdditionalPropertiesDictionary.cs | 92 ++++++++++++++++++- .../AdditionalPropertiesDictionaryTests.cs | 41 +++++++++ 2 files changed, 131 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/AdditionalPropertiesDictionary.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/AdditionalPropertiesDictionary.cs index 616ad284198..4a681d4679a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/AdditionalPropertiesDictionary.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/AdditionalPropertiesDictionary.cs @@ -4,13 +4,21 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; +using Microsoft.Shared.Diagnostics; + +#pragma warning disable S1144 // Unused private types or members should be removed +#pragma warning disable S2365 // Properties should not make collection or array copies +#pragma warning disable S3604 // Member initializer values should not be redundant namespace Microsoft.Extensions.AI; /// Provides a dictionary used as the AdditionalProperties dictionary on Microsoft.Extensions.AI objects. +[DebuggerTypeProxy(typeof(DebugView))] +[DebuggerDisplay("Count = {Count}")] public sealed class AdditionalPropertiesDictionary : IDictionary, IReadOnlyDictionary { /// The underlying dictionary. @@ -77,6 +85,25 @@ public object? this[string key] /// public void Add(string key, object? value) => _dictionary.Add(key, value); + /// Attempts to add the specified key and value to the dictionary. + /// The key of the element to add. + /// The value of the element to add. + /// if the key/value pair was added to the dictionary successfully; otherwise, . + public bool TryAdd(string key, object? value) + { +#if NET + return _dictionary.TryAdd(key, value); +#else + if (!_dictionary.ContainsKey(key)) + { + _dictionary.Add(key, value); + return true; + } + + return false; +#endif + } + /// void ICollection>.Add(KeyValuePair item) => ((ICollection>)_dictionary).Add(item); @@ -93,11 +120,17 @@ public object? this[string key] void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => ((ICollection>)_dictionary).CopyTo(array, arrayIndex); + /// + /// Returns an enumerator that iterates through the . + /// + /// An that enumerates the contents of the . + public Enumerator GetEnumerator() => new(_dictionary.GetEnumerator()); + /// - public IEnumerator> GetEnumerator() => _dictionary.GetEnumerator(); + IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); /// - IEnumerator IEnumerable.GetEnumerator() => _dictionary.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// public bool Remove(string key) => _dictionary.Remove(key); @@ -156,4 +189,59 @@ public bool TryGetValue(string key, [NotNullWhen(true)] out T? value) value = default; return false; } + + /// Enumerates the elements of an . + public struct Enumerator : IEnumerator> + { + /// The wrapped dictionary enumerator. + private Dictionary.Enumerator _dictionaryEnumerator; + + /// Initializes a new instance of the struct with the dictionary enumerator to wrap. + /// The dictionary enumerator to wrap. + internal Enumerator(Dictionary.Enumerator dictionaryEnumerator) + { + _dictionaryEnumerator = dictionaryEnumerator; + } + + /// + public KeyValuePair Current => _dictionaryEnumerator.Current; + + /// + object IEnumerator.Current => Current; + + /// + public void Dispose() => _dictionaryEnumerator.Dispose(); + + /// + public bool MoveNext() => _dictionaryEnumerator.MoveNext(); + + /// + public void Reset() => Reset(ref _dictionaryEnumerator); + + /// Calls on an enumerator. + private static void Reset(ref TEnumerator enumerator) + where TEnumerator : struct, IEnumerator + { + enumerator.Reset(); + } + } + + /// Provides a debugger view for the collection. + private sealed class DebugView(AdditionalPropertiesDictionary properties) + { + private readonly AdditionalPropertiesDictionary _properties = Throw.IfNull(properties); + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public AdditionalProperty[] Items => (from p in _properties select new AdditionalProperty(p.Key, p.Value)).ToArray(); + + [DebuggerDisplay("{Value}", Name = "[{Key}]")] + public readonly struct AdditionalProperty(string key, object? value) + { + [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] + public string Key { get; } = key; + + [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] + public object? Value { get; } = value; + } + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AdditionalPropertiesDictionaryTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AdditionalPropertiesDictionaryTests.cs index a9a544c8ca8..09f515fa066 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AdditionalPropertiesDictionaryTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AdditionalPropertiesDictionaryTests.cs @@ -90,4 +90,45 @@ static void AssertNotFound(T1 input) Assert.Equal(default(T2), value); } } + + [Fact] + public void TryAdd_AddsOnlyIfNonExistent() + { + AdditionalPropertiesDictionary d = []; + + Assert.False(d.ContainsKey("key")); + Assert.True(d.TryAdd("key", "value")); + Assert.True(d.ContainsKey("key")); + Assert.Equal("value", d["key"]); + + Assert.False(d.TryAdd("key", "value2")); + Assert.True(d.ContainsKey("key")); + Assert.Equal("value", d["key"]); + } + + [Fact] + public void Enumerator_EnumeratesAllItems() + { + AdditionalPropertiesDictionary d = []; + + const int NumProperties = 10; + for (int i = 0; i < NumProperties; i++) + { + d.Add($"key{i}", $"value{i}"); + } + + Assert.Equal(NumProperties, d.Count); + + // This depends on an implementation detail of the ordering in which the dictionary + // enumerates items. If that ever changes, this test will need to be updated. + int count = 0; + foreach (KeyValuePair item in d) + { + Assert.Equal($"key{count}", item.Key); + Assert.Equal($"value{count}", item.Value); + count++; + } + + Assert.Equal(NumProperties, count); + } }