AsyncEvents 1.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package AsyncEvents --version 1.0.0
                    
NuGet\Install-Package AsyncEvents -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AsyncEvents" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AsyncEvents" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="AsyncEvents" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add AsyncEvents --version 1.0.0
                    
#r "nuget: AsyncEvents, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package AsyncEvents@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=AsyncEvents&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=AsyncEvents&version=1.0.0
                    
Install as a Cake Tool

Delegates/Events

Demonstrating delegate/event with asynchronous programming. If you are interested in final solution, without explanation how it works under the hoods and "not to do" with async delagates, refer to library AsyncEvents Information about delegates/events multicast, which inspired this example can be found here:

  1. https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/delegates/
  2. https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/events/
  3. https://stackoverflow.com/questions/73443885/when-adding-an-async-delegate-to-an-event-how-to-make-the-process-continue-afte
  4. https://stackoverflow.com/questions/12451609/how-to-await-raising-an-eventhandler-event
  5. https://blog.stephencleary.com/2013/02/async-oop-5-events.html
  6. https://www.c-sharpcorner.com/UploadFile/vendettamit/delegates-and-async-programming/
  7. https://github.com/microsoft/vs-threading/blob/main/src/Microsoft.VisualStudio.Threading/README.md

Demo0 - synchronous (delegate void)

This demo demonstrates using System.EventHandler in synchronous manner. It demosntrates that handlers/subscribers subscibed to publisher using eventPublisher.OnEventCounting += (sender, value) => ...

Main principle is below (one subscriber and on event)

  1. Publisher enables to subscribe event OnEventCounting and for demo purposes enables to trigger the event using RaiseCountingEvent()
  2. In the Main() method we subscribe using eventPublisher.OnEventCounting += and than asks publisher to simulated event by eventPublisher.RaiseCountingEvent(123)
namespace System
{
    public delegate void EventHandler<TEventArgs>(object? sender, TEventArgs e);
}

class Publisher
{
    public event EventHandler<int>? OnEventCounting;

    public void  RaiseCountingEvent(int value)
    {
        OnEventCounting?.Invoke(this, value);
    }
}

static Main()
{
    var eventPublisher = new EventPublisher();
    eventPublisher.OnEventCounting +=  (sender, value) => Console.WriteLine($"OnEventCounting({value}) received (subscriber 1)");
    eventPublisher.RaiseCountingEvent(123);
}

The full demo below is demonstrating multiple events and multiple subscribers and faulty handler(s) which throws an exception. As described in MS documetation, handlers are called/processed sequentially in the same order as they were subscribed. Main method triggers via publisher the event OnEventCounting 1 .. 5 with 1 second delay. In this example second subscriber is faulty (raises exception throw new Exception("Exception in subscriber 2")) and it causes that program stops and only first handler finish processing. Handlers 3,4,5 are not triggered at all.

Source code

namespace EventDemo
{
    internal class ProgramEventDemo
    {
        static void  Main(string[] args)
        {
            try
            {
                Console.WriteLine("App started");

                var eventPublisher = new EventPublisher();

                Console.WriteLine("Subscribing event handlers");

                // Subscribe to the event OnLongRunningTaskFinished
                eventPublisher.OnLongRunningTaskFinished += (sender, args) =>
                {
                    Console.WriteLine("***Long running task finished***");
                };

                // Subscribe to the event OnEventCounting first subscriber
                eventPublisher.OnEventCounting +=  (sender, value) =>
                {
                    Console.WriteLine($"OnEventCounting({value}) received (subscriber 1)");

                };

                // Subscribe to the event OnEventCounting second subscriber
                eventPublisher.OnEventCounting +=  (sender, value) =>
                {
                    Console.WriteLine($"OnEventCounting({value}) received (subscriber 2) - raising Exception");
                    throw new Exception("Exception in subscriber 2");
                    //Console.WriteLine($"OnEventCounting({value}) processed (subscriber 2)");
                };

                // Subscribe to the event OnEventCounting third subscriber (this handler is performing very time expensive handling). 
                eventPublisher.OnEventCounting +=  (sender, value) =>
                {
                    Console.WriteLine($"OnEventCounting({value}) received (subscriber 3) - long running handler");
                    Thread.Sleep(5000);
                };

                // Subscribe to the event OnEventCounting forth subscriber
                eventPublisher.OnEventCounting +=  (sender, value) =>
                {
                    Console.WriteLine($"OnEventCounting({value}) received (subscriber 4) - raising Exception");
                    throw new Exception("Exception in subscriber 4");
                    //Console.WriteLine($"OnEventCounting({value}) processed (subscriber 4)");
                    // Task.CompletedTask;
                };

                // Subscribe to the event OnEventCounting fifth subscriber
                eventPublisher.OnEventCounting +=  (sender, value) =>
                {
                    Console.WriteLine($"OnEventCounting({value}) received (subscriber 5)");

                };

                // Start/simulate long running task (3 seconds)
                eventPublisher.StartLongRunningTask(TimeSpan.FromSeconds(3));

                // Raise the event OnEventCounting 1 .. 5 with 1 second delay
                for (int i = 1; i <= 5; i++)
                {
                    // Raise the event OnEventCounting i
                    Console.WriteLine($" -- Raising event OnEventCounting {i} -- ");
                     eventPublisher.RaiseCountingEvent(i);
                    Thread.Sleep(1000);
                }

                // Wait indefinitely
                Thread.Sleep(10000);
            }
            catch (Exception exc)
            {
                Console.WriteLine($"App crashed {exc.Message}\n{exc.ToString()}");
            }
            finally
            {
                Console.WriteLine("App finished");
            }
        }

        private class EventPublisher
        {
            public event EventHandler? OnLongRunningTaskFinished;

            public event EventHandler<int>? OnEventCounting;

            /// <summary>
            /// Raises (invokes) the event OnEventCounting for the specified value.
            /// It calls the subscribers one by one (as normal delegate/event would do) 
            /// </summary>
            /// <param name="value">Value which will be sent to subscriber</param>
            /// <returns></returns>
            public void  RaiseCountingEvent(int value)
            {
                //Handlers are started sequentionally and exception in one stops the rest from processing
                 OnEventCounting?.Invoke(this, value);
            }

            /// <summary>
            /// Raises (invokes) the event OnLongRunningTaskFinished event ater the task is finished ( duration is defined by input <paramref name="taskDuration"/> )
            /// It calls the subscribers one by one (as normal delegate/event would do).
            /// </summary>
            /// <param name="taskDuration"></param>
            /// <returns></returns>
            public void StartLongRunningTask(TimeSpan taskDuration)
            {
                Console.WriteLine($"Long Running Task started with duration {taskDuration.TotalSeconds} seconds.");
                // Simulate long running mthod
                Thread.Sleep(taskDuration);
                //Handlers are started sequentionally and exception in one stops the rest from processing
                OnLongRunningTaskFinished?.Invoke(this,EventArgs.Empty);
            }

        }
    }
}

Output

App started
Subscribing event handlers
Long Running Task started with duration 3 seconds.
***Long running task finished***
 -- Raising event OnEventCounting 1 --
OnEventCounting(1) received (subscriber 1)
OnEventCounting(1) processed (subscriber 1)
OnEventCounting(1) received (subscriber 2) - raising Exception
App crashed Exception in subscriber 2
System.Exception: Exception in subscriber 2
   at EventDemo.ProgramEventDemo.<>c.<Main>b__0_2(Object sender, Int32 value) in Y:\Personal_Projects\AsyncEvents\AsyncEventDemo\EventDemo\ProgramEventDemo.cs:line 36
   at EventDemo.ProgramEventDemo.EventPublisher.RaiseCountingEvent(Int32 value) in Y:\Personal_Projects\AsyncEvents\AsyncEventDemo\EventDemo\ProgramEventDemo.cs:line 110
   at EventDemo.ProgramEventDemo.Main(String[] args) in Y:\Personal_Projects\AsyncEvents\AsyncEventDemo\EventDemo\ProgramEventDemo.cs:line 77
App finished

Demo1: delegate (async) Task

This demo is trying to solve how to subscibe asynchronous handlers ( for example method with signature async Task DoSomething(int value) ) It is possible to declare delagate with return type Task which can be later used by publisher. For example (simplified example without null checking):

public delegate Task EventHandlerAsync<TEventArgs>(object? sender, TEventArgs e, CancellationToken ct = default);

class Publisher
{
    public event EventHandlerAsync<int>? OnEventCounting;

    public async Task RaiseCountingEventAsync(int value)
    {
        await OnEventCounting.Invoke(this, value);
    }
}

Complete demo source code is below.

The code below compiles, runs, events are raised/multicasted (in other words handlers are called) with following in mind:

  1. Handlers are started sequentionally (in the same order as subscribed). The same way as synchronous (void delegate) would be called (refer to https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/delegates/using-delegates)
  2. BUT !!! Handlers are not awaited at all, so result of the previus handler cannot be used as an input to the following handler. Following handler might be even strated before previous handler finishes.
  3. Exceptions thrown in hadlers (for example throw new Exception("Exception in subscriber 2")) disseapear:
    1. For example try-catch block in Program does not catch such exception(s).
    2. Handlers subscribed after faulty handler are triggered/called ( handlers 3. 4. and 5. are triggered/called). This would not happen with non-async delagate (void delegate):
      .
      .
      OnEventCounting(1) received (subscriber 2) - raising Exception // no following OnEventCounting(1) processed (subscriber 2)
      OnEventCounting(1) received (subscriber 3)
      OnEventCounting(1) received (subscriber 4)
      OnEventCounting(1) received (subscriber 5)
      
      The root cause (TODO confirm in delegate source code) is that exception end up in TaskScheduler as UnobservedTaskException. It can be caught by:
      TaskScheduler.UnobservedTaskException += (s, e) => Console.WriteLine($"TaskScheduler_UnobservedTaskException: {e.Exception.Message}");
      
      The Output would look like this:
      App started
      Subscribing event handlers
      Long Running Task started with duration 3 seconds.
       -- Raising event OnEventCounting 1 --
      OnEventCounting(1) received (subscriber 1)
      OnEventCounting(1) received (subscriber 2) - raising Exception
      OnEventCounting(1) received (subscriber 3) - long running handler
      OnEventCounting(1) received (subscriber 4) - raising Exception
      OnEventCounting(1) processed (subscriber 1)
      OnEventCounting(1) received (subscriber 5)
      TaskScheduler_UnobservedTaskException: Exception in subscriber 4
      TaskScheduler_UnobservedTaskException: Exception in subscriber 2
      OnEventCounting(1) processed (subscriber 5)
       -- Raising event OnEventCounting 2 --
       .
       .
       .
      

Source code

Above described example with full source code and output

namespace AsyncEventDemo
{	
    public delegate Task EventHandlerAsync(object? sender, CancellationToken ct = default);
    public delegate Task EventHandlerAsync<TEventArgs>(object? sender, TEventArgs e, CancellationToken ct = default);
	
    internal class Program
    {
 
        static async Task Main(string[] args)
        {
            try
            {
                Console.WriteLine("App started");
 
                var eventPublisher = new EventPublisherAsync();
 
                Console.WriteLine("Subscribing event handlers");
 
                // Subscribe to the event OnLongRunningTaskFinished
                eventPublisher.OnLongRunningTaskFinished += async (sender, ct) =>
                {
                    await Task.Delay(1);
                    Console.WriteLine("***Long running task finished***");
                };
 
                // Subscribe to the event OnEventCounting first subscriber
                eventPublisher.OnEventCounting += async (sender, value, ct) =>
                {
                    Console.WriteLine($"OnEventCounting({value}) received (subscriber 1)");
                    await Task.Delay(1);
                    Console.WriteLine($"OnEventCounting({value}) processed (subscriber 1)");
 
                };
 
                // Subscribe to the event OnEventCounting second subscriber
                eventPublisher.OnEventCounting += async (sender, value, ct) =>
                {
                    Console.WriteLine($"OnEventCounting({value}) received (subscriber 2) - raising Exception");
                    await Task.Delay(1);
                    throw new Exception("Exception in subscriber 2");
                    Console.WriteLine($"OnEventCounting({value}) processed (subscriber 2)");
                    await Task.CompletedTask;
                };
 
                // Subscribe to the event OnEventCounting third subscriber (this handler is performing very time expensive handling). 
                eventPublisher.OnEventCounting += async (sender, value, ct) =>
                {
                    Console.WriteLine($"OnEventCounting({value}) received (subscriber 3) - long running handler");
                    await Task.Delay(5000);
                    Console.WriteLine($"OnEventCounting({value}) processed (subscriber 3)");
                };
 
                // Subscribe to the event OnEventCounting forth subscriber
                eventPublisher.OnEventCounting += async (sender, value, ct) =>
                {
                    Console.WriteLine($"OnEventCounting({value}) received (subscriber 4) - raising Exception");
                    await Task.Delay(1);
                    throw new Exception("Exception in subscriber 4");
                    Console.WriteLine($"OnEventCounting({value}) processed (subscriber 4)");
                    //await Task.CompletedTask;
                };
 
                // Subscribe to the event OnEventCounting fifth subscriber
                eventPublisher.OnEventCounting += async (sender, value, ct) =>
                {
                    Console.WriteLine($"OnEventCounting({value}) received (subscriber 5)");
                    await Task.Delay(1);
                    Console.WriteLine($"OnEventCounting({value}) processed (subscriber 5)");
 
                };
                // Start/simulate long running task (3 seconds)
#pragma warning disable CS4014 // Intentionally not waited for the task to finish
                eventPublisher.StartLongRunningTaskAsync(TimeSpan.FromSeconds(3));
#pragma warning restore CS4014
 
                // Raise the event OnEventCounting 1 .. 5 with 1 second delay
                for (int i = 1; i <= 5; i++)
                {
                    // Raise the event OnEventCounting i
                    Console.WriteLine($" -- Raising event OnEventCounting {i} -- ");
                    await eventPublisher.RaiseCountingEventAsync(i).ConfigureAwait(false);
                    await Task.Delay(1000).ConfigureAwait(false);
                }
 
                // Wait indefinitely
                await Task.Delay(10000);
            }
            catch (Exception exc)
            {
                Console.WriteLine($"App crashed {exc.Message}\n{exc.ToString()}");
            }
            finally
            {
                Console.WriteLine("App finished");
            }
        }
 
        private class EventPublisherAsync
        {
            public event EventHandlerAsync? OnLongRunningTaskFinished;
 
            public event EventHandlerAsync<int>? OnEventCounting;
 
            /// <summary>
            /// Raises (invokes) the event OnEventCounting for the specified value.
            /// It calls the subscribers one by one (as normal delegate/event would do) 
            /// </summary>
            /// <param name="value">Value which will be sent to subscriber</param>
            /// <returns></returns>
            public async Task RaiseCountingEventAsync(int value)
            {
                // No exceptions are caught and if any of the subscribers throws an exception, the following subscribers would be notified as well.
                //Handlers are started sequentionally, but not awaited at all.
                await (OnEventCounting?.Invoke(this, value) ?? Task.CompletedTask).ConfigureAwait(false);
            }
 
            /// <summary>
            /// Raises (invokes) the event OnLongRunningTaskFinished event ater the task is finished ( duration is defined by input <paramref name="taskDuration"/> )
            /// It calls the subscribers one by one (as normal delegate/event would do).
            /// </summary>
            /// <param name="taskDuration"></param>
            /// <returns></returns>
            public async Task StartLongRunningTaskAsync(TimeSpan taskDuration)
            {
                Console.WriteLine($"Long Running Task started with duration {taskDuration.TotalSeconds} seconds.");
                // Simulate long running task
                await Task.Delay(taskDuration).ConfigureAwait(false);
                // Notify subscribers
                // No exceptions are caught and if any of the subscribers throws an exception, the following subscribers would be notified as well.
                //Handlers are started sequentionally, but not awaited at all.
	                await (OnLongRunningTaskFinished?.Invoke(this) ?? Task.CompletedTask).ConfigureAwait(false);
            }
        }
    }
}

Output

Console output

	App started
	Long Running Task started with duration 3 seconds.
	 -- Raising event OnEventCounting 1 --
	OnEventCounting(1) received (subscriber 1)
	OnEventCounting(1) received (subscriber 2) - raising Exception // no following OnEventCounting(1) processed (subscriber 2)
	OnEventCounting(1) processed (subscriber 1)
	OnEventCounting(1) received (subscriber 3) - long running handler
	OnEventCounting(1) received (subscriber 4) - raising Exception
	OnEventCounting(1) received (subscriber 5)
	OnEventCounting(1) processed (subscriber 5)
	 -- Raising event OnEventCounting 2 --
	OnEventCounting(2) received (subscriber 1)
	OnEventCounting(2) received (subscriber 2) - raising Exception
	OnEventCounting(2) received (subscriber 3) - long running handler
	OnEventCounting(2) processed (subscriber 1)
	OnEventCounting(2) received (subscriber 4) - raising Exception
	OnEventCounting(2) received (subscriber 5)
	OnEventCounting(2) processed (subscriber 5)
	 -- Raising event OnEventCounting 3 --
	OnEventCounting(3) received (subscriber 1)
	OnEventCounting(3) received (subscriber 2) - raising Exception
	OnEventCounting(3) received (subscriber 3) - long running handler
	OnEventCounting(3) processed (subscriber 1)
	OnEventCounting(3) received (subscriber 4) - raising Exception
	OnEventCounting(3) received (subscriber 5)
	OnEventCounting(3) processed (subscriber 5)
	***Long running task finished***
	 -- Raising event OnEventCounting 4 --
	OnEventCounting(4) received (subscriber 1)
	OnEventCounting(4) received (subscriber 2) - raising Exception
	OnEventCounting(4) processed (subscriber 1)
	OnEventCounting(4) received (subscriber 3) - long running handler
	OnEventCounting(4) received (subscriber 4) - raising Exception
	OnEventCounting(4) received (subscriber 5)
	OnEventCounting(4) processed (subscriber 5)
	 -- Raising event OnEventCounting 5 --
	OnEventCounting(5) received (subscriber 1)
	OnEventCounting(5) received (subscriber 2) - raising Exception
	OnEventCounting(5) processed (subscriber 1)
	OnEventCounting(5) received (subscriber 3) - long running handler
	OnEventCounting(5) received (subscriber 4) - raising Exception
	OnEventCounting(5) received (subscriber 5)
	OnEventCounting(5) processed (subscriber 5)
	OnEventCounting(1) processed (subscriber 3)
	OnEventCounting(2) processed (subscriber 3)
	OnEventCounting(3) processed (subscriber 3)
	OnEventCounting(4) processed (subscriber 3)
	OnEventCounting(5) processed (subscriber 3)

Demo2: delegate Task - Not awaited

Handlers are not awaited at all, so result of the previus handler cannot be used as an input to the following handler. TODO confirm and add example

Demo3: delegate Task - without async

Non-async handlers exceptions are propagated as from non-async and exception breaks hanlers chain and is propagated to Program.Main Example of such handler (the rest of the code is the same)

// Subscribe to the event OnEventCounting second subscriber
eventPublisher.OnEventCounting += (sender, value, ct) =>    // <========== Here is missing keyword async
{
    Console.WriteLine($"OnEventCounting({value}) received (subscriber 2) - raising Exception");
    throw new Exception("Exception in subscriber 2");   // <========== This exception breaks hanlers chain and is propagated to Program.Main
};

Output

App started
Subscribing event handlers
Long Running Task started with duration 3 seconds.
 -- Raising event OnEventCounting 1 --
OnEventCounting(1) received (subscriber 1)
OnEventCounting(1) received (subscriber 2) - raising Exception
OnEventCounting(1) processed (subscriber 1)
App crashed Exception in subscriber 2
System.Exception: Exception in subscriber 2
   at AsyncEventDemo.ProgramAsyncEventDemo.<>c.<Main>b__0_5(Object sender, Int32 value, CancellationToken ct) in Y:\Personal_Projects\AsyncEvents\AsyncEventDemo\AsyncEventDemo\ProgramAsyncEventDemo.cs:line 42
   at AsyncEventDemo.ProgramAsyncEventDemo.EventPublisherAsync.RaiseCountingEventAsync(Int32 value) in Y:\Personal_Projects\AsyncEvents\AsyncEventDemo\AsyncEventDemo\ProgramAsyncEventDemo.cs:line 117
   at AsyncEventDemo.ProgramAsyncEventDemo.Main(String[] args) in Y:\Personal_Projects\AsyncEvents\AsyncEventDemo\AsyncEventDemo\ProgramAsyncEventDemo.cs:line 83
App finished

Demo4: : delegate (async) Task awaited

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.1

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.1 135 9/17/2024
1.0.0 111 9/17/2024