Is there an elegant solution for writing async delegates in C# (Unity)

Solution for Is there an elegant solution for writing async delegates in C# (Unity)
is Given Below:

I am familiar with how to write common delegates and subscribing to them, but is there a way to have the usefulness of delegates (subscribing and unsubscribing from other classes) with the awaitable functionality?

For example, with a simple delegate like this:

public delegate void SimpleDelegate();
public static SimpleDelegate OnDelegateInvoke;

I then can subscribe to it from another class like:

    public void SomeFunction(){};

    OnDelegateInvoke += SomeFunction();

The behaviour I want is for the OnDelegateInvoke call to be awaitable, so it awaits there until all subscribed functions complete:

     await OnDelegateInvoke?.Invoke();
     await DoSomethingThatNeedsAboveCompleted();

I tried writing the delegates with a Task return type but to my understanding that wouldn’t work since there are multiple functions returning multiple tasks, so the await would only wait for the first Task to complete.

Since thinking about this I am also not sure if this completely breaks the paradigm of why delegates are useful, so answers about that are also appreciated.

You could do this with delegates returning a Task:

public delegate Task SimpleAsyncDelegate();
public static SimpleAsyncDelegate OnAsyncDelegateInvoke;

To invoke and await all added delegates, we invoke them one-by-one and await the produced Tasks:

var delegateTasks = OnAsyncDelegateInvoke.GetInvocationList()
    .Cast<SimpleAsyncDelegate>()
    .Select(del => del.Invoke());
await Task.WhenAll(delegateTasks);

For an example with and without awaiting see this fiddle.


To address the question in the comments, I think the closest you get to a more generic way of awaiting all delegates, is a helper class similar to the following:

public class DelegateHelper
{
    public static async Task WhenAllDelegates(Delegate del)
    {
        var delegateTasks = del.GetInvocationList()
            .Select(del => del.DynamicInvoke())
            .Where(obj => obj is Task)
            .Cast<Task>();
        await Task.WhenAll(delegateTasks);
    }
}

You can use it like so:

await DelegateHelper.WhenAllDelegates(OnAsyncDelegateInvoke);

NOTE: For the generic purpose we need to use DynamicInvoke, which according to this answer, performs significantly worse. Whether this performance is really an issue depends on the use-case, so you can test to see if it’s worth it.