Skip to content

Commit 6b88a39

Browse files
thomhurstclaude
andcommitted
feat: Add SelectMany and SelectManyAsync extension methods
- Add SelectMany for IAsyncEnumerable to flatten IEnumerable and IAsyncEnumerable results - Add SelectManyAsync for IAsyncEnumerable to flatten async projections - Add SelectManyAsync for IEnumerable to support async flattening operations - Include array-specific overloads for automatic type inference - Add comprehensive unit tests for all SelectMany variations - Add example demonstrating usage patterns The array overloads enable type inference to work automatically when returning arrays from async lambdas, eliminating the need for explicit type parameters in common scenarios. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent c1e7400 commit 6b88a39

File tree

4 files changed

+510
-0
lines changed

4 files changed

+510
-0
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Runtime.CompilerServices;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using EnumerableAsyncProcessor.Extensions;
8+
9+
namespace EnumerableAsyncProcessor.Example;
10+
11+
public static class SelectManyExample
12+
{
13+
private static async IAsyncEnumerable<int> GenerateAsyncEnumerable(int count, [EnumeratorCancellation] CancellationToken cancellationToken = default)
14+
{
15+
for (int i = 1; i <= count; i++)
16+
{
17+
await Task.Yield();
18+
yield return i;
19+
}
20+
}
21+
22+
public static async Task RunExample()
23+
{
24+
Console.WriteLine("SelectMany Extension Examples");
25+
Console.WriteLine("=============================\n");
26+
27+
// Example 1: SelectMany on IAsyncEnumerable with IEnumerable
28+
Console.WriteLine("Example 1: IAsyncEnumerable.SelectMany with IEnumerable:");
29+
var asyncEnum = GenerateAsyncEnumerable(3);
30+
var results1 = new List<int>();
31+
await foreach (var item in asyncEnum.SelectMany(x => Enumerable.Range(x * 10, 2)))
32+
{
33+
results1.Add(item);
34+
}
35+
Console.WriteLine($"Input: [1, 2, 3] -> Output: [{string.Join(", ", results1)}]");
36+
37+
// Example 2: SelectManyAsync on IAsyncEnumerable with Task<IEnumerable>
38+
Console.WriteLine("\nExample 2: IAsyncEnumerable.SelectManyAsync with Task<IEnumerable>:");
39+
var asyncEnum2 = GenerateAsyncEnumerable(3);
40+
var results2 = new List<int>();
41+
await foreach (var item in asyncEnum2.SelectManyAsync(async x =>
42+
{
43+
await Task.Delay(10);
44+
return new[] { x * 100, x * 100 + 1 };
45+
}))
46+
{
47+
results2.Add(item);
48+
}
49+
Console.WriteLine($"Input: [1, 2, 3] -> Output: [{string.Join(", ", results2)}]");
50+
51+
// Example 3: SelectManyAsync on IEnumerable with IAsyncEnumerable
52+
Console.WriteLine("\nExample 3: IEnumerable.SelectManyAsync with IAsyncEnumerable:");
53+
var enumerable = Enumerable.Range(1, 3);
54+
var results3 = new List<int>();
55+
await foreach (var item in enumerable.SelectManyAsync(x => GenerateAsyncEnumerable(2)))
56+
{
57+
results3.Add(item);
58+
}
59+
Console.WriteLine($"Input: [1, 2, 3] -> Output: [{string.Join(", ", results3)}]");
60+
61+
// Example 4: Flattening nested collections
62+
Console.WriteLine("\nExample 4: Flattening nested async collections:");
63+
var categories = new[] { "A", "B" };
64+
var results4 = new List<string>();
65+
await foreach (var item in categories.SelectManyAsync(async cat =>
66+
{
67+
await Task.Delay(10); // Simulate async work
68+
return Enumerable.Range(1, 3).Select(n => $"{cat}{n}").ToArray();
69+
}))
70+
{
71+
results4.Add(item);
72+
}
73+
Console.WriteLine($"Categories: [A, B] -> Flattened: [{string.Join(", ", results4)}]");
74+
}
75+
}
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Runtime.CompilerServices;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using EnumerableAsyncProcessor.Extensions;
8+
using TUnit.Assertions;
9+
using TUnit.Core;
10+
11+
namespace EnumerableAsyncProcessor.UnitTests;
12+
13+
public class SelectManyExtensionsTests
14+
{
15+
private static async IAsyncEnumerable<int> GenerateAsyncEnumerable(int count, [EnumeratorCancellation] CancellationToken cancellationToken = default)
16+
{
17+
for (int i = 1; i <= count; i++)
18+
{
19+
await Task.Yield();
20+
cancellationToken.ThrowIfCancellationRequested();
21+
yield return i;
22+
}
23+
}
24+
25+
private static async IAsyncEnumerable<int> GenerateAsyncRange(int start, int count, [EnumeratorCancellation] CancellationToken cancellationToken = default)
26+
{
27+
for (int i = start; i < start + count; i++)
28+
{
29+
await Task.Yield();
30+
cancellationToken.ThrowIfCancellationRequested();
31+
yield return i;
32+
}
33+
}
34+
35+
[Test]
36+
public async Task SelectMany_IAsyncEnumerable_WithIEnumerable_FlattensResults()
37+
{
38+
var asyncEnumerable = GenerateAsyncEnumerable(3);
39+
40+
var results = new List<int>();
41+
await foreach (var item in asyncEnumerable.SelectMany(x => Enumerable.Range(x * 10, 3)))
42+
{
43+
results.Add(item);
44+
}
45+
46+
// Input: [1, 2, 3]
47+
// Output: [10,11,12, 20,21,22, 30,31,32]
48+
await Assert.That(results).IsEquivalentTo(new[] { 10, 11, 12, 20, 21, 22, 30, 31, 32 });
49+
}
50+
51+
[Test]
52+
public async Task SelectMany_IAsyncEnumerable_WithIAsyncEnumerable_FlattensResults()
53+
{
54+
var asyncEnumerable = GenerateAsyncEnumerable(3);
55+
56+
var results = new List<int>();
57+
await foreach (var item in asyncEnumerable.SelectMany(x => GenerateAsyncRange(x * 10, 2)))
58+
{
59+
results.Add(item);
60+
}
61+
62+
// Input: [1, 2, 3]
63+
// Output: [10,11, 20,21, 30,31]
64+
await Assert.That(results).IsEquivalentTo(new[] { 10, 11, 20, 21, 30, 31 });
65+
}
66+
67+
[Test]
68+
public async Task SelectManyAsync_IAsyncEnumerable_WithTaskIEnumerable_FlattensResults()
69+
{
70+
var asyncEnumerable = GenerateAsyncEnumerable(3);
71+
72+
var results = new List<string>();
73+
await foreach (var item in asyncEnumerable.SelectManyAsync(async x =>
74+
{
75+
await Task.Delay(10);
76+
return Enumerable.Range(x * 10, 2).Select(n => n.ToString());
77+
}))
78+
{
79+
results.Add(item);
80+
}
81+
82+
// Input: [1, 2, 3]
83+
// Output: ["10","11", "20","21", "30","31"]
84+
await Assert.That(results).IsEquivalentTo(new[] { "10", "11", "20", "21", "30", "31" });
85+
}
86+
87+
[Test]
88+
public async Task SelectManyAsync_IAsyncEnumerable_WithTaskIAsyncEnumerable_FlattensResults()
89+
{
90+
var asyncEnumerable = GenerateAsyncEnumerable(2);
91+
92+
var results = new List<int>();
93+
await foreach (var item in asyncEnumerable.SelectManyAsync(async x =>
94+
{
95+
await Task.Delay(10);
96+
return GenerateAsyncRange(x * 100, 3);
97+
}))
98+
{
99+
results.Add(item);
100+
}
101+
102+
// Input: [1, 2]
103+
// Output: [100,101,102, 200,201,202]
104+
await Assert.That(results).IsEquivalentTo(new[] { 100, 101, 102, 200, 201, 202 });
105+
}
106+
107+
[Test]
108+
public async Task SelectManyAsync_IEnumerable_WithIAsyncEnumerable_FlattensResults()
109+
{
110+
var enumerable = Enumerable.Range(1, 3);
111+
112+
var results = new List<int>();
113+
await foreach (var item in enumerable.SelectManyAsync(x => GenerateAsyncRange(x * 10, 2)))
114+
{
115+
results.Add(item);
116+
}
117+
118+
// Input: [1, 2, 3]
119+
// Output: [10,11, 20,21, 30,31]
120+
await Assert.That(results).IsEquivalentTo(new[] { 10, 11, 20, 21, 30, 31 });
121+
}
122+
123+
[Test]
124+
public async Task SelectManyAsync_IEnumerable_WithTaskIEnumerable_FlattensResults()
125+
{
126+
var enumerable = Enumerable.Range(1, 3);
127+
128+
var results = new List<string>();
129+
await foreach (var item in enumerable.SelectManyAsync(async x =>
130+
{
131+
await Task.Delay(10);
132+
return new[] { $"A{x}", $"B{x}" };
133+
}))
134+
{
135+
results.Add(item);
136+
}
137+
138+
// Input: [1, 2, 3]
139+
// Output: ["A1","B1", "A2","B2", "A3","B3"]
140+
await Assert.That(results).IsEquivalentTo(new[] { "A1", "B1", "A2", "B2", "A3", "B3" });
141+
}
142+
143+
[Test]
144+
public async Task SelectManyAsync_IEnumerable_WithTaskIAsyncEnumerable_FlattensResults()
145+
{
146+
var enumerable = Enumerable.Range(1, 2);
147+
148+
var results = new List<int>();
149+
await foreach (var item in enumerable.SelectManyAsync(async x =>
150+
{
151+
await Task.Delay(10);
152+
return GenerateAsyncRange(x * 100, 3);
153+
}))
154+
{
155+
results.Add(item);
156+
}
157+
158+
// Input: [1, 2]
159+
// Output: [100,101,102, 200,201,202]
160+
await Assert.That(results).IsEquivalentTo(new[] { 100, 101, 102, 200, 201, 202 });
161+
}
162+
163+
[Test]
164+
public async Task SelectMany_WithEmptySubCollections_HandlesCorrectly()
165+
{
166+
var asyncEnumerable = GenerateAsyncEnumerable(3);
167+
168+
var results = new List<int>();
169+
await foreach (var item in asyncEnumerable.SelectMany(x =>
170+
x == 2 ? Enumerable.Empty<int>() : new[] { x * 10 }))
171+
{
172+
results.Add(item);
173+
}
174+
175+
// Input: [1, 2, 3]
176+
// Output: [10, 30] (2 produces empty)
177+
await Assert.That(results).IsEquivalentTo(new[] { 10, 30 });
178+
}
179+
180+
[Test]
181+
public async Task SelectMany_WithCancellation_ThrowsOperationCanceledException()
182+
{
183+
using var cts = new CancellationTokenSource();
184+
var asyncEnumerable = GenerateAsyncEnumerable(100);
185+
186+
cts.CancelAfter(50);
187+
188+
var results = new List<int>();
189+
await Assert.ThrowsAsync<OperationCanceledException>(async () =>
190+
{
191+
await foreach (var item in asyncEnumerable.SelectMany(
192+
x => Enumerable.Range(x * 10, 10), cts.Token))
193+
{
194+
results.Add(item);
195+
await Task.Delay(10);
196+
}
197+
});
198+
}
199+
200+
[Test]
201+
public async Task SelectManyAsync_HandlesExceptions()
202+
{
203+
var asyncEnumerable = GenerateAsyncEnumerable(3);
204+
205+
var results = asyncEnumerable.SelectManyAsync(async x =>
206+
{
207+
if (x == 2)
208+
{
209+
throw new InvalidOperationException("Test exception");
210+
}
211+
await Task.Delay(10);
212+
return new[] { x * 10 };
213+
});
214+
215+
var items = new List<int>();
216+
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
217+
{
218+
await foreach (var item in results)
219+
{
220+
items.Add(item);
221+
}
222+
});
223+
224+
// Should have processed first item before exception
225+
await Assert.That(items).IsEquivalentTo(new[] { 10 });
226+
}
227+
228+
[Test]
229+
public async Task SelectMany_ComplexNesting_WorksCorrectly()
230+
{
231+
// Test a more complex scenario with nested SelectMany using LINQ's built-in
232+
var data = new[] { 1, 2 };
233+
234+
var results = System.Linq.Enumerable.SelectMany(data, x =>
235+
System.Linq.Enumerable.SelectMany(Enumerable.Range(1, 2), y =>
236+
new[] { $"{x}-{y}-A", $"{x}-{y}-B" })).ToList();
237+
238+
await Assert.That(results).IsEquivalentTo(new[]
239+
{
240+
"1-1-A", "1-1-B", "1-2-A", "1-2-B",
241+
"2-1-A", "2-1-B", "2-2-A", "2-2-B"
242+
});
243+
}
244+
}

0 commit comments

Comments
 (0)