Skip to content

Commit 1967e0b

Browse files
thomhurstclaude
andcommitted
feat: Enhance IAsyncEnumerable support with full parallel processing capabilities
- Add unbounded parallel processing support for IAsyncEnumerable - Implement ProcessInParallel() overloads matching IEnumerable API - ProcessInParallel() - unbounded concurrency - ProcessInParallel(int?) - configurable concurrency limit - ProcessInParallel(bool) - thread pool scheduling option - ProcessInParallel(int?, bool) - full control - Add batch processing support with ProcessInBatches(int) - Update AsyncEnumerableParallelProcessor to handle unbounded and limited concurrency - Add AsyncEnumerableBatchProcessor for batch-based processing - Update result processors to support new processing modes - Add comprehensive unit tests for all new features - Maintain backward compatibility with existing API This provides complete feature parity between IAsyncEnumerable and IEnumerable processing, allowing developers to use the same fluent API patterns for both collection types. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e25fa7b commit 1967e0b

7 files changed

Lines changed: 485 additions & 70 deletions

File tree

EnumerableAsyncProcessor.UnitTests/AsyncEnumerableProcessorTests.cs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,145 @@ public async Task ForEachAsync_WithException_PropagatesException()
215215
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () => await task);
216216
await Assert.That(exception!.Message).IsEqualTo("Test exception");
217217
}
218+
219+
[Test]
220+
public async Task ForEachAsync_ProcessInParallel_UnboundedConcurrency_ProcessesAllItems()
221+
{
222+
var processedItems = new List<int>();
223+
var asyncEnumerable = GenerateAsyncEnumerable(50);
224+
225+
await asyncEnumerable
226+
.ForEachAsync(async item =>
227+
{
228+
await Task.Delay(5);
229+
lock (processedItems)
230+
{
231+
processedItems.Add(item);
232+
}
233+
})
234+
.ProcessInParallel() // Unbounded concurrency
235+
.ExecuteAsync();
236+
237+
await Assert.That(processedItems.Count).IsEqualTo(50);
238+
await Assert.That(processedItems.OrderBy(x => x)).IsEquivalentTo(Enumerable.Range(1, 50));
239+
}
240+
241+
[Test]
242+
public async Task SelectAsync_ProcessInParallel_UnboundedConcurrency_ReturnsAllResults()
243+
{
244+
var asyncEnumerable = GenerateAsyncEnumerable(30);
245+
246+
var results = await asyncEnumerable
247+
.SelectAsync(async item =>
248+
{
249+
await Task.Delay(5);
250+
return item * 2;
251+
})
252+
.ProcessInParallel() // Unbounded concurrency
253+
.ExecuteAsync()
254+
.ToListAsync();
255+
256+
await Assert.That(results.Count).IsEqualTo(30);
257+
await Assert.That(results.OrderBy(x => x)).IsEquivalentTo(Enumerable.Range(1, 30).Select(x => x * 2));
258+
}
259+
260+
[Test]
261+
public async Task ForEachAsync_ProcessInParallel_WithThreadPoolScheduling_ProcessesAllItems()
262+
{
263+
var processedItems = new List<int>();
264+
var asyncEnumerable = GenerateAsyncEnumerable(20);
265+
266+
await asyncEnumerable
267+
.ForEachAsync(async item =>
268+
{
269+
await Task.Delay(5);
270+
lock (processedItems)
271+
{
272+
processedItems.Add(item);
273+
}
274+
})
275+
.ProcessInParallel(scheduleOnThreadPool: true)
276+
.ExecuteAsync();
277+
278+
await Assert.That(processedItems.Count).IsEqualTo(20);
279+
await Assert.That(processedItems.OrderBy(x => x)).IsEquivalentTo(Enumerable.Range(1, 20));
280+
}
281+
282+
[Test]
283+
public async Task ForEachAsync_ProcessInBatches_ProcessesAllItemsInBatches()
284+
{
285+
var processedBatches = new List<int>();
286+
var asyncEnumerable = GenerateAsyncEnumerable(25);
287+
288+
await asyncEnumerable
289+
.ForEachAsync(async item =>
290+
{
291+
await Task.Delay(5);
292+
lock (processedBatches)
293+
{
294+
processedBatches.Add(item);
295+
}
296+
})
297+
.ProcessInBatches(5)
298+
.ExecuteAsync();
299+
300+
await Assert.That(processedBatches.Count).IsEqualTo(25);
301+
await Assert.That(processedBatches.OrderBy(x => x)).IsEquivalentTo(Enumerable.Range(1, 25));
302+
}
303+
304+
[Test]
305+
public async Task SelectAsync_ProcessInBatches_ReturnsAllResultsInBatches()
306+
{
307+
var asyncEnumerable = GenerateAsyncEnumerable(23);
308+
309+
var results = await asyncEnumerable
310+
.SelectAsync(async item =>
311+
{
312+
await Task.Delay(5);
313+
return item * 3;
314+
})
315+
.ProcessInBatches(5)
316+
.ExecuteAsync()
317+
.ToListAsync();
318+
319+
await Assert.That(results.Count).IsEqualTo(23);
320+
// Batches maintain order within batch, so results should be in order
321+
await Assert.That(results).IsEquivalentTo(Enumerable.Range(1, 23).Select(x => x * 3));
322+
}
323+
324+
[Test]
325+
public async Task ProcessInParallel_NullableConcurrency_WorksCorrectly()
326+
{
327+
var asyncEnumerable = GenerateAsyncEnumerable(15);
328+
var processedCount = 0;
329+
330+
// Test with null concurrency (unbounded)
331+
await asyncEnumerable
332+
.ForEachAsync(async item =>
333+
{
334+
await Task.Delay(5);
335+
Interlocked.Increment(ref processedCount);
336+
})
337+
.ProcessInParallel((int?)null)
338+
.ExecuteAsync();
339+
340+
await Assert.That(processedCount).IsEqualTo(15);
341+
342+
// Reset and test with specified concurrency
343+
processedCount = 0;
344+
asyncEnumerable = GenerateAsyncEnumerable(15);
345+
346+
await asyncEnumerable
347+
.ForEachAsync(async item =>
348+
{
349+
await Task.Delay(5);
350+
Interlocked.Increment(ref processedCount);
351+
})
352+
.ProcessInParallel((int?)5)
353+
.ExecuteAsync();
354+
355+
await Assert.That(processedCount).IsEqualTo(15);
356+
}
218357
}
219358

220359
internal static class AsyncEnumerableExtensionsForTests

EnumerableAsyncProcessor/Builders/AsyncEnumerableActionAsyncProcessorBuilder_1.cs

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,54 @@ public AsyncEnumerableActionAsyncProcessorBuilder(
2121
}
2222

2323
/// <summary>
24-
/// Process items in parallel with a specified level of parallelism.
24+
/// Process items in parallel without concurrency limits.
2525
/// </summary>
26+
/// <returns>An async processor configured for parallel execution.</returns>
27+
public IAsyncEnumerableProcessor ProcessInParallel()
28+
{
29+
return ProcessInParallel(null, false);
30+
}
31+
32+
/// <summary>
33+
/// Process items in parallel without concurrency limits.
34+
/// </summary>
35+
/// <param name="scheduleOnThreadPool">If true, schedules tasks on thread pool to prevent blocking. Default is false for maximum performance.</param>
36+
/// <returns>An async processor configured for parallel execution.</returns>
37+
public IAsyncEnumerableProcessor ProcessInParallel(bool scheduleOnThreadPool)
38+
{
39+
return ProcessInParallel(null, scheduleOnThreadPool);
40+
}
41+
42+
/// <summary>
43+
/// Process items in parallel with specified concurrency limit.
44+
/// </summary>
45+
/// <param name="maxConcurrency">Maximum concurrent operations.</param>
46+
/// <returns>An async processor configured for parallel execution.</returns>
2647
public IAsyncEnumerableProcessor ProcessInParallel(int maxConcurrency)
2748
{
28-
return new AsyncEnumerableParallelProcessor<TInput>(
29-
_items, _taskSelector, maxConcurrency, _cancellationTokenSource);
49+
return ProcessInParallel((int?)maxConcurrency, false);
3050
}
3151

3252
/// <summary>
33-
/// Process items in parallel with default concurrency (processor count).
53+
/// Process items in parallel with specified concurrency limit.
3454
/// </summary>
35-
public IAsyncEnumerableProcessor ProcessInParallel()
55+
/// <param name="maxConcurrency">Maximum concurrent operations.</param>
56+
/// <returns>An async processor configured for parallel execution.</returns>
57+
public IAsyncEnumerableProcessor ProcessInParallel(int? maxConcurrency)
58+
{
59+
return ProcessInParallel(maxConcurrency, false);
60+
}
61+
62+
/// <summary>
63+
/// Process items in parallel with specified concurrency limit.
64+
/// </summary>
65+
/// <param name="maxConcurrency">Maximum concurrent operations.</param>
66+
/// <param name="scheduleOnThreadPool">If true, schedules tasks on thread pool to prevent blocking.</param>
67+
/// <returns>An async processor configured for parallel execution.</returns>
68+
public IAsyncEnumerableProcessor ProcessInParallel(int? maxConcurrency, bool scheduleOnThreadPool)
3669
{
37-
return ProcessInParallel(Environment.ProcessorCount);
70+
return new AsyncEnumerableParallelProcessor<TInput>(
71+
_items, _taskSelector, maxConcurrency, scheduleOnThreadPool, _cancellationTokenSource);
3872
}
3973

4074

@@ -46,6 +80,17 @@ public IAsyncEnumerableProcessor ProcessOneAtATime()
4680
return new AsyncEnumerableOneAtATimeProcessor<TInput>(
4781
_items, _taskSelector, _cancellationTokenSource);
4882
}
83+
84+
/// <summary>
85+
/// Process items in batches.
86+
/// </summary>
87+
/// <param name="batchSize">The size of each batch.</param>
88+
/// <returns>An async processor configured for batch execution.</returns>
89+
public IAsyncEnumerableProcessor ProcessInBatches(int batchSize)
90+
{
91+
return new AsyncEnumerableBatchProcessor<TInput>(
92+
_items, _taskSelector, batchSize, _cancellationTokenSource);
93+
}
4994

5095
}
5196
#endif

EnumerableAsyncProcessor/Builders/AsyncEnumerableActionAsyncProcessorBuilder_2.cs

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,54 @@ public AsyncEnumerableActionAsyncProcessorBuilder(
2121
}
2222

2323
/// <summary>
24-
/// Process items in parallel with a specified level of parallelism and return results.
24+
/// Process items in parallel without concurrency limits and return results.
2525
/// </summary>
26+
/// <returns>An async processor configured for parallel execution.</returns>
27+
public IAsyncEnumerableProcessor<TOutput> ProcessInParallel()
28+
{
29+
return ProcessInParallel(null, false);
30+
}
31+
32+
/// <summary>
33+
/// Process items in parallel without concurrency limits and return results.
34+
/// </summary>
35+
/// <param name="scheduleOnThreadPool">If true, schedules tasks on thread pool to prevent blocking. Default is false for maximum performance.</param>
36+
/// <returns>An async processor configured for parallel execution.</returns>
37+
public IAsyncEnumerableProcessor<TOutput> ProcessInParallel(bool scheduleOnThreadPool)
38+
{
39+
return ProcessInParallel(null, scheduleOnThreadPool);
40+
}
41+
42+
/// <summary>
43+
/// Process items in parallel with specified concurrency limit and return results.
44+
/// </summary>
45+
/// <param name="maxConcurrency">Maximum concurrent operations.</param>
46+
/// <returns>An async processor configured for parallel execution.</returns>
2647
public IAsyncEnumerableProcessor<TOutput> ProcessInParallel(int maxConcurrency)
2748
{
28-
return new ResultAsyncEnumerableParallelProcessor<TInput, TOutput>(
29-
_items, _taskSelector, maxConcurrency, _cancellationTokenSource);
49+
return ProcessInParallel((int?)maxConcurrency, false);
3050
}
3151

3252
/// <summary>
33-
/// Process items in parallel with default concurrency and return results.
53+
/// Process items in parallel with specified concurrency limit and return results.
3454
/// </summary>
35-
public IAsyncEnumerableProcessor<TOutput> ProcessInParallel()
55+
/// <param name="maxConcurrency">Maximum concurrent operations.</param>
56+
/// <returns>An async processor configured for parallel execution.</returns>
57+
public IAsyncEnumerableProcessor<TOutput> ProcessInParallel(int? maxConcurrency)
58+
{
59+
return ProcessInParallel(maxConcurrency, false);
60+
}
61+
62+
/// <summary>
63+
/// Process items in parallel with specified concurrency limit and return results.
64+
/// </summary>
65+
/// <param name="maxConcurrency">Maximum concurrent operations.</param>
66+
/// <param name="scheduleOnThreadPool">If true, schedules tasks on thread pool to prevent blocking.</param>
67+
/// <returns>An async processor configured for parallel execution.</returns>
68+
public IAsyncEnumerableProcessor<TOutput> ProcessInParallel(int? maxConcurrency, bool scheduleOnThreadPool)
3669
{
37-
return ProcessInParallel(Environment.ProcessorCount);
70+
return new ResultAsyncEnumerableParallelProcessor<TInput, TOutput>(
71+
_items, _taskSelector, maxConcurrency, scheduleOnThreadPool, _cancellationTokenSource);
3872
}
3973

4074

@@ -46,6 +80,17 @@ public IAsyncEnumerableProcessor<TOutput> ProcessOneAtATime()
4680
return new ResultAsyncEnumerableOneAtATimeProcessor<TInput, TOutput>(
4781
_items, _taskSelector, _cancellationTokenSource);
4882
}
83+
84+
/// <summary>
85+
/// Process items in batches and return results.
86+
/// </summary>
87+
/// <param name="batchSize">The size of each batch.</param>
88+
/// <returns>An async processor configured for batch execution.</returns>
89+
public IAsyncEnumerableProcessor<TOutput> ProcessInBatches(int batchSize)
90+
{
91+
return new ResultAsyncEnumerableBatchProcessor<TInput, TOutput>(
92+
_items, _taskSelector, batchSize, _cancellationTokenSource);
93+
}
4994

5095
}
5196
#endif
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#if NET6_0_OR_GREATER
2+
using EnumerableAsyncProcessor.Extensions;
3+
4+
namespace EnumerableAsyncProcessor.RunnableProcessors.AsyncEnumerable;
5+
6+
public class AsyncEnumerableBatchProcessor<TInput> : IAsyncEnumerableProcessor
7+
{
8+
private readonly IAsyncEnumerable<TInput> _items;
9+
private readonly Func<TInput, Task> _taskSelector;
10+
private readonly int _batchSize;
11+
private readonly CancellationTokenSource _cancellationTokenSource;
12+
13+
internal AsyncEnumerableBatchProcessor(
14+
IAsyncEnumerable<TInput> items,
15+
Func<TInput, Task> taskSelector,
16+
int batchSize,
17+
CancellationTokenSource cancellationTokenSource)
18+
{
19+
_items = items;
20+
_taskSelector = taskSelector;
21+
_batchSize = batchSize;
22+
_cancellationTokenSource = cancellationTokenSource;
23+
}
24+
25+
public async Task ExecuteAsync()
26+
{
27+
var cancellationToken = _cancellationTokenSource.Token;
28+
var batch = new List<TInput>(_batchSize);
29+
30+
await foreach (var item in _items.WithCancellation(cancellationToken).ConfigureAwait(false))
31+
{
32+
batch.Add(item);
33+
34+
if (batch.Count >= _batchSize)
35+
{
36+
await ProcessBatch(batch, cancellationToken).ConfigureAwait(false);
37+
batch = new List<TInput>(_batchSize);
38+
}
39+
}
40+
41+
// Process any remaining items in the final batch
42+
if (batch.Count > 0)
43+
{
44+
await ProcessBatch(batch, cancellationToken).ConfigureAwait(false);
45+
}
46+
}
47+
48+
private async Task ProcessBatch(List<TInput> batch, CancellationToken cancellationToken)
49+
{
50+
var tasks = batch.Select(item => _taskSelector(item)).ToArray();
51+
await Task.WhenAll(tasks).ConfigureAwait(false);
52+
}
53+
}
54+
#endif

0 commit comments

Comments
 (0)