Skip to content

Commit 53783e7

Browse files
authored
Improve AdditionalPropertiesDictionary (#5593)
- Add a strongly-typed Enumerator - Add a TryAdd method - Add a DebuggerDisplay for Count - Add a DebuggerTypeProxy for the collection of properties
1 parent 0672220 commit 53783e7

File tree

2 files changed

+131
-2
lines changed

2 files changed

+131
-2
lines changed

src/Libraries/Microsoft.Extensions.AI.Abstractions/AdditionalPropertiesDictionary.cs

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,21 @@
44
using System;
55
using System.Collections;
66
using System.Collections.Generic;
7+
using System.Diagnostics;
78
using System.Diagnostics.CodeAnalysis;
89
using System.Globalization;
910
using System.Linq;
11+
using Microsoft.Shared.Diagnostics;
12+
13+
#pragma warning disable S1144 // Unused private types or members should be removed
14+
#pragma warning disable S2365 // Properties should not make collection or array copies
15+
#pragma warning disable S3604 // Member initializer values should not be redundant
1016

1117
namespace Microsoft.Extensions.AI;
1218

1319
/// <summary>Provides a dictionary used as the AdditionalProperties dictionary on Microsoft.Extensions.AI objects.</summary>
20+
[DebuggerTypeProxy(typeof(DebugView))]
21+
[DebuggerDisplay("Count = {Count}")]
1422
public sealed class AdditionalPropertiesDictionary : IDictionary<string, object?>, IReadOnlyDictionary<string, object?>
1523
{
1624
/// <summary>The underlying dictionary.</summary>
@@ -77,6 +85,25 @@ public object? this[string key]
7785
/// <inheritdoc />
7886
public void Add(string key, object? value) => _dictionary.Add(key, value);
7987

88+
/// <summary>Attempts to add the specified key and value to the dictionary.</summary>
89+
/// <param name="key">The key of the element to add.</param>
90+
/// <param name="value">The value of the element to add.</param>
91+
/// <returns><see langword="true"/> if the key/value pair was added to the dictionary successfully; otherwise, <see langword="false"/>.</returns>
92+
public bool TryAdd(string key, object? value)
93+
{
94+
#if NET
95+
return _dictionary.TryAdd(key, value);
96+
#else
97+
if (!_dictionary.ContainsKey(key))
98+
{
99+
_dictionary.Add(key, value);
100+
return true;
101+
}
102+
103+
return false;
104+
#endif
105+
}
106+
80107
/// <inheritdoc />
81108
void ICollection<KeyValuePair<string, object?>>.Add(KeyValuePair<string, object?> item) => ((ICollection<KeyValuePair<string, object?>>)_dictionary).Add(item);
82109

@@ -93,11 +120,17 @@ public object? this[string key]
93120
void ICollection<KeyValuePair<string, object?>>.CopyTo(KeyValuePair<string, object?>[] array, int arrayIndex) =>
94121
((ICollection<KeyValuePair<string, object?>>)_dictionary).CopyTo(array, arrayIndex);
95122

123+
/// <summary>
124+
/// Returns an enumerator that iterates through the <see cref="AdditionalPropertiesDictionary"/>.
125+
/// </summary>
126+
/// <returns>An <see cref="AdditionalPropertiesDictionary.Enumerator"/> that enumerates the contents of the <see cref="AdditionalPropertiesDictionary"/>.</returns>
127+
public Enumerator GetEnumerator() => new(_dictionary.GetEnumerator());
128+
96129
/// <inheritdoc />
97-
public IEnumerator<KeyValuePair<string, object?>> GetEnumerator() => _dictionary.GetEnumerator();
130+
IEnumerator<KeyValuePair<string, object?>> IEnumerable<KeyValuePair<string, object?>>.GetEnumerator() => GetEnumerator();
98131

99132
/// <inheritdoc />
100-
IEnumerator IEnumerable.GetEnumerator() => _dictionary.GetEnumerator();
133+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
101134

102135
/// <inheritdoc />
103136
public bool Remove(string key) => _dictionary.Remove(key);
@@ -156,4 +189,59 @@ public bool TryGetValue<T>(string key, [NotNullWhen(true)] out T? value)
156189
value = default;
157190
return false;
158191
}
192+
193+
/// <summary>Enumerates the elements of an <see cref="AdditionalPropertiesDictionary"/>.</summary>
194+
public struct Enumerator : IEnumerator<KeyValuePair<string, object?>>
195+
{
196+
/// <summary>The wrapped dictionary enumerator.</summary>
197+
private Dictionary<string, object?>.Enumerator _dictionaryEnumerator;
198+
199+
/// <summary>Initializes a new instance of the <see cref="Enumerator"/> struct with the dictionary enumerator to wrap.</summary>
200+
/// <param name="dictionaryEnumerator">The dictionary enumerator to wrap.</param>
201+
internal Enumerator(Dictionary<string, object?>.Enumerator dictionaryEnumerator)
202+
{
203+
_dictionaryEnumerator = dictionaryEnumerator;
204+
}
205+
206+
/// <inheritdoc />
207+
public KeyValuePair<string, object?> Current => _dictionaryEnumerator.Current;
208+
209+
/// <inheritdoc />
210+
object IEnumerator.Current => Current;
211+
212+
/// <inheritdoc />
213+
public void Dispose() => _dictionaryEnumerator.Dispose();
214+
215+
/// <inheritdoc />
216+
public bool MoveNext() => _dictionaryEnumerator.MoveNext();
217+
218+
/// <inheritdoc />
219+
public void Reset() => Reset(ref _dictionaryEnumerator);
220+
221+
/// <summary>Calls <see cref="IEnumerator.Reset"/> on an enumerator.</summary>
222+
private static void Reset<TEnumerator>(ref TEnumerator enumerator)
223+
where TEnumerator : struct, IEnumerator
224+
{
225+
enumerator.Reset();
226+
}
227+
}
228+
229+
/// <summary>Provides a debugger view for the collection.</summary>
230+
private sealed class DebugView(AdditionalPropertiesDictionary properties)
231+
{
232+
private readonly AdditionalPropertiesDictionary _properties = Throw.IfNull(properties);
233+
234+
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
235+
public AdditionalProperty[] Items => (from p in _properties select new AdditionalProperty(p.Key, p.Value)).ToArray();
236+
237+
[DebuggerDisplay("{Value}", Name = "[{Key}]")]
238+
public readonly struct AdditionalProperty(string key, object? value)
239+
{
240+
[DebuggerBrowsable(DebuggerBrowsableState.Collapsed)]
241+
public string Key { get; } = key;
242+
243+
[DebuggerBrowsable(DebuggerBrowsableState.Collapsed)]
244+
public object? Value { get; } = value;
245+
}
246+
}
159247
}

test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AdditionalPropertiesDictionaryTests.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,45 @@ static void AssertNotFound<T1, T2>(T1 input)
9090
Assert.Equal(default(T2), value);
9191
}
9292
}
93+
94+
[Fact]
95+
public void TryAdd_AddsOnlyIfNonExistent()
96+
{
97+
AdditionalPropertiesDictionary d = [];
98+
99+
Assert.False(d.ContainsKey("key"));
100+
Assert.True(d.TryAdd("key", "value"));
101+
Assert.True(d.ContainsKey("key"));
102+
Assert.Equal("value", d["key"]);
103+
104+
Assert.False(d.TryAdd("key", "value2"));
105+
Assert.True(d.ContainsKey("key"));
106+
Assert.Equal("value", d["key"]);
107+
}
108+
109+
[Fact]
110+
public void Enumerator_EnumeratesAllItems()
111+
{
112+
AdditionalPropertiesDictionary d = [];
113+
114+
const int NumProperties = 10;
115+
for (int i = 0; i < NumProperties; i++)
116+
{
117+
d.Add($"key{i}", $"value{i}");
118+
}
119+
120+
Assert.Equal(NumProperties, d.Count);
121+
122+
// This depends on an implementation detail of the ordering in which the dictionary
123+
// enumerates items. If that ever changes, this test will need to be updated.
124+
int count = 0;
125+
foreach (KeyValuePair<string, object?> item in d)
126+
{
127+
Assert.Equal($"key{count}", item.Key);
128+
Assert.Equal($"value{count}", item.Value);
129+
count++;
130+
}
131+
132+
Assert.Equal(NumProperties, count);
133+
}
93134
}

0 commit comments

Comments
 (0)