Skip to content

Yes — in ASP.NET Core, the CTS usually exists inside the framework, not in your controller code.

What you typically see in controllers is only the CancellationToken view, not the CancellationTokenSource.

The key idea

For an HTTP request, ASP.NET Core exposes:

csharp
HttpContext.RequestAborted

This is a CancellationToken that is canceled when the underlying request is aborted, such as when the client disconnects or the server aborts the request. (Microsoft Learn)

In MVC/controllers, if your action has a CancellationToken parameter, model binding automatically binds it to HttpContext.RequestAborted. (Microsoft Learn)

So this:

csharp
[HttpGet]
public async Task<IActionResult> Get(CancellationToken cancellationToken)
{
    ...
}

is effectively giving you:

csharp
var cancellationToken = HttpContext.RequestAborted;

not a new CTS created by your controller. (Microsoft Learn)


Why you do not see CancellationTokenSource in controllers

Because controller code is normally not the owner of request lifetime cancellation.

ASP.NET Core owns the request lifecycle, so it also owns the source that represents “this request has been aborted.”

Your controller is only a consumer of that signal.

That is actually good design:

  • framework owns request lifetime
  • controller receives token
  • downstream services just propagate token further

So in a normal request pipeline, you should think:

  • Kestrel / ASP.NET Core creates and manages request-abort cancellation internally
  • controller/minimal API endpoint receives RequestAborted
  • service/repository/HTTP/database calls receive the same token passed downward

Real flow through layers

Controller

csharp
[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(
    int id,
    CancellationToken cancellationToken)
{
    var order = await _orderService.GetOrderAsync(id, cancellationToken);
    return Ok(order);
}

Service

csharp
public Task<OrderDto> GetOrderAsync(int id, CancellationToken cancellationToken)
{
    return _repository.GetOrderAsync(id, cancellationToken);
}

Repository

csharp
public async Task<OrderDto> GetOrderAsync(int id, CancellationToken cancellationToken)
{
    return await _dbContext.Orders
        .Where(x => x.Id == id)
        .Select(x => new OrderDto { Id = x.Id, Name = x.Name })
        .SingleAsync(cancellationToken);
}

No CTS appears in your code, because all layers are just passing the token through.

The source is hidden in the framework.


Where the CTS appears in application code

You usually create your own CancellationTokenSource only when your code owns a separate cancellation scope.

For example:

1. Add your own timeout on top of request cancellation

csharp
[HttpGet]
public async Task<IActionResult> Get(CancellationToken requestToken)
{
    using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
    using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
        requestToken,
        timeoutCts.Token);

    var result = await _service.DoWorkAsync(linkedCts.Token);
    return Ok(result);
}

Now there are two cancellation reasons:

  • client disconnected
  • your 5-second timeout fired

Your code created CTS because your code introduced a new cancellation boundary.

2. Background service owns operation lifetime

csharp
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        await _worker.RunOnceAsync(stoppingToken);
    }
}

Again, the host owns the source, and your background service gets the token.

3. Internal sub-operation needs its own policy

csharp
public async Task SyncAsync(CancellationToken outerToken)
{
    using var subOpCts = CancellationTokenSource.CreateLinkedTokenSource(outerToken);
    subOpCts.CancelAfter(TimeSpan.FromSeconds(2));

    await _remoteClient.PushAsync(subOpCts.Token);
}

What ASP.NET Core is really doing

The framework effectively gives each request a cancellation signal tied to connection/request lifetime.

When the client disconnects, or the server aborts the request, RequestAborted is canceled. Microsoft docs explicitly describe HttpContext.RequestAborted this way, and also note that controller CancellationToken parameters bind to it. (Microsoft Learn)

So the mental model is:

text
Kestrel / server transport
    -> HttpContext.RequestAborted token
        -> controller action parameter CancellationToken
            -> service
                -> repository
                    -> EF Core / HttpClient / Stream / etc.

Why this design is important

Because ASP.NET Core wants cancellation to be:

  • automatic for request lifetime
  • cheap to consume
  • easy to propagate
  • not something every controller has to manually wire up

If every controller had to do this:

csharp
using var cts = new CancellationTokenSource();

that would be the wrong ownership model. The controller does not own the HTTP connection lifetime.


Very important subtle point

A controller receiving a token does not mean all downstream work will cancel automatically.

It only works if each layer passes the token into cancellation-aware APIs.

For example, this is good:

csharp
await _httpClient.GetAsync(url, cancellationToken);
await _dbContext.SaveChangesAsync(cancellationToken);
await Task.Delay(5000, cancellationToken);

This is bad:

csharp
await _httpClient.GetAsync(url);           // token dropped
await _dbContext.SaveChangesAsync();       // token dropped
await Task.Delay(5000);                    // token dropped

That is why in real code you may “have cancellation in controller” but still feel like cancellation does not work: the token got lost in lower layers.


Minimal API is similar

In minimal APIs, CancellationToken can also be bound as a handler parameter by the framework’s parameter binding system, same idea as controllers. (Microsoft Learn)

Example:

csharp
app.MapGet("/orders/{id}", async (
    int id,
    CancellationToken cancellationToken,
    IOrderService service) =>
{
    var order = await service.GetOrderAsync(id, cancellationToken);
    return Results.Ok(order);
});

Again: still no visible CTS, because framework owns request-abort cancellation.


Senior-level mental model

When you are in ASP.NET Core, there are usually three cancellation ownership layers:

1. Framework-owned request cancellation

This is HttpContext.RequestAborted.

You do not create it. You consume it.

2. App-owned policy cancellation

This is where you create your own CTS:

  • timeout
  • internal workflow cancellation
  • linked cancellation
  • fan-out coordination

3. Operation-level cooperation

This is your services/repos/libraries honoring the token and passing it further.


Why interviewers may ask about this

Because many engineers know:

“Add CancellationToken cancellationToken to controller”

but do not really know:

  • where it comes from
  • who owns it
  • why no CTS is visible
  • why cancellation often fails in practice

The strong answer is:

In ASP.NET Core, the framework owns request-lifetime cancellation and exposes it through HttpContext.RequestAborted. MVC binds action CancellationToken parameters to that token automatically. We usually do not see a CancellationTokenSource in controllers because the controller is not the owner of the request lifetime. We only create our own CTS when introducing an additional cancellation scope, such as timeouts or linked internal operations. (Microsoft Learn)

If you want, next I can explain this even deeper at runtime level: how Kestrel triggers RequestAborted, what happens when the client disconnects, and how EF Core / HttpClient actually react to that token.

Docs-first project memory for AI-assisted implementation.