Home > Software engineering >  C# .NET 6 - How to await for an operation with async lambda on an IEnumerated to finish
C# .NET 6 - How to await for an operation with async lambda on an IEnumerated to finish

Time:01-17

I am having issues making a leaderboard for a Discord bot's minigame.

For context: I have a small collection of 'users', which, as the name implies, contains a selection of users of the system, represented by a 'User' object.

I'd like to obtain the top 10 users in this collection by their amount of 'sushi', a score if you will. This sushi is individually fetched from a database (don't worry about perf, the database is on SSD, on the same box, and the collection of users that this code will go through is very trimmed down)

IEnumerable<User> topUsers = users.OrderByDescending(async x => await FishEngine.GetItem(x, "sushi")).Take(10);

If I was doing this in a non-async way, sorting by anything that's locally stored in the user object and doesn't need to be awaited, this works perfectly fine. But when I do this, there is an exception in foreach, which indicates the collection topUsers doesn't contain users, but rather what seem to be tasks.

Why is this happening if I am awaiting? What should I do to solve this?

Thanks beforehand.

PS, because I know someone will suggest this: I understand that at this point it'd be faster, both as a solution and potentially for perf too (though as I indicated before perf here is not a realistic issue) to just query everything through SQL and call it a day, but I truly wish to understand what I am doing wrong here with the async lambda. Cheers.

Edit: Because GetItem was requested

public static async Task<int> GetItem(User dbUser, string name)
    {
        return await UserEngine._interface.FetchInventoryItem(dbUser, name);
    }

Don't worry about going any deeper than that. This function is used elsewhere and works fine.

CodePudding user response:

Enumerable methods such as OrderBy must either be translated into SQL (as part of a query) or operate on in-memory data. The best way to solve this is to translate it into SQL, but since you're interested in the other way, you would load all the values into memory first and then order. You can do this by creating a sequence of tasks and then using Task.WhenAll:

var getSushiTasks = users.Select(x => FishEngine.GetItem(x, "sushi")).ToList();
var userSushi = await Task.WhenAll(getSushiTasks);
var usersWithSushi = users.Zip(userSushi, (User, Sushi) => (User, Sushi));
var topUsers = usersWithSushi.OrderByDescending(x => x.Sushi).Take(10);

Of course, this is horribly inefficient, since you're loading every user's sushi individually just to compare them and take the top 10 in memory. Again, the proper solution is to do this in SQL.

CodePudding user response:

async x => await FishEngine.GetItem(x, "sushi") is almost the same as x => FishEngine.GetItem(x, "sushi"): the await is used within an async method, so the result must still be awaited in one way or another to obtain a value. That async makes your lambda async, i.e. it returns Task<User> rather than User as you seem to expect. A Task<User> is sort of equivalent to User on a high level of abstraction, but they are not interchangable. The same is true for Task<int> and int: FishEngine.GetItem does not return an integer but (to use a bit of JS terminology) a promise to deliver an integer at some later point, so you have to await the promises returned by the database engine for every user to be able to sort users by sushi. Stephen Cleary's answer shows you how to do it.

CodePudding user response:

The code you provided will not work as expected because the OrderByDescending method does not support async lambda expressions.

To obtain the top 10 users in the collection based on their 'sushi' score, you can first fetch the 'sushi' scores for all the users in parallel using Task.WhenAll and then use the OrderByDescending method to order the users based on their 'sushi' scores. Finally, use the Take method to get the top 10 users. Here's an example:

using System;
using System.Linq;
using System.Threading.Tasks;

class Example
{
    public static async Task Main()
    {
        var users = new List<User> { /* users */ };

        // Fetch sushi scores for all users in parallel
        var sushiTasks = users.Select(async user => new { User = user, Sushi = await FishEngine.GetItem(user, "sushi") });
        var sushiScores = await Task.WhenAll(sushiTasks);

        // Order users by sushi scores and take the top 10
        var topUsers = sushiScores.OrderByDescending(x => x.Sushi).Take(10).Select(x => x.User);

        // Do something with the top users
        foreach (var user in topUsers)
        {
            Console.WriteLine(user.Name);
        }
    }
}

This script will fetch the sushi scores for all the users in parallel using the Task.WhenAll method, then it will order the users based on the obtained sushi scores, and then it will take the top 10 users.

Keep in mind that Task.WhenAll will wait for all the tasks to complete before it continues, this means that it will execute all the calls to the database in parallel but will wait for all of them to complete before continuing with the next step.

Also, it's important to note that ordering a large collection of items based on a specific property can be a performance-intensive task, you should consider this if your collection of users is very large.

  • Related