Based on what i have read asp.net core
have dropped the synchronization context. This means that the thread that executes codes after await
call might not be the same one that executes codes before await
So is HttpContext
still safe to use in async
methods? or is it possible to get a different context after the await
call?
For example in a controller action
public async Task<IActionResult> Index()
{
var context1 = HttpContext;
await Task.Delay(1000);
var context2 = HttpContext;
....
}
could context1 be different from context2?
and the recommended way to get the context in none controller method is by dependency injecting IHttpContextAccessor
Is IHttpContextAccessor.HttpContext
safe from async
await
pattern?
I.E. could context1 be different from context2?
public async void Foo(IHttpContextAccessor accessor)
{
var context1 = accessor.HttpContext;
await Task.Delay(1000);
var context2 = accessor.HttpContext;
}
The HttpContext is NOT thread safe, accessing it from multiple threads can result in exceptions, data corruption and generally unpredictable results. The IHttpContextAccessor interface should be used with caution.
The HttpContext object constructed by the ASP.NET Core web server acts as a container for a single request. It stores the request and response information, such as the properties of request, request-related services, and any data to/from the request or errors, if there are any.
In ASP.NET Core, if we need to access the HttpContext in service, we can do so with the help of IHttpContextAccessor interface and its default implementation of HttpContextAccessor. It's only necessary to add this dependency if we want to access HttpContext in service.
So is HttpContext still safe to use in async methods? or is it possible to get a different context after the await call?
The whole problem with async
and HttpContext
and ASP.NET pre-Core was due to the fact that code usually got its HttpContext
from HttpContext.Current
. ASP.NET is a multithreaded server, and each await
could resume on a different thread. So ASP.NET pre-Core had to have an AspNetSynchronizationContext
that managed setting HttpContext.Current
before the asynchronous code resumed.
The modern ASP.NET Core does not have a synchronization context. But that's fine, because it also doesn't have HttpContext.Current
. The only way of getting the HttpContext
instance is through a local property (e.g., HttpContext
on your controller class) or dependency injection (IHttpContextAccessor
).
(Pedantic note: the explanation above is a bit simplified - the ASP.NET pre-Core synchronization context did handle other things besides HttpContext.Current
- but the same overall exaplanation holds for all of its other responsibilities - i.e., they are not necessary in the Core world)
So, it is not possible for the context to be different. They are the same property - the same object instance. The problem with ASP.NET pre-Core was a static property value HttpContext.Current
, which has been removed in ASP.NET Core.
According to documentation: https://learn.microsoft.com/en-us/aspnet/core/performance/performance-best-practices?view=aspnetcore-5.0#do-not-access-httpcontext-from-multiple-threads
HttpContext is NOT thread-safe. Accessing HttpContext from multiple threads in parallel can result in undefined behavior such as hangs, crashes, and data corruption.
Do not do this: The following example makes three parallel requests and logs the incoming request path before and after the outgoing HTTP request. The request path is accessed from multiple threads, potentially in parallel.
public class AsyncBadSearchController : Controller
{
[HttpGet("/search")]
public async Task<SearchResults> Get(string query)
{
var query1 = SearchAsync(SearchEngine.Google, query);
var query2 = SearchAsync(SearchEngine.Bing, query);
var query3 = SearchAsync(SearchEngine.DuckDuckGo, query);
await Task.WhenAll(query1, query2, query3);
var results1 = await query1;
var results2 = await query2;
var results3 = await query3;
return SearchResults.Combine(results1, results2, results3);
}
private async Task<SearchResults> SearchAsync(SearchEngine engine, string query)
{
var searchResults = _searchService.Empty();
try
{
_logger.LogInformation("Starting search query from {path}.",
HttpContext.Request.Path);
searchResults = _searchService.Search(engine, query);
_logger.LogInformation("Finishing search query from {path}.",
HttpContext.Request.Path);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed query from {path}",
HttpContext.Request.Path);
}
return await searchResults;
}
}
Do this: The following example copies all data from the incoming request before making the three parallel requests.
public class AsyncGoodSearchController : Controller
{
[HttpGet("/search")]
public async Task<SearchResults> Get(string query)
{
string path = HttpContext.Request.Path;
var query1 = SearchAsync(SearchEngine.Google, query,
path);
var query2 = SearchAsync(SearchEngine.Bing, query, path);
var query3 = SearchAsync(SearchEngine.DuckDuckGo, query, path);
await Task.WhenAll(query1, query2, query3);
var results1 = await query1;
var results2 = await query2;
var results3 = await query3;
return SearchResults.Combine(results1, results2, results3);
}
private async Task<SearchResults> SearchAsync(SearchEngine engine, string query,
string path)
{
var searchResults = _searchService.Empty();
try
{
_logger.LogInformation("Starting search query from {path}.",
path);
searchResults = await _searchService.SearchAsync(engine, query);
_logger.LogInformation("Finishing search query from {path}.", path);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed query from {path}", path);
}
return await searchResults;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With