What are the differences between CancellationToken
and CancellationChangeToken
? When do I use which one?
It seems that they can be used for the same purpose. What am I missing?
CancellationChangeToken
is a thin wrapper around CancellationToken
, which exposes the token's cancellation state through the HasChanged
property.
Since CancellationChangeToken
implements the IChangeToken
interface, it can be passed to any object that reacts to token changes, for example an IMemoryCache
: Each cache entry can be set to expire under a specific set of conditions, which are abstracted by IChangeToken
instances. These conditions may be, for example, configuration changes, or explicit invalidation requests by the user.
The CancellationChangeToken
wrapper can be passed to one or more cache entries, in order to evict them upon cancellation. The devs could also have decided to directly take a CancellationToken
; this would however have complicated the internal logic and the external API, compared to the abstraction through IChangeToken
, which is very clean, extensible and working just as well.
IChangeToken
IChangeToken
defines a means to track some kind of token, check whether it has changed (HasChanged
), and possibly automatically trigger some callbacks once it has changed.
There are various implementations for IChangeToken
; for example, they track files or configuration options.
CancellationChangeToken
doesA look at the source code quickly shows that the CancellationChangeToken
is a thin IChangeToken
wrapper around CancellationToken
:
public bool HasChanged => Token.IsCancellationRequested;
So its "changed" state directly corresponds to the underlying token's cancellation state.
On first sight, this seems quite odd: A CancellationToken
already offers support for registering automatic callbacks, so why introduce a wrapper that exposes much less functionality?
Well, there is actually at least one use case where an abstracted way of observing token states is needed:
ASP.NET Core offers different means of caching; I will use IMemoryCache
as an example here, which offers a simple built-in memory-based key/value cache, and is usable with minimal effort.
To reduce performance impact for each request, it makes sense to cache frequently needed, expensive computations. However, one needs a way to keep track of active cache entries in order to evict them as soon as they become outdated. The problem is, that the cache entries can have very different conditions for becoming outdated, e.g. they depend on configuration values or local files (and need to be updated as soon as these configuration values or local files change), or have a regular time out.
The IChangeToken
interface offers a simple way to abstract these eviction conditions away, such that the cache only has to check the state of the given IChangeToken
object, instead of directly watching files, timers, and so on. The cache might even register a callback, such that a change of the token state directly triggers the eviction logic, so no polling is needed.
CancellationChangeToken
A CancellationToken
allows to set a delay until it is automatically cancelled, so after wrapping it with a CancellationChangeToken
, we can pass it to a cache entry for automated eviction:
public IActionResult Index()
{
if(!_memoryCache.TryGetValue("entry1", out DateTime cachedValue))
{
cachedValue = DateTime.Now;
var cacheOptions = new MemoryCacheEntryOptions()
.AddExpirationToken(new CancellationChangeToken(new CancellationTokenSource(5000).Token));
_memoryCache.Set("entry1", cachedValue, cacheOptions);
}
return View("Index", cachedValue);
}
In this example, we cache the current timestamp. After 5 seconds, the cancellation token is cancelled, and the timestamp gets evicted.
However, this is does not yet justify the use of a CancellationToken
, since the IMemoryCache
already offers a way to set an expiration time through MemoryCacheEntryOptions
.
Now we get to the real strengths of the token-based eviction: We can
IChangeToken
instances into one CompositeChangeToken
, and thus evict a cache entry once any of these instances change.The latter case is particularly interesting for us: Some cache entries might depend on each other, so when one becomes invalid, we might want to evict all of them. This can be achieved by assigning a shared CancellationToken
to their values, and a corresponding CancellationChangeToken
to the entries themselves. Once one of the cached values notices that it has become outdated (e.g. by being polled by some other part of the application), it cancels the token. The cancellation propagates to all cache entries that track the corresponding change token, and thus invalidates them all at once.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With