An open API service indexing awesome lists of open source software.

https://github.com/amarok79/amarok.events

A fast and light-weight implementation of the observer pattern that supports synchronous and asynchronous invocation and/or subscribers. A potential replacement for regular .NET events.
https://github.com/amarok79/amarok.events

async dotnet events observer-pattern

Last synced: 3 months ago
JSON representation

A fast and light-weight implementation of the observer pattern that supports synchronous and asynchronous invocation and/or subscribers. A potential replacement for regular .NET events.

Awesome Lists containing this project

README

          

[![NuGet](https://img.shields.io/nuget/v/Amarok.Events.svg?logo=)](https://www.nuget.org/packages/Amarok.Events/)

# Introduction

This library provides a fast and light-weight implementation of the observer pattern, which can be used as a replacement
for regular .NET events. The implementation supports raising events in a *synchronous, blocking* or *asynchronous,
await-able* fashion. Besides, both *synchronous* and *asynchronous* event handler can be registered.

The implementation is a bit slower than ordinary .NET events regarding raw call performance, but optimized to avoid
allocations and therefore suitable for high-performance scenarios or resource-constraint embedded systems.

# Redistribution

The library is redistributed as NuGet package: [Amarok.Events](https://www.nuget.org/packages/Amarok.Events/)

The package provides strong-named binaries for *.NET Standard 2.0*, *.NET 6.0*, *.NET 8.0*, and *.NET 10.0*. Tests are performed with *.NET Framework 4.8*, *.NET 6.0*, *.NET 8.0*, and *.NET 10.0*.

# Documentation

Table of Content:

- [Introduction](#introduction)
- [Redistribution](#redistribution)
- [Documentation](#documentation)
- [Event Source and Event](#event-source-and-event)
- [Invoke with Synchronous Event Handler](#invoke-with-synchronous-event-handler)
- [Invoke with Asynchronous Event Handler](#invoke-with-asynchronous-event-handler)
- [InvokeAsync with Asynchronous Event Handler](#invokeasync-with-asynchronous-event-handler)
- [InvokeAsync with Synchronous Event Handler](#invokeasync-with-synchronous-event-handler)
- [Raising Events](#raising-events)
- [Weak Subscriptions](#weak-subscriptions)
- [Exception Behavior](#exception-behavior)
- [IProgress\ Integration](#iprogresst-integration)
- [Event Recorder](#event-recorder)
- [Frequently Asked Questions](#frequently-asked-questions)
- [Is this library thread-safe?](#is-this-library-thread-safe)
- [What are the advantages compared to .NET events?](#what-are-the-advantages-compared-to-net-events)
- [What about Reactive Extensions?](#what-about-reactive-extensions)
- [Who uses this library?](#who-uses-this-library)

### Event Source and Event

Suppose you have an interface and you want to expose an event on that interface. You do that as follows:

```cs
public interface IFooService
{
Event Progress { get; }
}
```

The event is declared as *getter-only property* of type **Event\**, where **T** represents the type of event
argument. **T** can be of any type.

The implementation class of that interface then initializes a field of type **EventSource\** and implements the
getter-only event property.

```cs
internal sealed class FooServiceImpl : IFooService
{
private readonly EventSource mProgressEventSource = new EventSource();

public Event Progress => mProgressEventSource.Event;

public void DoSomething()
{
// raises the event
mProgressEventSource.Invoke(50);
}
}
```

In general, the *event source* should be kept private, while the associated **Event\** is made public. This is
similar to the pattern used for *CancellationToken* and *CancellationTokenSource*, or *Task\* and
*TaskCompletionSource\*.

For raising the event, one calls **Invoke(**..**)** on the *event source*. Here you supply the event argument that is
forwarded to all event handlers.

Next, a consumer of the service can subscribe to the event. It just has to call **Subscribe(**..**)** on the *event*
that is made public by the service.

```cs
FooServiceImpl serviceImpl = new FooServiceImpl();
IFooService service = serviceImpl;

IDisposable subscription = service.Progress.Subscribe(x => {
Console.WriteLine(x + "%");
});

serviceImpl.DoSomething(); // internally raises the event
// console output: 50%
```

The object returned from **Subscribe(**..**)** can be used to cancel the subscription at any time.

```cs
subscription.Dispose();

serviceImpl.DoSomething();
// does nothing, since no subscribers are registered anymore
```

It is recommended that subscribers store these subscription objects somewhere. Otherwise, they won't be able to remove
their registered event handlers.

If instead the class exposing the event wants to cancel all subscriptions, for example, because it gets disposed, it can
dispose the *event source* too, which automatically cancels all subscriptions and ignores further calls to **Invoke(**..
**)**.

```cs
internal sealed class FooServiceImpl : IFooService
{
...

public void Dispose()
{
mProgressEventSource.Dispose();
// cancels all subscriptions, discards new subscriptions and
// ignores any call to Invoke()
}
}
```

### Invoke with Synchronous Event Handler

The following code snippet shows a single *event source* with two event handlers. Both event handler and also the code
invoking the event print to the console. What`s the console output?

```cs
var source = new EventSource();

source.Event.Subscribe(x => { // sync event handler
Console.WriteLine(x + "1");
});

source.Event.Subscribe(x => { // sync event handler
Console.WriteLine(x + "2");
});

Console.WriteLine("A");
source.Invoke("B");
Console.WriteLine("C");
```

The output is:

A
B1
B2
C

This shows that event handlers are invoked directly by the thread that calls **Invoke()**. There is no additional
threading introduced by the library. Also, **Invoke()** returns directly after all event handlers have completed.

Please note, the order in which event handlers are invoked is not deterministic. You shouldn't rely on that.

### Invoke with Asynchronous Event Handler

Now, let's take the same example but slightly modified with *async* event handlers. What's the output of this?

```cs
var source = new EventSource();

source.Event.Subscribe(async x => { // async event handler
await Task.Delay(100);
Console.WriteLine(x + "1");
});

source.Event.Subscribe(async x => { // async event handler
await Task.Delay(200);
Console.WriteLine(x + "2");
});

Console.WriteLine("A");
source.Invoke("B");
Console.WriteLine("C");
```

The output is:

A
C
...
B1 (100 ms delayed)
B2 (200 ms delayed)

Again, the thread calling **Invoke()** is also calling the event handlers. However, this time, it returns after
encountering the first *await* statement, causing **Invoke()** to return earlier as the event handler's continuations.

That means a consumer can decide whether it wants to register a synchronous or asynchronous event handler. In the latter
case, from a perspective of the event raiser, the behavior is kind of fire-and-forget, because the event raiser can't be
sure that all event handlers have completed when **Invoke()** returned.

If you need that guarantee then use **InvokeAsync()** instead.

### InvokeAsync with Asynchronous Event Handler

As mentioned previously, **InvokeAsync()** can be used if awaiting the completion of all event handlers is necessary.

```cs
var source = new EventSource();

source.Event.Subscribe(async x => { // async event handler
await Task.Delay(100);
Console.WriteLine(x + "1");
});

source.Event.Subscribe(async x => { // async event handler
await Task.Delay(200);
Console.WriteLine(x + "2");
});

Console.WriteLine("A");
await source.InvokeAsync("B"); // await !!
Console.WriteLine("C");
```

This time the output is:

A
...
B1 (100 ms delayed)
B2 (200 ms delayed)
C

Feels sequential, although it runs fully asynchronous due to the magic of *async* and *await*.

Please note that there is still no additional threading involved. The thread calling **InvokeAsync()** still starts to
execute the event handlers. The only special thing here is that **InvokeAsync()** awaits the completion of all those
event handlers.

If for example, all registered event handlers are async methods but don't await anything, then the entire event
invocation would be processed in a synchronous fashion. In fact, the library implementation has special optimizations in
place for this specific scenario of async handlers that don't await and complete immediately.

### InvokeAsync with Synchronous Event Handler

Of course, it is also possible to use **InvokeAsync()** for raising events, but with subscribers that register only
synchronous event handlers. This is valid, and the library implementation optimizes this scenario so that there is
little overhead even though *async/await* is involved.

```cs
var source = new EventSource();

source.Event.Subscribe(x => { // sync event handler
Console.WriteLine(x + "1");
});

source.Event.Subscribe(x => { // sync event handler
Console.WriteLine(x + "2");
});

Console.WriteLine("A");
await source.InvokeAsync("B"); // await !!
Console.WriteLine("C");
```

Of course, the output is:

A
B1
B2
C

### Raising Events

We have already learned that events are raised by calling **Invoke()** or **InvokeAsync()** providing the event
argument. In most cases, the event argument won't be a simple string or integer value but some class that must be
constructed and filled with information.

```cs
var source = new EventSource();

var arg = new FooEventArg() { ... };
source.Invoke(arg);
```

Now, what happens if you raise an event and not a single event handler has been registered? In that case, the
construction of a new event argument is wasted CPU instructions and memory allocation, because **Invoke()** returns
immediately without doing anything with the supplied event argument.

What if you want to avoid such wasteful instructions?

Well, you can use one of the provided overloads that accept a *value factory* for constructing the event argument.

```cs
source.Invoke(() => {
return new FooEventArg() { ... }
// value factory is only called, if at least a single
// event handler is registered
});
```

If you need to pass some value to the *value factory*, you can do that too. Use it to avoid closure allocations.

```cs
source.Invoke((arg1) => {
return new FooEventArg() { ... }
// value factory is only called, if at least a single
// event handler is registered
},
123); // supplied as arg1
```

The same overloads are available for **InvokeAsync()** too.

### Weak Subscriptions

It is possible to register an event handler (synchronous or asynchronous) via weak subscription. That is one that is
automatically removed after the event handler has been garbage collected.

Weak subscriptions support a particular but common use case. Using weak subscriptions in a wrong way can be dangerous as
it can cause hard-to-reproduce bugs. Consider this as a warning. However, correctly used they can help in preventing
memory leaks, for example, just because you forgot to manually remove an event handler.

Let's imagine an application where you have a set of services. These services are mainly persistent and live for the
entire application lifetime. Such services are commonly registered as singletons in a dependency injection container.

Suppose **IUserManagement** represents such a service. As in all the previous examples, the service exposes an event.

```cs
public interface IUserManagement
{
Event UserAdded { get; }
}
```

Next, imagine we have consumers of that service that register on that event.

For example, we might have other persistent services, but they are not a big deal, because they get constructed at some
time, register on our **UserAdded** event and the event subscription exists for the remaining application lifetime, same
as the involved services.

Quite different are user interface related objects like views. Those don't live for the entire application lifetime, but
get constructed, register on events, get closed, disposed. When you forget to remove a subscription taken by such a view
you have a memory leak. The size of leaked memory increases as the view gets opened and closed multiple times.
This happens because as in any other observer pattern implementation the observer (in our case the event source)
maintains a strong reference to the observable (event handler in our case). This causes quite often memory leaks.

Here come weak subscriptions into play as they can help prevent such memory leaks. They free the developer from the
burden to manually remove event subscriptions.

Such a UI view can use weak subscriptions on our **UserAdded** event as follows.

```cs
public sealed class BarView
{
// this field is necessary to hold the event subscription
private IDisposable mUserAddedSubscription;

public BarView(IFooService service)
{
// when using SubscribeWeak() the returned object must be
// stored into a field, otherwise the subscription will get
// out of scope and get garbage collected too early
mUserAddedSubscription = service.UserAdded.SubscribeWeak(
x => HandleUserAdded(x)
);
}

private void HandleUserAdded(User user)
{
...
}
}
```

That's it.

In fact, there is just one important point here: Store the subscription object returned by **SubscribeWeak()** into a
member field of the same object as your event handler.

You can use that returned object also to cancel the subscription at any time, for example, when the view gets closed.
That makes subscription cancellation more deterministic.

If you don't cancel the subscription manually, it is automatically removed from the service event after the view gets
closed and garbage collected. This works since only the view hold a strong reference to the subscription (as shown in
the previous example). With weak subscriptions, the *event source* of our service doesn't maintain a strong reference to
the subscription and the event handler anymore.

Since the view is kept in memory from other root objects (the UI framework) the subscription is kept alive, and the
event handler in the view is invoked as expected. After the view gets closed, all strong references to the view are
removed, meaning the view and also it's (the only) strong reference to the subscription are garbage collected.

Weak subscriptions are no silver bullet. As always choose the right tool for the job.

### Exception Behavior

Consider the following example. What if one of the event handlers throws an exception.

```cs
var source = new EventSource();

source.Event.Subscribe(x => { // sync event handler
Console.WriteLine(x + "1");
throw new Exception();
});

source.Event.Subscribe(x => { // sync event handler
Console.WriteLine(x + "2");
});

Console.WriteLine("A");
source.Invoke("B");
Console.WriteLine("C");
```

What would you expect to happen? Is the second event handler called regardless of the exception? Is the exception
propagated back to the caller?

Well, regarding exception handling, this library takes a different, maybe controversial approach. I consider it a design
flaw of regular .NET events that invocation of the remaining event handlers is aborted if one of the previously invoked
event handlers threw an exception. Since the thrown exception is reported back to the event publisher, this makes the
event publisher dependent on its subscribers, but the entire observer design pattern exists to decouple both, publisher
and subscribers.

In my opinion, the pattern only makes sense, if a publisher doesn't need to care about whether there are subscribers, or
whether those subscribers fail with exceptions. The publisher's only responsibility is to invoke all registered event
handlers in all cases.

So, our example generates following output:

A
B1
B2
C

B2 is reliably invoked, even though B1 threw an exception.

But, what happened with the exception?

Well, the event publisher is NOT bothered with exception handling. Events are there to notify other parts of the
application. This is kind of one-way communication. Exceptions are therefore not propagated back to the caller.

Instead, the library catches all exceptions thrown in event handlers and forwards them to the global event *
*UnobservedException** on **EventSystem**. That way an application can observe all otherwise unobserved exceptions
thrown in event handlers and at least log them.

```cs
EventSystem.UnobservedException.Subscribe(ex => {
Console.WriteLine(ex);
});
```

If an application wants to handle those exceptions - and that is generally recommended - then the exception handling
should happen in the event handlers itself. If exceptions can occur in an event handler, then the event handler is
responsible for careful handling.

### IProgress\ Integration

The interface **IProgress\** defined by .NET BCL is a commonly-used way to transfer progress.

For example, long-running methods often accept an **IProgress\** as an argument to report back progress.

```cs
public void SomeLongRunningMethod(IProgress progress)
{
// reports progress back to caller
for(Int32 i = 0; i < 100; i++)
progress.Report(i);
}
```

The caller then supplies an implementation to receive the progress. This is often done using the ready-made *
*Progress\** class.

```cs
var progress = new Progress(x =>
Console.WriteLine(x)
);

SomeLongRunningMethod(progress);

// output might be out-of-order
0
1
2
6
3
4
...
99
98
```

Depending on from which thread you call this long-running method you might be surprised that the console output is not
always sequentially increasing numbers from 0 to 99. That's because **Progress\** invokes its callback via a
synchronization context.

That can be the UI thread, then sequential numbers 0 to 99 will be the result because everything is synchronized via the
UI thread.

If it's not the UI thread, then the callback will be invoked via the thread-pool thread, which means callbacks can come
out-of-order!

As an alternative, you can supply an **EventSource\** as progress object. It forwards progress to its subscribers in
the correct order.

```cs
var progress = new EventSource();

progress.Event.Subscribe(x =>
Console.WriteLine(x)
);

SomeLongRunningMethod(progress);

// always generates:
0
1
2
...
97
98
99
```

Of course, it is also possible to subscribe an **IProgress\** onto an event, so that raised event arguments are
forwarded to the progress object.

```cs
IProgress progress = new Progress(x =>
Console.WriteLine(x)
);

var source = new EventSource();
source.Event.Subscribe(progress);

source.Invoke(123);

// output:
123
```

### Event Recorder

If you are writing unit tests, then there will come the time where you want to ensure that an event on your
subject-under-test is correctly raised with the expected event arguments.

To ease unit testing, this library provides a ready-made event recorder that can be used to record events and then,
later on, analyze them.

As an example, suppose we have the following implementation class.

```cs
public sealed class UserManagementService
{
private readonly EventSource mUserAddedEvent = new EventSource();

public void AddUser(String name)
{
mUserAddedEvent.Invoke(name);
}
}
```

In a unit test, we want to assert that the event is raised and that the supplied name is supplied to the event.

```cs
[Test]
public void AddUserRaisesEventWithUserName
{
// arrange
var sut = new UserManagementService();
var recorder = EventRecorder.From(sut.UserAdded);

// act
sut.AddUser("Foo");
Thread.Sleep(50);
sut.AddUser("Bar");

// assert (using NFluent assertions)
Check.That(recorder.Events)
.HasSize(2); // we expect two events

Check.That(recorder.Events[0])
.IsEqualTo("Foo"); // first "Foo" was recorded
Check.That(recorder.Events[1])
.IsEqualTo("Bar"); // then "Bar" as expected
}
```

The event recorder can be used to gain even more information. Instead of accessing property **Events**, one can use *
*EventInfos**, which returns timing and thread information about the recorded events.

```cs
recorder.EventInfos[0].Value // "Foo"
recorder.EventInfos[0].Index // 0
recorder.EventInfos[0].Timestamp // DateTimeOffset
recorder.EventInfos[0].TimeOffset // 0 ms
recorder.EventInfos[0].Thread // the calling thread

recorder.EventInfos[1].Value // "Bar"
recorder.EventInfos[1].Index // 1
recorder.EventInfos[1].Timestamp // DateTimeOffset
recorder.EventInfos[1].TimeOffset // 50 ms
recorder.EventInfos[1].Thread // the calling thread
```

If you don't want to record events temporarily, you can **Pause()** and finally **Resume()** the event recorder. If you
want to turn off recording completely, call **Dispose()**. To clear the list of recorded events, you can use **Reset()
**.

# Frequently Asked Questions

### Is this library thread-safe?

Yes, this library is fully thread-safe.

### What are the advantages compared to .NET events?

.NET events are nice. It's great to have a runtime that natively supports events. However, there are also some
real-world "problems" that this library tries to solve.

- .NET events don't support *async* event handler. You can have *async void* handlers that are invoked with
fire-and-forget semantic, but you can't natively await the completion of an async event handler.
- .NET events don't invoke all remaining event handlers when a previously invoked event handler threw an exception.
- Removal of event handlers is sometimes a bit hard, especially when you use lambda expressions a lot.
- .NET events don't support weak subscriptions that are automatically removed after the event handler got
garbage-collected.

### What about Reactive Extensions?

[Rx.NET](http://reactivex.io/) is a great technology, but it's API can be a bit difficult to use with it's *OnNext()*,
*OnCompleted()* and *OnError()* methods. Its strength lies in the processing and coordination of streams of events, not
in simplicity.

### Who uses this library?

A few years ago, at my day job, we started development of a new software platform for a next-generation product family.
We considered using Rx.NET as a replacement for all .NET events because we weren't happy with some limitations of .NET
events. However, Rx.NET also doesn't fit well with our requirements.

So, I started to experiment with a simple observer-pattern implementation on my own that would fulfill our requirements.
I did that in my free time, but soon the library development was also partly done at my day job, which finally resulted
in a closed-source solution.

From that time on, our closed-source event library was a cornerstone of our new software platform.

Now, again a few years later, I started to rewrite the entire library once again from scratch with the goal to make it
open source. I wanted to share it with the community and I also wanted to be able to use it for my own side projects.

That said, *Amarok.Events* isn't that widely used, but the concepts already have proven to work well. The implementation
provided here is already used is several proprietary projects.