Recently I came across a Microsoft interface with a quite unusual API:
public interface IHostApplicationLifetime
{
public CancellationToken ApplicationStarted { get; }
public CancellationToken ApplicationStopping { get; }
public CancellationToken ApplicationStopped { get; }
}
The documentation of the property ApplicationStopping
suggests confusingly that this property is actually an event (emphasis added):
Triggered when the application host is performing a graceful shutdown. Shutdown will block until this event completes.
It seems that what should be a traditional EventHandler
event, has been replaced with a CancellationToken
property. This is how I expected this
interface to be:
public interface IHostApplicationLifetime
{
public event EventHandler ApplicationStarted;
public event EventHandler ApplicationStopping;
public event EventHandler ApplicationStopped;
}
My question is, are these two notifications mechanisms equivalent? If not, what are the pros and cons of each approach, from the perspective of an API designer? In which circumstances a CancellationToken
property is superior to a classic event?
The
CancellationToken
is not equivalent with the classic events. The differences are numerous, with the most obvious being that theCancellationToken
can be triggered (canceled) only once. So in case it makes sense for an event to be raised more than once, there is no dilemma: a classic event is the only option. So the comparison must be narrowed to the cases of one-time-events, where theCancellationToken
has many advantages, and only one potential disadvantage. First the advantages:The
CancellationToken
notifies its subscribers not only about a cancellation that may happen in the future, but also about a cancellation that has already happened. Trying to do the same in a multithreaded application with a classic event and abool
field creates a race condition. It is possible for the event to be triggered between checking thebool
field and subscribing to the event, in which case the notification will be lost.It is possible to attach and detach an event handler to a
CancellationToken
, even if the handler is an anonymous lambda function. This is not possible with a classic event. The detach operator (-=
) requires the same handler that was passed previously at the subscription (+=
), so the handler must be assigned to a variable, or be a named function.It is possible to pass a
CancellationToken
as an argument to a method of another class, to allow registering (and optionally unregistering) a callback. This is not possible with the classic events. The C# language does not allow it. The only way to achieve this functionality is by passing attach/detach lambdas as arguments to the method:otherClass.SomeMethod(h => this.MyEvent += h, h => this.MyEvent -= h)
. This is complicated, awkward, and less readable thanotherClass.SomeMethod(this.MyToken)
.There are thousands of APIs that accept a
CancellationToken
parameter. This makes the consumption of this kind of notification very convenient in a multitude of scenarios. On the contrary the APIs that can consume events by acceptingaddHandler
/removeHandler
arguments are extremely rare (example:Observable.FromEventPattern
).Unregistering a callback from a
CancellationToken
with the methodUnregister
¹ gives abool
feedback whether the callback has already been invoked or not. On the contrary unsubscribing from a classic event with the-=
operator gives no feedback. This means that in a multithreaded application the caller has no way of knowing whether the detached handler is already running on another thread, so that it can safely dispose any disposable resources referenced by the handler. This leaves the caller with awkward options like not disposing the resources, or catching possibleObjectDisposedException
s inside the handler.The disadvantage of using a
CancellationToken
as an one-time-notification is purely semantic. This type is strongly associated with the concept of cancellation, so using it to notify that, for example, something started or stopped, has great chances to create confusion.¹ Not available for the .NET Framework, so this advantage does not apply for this platform.