9
\$\begingroup\$

Brevity is for kings as it requires context and context is for kings. Me.

GitHub and NuGet

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();        
}
\$\endgroup\$
  • \$\begingroup\$ This CancellationTokenSource Parent { get; } won't work in certain context switching situations that's why the T of AsyncLocal 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 the Action<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\$ – t3chb0t Oct 3 at 6:38
  • \$\begingroup\$ This is the issue I experienced: Restore AsyncLocal.Value on ThreadContextChanged - the code was running fine with the debugger but it was failing when run under xunit where it was loosing its Value property of AsyncLocal. \$\endgroup\$ – t3chb0t Oct 3 at 6:45
  • \$\begingroup\$ Am I wrong to assume you couldn't use Cancellation twice at the same time? \$\endgroup\$ – IEatBagels Oct 10 at 20:16
  • \$\begingroup\$ @IEatBagels There is only one Cancellation in the context at a given moment, but you could create a nested one with using(new Cancellation(...) { ... }). You would need to pass old plain CancellationToken explicitly where two or more are required the same time at the same place though - rarely thing to happen. \$\endgroup\$ – Dmitry Nogin Oct 10 at 22:10
  • \$\begingroup\$ @IEatBagels Here you could find a ready to be tried demo. \$\endgroup\$ – Dmitry Nogin Oct 10 at 22:12
3
+250
\$\begingroup\$

Not sure if it counts as an answer, but the most important missing part was the following ASP.NET Core attribute:

public class AmbientContextAttribute : Attribute, IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        using (new Op(context.HttpContext.Request.GetDisplayUrl()))
        using (new Cancellation(context.HttpContext.RequestAborted))
            await next();
    }
}
\$\endgroup\$
  • \$\begingroup\$ By lack of more answers, I grant you the bounty for the combined question/answer. \$\endgroup\$ – dfhwze Oct 11 at 19:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.