Skip to content

Commit

Permalink
Improve AdditionalPropertiesDictionary (#5593)
Browse files Browse the repository at this point in the history
- Add a strongly-typed Enumerator
- Add a TryAdd method
- Add a DebuggerDisplay for Count
- Add a DebuggerTypeProxy for the collection of properties
  • Loading branch information
stephentoub authored Nov 1, 2024
1 parent 0672220 commit 53783e7
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

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

/// <summary>Attempts to add the specified key and value to the dictionary.</summary>
/// <param name="key">The key of the element to add.</param>
/// <param name="value">The value of the element to add.</param>
/// <returns><see langword="true"/> if the key/value pair was added to the dictionary successfully; otherwise, <see langword="false"/>.</returns>
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
}

/// <inheritdoc />
void ICollection<KeyValuePair<string, object?>>.Add(KeyValuePair<string, object?> item) => ((ICollection<KeyValuePair<string, object?>>)_dictionary).Add(item);

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

/// <summary>
/// Returns an enumerator that iterates through the <see cref="AdditionalPropertiesDictionary"/>.
/// </summary>
/// <returns>An <see cref="AdditionalPropertiesDictionary.Enumerator"/> that enumerates the contents of the <see cref="AdditionalPropertiesDictionary"/>.</returns>
public Enumerator GetEnumerator() => new(_dictionary.GetEnumerator());

/// <inheritdoc />
public IEnumerator<KeyValuePair<string, object?>> GetEnumerator() => _dictionary.GetEnumerator();
IEnumerator<KeyValuePair<string, object?>> IEnumerable<KeyValuePair<string, object?>>.GetEnumerator() => GetEnumerator();

/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => _dictionary.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

/// <inheritdoc />
public bool Remove(string key) => _dictionary.Remove(key);
Expand Down Expand Up @@ -156,4 +189,59 @@ public bool TryGetValue<T>(string key, [NotNullWhen(true)] out T? value)
value = default;
return false;
}

/// <summary>Enumerates the elements of an <see cref="AdditionalPropertiesDictionary"/>.</summary>
public struct Enumerator : IEnumerator<KeyValuePair<string, object?>>
{
/// <summary>The wrapped dictionary enumerator.</summary>
private Dictionary<string, object?>.Enumerator _dictionaryEnumerator;

/// <summary>Initializes a new instance of the <see cref="Enumerator"/> struct with the dictionary enumerator to wrap.</summary>
/// <param name="dictionaryEnumerator">The dictionary enumerator to wrap.</param>
internal Enumerator(Dictionary<string, object?>.Enumerator dictionaryEnumerator)
{
_dictionaryEnumerator = dictionaryEnumerator;
}

/// <inheritdoc />
public KeyValuePair<string, object?> Current => _dictionaryEnumerator.Current;

/// <inheritdoc />
object IEnumerator.Current => Current;

/// <inheritdoc />
public void Dispose() => _dictionaryEnumerator.Dispose();

/// <inheritdoc />
public bool MoveNext() => _dictionaryEnumerator.MoveNext();

/// <inheritdoc />
public void Reset() => Reset(ref _dictionaryEnumerator);

/// <summary>Calls <see cref="IEnumerator.Reset"/> on an enumerator.</summary>
private static void Reset<TEnumerator>(ref TEnumerator enumerator)
where TEnumerator : struct, IEnumerator
{
enumerator.Reset();
}
}

/// <summary>Provides a debugger view for the collection.</summary>
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,45 @@ static void AssertNotFound<T1, T2>(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<string, object?> item in d)
{
Assert.Equal($"key{count}", item.Key);
Assert.Equal($"value{count}", item.Value);
count++;
}

Assert.Equal(NumProperties, count);
}
}

0 comments on commit 53783e7

Please sign in to comment.