Asynchronous Event Planning

Forward

This post deals with a more advanced topic consisting of what are basically function pointers – references to methods themselves. In C#, these are known as delegates, and they offer a way to pass methods as values to other methods. This can be used to express very complex behaviour in your software, such as event callbacks.

Every so often in computer programming, you’ll encounter the issue where something you’re programming needs to access code written by another developer – an application programming interface, or API – and how you’re able to interface with that API depends entirely upon how that API is written. There are a number of common design patterns used, and one of these models is the Publisher/Subscriber model. In the Publisher/Subscriber model, any number of publishers “publish” any number of messages, unaware and independently of any subscribers. Likewise, any number of subscribers listen for any number of messages on specified “message channels”, unaware and independently of any publishers.

A common implementation of this pattern is to expose callbacks in your software, which other developer’s may subscribe to, and listen for events. A simple implementation might look something like:

namespace MyNamespace
{
    public static class Events
    {
        public delegate void MyEvent();

        public static MyEvent OnMyEvent;
    }
}

Other developers would then by able to register their code to listen for “messages” sent on that “channel”:

void TheirMethod()
{
    // Do their code
}

// ...

MyNamespace.Events.OnMyEvent += TheirMethod;

And you could “send” messages on that “channel”:

Events.OnMyEvent.Invoke();

This pattern describes the very basic uses of delegates in C#. They are a whole topic upon themselves, and their usage along with unnamed delegates as well as anonymous functions deserves an entire write-up on its own.

Delegates can be further expanded to pass data back and forth. Generally speaking, it would be useful for the Subscriber to receive some Data with the Message, so lets go ahead and add an integer value:

public static class Events
{
    public delegate void MyEvent(int value);

    public static MyEvent OnMyEvent;
}

By simply updating the delegate’s signature to include an integer value, we can send an integer along to our Subscribers each time we raise our event. Lets update the Subscriber code to match:

void TheirMethod(int value)
{
    // Do their code
    Console.WriteLine($"Event Raised: {value}");
}

// ...

MyNamespace.Events.OnMyEvent += TheirMethod;

While we are at it, we can actually get rid of the method definition here by converting it to an anonymous function. In some cases, this can make the code a lot cleaner, but in other cases you may only be making a mess. Knowing when and where to use anonymous functions takes time and experience, but for now, lets go ahead and convert it:

MyNamespace.Events.OnMyEvent += (value) =>
{
    Console.WriteLine($"Event Raised: {value}");
};

And then just like before, we can raise our event, but this time we are also able to pass in an integer:

Events.OnMyEvent.Invoke(5);

Now, when you execute your code, the OnMyEvent event is raised, a value of 5 is passed in, their subscribed code activates, and “Event Raised: 5” is put into the console. There are many more ways to expand upon this, but you now have a functional synchronous event system. Your code is able to run, and without knowing how their code functions, how many subscribers there are, or what those subscribers are doing, your code continues to function independently, modularly, raising events as they come, so the other developer’s code can respond to them and interface with your code.

This pattern is nice, but lets talk about the way we’re asking other developers to subscribe to our code:

MyNamespace.Events.OnMyEvent += TheirMethod;

In this instance, the developer has added a reference to TheirMethod to the stack of delegates which will be called when you Invoke your OnMyEvent.

What happens then, if they do this:

MyNamespace.Events.OnMyEvent += TheirMethod;
MyNamespace.Events.OnMyEvent += TheirMethod;

Or worse, what if they do this:

MyNamespace.Events.OnMyEvent -= TheirMethod;

The fact is we can’t really know how other developers will use our API, so we should do everything we can to protect them from themselves and ensure that our API can only be used in valid contexts. To start, lets switch up how we’re handling our events, and wrap our code up into a new class which will make it easier to create new events in the future:

public class Event
{
    private readonly Dictionary<object, Action> _actions = new();

    /// <summary>
    /// Adds <paramref name="receiver"/> to the list of subscribers for this event.<br/>
    /// When the <see cref="RaiseEvent"/> method is called, any subscribed <paramref name="action"/>s will be invoked.<br/>
    /// </summary>
    public void Subscribe(object receiver, Action action)
    {
        if (!_actions.ContainsKey(receiver))
        {
            _actions.Add(receiver, action);
            return;
        }
        _actions[receiver] = action;
    }
    /// <summary> Removes <paramref name="receiver"/> from the list of subscribers for this event. </summary>
    public void Unsubscribe(object receiver)
    {
        if (!_actions.ContainsKey(receiver)) return;
        _actions.Remove(receiver);
    }
    /// <summary> Raises this event, invoking any actions for receivers which are currently subscribed to this event. </summary>
    public void RaiseEvent()
    {
        foreach (var action in _actions)
        {
            action.Value?.Invoke();
        }
    }
}

And just like that, we’ve ensured other developers can’t accidentally subscribe to our event twice or unsubscribe when they aren’t subscribed:

public static class Events
{
    public static Event OnEvent = new();
}
// They subscribe like this
MyNamespace.Events.OnEvent.Subscribe(this, () => Console.WriteLine("Hello World"));
// You raise the event like this
Events.OnEvent.RaiseEvent();

Unfortunately, our modifications to the Event class have removed the ability for us to pass that integer back and forth. We still need to send the subscribers data, so lets genericize the Event class so that we can reuse it for different types any time we need. All we’ll need to do, is add <T> in a few places like so:

public class Event<T>
{
    private readonly Dictionary<object, Action<T>> _actions = new();

    /// <summary>
    /// Adds <paramref name="receiver"/> to the list of subscribers for this event.<br/>
    /// When the <see cref="RaiseEvent"/> method is called, any subscribed <paramref name="action"/>s will be invoked.<br/>
    /// </summary>
    public void Subscribe(object receiver, Action<T> action)
    {
        if (!_actions.ContainsKey(receiver))
        {
            _actions.Add(receiver, action);
            return;
        }
        _actions[receiver] = action;
    }
    /// <summary> Removes <paramref name="receiver"/> from the list of subscribers for this event. </summary>
    public void Unsubscribe(object receiver)
    {
        if (!_actions.ContainsKey(receiver)) return;
        _actions.Remove(receiver);
    }
    /// <summary> Raises this event, invoking any actions for receivers which are currently subscribed to this event. </summary>
    public void RaiseEvent(T value)
    {
        foreach (var action in _actions)
        {
            action.Value?.Invoke(value);
        }
    }
}

Now we’re able to tell the compiler we want to pass an integer like this

public static class Events
{
    public static Event<int> OnEvent = new();
}

...

// Subscriber
MyNamespace.Events.OnEvent.Subscribe(this, (value) => Console.WriteLine($"Event Raised: {value}"));

// Publisher
Events.OnEvent.RaiseEvent(5);

Our Event System is really starting to shape up now! Subscribers can subscribe and unsubscribe, and Publishers can publish data! There are a few more changes we could make to really round off our Event class. There may be times where one system depends on another system to be in a certain state – maybe a system needs to be initialized, or have state data loaded before we can continue. It would be handy if we could tell if an Event had already been fired, and if it had, we could skip waiting and just execute our action. Lets go ahead and add in that functionality:

/// <summary> Indicates whether this event has been fired at least one time since it was last <see cref="Reset"/>. </summary>
public bool Fired { get; private set; }

...

/// <summary>
/// Adds <paramref name="receiver"/> to the list of subscribers for this event.<br/>
/// When the <see cref="RaiseEvent"/> method is called, any subscribed <paramref name="action"/>s will be invoked.<br/>
/// </summary>
/// <remarks>
/// If the event has already been raised at least once, the <paramref name="action"/> will automatically be invoked.<br/>
/// This is useful for synchronization, such as state data which may not be available until the event has been raised at least once.<br/>
/// Using the <see cref="SubscribeAndDo"/> method we can subscribe to be notified as soon as state data is available,<br/>
/// and invoke the <paramref name="action"/> as soon as it is, invoking immediately if it already available.
/// </remarks>
public void SubscribeAndDo(object receiver, Action action)
{
    if (receiver == null || action == null) return;
    if (!_actions.ContainsKey(receiver))
    {
        _actions.Add(receiver, action);
        if (!Fired) return;
        action.Invoke();
    }
    _actions[receiver] = action;
    if (!Fired) return;
    action.Invoke();
}

...

This small change makes it trivial to wait for state data:

public static class SomeSystem
{
    public static Event OnInitialized = new();
    public static Event OnDataLoaded = new();
    public static Event OnDataUnloaded = new();

    static SomeSystem()
    {
        OnInitialized.RaiseEvent();
        LoadData();
    }

    private static void UnloadData()
    {
        OnDataLoaded.Reset();
        // Unload data
        OnDataUnloaded.RaiseEvent();
    }
    private static void LoadData()
    {
        OnDataUnloaded.Reset();
        // Do intensive process.. load files off drive, compute models, download data, etc..
        OnDataLoaded.RaiseEvent();
    }

    public static void ForceReload()
    {
        UnloadData();
        LoadData();
    }
}

...

SomeSystem.OnInitialized.SubscribeAndDo(this, () => { Console.WriteLine("SomeSystem.OnInitialized"); });
SomeSystem.OnDataLoaded.SubscribeAndDo(this, () => { Console.WriteLine("SomeSystem.OnDataLoaded"); });
SomeSystem.OnDataUnloaded.SubscribeAndDo(this, () => { Console.WriteLine("SomeSystem.OnDataUnloaded"); });

And just like that, we guarantee our subscriber will receive the calls to each event, regardless of if the subscriber subscribes before or after the event is called. For example, the subscriber in this case will still receive the call to OnInitialized even though the subscriber is subscribing well after it will have been initialized, but if the data loading takes a long time and the call to OnDataLoaded doesn’t happen until later, our subscriber will just keep waiting asynchronously 🙂

One additional feature that would be nice would be the ability to subscribe to an event and receive the callback only once. A hacky solution to this would be:

MyNamespace.Events.OnEvent.Subscribe(this, () =>
{
    Console.WriteLine("Do something one time.");
    MyNamespace.Events.OnEvent.Unsubscribe(this);
});

This works, and functionally it’s fine, but it looks a bit hacky, and it would cause issues later when adapting this code for an async-await pattern, so lets go ahead and write up a proper solution:

...

/// <summary>
/// Adds <paramref name="receiver"/> to the list of subscribers for this event.<br/>
/// When the <see cref="RaiseEvent"/> method is called, any subscribed <paramref name="action"/>s will be invoked.<br/>
/// After the <see cref="RaiseEvent"/> method is called, this <paramref name="action"/> will be unsubscribed automatically.<br/>
/// </summary>
/// <remarks>
/// If the event has already been raised at least once, the <paramref name="action"/> will automatically be invoked.<br/>
/// This is useful for synchronization, such as state data which may not be available until the event has been raised at least once.<br/>
/// Using the <see cref="SubscribeAndDo"/> method we can subscribe to be notified as soon as state data is available,<br/>
/// and invoke the <paramref name="action"/> as soon as it is, invoking immediately if it already available.<br/>
/// Using the <see cref="SubscribeAndDoOnce"/> method we can subscribe just like <br/>
/// using the <see cref="SubscribeAndDo"/> method, but our action will also be automatically unsubscribed once it is called.
/// </remarks>
public void SubscribeAndDoOnce(object receiver, Action action)
{
    if (receiver == null || action == null) return;
    if (Fired)
    {
        action.Invoke();
        return;
    }
    if (!_oneTimeActions.Contains(receiver))
        _oneTimeActions.Add(receiver);
    if (!_actions.ContainsKey(receiver))
    {
        _actions.Add(receiver, action);
        return;
    }
    _actions[receiver] = action;
}

...

/// <summary> Raises this event, invoking any actions for receivers which are currently subscribed to this event. </summary>
public void RaiseEvent()
{
    Fired = true;
    foreach (var action in _actions)
    {
        action.Value?.Invoke();
    }
    foreach (var receiver in _oneTimeActions)
    {
        if (_actions.ContainsKey(receiver))
            _actions.Remove(receiver);
    }
    _oneTimeActions.Clear();
}

...

And just like that we’ve given other developer’s (and ourselves) a clean way to subscribe to an event just once – which can be very useful for asynchronously waiting until another system is ready, among other things. Lets see how that looks:

MyNamespace.Events.OnEvent.SubscribeAndDoOnce(this, () =>
{
    Console.WriteLine("Do something only one time, as soon as OnEvent is called - immediately if it has already been called previously.");
});

This brings us really close to a robust event system, with only two major issues. The first, and most pressing issue, is that the newly added SubscribeAndDo and SubscribeAndDoOnce methods both Invoke the Action immediately if the Event has already been fired – that works perfect when we aren’t passing any values, but if we want to pass a value – like that integer from way back when – then we’ll need some way to remember the previous value. Modifying our class like this is easy enough:

public class Event<T>
{

    ...

    private readonly Dictionary<object, Action<T>> _actions = new();

    private T LastValue;
    
    ...
    
    
    public void Subscribe(object receiver, Action<T> action) { }
    
    ...        
    
    public void SubscribeAndDo(object receiver, Action<T> action)
    {
        if (receiver == null || action == null) return;
        if (!_actions.ContainsKey(receiver))
        {
            _actions.Add(receiver, action);
            if (!Fired) return;
            action.Invoke(LastValue);
        }
        _actions[receiver] = action;
        if (!Fired) return;
        action.Invoke(LastValue);
    }
    
    ...
    
    public void SubscribeAndDoOnce(object receiver, Action<T> action)
    {
        if (receiver == null || action == null) return;
        if (Fired)
        {
            action.Invoke(LastValue);
            return;
        }
        ...
    }
    
    ...
    
    public void RaiseEvent(T value)
    {
        Fired = true;
        LastValue = value;
        ...
    }

    public void Reset()
    {
        Fired = false;
        LastValue = default;
    }
}

With those small changes we could then pass data forward from our Publisher to our Subscribers, just like before, then the only issue comes down to adapting it for async-away patterns. In order to convert this to support async, the Action’s we’re passing around would need to be able to return a Task, but unfortunately Action’s can’t return values. Fortunately for us, Microsoft planned ahead for that one, and they introduced the Func class. Func is essentially the same thing as an Action, however it also supports passing a return object back to the caller – that’s exactly what we need to support async-await, so lets try and replace all of the instances of Action with Func:

public class Event
{
    /// <summary> Indicates whether this event has been fired at least one time since it was last <see cref="Reset"/>. </summary>
    public bool Fired { get; private set; }
    
    private readonly SemaphoreSlim _semaphoreSlim = new(1, 1);
    private readonly List<object> _oneTimeActions = new();
    private readonly Dictionary<object, Func<Task>> _actions = new();

    /// <summary>
    /// Adds <paramref name="receiver"/> to the list of subscribers for this event.<br/>
    /// When the <see cref="RaiseEvent"/> method is called, any subscribed <paramref name="action"/>s will be invoked.<br/>
    /// </summary>
    public async Task Subscribe(object receiver, Func<Task> action)
    {
        await _semaphoreSlim.WaitAsync();
        try
        {
            if (receiver == null || action == null) return;
            if (!_actions.ContainsKey(receiver))
            {
                _actions.Add(receiver, action);
                return;
            }
            _actions[receiver] = action;
        }
        finally
        {
            _semaphoreSlim.Release();
        }
    }

    /// <summary>
    /// Adds <paramref name="receiver"/> to the list of subscribers for this event.<br/>
    /// When the <see cref="RaiseEvent"/> method is called, any subscribed <paramref name="action"/>s will be invoked.<br/>
    /// </summary>
    /// <remarks>
    /// If the event has already been raised at least once, the <paramref name="action"/> will automatically be invoked.<br/>
    /// This is useful for synchronization, such as state data which may not be available until the event has been raised at least once.<br/>
    /// Using the <see cref="SubscribeAndDo"/> method we can subscribe to be notified as soon as state data is available,<br/>
    /// and invoke the <paramref name="action"/> as soon as it is, invoking immediately if it already available.
    /// </remarks>
    public async Task SubscribeAndDo(object receiver, Func<Task> action)
    {
        await _semaphoreSlim.WaitAsync();
        try
        {
            if (receiver == null || action == null) return;
            if (!_actions.ContainsKey(receiver))
            {
                _actions.Add(receiver, action);
                if (!Fired) return;
                await action.Invoke();
            }
            _actions[receiver] = action;
            if (!Fired) return;
            await action.Invoke();
        }
        finally
        {
            _semaphoreSlim.Release();
        }
    }
    /// <summary>
    /// Adds <paramref name="receiver"/> to the list of subscribers for this event.<br/>
    /// When the <see cref="RaiseEvent"/> method is called, any subscribed <paramref name="action"/>s will be invoked.<br/>
    /// After the <see cref="RaiseEvent"/> method is called, this <paramref name="action"/> will be unsubscribed automatically.<br/>
    /// </summary>
    /// <remarks>
    /// If the event has already been raised at least once, the <paramref name="action"/> will automatically be invoked.<br/>
    /// This is useful for synchronization, such as state data which may not be available until the event has been raised at least once.<br/>
    /// Using the <see cref="SubscribeAndDo"/> method we can subscribe to be notified as soon as state data is available,<br/>
    /// and invoke the <paramref name="action"/> as soon as it is, invoking immediately if it already available.<br/>
    /// Using the <see cref="SubscribeAndDoOnce"/> method we can subscribe just like <br/>
    /// using the <see cref="SubscribeAndDo"/> method, but our action will also be automatically unsubscribed once it is called.
    /// </remarks>
    public async Task SubscribeAndDoOnce(object receiver, Func<Task> action)
    {
        await _semaphoreSlim.WaitAsync();
        try
        {
            if (receiver == null || action == null) return;
            if (Fired)
            {
                await action.Invoke();
                return;
            }
            if (!_oneTimeActions.Contains(receiver))
                _oneTimeActions.Add(receiver);
            if (!_actions.ContainsKey(receiver))
            {
                _actions.Add(receiver, action);
                return;
            }
            _actions[receiver] = action;
        }
        finally
        {
            _semaphoreSlim.Release();
        }
    }
    /// <summary> Removes <paramref name="receiver"/> from the list of subscribers for this event. </summary>
    public async Task Unsubscribe(object receiver)
    {
        await _semaphoreSlim.WaitAsync();
        try
        {
            if (!_actions.ContainsKey(receiver)) return;
            _actions.Remove(receiver);
        }
        finally
        {
            _semaphoreSlim.Release();
        }
    }
    /// <summary> Raises this event, invoking any actions for receivers which are currently subscribed to this event. </summary>
    public async Task RaiseEvent()
    {
        await _semaphoreSlim.WaitAsync();
        try
        {
            Fired = true;
            foreach (var action in _actions)
            {
                if (action.Value == null) continue;
                await action.Value.Invoke();
            }
            foreach (var receiver in _oneTimeActions)
            {
                if (_actions.ContainsKey(receiver))
                    _actions.Remove(receiver);
            }
            _oneTimeActions.Clear();
        }
        finally
        {
            _semaphoreSlim.Release();
        }
    }
    /// <summary> Resets this event back to the default unfired state, setting the <see cref="Fired"/> property back to false. </summary>
    public async Task Reset()
    {
        await _semaphoreSlim.WaitAsync();
        try
        {
            Fired = false;
        }
        finally
        {
            _semaphoreSlim.Release();
        }
    }
}

At last! We have a robust asynchronous event handler!

We can now rely on it like so:

// Subscriber
await MyNamespace.Events.OnEvent.SubscribeAndDoOnce(this, async () =>
{
    Console.WriteLine("Do something only one time, as soon as OnEvent is called - immediately if it has already been called previously.");
});

// Publisher
await Events.OnEvent.RaiseEvent();

Voila! Asynchronous Events!

Final Thoughts

Method groups, Delegates, Actions, Funcs, etc. are extremely powerful, and offer many different ways to manipulate the programming flow. Understanding how and when to use the different flow techniques available is vital to maintaining a consistent and predictable logic structure within your software.

The code given here is a very simplified example.

It is important as a developer that you learn when and where to use the correct algorithms, programming patterns, techniques, etc, but it’s also important to remember in many cases there is no one absolute “right” way to do something – often times, choosing one way over another means weighing the pros and cons. An API like this offers a lot of flexibility and power, but it can also be difficult to maintain if you don’t structure your data correctly – learning exactly what that means is part of becoming a real software engineer, and comes with time.

Project Files

You may view the project files here on GitHub. The final version of the demo code is attached below.

The final version has been altered slightly:

  • Two generic forms, Event<T> and Event<T1, T2> have been added.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Hotrian.com_Tutorials
{
    public class Event
    {
        /// <summary> Indicates whether this event has been fired at least one time since it was last <see cref="Reset"/>. </summary>
        public bool Fired { get; private set; }

        private readonly SemaphoreSlim _semaphoreSlim = new(1, 1);
        private readonly List<object> _oneTimeActions = new();
        private readonly Dictionary<object, Func<Task>> _actions = new();

        /// <summary>
        /// Adds <paramref name="receiver"/> to the list of subscribers for this event.<br/>
        /// When the <see cref="RaiseEvent"/> method is called, any subscribed <paramref name="action"/>s will be invoked.<br/>
        /// </summary>
        public async Task Subscribe(object receiver, Func<Task> action)
        {
            await _semaphoreSlim.WaitAsync();
            try
            {
                if (receiver == null || action == null) return;
                if (!_actions.ContainsKey(receiver))
                {
                    _actions.Add(receiver, action);
                    return;
                }

                _actions[receiver] = action;
            }
            finally
            {
                _semaphoreSlim.Release();
            }
        }
        /// <summary>
        /// Adds <paramref name="receiver"/> to the list of subscribers for this event.<br/>
        /// When the <see cref="RaiseEvent"/> method is called, any subscribed <paramref name="action"/>s will be invoked.<br/>
        /// </summary>
        /// <remarks>
        /// If the event has already been raised at least once, the <paramref name="action"/> will automatically be invoked.<br/>
        /// This is useful for synchronization, such as state data which may not be available until the event has been raised at least once.<br/>
        /// Using the <see cref="SubscribeAndDo"/> method we can subscribe to be notified as soon as state data is available,<br/>
        /// and invoke the <paramref name="action"/> as soon as it is, invoking immediately if it already available.
        /// </remarks>
        public async Task SubscribeAndDo(object receiver, Func<Task> action)
        {
            await _semaphoreSlim.WaitAsync();
            try
            {
                if (receiver == null || action == null) return;
                if (!_actions.ContainsKey(receiver))
                {
                    _actions.Add(receiver, action);
                    if (!Fired) return;
                    await action.Invoke();
                }

                _actions[receiver] = action;
                if (!Fired) return;
                await action.Invoke();
            }
            finally
            {
                _semaphoreSlim.Release();
            }
        }
        /// <summary>
        /// Adds <paramref name="receiver"/> to the list of subscribers for this event.<br/>
        /// When the <see cref="RaiseEvent"/> method is called, any subscribed <paramref name="action"/>s will be invoked.<br/>
        /// After the <see cref="RaiseEvent"/> method is called, this <paramref name="action"/> will be unsubscribed automatically.<br/>
        /// </summary>
        /// <remarks>
        /// If the event has already been raised at least once, the <paramref name="action"/> will automatically be invoked.<br/>
        /// This is useful for synchronization, such as state data which may not be available until the event has been raised at least once.<br/>
        /// Using the <see cref="SubscribeAndDo"/> method we can subscribe to be notified as soon as state data is available,<br/>
        /// and invoke the <paramref name="action"/> as soon as it is, invoking immediately if it already available.<br/>
        /// Using the <see cref="SubscribeAndDoOnce"/> method we can subscribe just like <br/>
        /// using the <see cref="SubscribeAndDo"/> method, but our action will also be automatically unsubscribed once it is called.
        /// </remarks>
        public async Task SubscribeAndDoOnce(object receiver, Func<Task> action)
        {
            await _semaphoreSlim.WaitAsync();
            try
            {
                if (receiver == null || action == null) return;
                if (Fired)
                {
                    await action.Invoke();
                    return;
                }

                if (!_oneTimeActions.Contains(receiver))
                    _oneTimeActions.Add(receiver);
                if (!_actions.ContainsKey(receiver))
                {
                    _actions.Add(receiver, action);
                    return;
                }

                _actions[receiver] = action;
            }
            finally
            {
                _semaphoreSlim.Release();
            }
        }

        /// <summary> Removes <paramref name="receiver"/> from the list of subscribers for this event. </summary>
        public async Task Unsubscribe(object receiver)
        {
            await _semaphoreSlim.WaitAsync();
            try
            {
                if (!_actions.ContainsKey(receiver)) return;
                _actions.Remove(receiver);
            }
            finally
            {
                _semaphoreSlim.Release();
            }
        }

        /// <summary> Raises this event, invoking any actions for receivers which are currently subscribed to this event. </summary>
        public async Task RaiseEvent()
        {
            await _semaphoreSlim.WaitAsync();
            try
            {
                Fired = true;
                foreach (var action in _actions.Where(action => action.Value != null))
                {
                    await action.Value.Invoke();
                }
                foreach (var receiver in _oneTimeActions.Where(receiver => _actions.ContainsKey(receiver)))
                {
                    _actions.Remove(receiver);
                }
                _oneTimeActions.Clear();
            }
            finally
            {
                _semaphoreSlim.Release();
            }
        }
        /// <summary> Resets this event back to the default unfired state, setting the <see cref="Fired"/> property back to false. </summary>
        public async Task Reset()
        {
            await _semaphoreSlim.WaitAsync();
            try
            {
                Fired = false;
            }
            finally
            {
                _semaphoreSlim.Release();
            }
        }
    }
    public class Event<T>
    {
        /// <summary> Indicates whether this event has been fired at least one time since it was last <see cref="Reset"/>. </summary>
        public bool Fired { get; private set; }
        public T LastValue { get; private set; }

        private readonly SemaphoreSlim _semaphoreSlim = new(1, 1);
        private readonly List<object> _oneTimeActions = new();
        private readonly Dictionary<object, Func<T, Task>> _actions = new();

        /// <summary>
        /// Adds <paramref name="receiver"/> to the list of subscribers for this event.<br/>
        /// When the <see cref="RaiseEvent"/> method is called, any subscribed <paramref name="action"/>s will be invoked.<br/>
        /// </summary>
        public async Task Subscribe(object receiver, Func<T, Task> action)
        {
            await _semaphoreSlim.WaitAsync();
            try
            {
                if (receiver == null || action == null) return;
                if (!_actions.ContainsKey(receiver))
                {
                    _actions.Add(receiver, action);
                    return;
                }

                _actions[receiver] = action;
            }
            finally
            {
                _semaphoreSlim.Release();
            }
        }
        /// <summary>
        /// Adds <paramref name="receiver"/> to the list of subscribers for this event.<br/>
        /// When the <see cref="RaiseEvent"/> method is called, any subscribed <paramref name="action"/>s will be invoked.<br/>
        /// </summary>
        /// <remarks>
        /// If the event has already been raised at least once, the <paramref name="action"/> will automatically be invoked.<br/>
        /// This is useful for synchronization, such as state data which may not be available until the event has been raised at least once.<br/>
        /// Using the <see cref="SubscribeAndDo"/> method we can subscribe to be notified as soon as state data is available,<br/>
        /// and invoke the <paramref name="action"/> as soon as it is, invoking immediately if it already available.
        /// </remarks>
        public async Task SubscribeAndDo(object receiver, Func<T, Task> action)
        {
            await _semaphoreSlim.WaitAsync();
            try
            {
                if (receiver == null || action == null) return;
                if (!_actions.ContainsKey(receiver))
                {
                    _actions.Add(receiver, action);
                    if (!Fired) return;
                    await action.Invoke(LastValue);
                }

                _actions[receiver] = action;
                if (!Fired) return;
                await action.Invoke(LastValue);
            }
            finally
            {
                _semaphoreSlim.Release();
            }
        }
        /// <summary>
        /// Adds <paramref name="receiver"/> to the list of subscribers for this event.<br/>
        /// When the <see cref="RaiseEvent"/> method is called, any subscribed <paramref name="action"/>s will be invoked.<br/>
        /// After the <see cref="RaiseEvent"/> method is called, this <paramref name="action"/> will be unsubscribed automatically.<br/>
        /// </summary>
        /// <remarks>
        /// If the event has already been raised at least once, the <paramref name="action"/> will automatically be invoked.<br/>
        /// This is useful for synchronization, such as state data which may not be available until the event has been raised at least once.<br/>
        /// Using the <see cref="SubscribeAndDo"/> method we can subscribe to be notified as soon as state data is available,<br/>
        /// and invoke the <paramref name="action"/> as soon as it is, invoking immediately if it already available.<br/>
        /// Using the <see cref="SubscribeAndDoOnce"/> method we can subscribe just like <br/>
        /// using the <see cref="SubscribeAndDo"/> method, but our action will also be automatically unsubscribed once it is called.
        /// </remarks>
        public async Task SubscribeAndDoOnce(object receiver, Func<T, Task> action)
        {
            await _semaphoreSlim.WaitAsync();
            try
            {
                if (receiver == null || action == null) return;
                if (Fired)
                {
                    await action.Invoke(LastValue);
                    return;
                }

                if (!_oneTimeActions.Contains(receiver))
                    _oneTimeActions.Add(receiver);
                if (!_actions.ContainsKey(receiver))
                {
                    _actions.Add(receiver, action);
                    return;
                }

                _actions[receiver] = action;
            }
            finally
            {
                _semaphoreSlim.Release();
            }
        }

        /// <summary> Removes <paramref name="receiver"/> from the list of subscribers for this event. </summary>
        public async Task Unsubscribe(object receiver)
        {
            await _semaphoreSlim.WaitAsync();
            try
            {
                if (!_actions.ContainsKey(receiver)) return;
                _actions.Remove(receiver);
            }
            finally
            {
                _semaphoreSlim.Release();
            }
        }

        /// <summary> Raises this event, invoking any actions for receivers which are currently subscribed to this event. </summary>
        public async Task RaiseEvent(T value)
        {
            await _semaphoreSlim.WaitAsync();
            try
            {
                Fired = true;
                LastValue = value;
                foreach (var action in _actions.Where(action => action.Value != null))
                {
                    await action.Value.Invoke(LastValue);
                }
                foreach (var receiver in _oneTimeActions.Where(receiver => _actions.ContainsKey(receiver)))
                {
                    _actions.Remove(receiver);
                }
                _oneTimeActions.Clear();
            }
            finally
            {
                _semaphoreSlim.Release();
            }
        }
        /// <summary> Resets this event back to the default unfired state, setting the <see cref="Fired"/> property back to false. </summary>
        public async Task Reset()
        {
            await _semaphoreSlim.WaitAsync();
            try
            {
                Fired = false;
                LastValue = default;
            }
            finally
            {
                _semaphoreSlim.Release();
            }
        }
    }
    public class Event<T1, T2>
    {
        /// <summary> Indicates whether this event has been fired at least one time since it was last <see cref="Reset"/>. </summary>
        public bool Fired { get; private set; }
        public T1 LastValue1 { get; private set; }
        public T2 LastValue2 { get; private set; }

        private readonly SemaphoreSlim _semaphoreSlim = new(1, 1);
        private readonly List<object> _oneTimeActions = new();
        private readonly Dictionary<object, Func<T1, T2, Task>> _actions = new();

        /// <summary>
        /// Adds <paramref name="receiver"/> to the list of subscribers for this event.<br/>
        /// When the <see cref="RaiseEvent"/> method is called, any subscribed <paramref name="action"/>s will be invoked.<br/>
        /// </summary>
        public async Task Subscribe(object receiver, Func<T1, T2, Task> action)
        {
            await _semaphoreSlim.WaitAsync();
            try
            {
                if (receiver == null || action == null) return;
                if (!_actions.ContainsKey(receiver))
                {
                    _actions.Add(receiver, action);
                    return;
                }

                _actions[receiver] = action;
            }
            finally
            {
                _semaphoreSlim.Release();
            }
        }
        /// <summary>
        /// Adds <paramref name="receiver"/> to the list of subscribers for this event.<br/>
        /// When the <see cref="RaiseEvent"/> method is called, any subscribed <paramref name="action"/>s will be invoked.<br/>
        /// </summary>
        /// <remarks>
        /// If the event has already been raised at least once, the <paramref name="action"/> will automatically be invoked.<br/>
        /// This is useful for synchronization, such as state data which may not be available until the event has been raised at least once.<br/>
        /// Using the <see cref="SubscribeAndDo"/> method we can subscribe to be notified as soon as state data is available,<br/>
        /// and invoke the <paramref name="action"/> as soon as it is, invoking immediately if it already available.
        /// </remarks>
        public async Task SubscribeAndDo(object receiver, Func<T1, T2, Task> action)
        {
            await _semaphoreSlim.WaitAsync();
            try
            {
                if (receiver == null || action == null) return;
                if (!_actions.ContainsKey(receiver))
                {
                    _actions.Add(receiver, action);
                    if (!Fired) return;
                    await action.Invoke(LastValue1, LastValue2);
                }

                _actions[receiver] = action;
                if (!Fired) return;
                await action.Invoke(LastValue1, LastValue2);
            }
            finally
            {
                _semaphoreSlim.Release();
            }
        }
        /// <summary>
        /// Adds <paramref name="receiver"/> to the list of subscribers for this event.<br/>
        /// When the <see cref="RaiseEvent"/> method is called, any subscribed <paramref name="action"/>s will be invoked.<br/>
        /// After the <see cref="RaiseEvent"/> method is called, this <paramref name="action"/> will be unsubscribed automatically.<br/>
        /// </summary>
        /// <remarks>
        /// If the event has already been raised at least once, the <paramref name="action"/> will automatically be invoked.<br/>
        /// This is useful for synchronization, such as state data which may not be available until the event has been raised at least once.<br/>
        /// Using the <see cref="SubscribeAndDo"/> method we can subscribe to be notified as soon as state data is available,<br/>
        /// and invoke the <paramref name="action"/> as soon as it is, invoking immediately if it already available.<br/>
        /// Using the <see cref="SubscribeAndDoOnce"/> method we can subscribe just like <br/>
        /// using the <see cref="SubscribeAndDo"/> method, but our action will also be automatically unsubscribed once it is called.
        /// </remarks>
        public async Task SubscribeAndDoOnce(object receiver, Func<T1, T2, Task> action)
        {
            await _semaphoreSlim.WaitAsync();
            try
            {
                if (receiver == null || action == null) return;
                if (Fired)
                {
                    await action.Invoke(LastValue1, LastValue2);
                    return;
                }

                if (!_oneTimeActions.Contains(receiver))
                    _oneTimeActions.Add(receiver);
                if (!_actions.ContainsKey(receiver))
                {
                    _actions.Add(receiver, action);
                    return;
                }

                _actions[receiver] = action;
            }
            finally
            {
                _semaphoreSlim.Release();
            }
        }

        /// <summary> Removes <paramref name="receiver"/> from the list of subscribers for this event. </summary>
        public async Task Unsubscribe(object receiver)
        {
            await _semaphoreSlim.WaitAsync();
            try
            {
                if (!_actions.ContainsKey(receiver)) return;
                _actions.Remove(receiver);
            }
            finally
            {
                _semaphoreSlim.Release();
            }
        }

        /// <summary> Raises this event, invoking any actions for receivers which are currently subscribed to this event. </summary>
        public async Task RaiseEvent(T1 value1, T2 value2)
        {
            await _semaphoreSlim.WaitAsync();
            try
            {
                Fired = true;
                LastValue1 = value1;
                LastValue2 = value2;
                foreach (var action in _actions.Where(action => action.Value != null))
                {
                    await action.Value.Invoke(LastValue1, LastValue2);
                }
                foreach (var receiver in _oneTimeActions.Where(receiver => _actions.ContainsKey(receiver)))
                {
                    _actions.Remove(receiver);
                }
                _oneTimeActions.Clear();
            }
            finally
            {
                _semaphoreSlim.Release();
            }
        }
        /// <summary> Resets this event back to the default unfired state, setting the <see cref="Fired"/> property back to false. </summary>
        public async Task Reset()
        {
            await _semaphoreSlim.WaitAsync();
            try
            {
                Fired = false;
                LastValue1 = default;
                LastValue2 = default;
            }
            finally
            {
                _semaphoreSlim.Release();
            }
        }
    }
}
Hotrian Avatar