Search code examples
c#.netasp.net-corecancellation-token

Cancellation Token Injection


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


Solution

  • 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);
        }
    }