Access HttpContext in ASP.NET Core

ASP.NET Core apps access HttpContext through the IHttpContextAccessor interface and its default implementation HttpContextAccessor. It's only necessary to use IHttpContextAccessor when you need access to the HttpContext inside a service.

HttpContext isn't thread safe

This article primarily discusses using HttpContext in request and response flow from Razor Pages, controllers, middleware, etc. Consider the following when using HttpContext outside the request and response flow:

  • 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. As always, the HttpContext must not be captured outside of the request flow. IHttpContextAccessor:
    • Relies on AsyncLocal<T> which can have a negative performance impact on asynchronous calls.
    • Creates a dependency on "ambient state" which can make testing more difficult.
  • IHttpContextAccessor.HttpContext may be null if accessed outside of the request flow.
  • To access information from HttpContext outside the request flow, copy the information inside the request flow. Be careful to copy the actual data and not just references. For example, rather than copying a reference to an IHeaderDictionary, copy the relevant header values or copy the entire dictionary key by key before leaving the request flow.
  • Don't capture IHttpContextAccessor.HttpContext in a constructor.

The following sample logs GitHub branches when requested from the /branch endpoint:

using System.Text.Json;
using HttpContextInBackgroundThread;
using Microsoft.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpContextAccessor();
builder.Services.AddHostedService<PeriodicBranchesLoggerService>();

builder.Services.AddHttpClient("GitHub", httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com/");

    // The GitHub API requires two headers. The Use-Agent header is added
    // dynamically through UserAgentHeaderHandler
    httpClient.DefaultRequestHeaders.Add(
        HeaderNames.Accept, "application/vnd.github.v3+json");
}).AddHttpMessageHandler<UserAgentHeaderHandler>();

builder.Services.AddTransient<UserAgentHeaderHandler>();

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapGet("/branches", async (IHttpClientFactory httpClientFactory,
                         HttpContext context, Logger<Program> logger) =>
{
    var httpClient = httpClientFactory.CreateClient("GitHub");
    var httpResponseMessage = await httpClient.GetAsync(
        "repos/dotnet/AspNetCore.Docs/branches");

    if (!httpResponseMessage.IsSuccessStatusCode) 
        return Results.BadRequest();

    await using var contentStream =
        await httpResponseMessage.Content.ReadAsStreamAsync();

    var response = await JsonSerializer.DeserializeAsync
        <IEnumerable<GitHubBranch>>(contentStream);

    app.Logger.LogInformation($"/branches request: " +
                              $"{JsonSerializer.Serialize(response)}");

    return Results.Ok(response);
});

app.Run();

The GitHub API requires two headers. The User-Agent header is added dynamically by the UserAgentHeaderHandler:

using System.Text.Json;
using HttpContextInBackgroundThread;
using Microsoft.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpContextAccessor();
builder.Services.AddHostedService<PeriodicBranchesLoggerService>();

builder.Services.AddHttpClient("GitHub", httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com/");

    // The GitHub API requires two headers. The Use-Agent header is added
    // dynamically through UserAgentHeaderHandler
    httpClient.DefaultRequestHeaders.Add(
        HeaderNames.Accept, "application/vnd.github.v3+json");
}).AddHttpMessageHandler<UserAgentHeaderHandler>();

builder.Services.AddTransient<UserAgentHeaderHandler>();

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapGet("/branches", async (IHttpClientFactory httpClientFactory,
                         HttpContext context, Logger<Program> logger) =>
{
    var httpClient = httpClientFactory.CreateClient("GitHub");
    var httpResponseMessage = await httpClient.GetAsync(
        "repos/dotnet/AspNetCore.Docs/branches");

    if (!httpResponseMessage.IsSuccessStatusCode) 
        return Results.BadRequest();

    await using var contentStream =
        await httpResponseMessage.Content.ReadAsStreamAsync();

    var response = await JsonSerializer.DeserializeAsync
        <IEnumerable<GitHubBranch>>(contentStream);

    app.Logger.LogInformation($"/branches request: " +
                              $"{JsonSerializer.Serialize(response)}");

    return Results.Ok(response);
});

app.Run();

The UserAgentHeaderHandler:

using Microsoft.Net.Http.Headers;

namespace HttpContextInBackgroundThread;

public class UserAgentHeaderHandler : DelegatingHandler
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly ILogger _logger;

    public UserAgentHeaderHandler(IHttpContextAccessor httpContextAccessor,
                                  ILogger<UserAgentHeaderHandler> logger)
    {
        _httpContextAccessor = httpContextAccessor;
        _logger = logger;
    }

    protected override async Task<HttpResponseMessage> 
                                    SendAsync(HttpRequestMessage request, 
                                    CancellationToken cancellationToken)
    {
        var contextRequest = _httpContextAccessor.HttpContext?.Request;
        string? userAgentString = contextRequest?.Headers["user-agent"].ToString();
        
        if (string.IsNullOrEmpty(userAgentString))
        {
            userAgentString = "Unknown";
        }

        request.Headers.Add(HeaderNames.UserAgent, userAgentString);
        _logger.LogInformation($"User-Agent: {userAgentString}");

        return await base.SendAsync(request, cancellationToken);
    }
}

In the preceding code, when the HttpContext is null, the userAgent string is set to "Unknown". If possible, HttpContext should be explicitly passed to the service. Explicitly passing in HttpContext data:

  • Makes the service API more useable outside the request flow.
  • Is better for performance.
  • Makes the code easier to understand and reason about than relying on ambient state.

When the service must access HttpContext, it should account for the possibility of HttpContext being null when not called from a request thread.

The application also includes PeriodicBranchesLoggerService, which logs the open GitHub branches of the specified repository every 30 seconds:

using System.Text.Json;

namespace HttpContextInBackgroundThread;

public class PeriodicBranchesLoggerService : BackgroundService
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly ILogger _logger;
    private readonly PeriodicTimer _timer;

    public PeriodicBranchesLoggerService(IHttpClientFactory httpClientFactory,
                                         ILogger<PeriodicBranchesLoggerService> logger)
    {
        _httpClientFactory = httpClientFactory;
        _logger = logger;
        _timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (await _timer.WaitForNextTickAsync(stoppingToken))
        {
            try
            {
                // Cancel sending the request to sync branches if it takes too long
                // rather than miss sending the next request scheduled 30 seconds from now.
                // Having a single loop prevents this service from sending an unbounded
                // number of requests simultaneously.
                using var syncTokenSource = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
                syncTokenSource.CancelAfter(TimeSpan.FromSeconds(30));
                
                var httpClient = _httpClientFactory.CreateClient("GitHub");
                var httpResponseMessage = await httpClient.GetAsync("repos/dotnet/AspNetCore.Docs/branches",
                                                                    stoppingToken);

                if (httpResponseMessage.IsSuccessStatusCode)
                {
                    await using var contentStream =
                        await httpResponseMessage.Content.ReadAsStreamAsync(stoppingToken);

                    // Sync the response with preferred datastore.
                    var response = await JsonSerializer.DeserializeAsync<
                        IEnumerable<GitHubBranch>>(contentStream, cancellationToken: stoppingToken);

                    _logger.LogInformation(
                        $"Branch sync successful! Response: {JsonSerializer.Serialize(response)}");
                }
                else
                {
                    _logger.LogError(1, $"Branch sync failed! HTTP status code: {httpResponseMessage.StatusCode}");
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(1, ex, "Branch sync failed!");
            }
        }
    }

    public override Task StopAsync(CancellationToken stoppingToken)
    {
        // This will cause any active call to WaitForNextTickAsync() to return false immediately.
        _timer.Dispose();
        // This will cancel the stoppingToken and await ExecuteAsync(stoppingToken).
        return base.StopAsync(stoppingToken);
    }
}

PeriodicBranchesLoggerService is a hosted service, which runs outside the request and response flow. Logging from the PeriodicBranchesLoggerService has a null HttpContext. The PeriodicBranchesLoggerService was written to not depend on the HttpContext.

using System.Text.Json;
using HttpContextInBackgroundThread;
using Microsoft.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpContextAccessor();
builder.Services.AddHostedService<PeriodicBranchesLoggerService>();

builder.Services.AddHttpClient("GitHub", httpClient =>
{

Use HttpContext from Razor Pages

The Razor Pages PageModel exposes the PageModel.HttpContext property:

public class IndexModel : PageModel
{
    public void OnGet()
    {
        var message = HttpContext.Request.PathBase;

        // ...
    }
}

The same property can be used in the corresponding Razor Page View:

@page
@model IndexModel

@{
    var message = HttpContext.Request.PathBase;

    // ...
}

Use HttpContext from a Razor view in MVC

Razor views in the MVC pattern expose the HttpContext via the RazorPage.Context property on the view. The following example retrieves the current username in an intranet app using Windows Authentication:

@{
    var username = Context.User.Identity.Name;

    // ...
}

Use HttpContext from a controller

Controllers expose the ControllerBase.HttpContext property:

public class HomeController : Controller
{
    public IActionResult About()
    {
        var pathBase = HttpContext.Request.PathBase;

        // ...

        return View();
    }
}

Use HttpContext from minimal APIs

To use HttpContext from minimal APIs, add a HttpContext parameter:

app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));

Use HttpContext from middleware

To use HttpContext from custom middleware components, use the HttpContext parameter passed into the Invoke or InvokeAsync method:

public class MyCustomMiddleware
{
    // ...

    public async Task InvokeAsync(HttpContext context)
    {
        // ...
    }
}

Use HttpContext from SignalR

To use HttpContext from SignalR, call the GetHttpContext method on Hub.Context:

public class MyHub : Hub
{
    public async Task SendMessage()
    {
        var httpContext = Context.GetHttpContext();

        // ...
    }
}

Use HttpContext from gRPC methods

To use HttpContext from gRPC methods, see Resolve HttpContext in gRPC methods.

Use HttpContext from custom components

For other framework and custom components that require access to HttpContext, the recommended approach is to register a dependency using the built-in Dependency Injection (DI) container. The DI container supplies the IHttpContextAccessor to any classes that declare it as a dependency in their constructors:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();
builder.Services.AddHttpContextAccessor();
builder.Services.AddTransient<IUserRepository, UserRepository>();

In the following example:

  • UserRepository declares its dependency on IHttpContextAccessor.
  • The dependency is supplied when DI resolves the dependency chain and creates an instance of UserRepository.
public class UserRepository : IUserRepository
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public UserRepository(IHttpContextAccessor httpContextAccessor) =>
        _httpContextAccessor = httpContextAccessor;

    public void LogCurrentUser()
    {
        var username = _httpContextAccessor.HttpContext.User.Identity.Name;

        // ...
    }
}

HttpContext access from a background thread

HttpContext isn't thread-safe. Reading or writing properties of the HttpContext outside of processing a request can result in a NullReferenceException.

Note

If your app generates sporadic NullReferenceException errors, review parts of the code that start background processing or that continue processing after a request completes. Look for mistakes, such as defining a controller method as async void.

To safely do background work with HttpContext data:

  • Copy the required data during request processing.
  • Pass the copied data to a background task.
  • Do not reference HttpContext data in parallel tasks. Extract the data needed from the context before starting the parallel tasks.

To avoid unsafe code, never pass HttpContext into a method that does background work. Pass the required data instead. In the following example, SendEmail calls SendEmailCoreAsync to start sending an email. The value of the X-Correlation-Id header is passed to SendEmailCoreAsync instead of the HttpContext. Code execution doesn't wait for SendEmailCoreAsync to complete:

public class EmailController : Controller
{
    public IActionResult SendEmail(string email)
    {
        var correlationId = HttpContext.Request.Headers["X-Correlation-Id"].ToString();

        _ = SendEmailCoreAsync(correlationId);

        return View();
    }

    private async Task SendEmailCoreAsync(string correlationId)
    {
        // ...
    }
}

Blazor and shared state

Blazor server apps live in server memory. That means that there are multiple apps hosted within the same process. For each app session, Blazor starts a circuit with its own DI container scope. That means that scoped services are unique per Blazor session.

Warning

We don't recommend apps on the same server share state using singleton services unless extreme care is taken, as this can introduce security vulnerabilities, such as leaking user state across circuits.

You can use stateful singleton services in Blazor apps if they are specifically designed for it. For example, it's ok to use a memory cache as a singleton because it requires a key to access a given entry, assuming users don't have control of what cache keys are used.

Additionally, again for security reasons, you must not use IHttpContextAccessor within Blazor apps. Blazor apps run outside of the context of the ASP.NET Core pipeline. The HttpContext isn't guaranteed to be available within the IHttpContextAccessor, nor is it guaranteed to be holding the context that started the Blazor app.

The recommended way to pass request state to the Blazor app is through parameters to the root component in the initial rendering of the app:

  • Define a class with all the data you want to pass to the Blazor app.
  • Populate that data from the Razor page using the HttpContext available at that time.
  • Pass the data to the Blazor app as a parameter to the root component (App).
  • Define a parameter in the root component to hold the data being passed to the app.
  • Use the user-specific data within the app; or alternatively, copy that data into a scoped service within OnInitializedAsync so that it can be used across the app.

For more information and example code, see ASP.NET Core Blazor Server additional security scenarios.

ASP.NET Core apps access HttpContext through the IHttpContextAccessor interface and its default implementation HttpContextAccessor. It's only necessary to use IHttpContextAccessor when you need access to the HttpContext inside a service.

Use HttpContext from Razor Pages

The Razor Pages PageModel exposes the PageModel.HttpContext property:

public class IndexModel : PageModel
{
    public void OnGet()
    {
        var message = HttpContext.Request.PathBase;

        // ...
    }
}

The same property can be used in the corresponding Razor Page View:

@page
@model IndexModel

@{
    var message = HttpContext.Request.PathBase;

    // ...
}

Use HttpContext from a Razor view in MVC

Razor views in the MVC pattern expose the HttpContext via the RazorPage.Context property on the view. The following example retrieves the current username in an intranet app using Windows Authentication:

@{
    var username = Context.User.Identity.Name;

    // ...
}

Use HttpContext from a controller

Controllers expose the ControllerBase.HttpContext property:

public class HomeController : Controller
{
    public IActionResult About()
    {
        var pathBase = HttpContext.Request.PathBase;

        // ...

        return View();
    }
}

Use HttpContext from middleware

When working with custom middleware components, HttpContext is passed into the Invoke or InvokeAsync method:

public class MyCustomMiddleware
{
    public Task InvokeAsync(HttpContext context)
    {
        // ...
    }
}

Use HttpContext from custom components

For other framework and custom components that require access to HttpContext, the recommended approach is to register a dependency using the built-in Dependency Injection (DI) container. The DI container supplies the IHttpContextAccessor to any classes that declare it as a dependency in their constructors:

public void ConfigureServices(IServiceCollection services)
{
     services.AddControllersWithViews();
     services.AddHttpContextAccessor();
     services.AddTransient<IUserRepository, UserRepository>();
}

In the following example:

  • UserRepository declares its dependency on IHttpContextAccessor.
  • The dependency is supplied when DI resolves the dependency chain and creates an instance of UserRepository.
public class UserRepository : IUserRepository
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public UserRepository(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public void LogCurrentUser()
    {
        var username = _httpContextAccessor.HttpContext.User.Identity.Name;
        service.LogAccessRequest(username);
    }
}

HttpContext access from a background thread

HttpContext isn't thread-safe. Reading or writing properties of the HttpContext outside of processing a request can result in a NullReferenceException.

Note

If your app generates sporadic NullReferenceException errors, review parts of the code that start background processing or that continue processing after a request completes. Look for mistakes, such as defining a controller method as async void.

To safely do background work with HttpContext data:

  • Copy the required data during request processing.
  • Pass the copied data to a background task.
  • Do not reference HttpContext data in parallel tasks. Extract the data needed from the context before starting the parallel tasks.

To avoid unsafe code, never pass the HttpContext into a method that does background work. Pass the required data instead. In the following example, SendEmailCore is called to start sending an email. The correlationId is passed to SendEmailCore, not the HttpContext. Code execution doesn't wait for SendEmailCore to complete:

public class EmailController : Controller
{
    public IActionResult SendEmail(string email)
    {
        var correlationId = HttpContext.Request.Headers["x-correlation-id"].ToString();

        _ = SendEmailCore(correlationId);

        return View();
    }

    private async Task SendEmailCore(string correlationId)
    {
        // ...
    }
}

Blazor and shared state

Blazor server apps live in server memory. That means that there are multiple apps hosted within the same process. For each app session, Blazor starts a circuit with its own DI container scope. That means that scoped services are unique per Blazor session.

Warning

We don't recommend apps on the same server share state using singleton services unless extreme care is taken, as this can introduce security vulnerabilities, such as leaking user state across circuits.

You can use stateful singleton services in Blazor apps if they are specifically designed for it. For example, it's ok to use a memory cache as a singleton because it requires a key to access a given entry, assuming users don't have control of what cache keys are used.

Additionally, again for security reasons, you must not use IHttpContextAccessor within Blazor apps. Blazor apps run outside of the context of the ASP.NET Core pipeline. The HttpContext isn't guaranteed to be available within the IHttpContextAccessor, nor is it guaranteed to be holding the context that started the Blazor app.

The recommended way to pass request state to the Blazor app is through parameters to the root component in the initial rendering of the app:

  • Define a class with all the data you want to pass to the Blazor app.
  • Populate that data from the Razor page using the HttpContext available at that time.
  • Pass the data to the Blazor app as a parameter to the root component (App).
  • Define a parameter in the root component to hold the data being passed to the app.
  • Use the user-specific data within the app; or alternatively, copy that data into a scoped service within OnInitializedAsync so that it can be used across the app.

For more information and example code, see ASP.NET Core Blazor Server additional security scenarios.