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:
HttpContext.RequestAbortedThis 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:
[HttpGet]
public async Task<IActionResult> Get(CancellationToken cancellationToken)
{
...
}is effectively giving you:
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
[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(
int id,
CancellationToken cancellationToken)
{
var order = await _orderService.GetOrderAsync(id, cancellationToken);
return Ok(order);
}Service
public Task<OrderDto> GetOrderAsync(int id, CancellationToken cancellationToken)
{
return _repository.GetOrderAsync(id, cancellationToken);
}Repository
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
[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
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
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:
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:
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:
await _httpClient.GetAsync(url, cancellationToken);
await _dbContext.SaveChangesAsync(cancellationToken);
await Task.Delay(5000, cancellationToken);This is bad:
await _httpClient.GetAsync(url); // token dropped
await _dbContext.SaveChangesAsync(); // token dropped
await Task.Delay(5000); // token droppedThat 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:
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 cancellationTokento 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 actionCancellationTokenparameters to that token automatically. We usually do not see aCancellationTokenSourcein 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.