Here is a generic method Zip
that you could use, implemented as an iterator. The cancellationToken
is decorated with the EnumeratorCancellation
attribute, so that the resulting IAsyncEnumerable
is WithCancellation
friendly.
using System.Runtime.CompilerServices;
public static async IAsyncEnumerable<TSource[]> Zip<TSource>(
IEnumerable<IAsyncEnumerable<TSource>> sources,
[EnumeratorCancellation]CancellationToken cancellationToken = default)
{
var enumerators = sources
.Select(x => x.GetAsyncEnumerator(cancellationToken))
.ToArray();
try
{
while (true)
{
var array = new TSource[enumerators.Length];
for (int i = 0; i < enumerators.Length; i++)
{
if (!await enumerators[i].MoveNextAsync().ConfigureAwait(false))
yield break;
array[i] = enumerators[i].Current;
}
yield return array;
}
}
finally
{
foreach (var enumerator in enumerators)
{
await enumerator.DisposeAsync().ConfigureAwait(false);
}
}
}
Usage example:
await foreach (int[] result in Zip(asyncEnumerables))
{
Console.WriteLine($"Result: {String.Join(", ", result)}");
}
This implementation is not concurrent. The MoveNextAsync
operations are launched subsequently, the one after the completion of the other. Making a concurrent implementation (ZipConcurrent
) is possible, and shouldn't be particularly difficult, but it should be done with caution.
This implementation is also not 100% robust. It assumes that no GetAsyncEnumerator
call can fail, and no DisposeAsync
call can fail synchronously or asynchronously, otherwise some enumerators might be left undisposed. These are reasonable assumptions, but a perfectly robust implementation shouldn't rely on any of those.