Skip to content

Instantly share code, notes, and snippets.

@idea-lei
Created March 11, 2025 17:16
Show Gist options
  • Save idea-lei/69a6fe397dea76ff3d4c9e1399addaa1 to your computer and use it in GitHub Desktop.
Save idea-lei/69a6fe397dea76ff3d4c9e1399addaa1 to your computer and use it in GitHub Desktop.
AsyncFuncExtensions

AsyncFuncExtensions

A static class containing extensions for Func<..., Task> that have multiple handlers registered. (Typically when e.g., func += anotherFunc)

These methods would solve following problems:

  • parallel invocation (Task.WhenAll), even for sync actions.
  • Waiting for Execution
  • Returning AggregateException instead of the first inner exception (default by await)

Currently support generic version up to 3 arguments.

Following options are supported:

  • WhenAll: run tasks until all finished. Throw AggregateException at the end if exception(s) occurred.
  • WhenAny and throw AggregateException on first exception: Rest tasks will keep running at background, or you cancel them if your funcs support CancellationToken.
  • Sequential: run tasks in registration sequence. throw on first exception. Rest tasks will not be executed. not recommended due to low efficiency.

Usage Example

Func<Task> waitDelegates = null!;
waitDelegates += async () => await Utils.DelayAndPrint(3000);
waitDelegates += async () =>
{
    await Utils.DelayAndPrint(200);
    throw new Exception("exception from 2nd handler");
};
waitDelegates += async () =>
{
    await Utils.DelayAndPrint(500);
    throw new Exception("exception from 3rd handler");
};
waitDelegates += async () => await Utils.DelayAndPrint(1000);

try
{
    await waitDelegates.InvokeAsync(true); // first exception will be thrown, and the rest tasks keep running in background (or you cancel them by yourself)
}
catch(AggregateException ex)
{
    Console.WriteLine($"exception count: {ex.InnerExceptions.Count}"); // 1
    await Task.Delay(3000);
}

// await waitDelegates(); // does not work as expected, only the last handler will be awaited and others run in background, exceptions will be swallowed
// await waitDelegates.InvokeAsync(); // exception will be thrown when all operations are finished
// await waitDelegates.InvokeAsync(true); // first exception will be thrown, and the rest tasks keep running in background (or you cancel them by yourself)
// await waitDelegates.SequentialInvokeAsync(); // first exception will be thrown, and the rest tasks will not be executed.

Console.WriteLine("WaitDelegates finished");

The Utils class:

internal static class Utils
{
    public static void SleepAndPrint(int milliSec)
    {
        Console.WriteLine($"Start sleep for {milliSec}ms, current thread: {Environment.CurrentManagedThreadId}");
        Thread.Sleep(milliSec);
        Console.WriteLine($"Stop sleep for {milliSec}ms, current thread: {Environment.CurrentManagedThreadId}");
    }

    public static async Task DelayAndPrint(int milliSec, CancellationToken ct = default)
    {
        Console.WriteLine($"Start delay for {milliSec}ms, current thread: {Environment.CurrentManagedThreadId}");
        await Task.Delay(milliSec, ct);
        Console.WriteLine($"Stop delay for {milliSec}ms, current thread: {Environment.CurrentManagedThreadId}");
    }
}

Remarks

  • Convention: CancellationToken should always be the last or the second last (only when the last is params) argument of the func if there is any.
public static class AsyncFuncExtensions
{
private static async Task RunTasksAsync(IEnumerable<Task> tasks, bool throwOnFirstException = false)
{
var taskList = tasks.ToList();
if (taskList.Count == 0) return;
if (throwOnFirstException)
{
while (taskList.Count > 0)
{
var completedTask = await Task.WhenAny(taskList);
taskList.Remove(completedTask);
if (completedTask.IsFaulted)
throw completedTask.Exception?.Flatten() ??
new AggregateException("Task failed but no exception was provided");
}
}
else
{
var resultTask = Task.WhenAll(taskList);
try
{
await resultTask;
}
catch // The default behavior of await unwraps the first exception from the AggregateException
{
throw resultTask.Exception!.Flatten();
}
}
}
#region parallel invocation
public static Task InvokeAsync(this Func<Task> handler, bool throwOnFirstException = false)
{
var tasks = handler.GetInvocationList()
.Cast<Func<Task>>()
.Select(del => Task.Run(del));
return RunTasksAsync(tasks, throwOnFirstException);
}
public static Task InvokeAsync<T>(this Func<T, Task> handler, T arg, bool throwOnFirstException = false)
{
var tasks = handler.GetInvocationList()
.Cast<Func<T, Task>>()
.Select(del =>
{
if (arg is CancellationToken ct)
return Task.Run(() => del(arg), ct);
else
return Task.Run(() => del(arg));
});
return RunTasksAsync(tasks, throwOnFirstException);
}
public static Task InvokeAsync<T1, T2>(this Func<T1, T2, Task> handler, T1 arg1, T2 arg2, bool throwOnFirstException = false)
{
var tasks = handler.GetInvocationList()
.Cast<Func<T1, T2, Task>>()
.Select(del => Task.Run(() =>
{
if (arg2 is CancellationToken ct2)
return Task.Run(() => del(arg1, arg2), ct2);
else if(arg1 is CancellationToken ct1)
return Task.Run(() => del(arg1, arg2), ct1);
else
return Task.Run(() => del(arg1, arg2));
}));
return RunTasksAsync(tasks, throwOnFirstException);
}
public static Task InvokeAsync<T1, T2, T3>(this Func<T1, T2, T3, Task> handler, T1 arg1, T2 arg2, T3 arg3, bool throwOnFirstException = false)
{
var tasks = handler.GetInvocationList()
.Cast<Func<T1, T2, T3, Task>>()
.Select(del => Task.Run(() =>
{
if (arg3 is CancellationToken ct3)
return Task.Run(() => del(arg1, arg2, arg3), ct3);
if (arg2 is CancellationToken ct2)
return Task.Run(() => del(arg1, arg2, arg3), ct2);
else
return Task.Run(() => del(arg1, arg2, arg3));
}));
return RunTasksAsync(tasks, throwOnFirstException);
}
#endregion
#region Sequential Invocation
public static async Task SequentialInvokeAsync(this Func<Task> handler)
{
var delegates = handler.GetInvocationList()
.Cast<Func<Task>>();
foreach (var d in delegates)
await d();
}
public static async Task SequentialInvokeAsync<T>(this Func<T, Task> handler, T arg)
{
var delegates = handler.GetInvocationList()
.Cast<Func<T, Task>>();
foreach (var d in delegates)
await d(arg);
}
public static async Task SequentialInvokeAsync<T1, T2>(this Func<T1, T2, Task> handler, T1 arg1, T2 arg2)
{
var delegates = handler.GetInvocationList()
.Cast<Func<T1, T2, Task>>();
foreach (var d in delegates)
await d(arg1, arg2);
}
public static async Task SequentialInvokeAsync<T1, T2, T3>(this Func<T1, T2, T3, Task> handler, T1 arg1, T2 arg2, T3 arg3)
{
var delegates = handler.GetInvocationList()
.Cast<Func<T1, T2, T3, Task>>();
foreach (var d in delegates)
await d(arg1, arg2, arg3);
}
#endregion
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment