For starters, this method could be reduced to :
public IAsyncEnumerable<Job> GetByPipeline(int pipelineId)
{
return context.Jobs
.Where(job => job.Pipeline.Id == pipelineId)
.AsAsyncEnumerable();
}
or even
public IAsyncEnumerable<Job> GetByPipeline(int pipelineId)
=> context.Jobs
.Where(job => job.Pipeline.Id == pipelineId)
.AsAsyncEnumerable();
The method doesn't do anything with job
so it doesn't need to iterate over it.
Cancellation
What if the method actually used job
, where should the cancellation token be used?
Let's clean up the method a bit. The equivalent is:
public async IAsyncEnumerable<Job> GetByPipeline(
int pipelineId,
[EnumeratorCancellation] CancellationToken ct = default)
{
//Just a query, doesn't execute anything
var query =context.Jobs.Where(job => job.Pipeline.Id == pipelineId);
//Executes the query and returns the *results* as soon as they arrive in an async stream
var jobStream=query.AsAsyncEnumerable();
//Process the results from the async stream as they arrive
await foreach (var job in jobStream.WithCancellation(ct).ConfigureAwait(false))
{
//Does *that* need cancelling?
DoSometingExpensive(job);
}
}
The IQueryable query
doesn't run anything, it represents the query. It doesn't need cancellation.
AsAsyncEnumerable()
, AsEnumerable()
, ToList()
etc execute the query and return some result. ToList()
etc consume all the results while the As...Enumerable()
methods produce results only when requested. The query can't be cancelled, the As_Enumerable()
methods won't return anything unless asked for it, so they don't need cancellation.
await foreach
will iterate over the entire async stream so if we want to be able to abort it, we do need to pass the cancellation token.
Finally, does DoSometingExpensive(job);
need cancellation? Is it so expensive that we want to be able to break out of it if it takes too long? Or can we wait until it's finished before exiting the loop? If it needs cancellation, it will need the CancellationToken too.
ConfigureAwait
Finally, ConfigureAwait(false)
isn't involved in cancellation, and may not be needed at all. Without it, after each await
execution returns to the original synchronization context. In a desktop application, this meant the UI thread. That's what allows us to modify the UI in an async event handler.
If GetByPipeline
runs on a desktop app and wanted to modify the UI, it would have to remove ConfugureAwait
:
await foreach (var job in jobStream.WithCancellation(ct))
{
//Update the UI
toolStripProgressBar.Increment(1);
toolStripStatusLabel.Text=job.Name;
//Do the actual job
DoSometingExpensive(job);
}
With ConfigureAwait(false)
, execution continues on a threadpool thread and we can't touch the UI.
Library code shouldn't affect how execution resumes, so most libraries use ConfigureAwait(false)
and leave the final decision to the UI developer.
If GetByPipeline
is a library method, do use ConfigureAwait(false)
.