13
\$\begingroup\$

I'm trying to do something like this:

var loudDogs = dogs.Where(async d => await d.IsYappyAsync);

The "IsYappyAsync" property would return a Task<bool>.

Obviously this isn't supported, so instead I've built an extension method called WhereAsync.

public static async Task<IEnumerable<T>> WhereAsync<T>(this IEnumerable<T> items, Func<T, Task<bool>> predicate)
{
    var results = new List<T>();
    var tasks = new List<Task<bool>>();

    foreach (var item in items)
    {
        var task = predicate.Invoke(item);
        tasks.Add(task);
    }

    var predicateResults = await Task.WhenAll<bool>(tasks);

    var counter = 0;
    foreach (var item in items)
    {
        var predicateResult = predicateResults.ElementAt(counter);

        if (predicateResult)
            results.Add(item);

        counter++;
    }

    return results.AsEnumerable();
}

This probably isn't the best approach, but I'm at a loss for something better. Any thoughts?

\$\endgroup\$

1 Answer 1

18
\$\begingroup\$

There are several ways to achieve what you're after and it depends on whether you want the results drip fed to you as they're available or whether you're happy to have them all in one bang. The way you've implemented your method gives it all in one bang - which is fine.

A shorter implementation could be

public static async Task<IEnumerable<T>> WhereAsync2<T>(this IEnumerable<T> items, Func<T, Task<bool>> predicate)
{
    var itemTaskList = items.Select(item=> new {Item = item, PredTask = predicate.Invoke(item)}).ToList();
    await Task.WhenAll(itemTaskList.Select(x=>x.PredTask));
    return itemTaskList.Where(x=>x.PredTask.Result).Select(x=>x.Item);
}
  1. It builds a list of an anonymous type where the type contains the item and the Task.

  2. It then waits for all of the tasks to complete.

  3. It then goes through that list from (1) and picks out the items that had a true result from the Task.

The other advantage is that you get rid of all of that counter and ElementAt() stuff. Either way still builds a complete List for the items in the given Enumerable...

Aside: You could investigate something like Rx if you wanted the results via an IObservable<T> instead. The result wouldn't even be async Task - it would just be an IObservable.

\$\endgroup\$
6
  • 1
    \$\begingroup\$ This is nice, bookmarked for later usage, thank you \$\endgroup\$ Oct 3, 2013 at 10:57
  • 1
    \$\begingroup\$ I don't like this approach. Using .Result is most likely never the correct answer. \$\endgroup\$
    – El Mac
    Sep 6, 2019 at 14:05
  • \$\begingroup\$ Thabks for the comment all these years later 😉The tasks are known to be complete, so it's safe in this context. I appreciate that's a distinction often missed by many, but it's OK in this context. (eventually, somewhere in the code, even if it's not your code but something invoked by the async/await state machine, something is calling .Result to actually get the result) \$\endgroup\$
    – Ian Yates
    Sep 7, 2019 at 20:34
  • \$\begingroup\$ Agree with @ElMac. Using .Result is usually a code smell, and in this case it certainly is. It is definitely not "safe" or "OK in this context". You have made the code synchronous as .Result blocks. You are also ignoring the return of await WhenAll which is also a code smell. The result of await WhenAll is exactly what you should be performing the Where on. \$\endgroup\$
    – Neo
    Nov 15, 2020 at 20:04
  • \$\begingroup\$ @neo, Thanks for the comment. .Result is synchronous, and blocks if there's no result yet. We know that every single one of these Tasks has a result. That's what awaiting Task.WhenAll is doing in this code. It is "OK in this context" - sorry you disagree. As it turns out, I'd probably write this differently all these years later (and would've at least deliberately noted via comment that .Result was intentional), but it's not broken Using Tasks that return an anonymous type, with a bool member, would allow for the filtering you suggest and is probably nicer overall. \$\endgroup\$
    – Ian Yates
    Nov 16, 2020 at 5:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.