Is calling FIrst and then Skip(1) on IAsyncEnumerable OK?

186 Views Asked by At

I have an IAsyncEnumerable where I need to store all the results in some DB, but I also need to apply some special processing to the first item. I did it this way:

var someAsyncEnumerable = GetSomeAsyncEnumerable(cancellationToken);

var firstValue = await someAsyncEnumerable.FirstOrDefaultAsync(cancellationToken);

if (firstValue is not null)
{
   // log something about first value
   await HandleItem(fitstValue);
}

await foreach(var value in someAsyncEnumerable.Skip(1).WithCancellation(cancellationToken))
{
   await HandleItem(value);
}

private async Task HandleItem(Item item) 
{
   // store item in DB
}

With code like this I'm getting a warning:

Possible multiple enumeration

I wonder, is this just a warning that I can entirely ignore in this case, or is my usage of the enumerable wrong?

Let's assume that my IAsyncEnumerable shouldn't be enumerated twice, because it's inefficient, or even breaking.

1

There are 1 best solutions below

3
Jon Skeet On BEST ANSWER

I wonder, is this just a warning that I can entirely ignore in this case, or is my usage of the enumerable wrong?

"Wrong" is a strong word - but it could certainly cause problems. You're enumerating the result twice, which is fine in some cases, and problematic in others.

As you've said:

Let's assume that my IAsyncEnumerable shouldn't be enumerated twice, because it's inefficient, or even breaking.

... then yes, you've got a problem. Because you absolutely are triggering enumeration twice.

It would be better to enumerate the sequence once, keeping track of whether you're looking at the first item or not and acting accordingly:

var someAsyncEnumerable = GetSomeAsyncEnumerable(cancellationToken);
bool first = true;
await foreach (var value in someAsyncEnumerable)
{
   if (first)
   {
       // Special handling of value
       ...
       // For subsequent loop iterations, skip this bit
       first = false;
   }
   else
   {
       await HandleItem(value);    
   }
}

Note that here I've assumed that you want an entirely different path for the first item. If you actually just want to do extra work beforehand, you don't need the else block:

var someAsyncEnumerable = GetSomeAsyncEnumerable(cancellationToken);
bool first = true;
await foreach (var value in someAsyncEnumerable)
{
   if (first)
   {
       // Special handling of value
       ...
       // For subsequent loop iterations, skip this bit
       first = false;
   }
   // Regardless of whether this is the first iteration or not,
   // handle the item.
   await HandleItem(value);    
}

Alternatively, you could potentially expand the foreach loop yourself into MoveNext calls etc, but that's a bit more error-prone.