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 anIHeaderDictionary
, 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 onIHttpContextAccessor
.- 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 onIHttpContextAccessor
.- 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.
Feedback
Submit and view feedback for