New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Why do async LINQ operations ignore synchronization context? #1582
Comments
Ideally, a LINQ query does not produce nor rely on side effects, i.e., the operators only work on the data that flow through them and constant data that is captured by the lamda. Whenever this simple rule is not followed, one should be carefull. In the In traditional LINQ the general advice is: "if you have side effects, use foreach". This should work here, too. Alternatively the following method could help you to bring your pipeline back on your synchronisation context: public static async IAsyncEnumerable<T> OnCurrentSynchronizationContext<T>(this IAsyncEnumerable<T> enumerable)
{
await foreach (var item in enumerable)
{
yield return item;
}
} And than: var data = await Read(cmd)
.OnCurrentSynchronizationContext()
.Select(t => HttpContext.Current == null)
.ToArrayAsync(); |
We've been trying to avoid |
I feel your pain. Personally, I stopped arguing about C#8 and .NET Framework, and just changed (some of) my projects manually. I think, using |
Can't you just reference this package @srogovtsev ? You should be able to use stuff like As long as the whole team (and build server) uses a modern version of Visual Studio to build your application, it should work fine. |
Yes, this is exactly the shady approach we've been trying to avoid. I say shady because it is not under official support (although it seems to work fins, as you suggest). |
I wouldn't worry too much about that @srogovtsev . Changing the language version for an entire solution is fairly trivial using We are in the process of migrating our current solution from 4.8 to 5 but the benefits some of the new compiler-driven improvements provide are very significant even before that. |
@quinmars you said that LINQ queries should not rely on side effects. I think that this is an utopic scenario on UI frameworks. Consider the code below.
that's why in my opinion SelectAwait should not be implemented using ConfigureAwait(false) by default. And that's also the reason why await foreach-ing by default captures the caller context. Of course one solution is to change the code below in order to avoidi LINQ. But at this point I can't see any real world scenario for use it. I think it would be too easy to introduce bugs, or synchronization issues. public record User(string Name);
public interface IPopupNotification
{
Task ShowMessage(string message);
}
public interface IUserService
{
Task<List<User>> GetUsers();
}
public class UserService : IUserService
{
private readonly IPopupNotification _popupNotification;
public async Task<List<User>> GetUsers()
{
try
{
return await WhateverFromApi();
}
catch (Exception)
{
await _popupNotification.ShowMessage("Error retreiving users");
throw;
}
}
}
internal class MainWindowViewModel : ObservableObject
{
CancellationTokenSource _cancellationTokenSource = new();
IUserService _userService;
async IAsyncEnumerable<int> Timer()
{
var token = _cancellationTokenSource.Token;
for (var i=0; !token.IsCancellationRequested; i++)
{
yield return i;
await Task.Delay(TimeSpan.FromSeconds(1), token);
}
}
public ObservableCollection<User> Users { get; } = new();
async Task Poll()
{
var usersEnumerable = Timer().SelectAwait(async _ => await _userService.GetUsers());
await foreach(var users in usersEnumerable)
foreach(var user in users)
Users.Add(user);
}
} |
The defining context of my problem is that we're trying to use
IAsyncEnumerable
andSystem.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 andWhere
, and, basically everything) is not guaranteed to run on captured synchronization context. This behavior is unexpected for me, especially given the following quote from Stephen Toub: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:
where
Read
returns a simplest possible implementation ofIAsyncEnumerable<T>
andIAsyncEnumerator<T>
overSqlDataReader
.Of course, the code easily supports our observations: as far as we were able to find, every operation in
System.Linq.Async
hasConfigureAwait(false)
.reactive/Ix.NET/Source/System.Linq.Async/System/Linq/Operators/Select.cs
Lines 59 to 67 in 44808e0
Of course, this looks like a deliberate design decision, so we would like to understand
IAsyncEnumerable
working on captured contextThe text was updated successfully, but these errors were encountered: