1
\$\begingroup\$

This class acts as an asynchronous event handler that will execute all attached tasks in an async/await context. Requires Nuget Immutables. Example usage:

class MyEventArgs : EventArgs {}
async Task SomeAsyncMethodAobject src, EventArgs args) {
    Console.WriteLine("Task A...");
    await Task.Delay(2000);
}
async Task SomeAsyncMethodB(object src, EventArgs args) {
    Console.WriteLine("Task B...");
    await Task.Delay(1000);
}

static async Task Main(string[] args) {
    AsyncEvent<MyEventArgs> Events;
    Events = new AsyncEvent<MyEventArgs>();
    Events += SomeAsyncMethodA;
    Events += SomeAsyncMethodB;
    await Events?.InvokeAsync(this, new MyEventArgs());
    // Use below to discard task and not await event task to finish.
    // _ = Events?.InvokeAsync(this, new MyEventArgs()).ConfigureAwait(false);
}

Source for the AsyncEvent<EventArgsT> class:

// T is the EventArgs class type to pass to the callbacks on Invoke.
public class AsyncEvent<T> where T : EventArgs {
    // List of task methods to await.
    public ImmutableList<Func<object, T, Task>> Invokables;

    // on += add new callback method to AsyncEvent.
    public static AsyncEvent<T> operator+(AsyncEvent<T> source, Func<object, T, Task> callback) {
        if (callback == null) throw new NullReferenceException("Callback is null! <AsyncEvent<T>>");
        if (source == null) return null;
        if (source.Invokables == null) source.Invokables = ImmutableList<Func<object, T, Task>>.Empty;
        source.Invokables = source.Invokables.Add(callback);
        return source;
    }

    // on -= remove existing callback from AsyncEvent.
    public static AsyncEvent<T> operator -(AsyncEvent<T> source, Func<object, T, Task> callback) {
        if (callback == null) throw new NullReferenceException("Callback is null! <AsyncEvent<T>>");
        if (source == null) return null;

        source.Invokables = source.Invokables.Remove(callback);
        return source;
    }

    // Invoke the tasks asynchronously with a cancelation token.
    public async Task InvokeAsync(object source, T evArgs, CancellationToken token) {
        List<Task> tasks = new List<Task>();
        if (Invokables != null)
            foreach (var callback in Invokables)
                if (!token.IsCancellationRequested)
                    tasks.Add(callback(source, evArgs));

        await Task.WhenAll(tasks.ToArray());
    }

    // Invoke the tasks asynchronously.
    public async Task InvokeAsync(object source, T evArgs) {
        List<Task> tasks = new List<Task>();
        if (Invokables != null)
            foreach (var callback in Invokables)
                tasks.Add(callback(source, evArgs));

        await Task.WhenAll(tasks.ToArray());
    }
}

Is there anything wrong with this asynchronous paradigm?

\$\endgroup\$
6
  • 1
    \$\begingroup\$ What's the purpose of the solution? Is it only for waiting when all the callbacks are completed? If so, here's some read. \$\endgroup\$
    – aepot
    May 31, 2021 at 0:00
  • \$\begingroup\$ @aepot in my particular situation I am using this to run events asynchronously for my async/await TCP server. Primary usefulness is that I can send data packets to events and let the events run async and continue reading data on client sockets without waiting for the events to finish from the previous data received from the client. \$\endgroup\$
    – FatalSleep
    May 31, 2021 at 1:04
  • \$\begingroup\$ @aepot the generic use here is that you can execute a bunch of different methods with similar contexts asynchronously to boost performance. \$\endgroup\$
    – FatalSleep
    May 31, 2021 at 1:06
  • \$\begingroup\$ Looks like you added async overhead to Server Core with no reason. OK, how many different really asynchronous subscribers to the AsyncEvent do you have? What jobs are they doing on the event was fired? What data is passed by Args? How often the event is fired, ~times/sec, peak/avg? Why I'm asking: It looks like you've solved some problem in a wrong way. \$\endgroup\$
    – aepot
    May 31, 2021 at 6:13
  • 1
    \$\begingroup\$ @FatalSleep I have found Pipelines to be superior to events when doing async TCP/IP. \$\endgroup\$ Oct 4, 2022 at 12:28

1 Answer 1

2
\$\begingroup\$

Not a review but an alternative solution to leave here. Just because the above one is overkill, as for me.

Probably you want to implement Publisher/Subscriber pattern in awaitable/Command mode. But it's possible using delegate itself.

Consider the extension method

public static class DelegateExtensions
{
    public static Task InvokeAsync<TArgs>(this Func<object, TArgs, Task> func, object sender, TArgs e)
    {
        return func == null ? Task.CompletedTask
            : Task.WhenAll(func.GetInvocationList().Cast<Func<object, TArgs, Task>>().Select(f => f(sender, e)));
    }
}

And the test

class Program
{
    public static event Func<object, EventArgs, Task> MyAsyncEvent;

    static async Task SomeAsyncMethodA(object src, EventArgs args)
    {
        Console.WriteLine("Task A...");
        await Task.Delay(2000);
        Console.WriteLine("Task A finished");
    }

    static async Task SomeAsyncMethodB(object src, EventArgs args)
    {
        Console.WriteLine("Task B...");
        await Task.Delay(1000);
        Console.WriteLine("Task B finished");
    }

    static async Task Main(string[] args)
    {
        MyAsyncEvent += SomeAsyncMethodA;
        MyAsyncEvent += SomeAsyncMethodB;

        await MyAsyncEvent.InvokeAsync(null, EventArgs.Empty);

        Console.WriteLine("Invoked");
        Console.ReadKey();
    }
}

Output

Task A...
Task B...
Task B finished
Task A finished
Invoked

Looks like it works.

\$\endgroup\$
8
  • \$\begingroup\$ I like the solution. One problem though is that Delegate[] does not contain a definition for *.Cast. \$\endgroup\$
    – FatalSleep
    Jun 1, 2021 at 0:19
  • 1
    \$\begingroup\$ @FatalSleep add using System.Linq; \$\endgroup\$
    – aepot
    Jun 1, 2021 at 5:58
  • \$\begingroup\$ thank you. Also how did you find DelegateExtensions? I've search the internet and cannot find any info on it. \$\endgroup\$
    – FatalSleep
    Jun 2, 2021 at 6:16
  • 1
    \$\begingroup\$ @FatalSleep this Func<object, TArgs, Task> func <--- this type marked as this. That's not for delegates but only for this type. \$\endgroup\$
    – aepot
    Jun 2, 2021 at 7:29
  • 1
    \$\begingroup\$ Beautiful solution! \$\endgroup\$ Oct 20, 2022 at 14:39

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.