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
CancellationTokenis not equivalent with the classic events. The differences are numerous, with the most obvious being that theCancellationTokencan 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 theCancellationTokenhas many advantages, and only one potential disadvantage. First the advantages:The
CancellationTokennotifies 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 aboolfield creates a race condition. It is possible for the event to be triggered between checking theboolfield 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
CancellationTokenas 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
CancellationTokenparameter. 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/removeHandlerarguments are extremely rare (example:Observable.FromEventPattern).Unregistering a callback from a
CancellationTokenwith the methodUnregister¹ gives aboolfeedback 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 possibleObjectDisposedExceptions inside the handler.The disadvantage of using a
CancellationTokenas 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.