Why do async LINQ operations ignore synchronization context?
- Dominant language
- C#
- Stars
- 7.2k
- Forks
- 798
- PR merge metrics
- No merged PRs in 30d
Description
The defining context of my problem is that we're trying to use `IAsyncEnumerable` and `System.Linq.Async` within **vanilla ASP.NET**, which, unlike Core, has and makes heavy usage of synchronization context.
While trying to use some LINQ operations (such as `Select`) we have unexpectedly found out that the selector functions (and the predicates and `Where`, and, basically everything) is not guaranteed to run on captured synchronization context. This behavior is unexpected for me, especially given the following [quote](https://devblogs.microsoft.com/dotnet/configureawait-faq/) from Stephen Toub:
> Consider, for example, an asynchronous version of LINQ’s Where method, e.g. `public static async IAsyncEnumerable WhereAsync(this IAsyncEnumerable source, Func predicate)`. Does predicate here need to be invoked back on the original `SynchronizationContext` of the caller? That’s up to the implementation of `WhereAsync` to decide, and it’s a reason it may choose not to use `ConfigureAwait(false)`.
which basically states that unless we specifically ask for opposite, we should expect the predicates and other callbacks to run on our synchronization context.
Here's a small piece of code to show what we mean:
```cs
using (var conn = new SqlConnection(connString))
{
conn.Open();
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = commandText;
if (HttpContext.Current == null)
//this is just a sanity check
throw new InvalidOperationException();
var cnt = await Read(cmd).CountAsync(); //a couple of hundred thousand records
if (HttpContext.Current == null)
//this works just fine - we're back after the `await`
throw new InvalidOperationException();
var data = await Read(cmd).Select(t =>
HttpContext.Current == null //we'll treat "context lost" as error below
).ToArrayAsync();
if (HttpContext.Current == null)
//this, too, works just fine - we're again back after the `await`
throw new InvalidOperationException();
if (data.Any())
//this fails: first couple of hundreds invocations are on the context, then they leave the context
throw new InvalidOperationException();
}
}
```
where `Read` returns a simplest possible implementation of `IAsyncEnumerable` and `IAsyncEnumerator` over `SqlDataReader`.
Of course, the code easily supports our observations: as far as we were able to find, every operation in `System.Linq.Async` has `ConfigureAwait(false)`.
https://github.com/dotnet/reactive/blob/44808e05561bf98bfc3c0e6ecd217b8e6ca21b24/Ix.NET/Source/System.Linq.Async/System/Linq/Operators/Select.cs#L59-L67
Of course, this looks like a deliberate design decision, so we would like to understand
1. its rationale
2. whether we miss something crucial here
3. whether there are any ways of circumventing this behavior, as we are in dire need of having operations over `IAsyncEnumerable` working on captured context
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.