You have the following code:


public async Task RunJobs()
{
    var jobs = Enumerable.Range(1, 20).Select(DoSomeWork);
}


// Simulates some "real" work
private static async Task<(string JobName, TimeSpan ExecutionTime)> DoSomeWork(int jobIndex)
{
    var stopwatch = Stopwatch.StartNew();
    await Task.Delay(Random.Shared.Next(100, 250));
    stopwatch.Stop(); 
    return ($"Job {jobIndex}", stopwatch.Elapsed);
}

You want to act on each task as soon as it completes, rather than waiting for all tasks to complete.

A solution to this problem could look like this:

var jobs = Enumerable.Range(1, 20).Select(DoSomeWork).ToList();

while (jobs.Any())
{
    var completedTask = await Task.WhenAny(jobs);
    jobs.Remove(completedTask);
    var result = await completedTask;
    Console.WriteLine($"Job done: {result.JobName} - Time: {result.ExecutionTime.TotalMilliseconds}ms");
}

This approach works, but it requires some "setup".

Task.WhenEach

Starting with .NET 9, you can solve this by using Task.WhenEach.

var jobs = Enumerable.Range(1, 20).Select(DoSomeWork);
await foreach (var job in Task.WhenEach(jobs))
{
    var result = await job;
    Console.WriteLine($"{result.JobName} is done, took {result.ExecutionTime.TotalMilliseconds}ms");
}

Since Task.WhenEach returns an IAsyncEnumerable<Task>, you can use await foreach. This loop will iterate over all the completed tasks as they finish, allowing you to act on finished tasks right away.