From 6c06c5cc4307a18ad8bfe65bb5eac5effe16da16 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 3 Feb 2026 10:57:36 +0000 Subject: [PATCH 1/3] fix #457 --- .../PublicAPI/PublicAPI.Unshipped.txt | 5 +++ src/NRedisStack/Search/Row.cs | 31 +++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt index 86228029..9de0c462 100644 --- a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt @@ -1,3 +1,8 @@ +NRedisStack.Search.Aggregation.Row.Enumerator +NRedisStack.Search.Aggregation.Row.Enumerator.Current.get -> System.Collections.Generic.KeyValuePair +NRedisStack.Search.Aggregation.Row.Enumerator.Enumerator() -> void +NRedisStack.Search.Aggregation.Row.Enumerator.MoveNext() -> bool +NRedisStack.Search.Aggregation.Row.GetEnumerator() -> NRedisStack.Search.Aggregation.Row.Enumerator NRedisStack.Search.Parameters static NRedisStack.Search.Parameters.From(T obj) -> System.Collections.Generic.IReadOnlyDictionary! [NRS001]abstract NRedisStack.Search.VectorData.AsRedisValue() -> StackExchange.Redis.RedisValue diff --git a/src/NRedisStack/Search/Row.cs b/src/NRedisStack/Search/Row.cs index 3bbf451d..b35a9f4c 100644 --- a/src/NRedisStack/Search/Row.cs +++ b/src/NRedisStack/Search/Row.cs @@ -1,8 +1,9 @@ -using StackExchange.Redis; +using System.Collections; +using StackExchange.Redis; namespace NRedisStack.Search.Aggregation; -public readonly struct Row +public readonly struct Row : IEnumerable> { private readonly Dictionary _fields; @@ -18,4 +19,30 @@ internal Row(Dictionary fields) public string? GetString(string key) => _fields.TryGetValue(key, out var result) ? result.ToString() : default; public long GetLong(string key) => _fields.TryGetValue(key, out var result) ? (long)(RedisValue)result : default; public double GetDouble(string key) => _fields.TryGetValue(key, out var result) ? (double)(RedisValue)result : default; + + /// + /// Access the fields as a sequence of key/value pairs. + /// + public Enumerator GetEnumerator() => new(in this); + + public struct Enumerator : IEnumerator> + { + private Dictionary.Enumerator _enumerator; + internal Enumerator(in Row row) => _enumerator = row._fields?.GetEnumerator() ?? default; + + /// + public bool MoveNext() => _enumerator.MoveNext(); + + /// + public KeyValuePair Current => _enumerator.Current; + + void IEnumerator.Reset() => throw new NotSupportedException(); + + object IEnumerator.Current => Current; + + void IDisposable.Dispose() => _enumerator.Dispose(); + } + + IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } \ No newline at end of file From e611ca139d337b5f692e1f8935c45a5e5965775d Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 3 Feb 2026 11:28:22 +0000 Subject: [PATCH 2/3] add unit test --- .../PublicAPI/PublicAPI.Unshipped.txt | 3 +- src/NRedisStack/Search/Row.cs | 46 +++++++++++++-- .../Search/AggregationResultUnitTests.cs | 57 +++++++++++++++++++ 3 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 tests/NRedisStack.Tests/Search/AggregationResultUnitTests.cs diff --git a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt index 9de0c462..4b6d98dd 100644 --- a/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/NRedisStack/PublicAPI/PublicAPI.Unshipped.txt @@ -1,7 +1,8 @@ NRedisStack.Search.Aggregation.Row.Enumerator -NRedisStack.Search.Aggregation.Row.Enumerator.Current.get -> System.Collections.Generic.KeyValuePair +NRedisStack.Search.Aggregation.Row.Enumerator.Current.get -> System.Collections.Generic.KeyValuePair NRedisStack.Search.Aggregation.Row.Enumerator.Enumerator() -> void NRedisStack.Search.Aggregation.Row.Enumerator.MoveNext() -> bool +NRedisStack.Search.Aggregation.Row.FieldCount() -> int NRedisStack.Search.Aggregation.Row.GetEnumerator() -> NRedisStack.Search.Aggregation.Row.Enumerator NRedisStack.Search.Parameters static NRedisStack.Search.Parameters.From(T obj) -> System.Collections.Generic.IReadOnlyDictionary! diff --git a/src/NRedisStack/Search/Row.cs b/src/NRedisStack/Search/Row.cs index b35a9f4c..b6e446b9 100644 --- a/src/NRedisStack/Search/Row.cs +++ b/src/NRedisStack/Search/Row.cs @@ -3,12 +3,13 @@ namespace NRedisStack.Search.Aggregation; -public readonly struct Row : IEnumerable> +public readonly struct Row : IEnumerable> { private readonly Dictionary _fields; internal Row(Dictionary fields) { + // note: only RedisValue fields are expected and exposed, due to how AggregationResult is constructed _fields = fields; } @@ -20,21 +21,56 @@ internal Row(Dictionary fields) public long GetLong(string key) => _fields.TryGetValue(key, out var result) ? (long)(RedisValue)result : default; public double GetDouble(string key) => _fields.TryGetValue(key, out var result) ? (double)(RedisValue)result : default; + /// + /// Gets the number of fields in this row. + /// + public int FieldCount() + { + // only include RedisValue fields, since nested aggregates are not supported via this API + var count = 0; + foreach (var field in _fields) + { + if (field.Value is RedisValue) + { + count++; + } + } + return count; + } + /// /// Access the fields as a sequence of key/value pairs. /// public Enumerator GetEnumerator() => new(in this); - public struct Enumerator : IEnumerator> + public struct Enumerator : IEnumerator> { private Dictionary.Enumerator _enumerator; internal Enumerator(in Row row) => _enumerator = row._fields?.GetEnumerator() ?? default; /// - public bool MoveNext() => _enumerator.MoveNext(); + public bool MoveNext() + { + while (_enumerator.MoveNext()) + { + if (_enumerator.Current.Value is RedisValue) + { + return true; + } + } + + return false; + } /// - public KeyValuePair Current => _enumerator.Current; + public KeyValuePair Current + { + get + { + var pair = _enumerator.Current; + return pair.Value is RedisValue value ? new(pair.Key, value) : default; + } + } void IEnumerator.Reset() => throw new NotSupportedException(); @@ -43,6 +79,6 @@ public struct Enumerator : IEnumerator> void IDisposable.Dispose() => _enumerator.Dispose(); } - IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); + IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } \ No newline at end of file diff --git a/tests/NRedisStack.Tests/Search/AggregationResultUnitTests.cs b/tests/NRedisStack.Tests/Search/AggregationResultUnitTests.cs new file mode 100644 index 00000000..1fecd008 --- /dev/null +++ b/tests/NRedisStack.Tests/Search/AggregationResultUnitTests.cs @@ -0,0 +1,57 @@ +using NRedisStack.Search.Aggregation; +using StackExchange.Redis; +using Xunit; + +namespace NRedisStack.Tests.Search; + +public class AggregationResultUnitTests +{ + [Fact] + public void CanIterateRow() + { + Row row = new(new() + { + // the fields must all be RedisValue internally, else ignored + { "name", (RedisValue)"John" }, + { "age", (RedisValue)30 }, + { "ignored", new RedisValue[] { "a", "b", "c" } }, + { "city", (RedisValue)"New York" }, + }); + + // test via Row + Assert.Equal(3, row.FieldCount()); + Assert.Equal("John", row.GetString("name")); + Assert.Equal(30, row.GetLong("age")); + Assert.Equal("New York", row.GetString("city")); + + // test via foreach + int count = 0; + foreach (var field in row) + { + switch (field.Key) + { + case "name": + Assert.Equal("John", (string?)field.Value); + break; + case "age": + Assert.Equal(30, (int)field.Value); + break; + case "city": + Assert.Equal("New York", (string?)field.Value); + break; + default: + Assert.Fail($"Unexpected field: {field.Key}"); + break; + } + count++; + } + Assert.Equal(3, count); + + // test via Select + var isolated = row.Select(x => (x.Key, x.Value)).ToArray(); + Assert.Equal(3, isolated.Length); + Assert.Contains(("name", (RedisValue)"John"), isolated); + Assert.Contains(("age", (RedisValue)30), isolated); + Assert.Contains(("city", (RedisValue)"New York"), isolated); + } +} \ No newline at end of file From 8b29132705a938ade468d231b108acda3bec618b Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 3 Feb 2026 11:35:11 +0000 Subject: [PATCH 3/3] dotnet format --- src/NRedisStack/Search/Row.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NRedisStack/Search/Row.cs b/src/NRedisStack/Search/Row.cs index b6e446b9..8374e45b 100644 --- a/src/NRedisStack/Search/Row.cs +++ b/src/NRedisStack/Search/Row.cs @@ -73,7 +73,7 @@ public KeyValuePair Current } void IEnumerator.Reset() => throw new NotSupportedException(); - + object IEnumerator.Current => Current; void IDisposable.Dispose() => _enumerator.Dispose();