13

I'd like to be able to pass cancellation tokens via dependency injection instead of as parameters every time. Is this a thing?

We have an asp.net-core 2.1 app, where we pass calls from controllers into a maze of async libraries, handlers, and other services to fulfill the byzantine needs of the fintech regulatory domain we service.

At the top of the request, I can declare that I want a cancellation token, and I'll get one:

[HttpPost]
public async Task<IActionResult> DoSomeComplexThingAsync(object thing, CancellationToken cancellationToken) {
    await _someComplexLibrary.DoThisComplexThingAsync(thing, cancellationToken);
    return Ok();
}

Now, I want to be a good async programmer and make sure my cancellationToken gets passed to every async method down through the call chain. I want to make sure it gets passed to EF, System.IO streams, etc. We have all the usual repository patterns and message passing practices you'd expect. We try to keep our methods concise and have a single responsibility. My tech lead gets visibly aroused by the word 'Fowler'. So our class sizes and function bodies are small, but our call chains are very, very deep.

What this comes to mean is that every layer, every function, has to hand off the damn token:

private readonly ISomething _something;
private readonly IRepository<WeirdType> _repository;

public SomeMessageHandler(ISomething<SomethingElse> something, IRepository<WeirdType> repository) {
    _something = something;
    _repository = repository;
}

public async Task<SomethingResult> Handle(ComplexThing request, CancellationToken cancellationToken) {
    var result = await DoMyPart(cancellationToken);
    cancellationToken.ThrowIfCancellationRequested();
    result.SomethingResult = await _something.DoSomethingElse(result, cancellationToken);
    return result;
}

public async Task<SomethingResult> DoMyPart(ComplexSubThing request, CancellationToken cancellationToken) {
    return await _repository.SomeEntityFrameworkThingEventually(request, cancellationToken);
}

This goes on ad infinitum, as per the needs of our domain complexity. It seems like CancellationToken appears more times in our codebase than any other term. Our arg lists are often already too long (i.e. more than one) as it is, even though we declare a million object types. And now we have this extra little cancellation token buddy hanging around in every arg list, every method decl.

My question is, since Kestrel and/or the pipeline gave me the token in the first place, it'd be great if I could just have something like this:

private readonly ISomething _something;
private readonly IRepository<WeirdType> _repository;
private readonly ICancellationToken _cancellationToken;

public SomeMessageHandler(ISomething<SomethingElse> something, ICancellationToken cancellationToken)
{
    _something = something;
    _repository = repository;
    _cancellationToken = cancellationToken;
}

public async Task<SomethingResult> Handle(ComplexThing request)
{
    var result = await DoMyPart(request);
    _cancellationToken.ThrowIfCancellationRequested();
    result.SomethingResult = await _something.DoSomethingElse(result);
    return result;
}

public async Task<SomethingResult> DoMyPart(ComplexSubThing request)
{
    return await _repository.SomeEntityFrameworkThingEventually(request);
}

This would then get passed around via DI composition, and when I had something that needs the token explicitly I could do this:

private readonly IDatabaseContext _context;
private readonly ICancellationToken _cancellationToken;

public IDatabaseRepository(IDatabaseContext context, ICancellationToken cancellationToken)
{
    _context = context;
    _cancellationToken = cancellationToken;
}

public async Task<SomethingResult> DoDatabaseThing()
{
    return await _context.EntityFrameworkThing(_cancellationToken);
}

Am I nuts? Do I just pass the damn token, every damn time, and praise the async gods for the bounty that has been given? Should I just retrain as a llama farmer? They seem nice. Is even asking this some kind of heresy? Should I be repenting now? I think for async/await to work properly, the token has to be in the func decl. So, maybe llamas it is

Hakan Fıstık
  • 16,800
  • 14
  • 110
  • 131
user326608
  • 2,210
  • 1
  • 26
  • 33
  • 1
    Is your question how to capture that token before action call? Not very sure where you stuck implementing that... (If you want to discuss whether it is good idea for your case or not SO is not the right place, possibly discussion of that design may be on topic on https://softwareengineering.stackexchange.com/) – Alexei Levenkov Sep 29 '20 at 15:34
  • 1
    This repetition of `CancellationToken` is something I've also considered many times while writing cancel-able code. AFAIK, it stems from .NET 4 when the cancellation concept was added to the BCL, long before DI was a concern. You're liable to get a better response at https://github.com/dotnet/runtime by filing either an issue or discussion. Also, the llamas will spit at your heresy so farming is probably inadvisable. – Ian Kemp Sep 29 '20 at 15:38
  • 4
    There isn’t just _one_ cancellation token. There may be any number of tokens that are maybe or maybe not related. How would a DI container possibly figure out which token to use? And how would you possibly actually cancel a token that is provided by the DI container? – So no, injecting a cancellation token doesn’t really make sense since a cancellation token is usually about a particular _call_, not an object (and its lifetime). – poke Sep 29 '20 at 21:06
  • 1
    That being said, you should consider if really everything in your system actually _needs_ a cancellation token. If you have something that isn’t actually cancellable, then it shouldn’t appear to be so by allowing to pass a cancellation token. For example, your `SomeMessageHandler.Handle` above is *not* cancellable. Checking the token right before returning a result but after all the work is already done is not what cancellation is about. Cancellation is about actually stopping _work_ from happening, not to just interrupt the call flow. – poke Sep 29 '20 at 21:09
  • 1
    You can check [this answer](https://stackoverflow.com/a/64206338/2698119) to see how a `CancellationToken` can be injected into a service via `IHttpAccessor`. – Métoule Oct 05 '20 at 10:11
  • @poke But what about the scenario given in this question, whereby the only way the "child" methods can ever receive a token (or be called at all, in fact) are by the "parent" method (in this case, the controller action) into which a token is implicitly passed? Here, you are in fact guaranteed that the token passed to all children will be the same token (the parent's). – Ian Kemp Oct 08 '20 at 15:13
  • @poke I edited the question to make the usage of the token to actually cancel work be a bit more sane, as I believe that's what the asker intended. I know that editing others' code is generally frowned upon but I believe that in this case, the issue of using the token correctly detracts from the actual question. As a side effect my edit has made your second comment obsolete - sorry about that! – Ian Kemp Oct 08 '20 at 15:22
  • @IanKemp _“Here, you are in fact guaranteed that the token passed to all children will be the same token”_ – No, that’s not correct. You can at any point create a new cancellation token source that is independent from the token that may be passed to the current method. This is pretty common actually; for example, you may have parallel tasks where you want one task to be able to cancel the other. – If you have a situation where you only ever pass a single token on, then that’s an okay thing to do for your app but it also ignores a major part of the capabilities you have here. – poke Oct 08 '20 at 15:45
  • Can you describe the circumstances under which this cancellation token is used? When do you have to cancel everything? – John Wu Feb 20 '21 at 20:37

4 Answers4

6

I think you are thinking in a great way, I do not think you need to regret or repent.
This is a great idea, I also thought about it, and I implemented my own solution

public abstract class RequestCancellationBase
{
    public abstract CancellationToken Token { get; }

    public static implicit operator CancellationToken(RequestCancellationBase requestCancellation) =>
        requestCancellation.Token;
}

public class RequestCancellation : RequestCancellationBase
{
    private readonly IHttpContextAccessor _context;

    public RequestCancellation(IHttpContextAccessor context)
    {
        _context = context;
    }

    public override CancellationToken Token => _context.HttpContext.RequestAborted;
}

and the registration should be like this

services.AddHttpContextAccessor();
services.AddScoped<RequestCancellationBase, RequestCancellation>();

now you can inject RequestCancellationBase wherever you want,
and the better thing is that you can directly pass it to every method that expects CancellationToken.
this is because of public static implicit operator CancellationToken(RequestCancellationBase requestCancellation)

here is an example of how to use it

public sealed class Service1
{
    private readonly RequestCancellationBase _cancellationToken;
    
    public Service1(RequestCancellationBase cancellationToken)
    {
         _cancellationToken = cancellationToken;
    }

    public async Task SomeMethod()
    {
        HttpClient client = new();
        // passing RequestCancellationBase object instead of passing CancellationToken
        // without any casting
        await client.GetAsync("url", _cancellationToken);
    }
}

this solution helped me, hope it is helpful for you also

Hakan Fıstık
  • 16,800
  • 14
  • 110
  • 131
  • This is a really nice idea for scoped services, but I'd be wary of this with singletons - a singleton service can get the HttpContext's token, but there will be multiple contexts and therefore tokens over the life of the service - you'll have to be aware of the token's implementation. I think the token needs to have the same lifecycle as the thing - so context-scoped on the class, but for a singleton services pass it to the method instead. – Keith Feb 28 '21 at 21:17
  • This no longer works for me since upgrading to .NET 6 and I have no idea why, it used to work perfectly. – Gerardo Buenrostro González Dec 07 '21 at 20:01
3

First of all, there are 3 injection scopes: Singleton, Scoped and Transient. Two of those rule out using a shared token.

DI services added with AddSingleton exist across all requests, so any cancellation token must be passed to the specific method (or across your entire application).

DI services added with AddTransient may be instantiated on demand and you may get issues where a new instance is created for a token that is already cancelled. They'd probably need some way for the current token to be passed to [FromServices] or some other library change.

However, for AddScoped I think there is a way, and I was helped by this answer to my similar question - you can't pass the token itself to DI, but you can pass IHttpContextAccessor.

So, in Startup.ConfigureServices or the extension method you use to register whatever IRepository use:


// For imaginary repository that looks something like
class RepositoryImplementation : IRepository {
    public RepositoryImplementation(string connection, CancellationToken cancellationToken) { }
}

// Add a scoped service that references IHttpContextAccessor on create
services.AddScoped<IRepository>(provider => 
    new RepositoryImplementation(
        "Repository connection string/options",
        provider.GetService<IHttpContextAccessor>()?.HttpContext?.RequestAborted ?? default))

That IHttpContextAccessor service will be retrieved once per HTTP request, and that ?.HttpContext?.RequestAborted will return the same CancellationToken as if you had called this.HttpContext.RequestAborted from inside a controller action or added it to the parameters on the action.

Keith
  • 150,284
  • 78
  • 298
  • 434
2

This is how I inject the HttpContext cancellation token into my repositories.

WebApi/Program.cs

builder.Services.AddScoped<ICancellationTokenService, CurrentCancellationTokenService>();

WebApi/Services/CurrentCancellationTokenService.cs

public class CurrentCancellationTokenService : ICancellationTokenService
{
    public CancellationToken CancellationToken { get; }

    public CurrentCancellationTokenService(IHttpContextAccessor httpContextAccessor)
    {
        CancellationToken = httpContextAccessor.HttpContext!.RequestAborted;
    }
}

Core/Domain/Interfaces/ICancellationTokenService

public class ICancellationTokenService
{
    public CancellationToken CancellationToken { get; }
}

Persistence/Dal/Repositories/ClientRepository.cs

public class ClientRepository : IClientRepository
{
    private readonly CancellationToken _cancellationToken;

    public ClientRepository(ICancellationTokenService cancellationTokenService, ...)
    {
        _cancellationToken = cancellationTokenService.CancellationToken;
    }

    public Task<Client?> GetByIdAsync(int clientId)
    {
        return _context.Client.FirstOrDefaultAsync(x => x.Id == clientId, _cancellationToken);
    }
}
Sylvain Rodrigue
  • 4,751
  • 5
  • 53
  • 67
1

I know it's a 2y old thread, but because I came here with the same question, a solution with direct token delivery:

    var services = new ServiceCollection();
    services.AddSingleton<CancellationTokenSource>();
    services.AddSingleton(typeof(CancellationToken),
        (IServiceProvider sp) =>
        {
            var cts = sp.GetService<CancellationTokenSource>();
            ArgumentNullException.ThrowIfNull(cts);
            return cts.Token;
        }
        );
Micke
  • 11
  • 3