Brevity is for kings as it requires context and context is for kings. Me.
How many times you have been writing something like this passing those tedious logger/token parameters?
interface IMyService
{
void Method1(…, ILogger logger, CancellationToken token);
void Method2(…, ILogger logger, CancellationToken token);
…
}
Enough is enough. Please see here about ambient logging. Below is about ambient cancellation.
What we are about to do is to use a special Cancellation
helper class like this:
static void Main(string[] args)
{
using (new Cancellation())
{
Task.Run(PingAsync);
ReadLine();
Cancellation.Request();
ReadLine();
}
}
static async Task PingAsync()
{
try
{
while (!Cancellation.Requested)
{
await Task.Delay(100, Cancellation.Token);
WriteLine("Ping");
}
Cancellation.ThrowIfRequested();
}
catch(OperationCanceledException)
{
WriteLine("Ping cancelled");
}
}
Where Cancellation
is defined as:
public class Cancellation : IDisposable
{
static AsyncLocal<CancellationTokenSource> Context { get; } =
new AsyncLocal<CancellationTokenSource>();
public Cancellation()
: this(CancellationToken.None)
{
}
public Cancellation(CancellationToken cancellationToken)
: this(cancellationToken, Timeout.InfiniteTimeSpan)
{
}
public Cancellation(int timeout)
: this(TimeSpan.FromMilliseconds(timeout))
{
}
public Cancellation(TimeSpan timeout)
: this(CancellationToken.None, timeout)
{
}
public Cancellation(CancellationToken cancellationToken, int timeout)
: this(cancellationToken, TimeSpan.FromMilliseconds(timeout))
{
}
public Cancellation(CancellationToken cancellationToken, TimeSpan timeout)
{
Parent = Context.Value;
Context.Value = CancellationTokenSource
.CreateLinkedTokenSource(cancellationToken);
Context.Value.CancelAfter(timeout);
}
public void Dispose()
{
var cts = Context.Value;
Context.Value = Parent;
cts.Dispose();
}
CancellationTokenSource Parent { get; }
public static CancellationToken Token =>
Context.Value?.Token ?? CancellationToken.None;
public static void Request() => Context.Value?.Cancel();
public static bool Requested => Token.IsCancellationRequested;
public static void ThrowIfRequested() => Token.ThrowIfCancellationRequested();
}
CancellationTokenSource Parent { get; }
won't work in certain context switching situations that's why theT
ofAsyncLocal
must be the class itself and link to itself too. I experimented with something similar to your solution and wanted to make it a generic AsycLocal helper but I had to use theAction<AsyncLocalValueChangedArgs<T>>
ctor overload to make it work when the thread context changed. As this was too tricky I reverted it to use the simpler code. \$\endgroup\$Value
property ofAsyncLocal
. \$\endgroup\$Cancellation
twice at the same time? \$\endgroup\$Cancellation
in the context at a given moment, but you could create a nested one withusing(new Cancellation(...) { ... })
. You would need to pass old plainCancellationToken
explicitly where two or more are required the same time at the same place though - rarely thing to happen. \$\endgroup\$