I'd like to know if I need to use EnumeratorCancellation when passing a cancellation token to my local function. I am thinking of using this code pattern often in the future:
public static IAsyncEnumerable<string> MyOuterFunctionAsync(
this Client client,
CancellationToken cancellationToken,
int? limit = null)
{
return MyLocalFunction().
TakeWhile(
(_, index) =>
limit is null ||
index < limit.Value);
async IAsyncEnumerable<string> MyLocalFunction()
{
var request = CreateRequest();
do
{
var page = await request.GetAsync(cancellationToken);
foreach (var item in page)
{
yield return item;
}
request = GetNextPageRequest();
}
while (request is not null)
}
}
Resharper doesn't mention the need for EnumeratorCancellation, and when I try to add it to the outer function it says it will have no effect, but if I try adding it to the local function Resharper stays happy, as without. Should I use it anywhere?
I checked the IL viewer but I don't see any difference between the versions.
Will MyOuterFunctionAsync work properly? Do I need to change MyLocalFunction signature to
async IAsyncEnumerable<string> MyLocalFunction(
[EnumeratorCancellation] CancellationToken cancellationToken)
To answer directly your question, the
MyOuterFunctionAsyncwill recognize correctly aCancellationTokenpassed as argument, but not if theCancellationTokenis passed with theWithCancellationoperator. For example if:...the
tokenwill be ignored. For correct behavior you do have to add theEnumeratorCancellationattribute in theMyLocalFunction.You could consider adopting the pattern used in the System.Linq.Async library. For example the
WhereLINQ operator:As you see the outer
Whereis notasync, and the innerCoreisasync. The public signature of theWheredoesn't include aCancellationTokenparameter. The caller can always use theWithCancellationoperator to attach aCancellationTokento an asynchronous sequence, so including a parameter is redundant. For example:On the other hand the local
Coreimplementation of the operator does include aCancellationTokenparameter, which is also decorated with theEnumeratorCancellationattribute. When the caller uses theWhereand attaches a token with theWithCancellation, this token is automatically passed to theCoreimplementation because of theEnumeratorCancellationattribute.So the general rule is: A method that returns an
IAsyncEnumerable<T>should include anCancellationTokenparameter only if it's implemented withasync, in which case the parameter should also be decorated with theEnumeratorCancellationattribute.Ideally public APIs that return
IAsyncEnumerable<T>should not be implemented withasync. That's because giving to the caller two different options to pass a token, either directly or through theWithCancellation, creates confusion without adding any value to the API. As an example of what not to do, see the implementation of theChannelReader<T>.ReadAllAsyncAPI.