Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

is HttpContext async safe in asp.net core?

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;
}
like image 392
Steve Avatar asked Sep 09 '19 21:09

Steve


People also ask

Is HttpContext thread safe?

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.

What is HttpContext .NET Core?

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.

How do I get HttpContext in .NET Core?

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.


2 Answers

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.

like image 187
Stephen Cleary Avatar answered Oct 10 '22 18:10

Stephen Cleary


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;
    }
}
like image 2
0lukasz0 Avatar answered Oct 10 '22 18:10

0lukasz0