Use HttpContext in ASP.NET Core
Note
This isn't the latest version of this article. For the current release, see the .NET 8 version of this article.
Warning
This version of ASP.NET Core is no longer supported. For more information, see .NET and .NET Core Support Policy. For the current release, see the .NET 8 version of this article.
Important
This information relates to a pre-release product that may be substantially modified before it's commercially released. Microsoft makes no warranties, express or implied, with respect to the information provided here.
For the current release, see the .NET 8 version of this article.
HttpContext encapsulates all information about an individual HTTP request and response. An HttpContext
instance is initialized when an HTTP request is received. The HttpContext
instance is accessible by middleware and app frameworks such as Web API controllers, Razor Pages, SignalR, gRPC, and more.
For more information about accessing the HttpContext
, see Access HttpContext in ASP.NET Core.
HttpRequest
HttpContext.Request provides access to HttpRequest. HttpRequest
has information about the incoming HTTP request, and it's initialized when an HTTP request is received by the server. HttpRequest
isn't read-only, and middleware can change request values in the middleware pipeline.
Commonly used members on HttpRequest
include:
Property | Description | Example |
---|---|---|
HttpRequest.Path | The request path. | /en/article/getstarted |
HttpRequest.Method | The request method. | GET |
HttpRequest.Headers | A collection of request headers. | user-agent=Edge x-custom-header=MyValue |
HttpRequest.RouteValues | A collection of route values. The collection is set when the request is matched to a route. | language=en article=getstarted |
HttpRequest.Query | A collection of query values parsed from QueryString. | filter=hello page=1 |
HttpRequest.ReadFormAsync() | A method that reads the request body as a form and returns a form values collection. For information about why ReadFormAsync should be used to access form data, see Prefer ReadFormAsync over Request.Form. |
email=user@contoso.com |
HttpRequest.Body | A Stream for reading the request body. | UTF-8 JSON payload |
Get request headers
HttpRequest.Headers provides access to the request headers sent with the HTTP request. There are two ways to access headers using this collection:
- Provide the header name to the indexer on the header collection. The header name isn't case-sensitive. The indexer can access any header value.
- The header collection also has properties for getting and setting commonly used HTTP headers. The properties provide a fast, IntelliSense driven way to access headers.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", (HttpRequest request) =>
{
var userAgent = request.Headers.UserAgent;
var customHeader = request.Headers["x-custom-header"];
return Results.Ok(new { userAgent = userAgent, customHeader = customHeader });
});
app.Run();
For information on efficiently handling headers that appear more than once, see A brief look at StringValues.
Read request body
An HTTP request can include a request body. The request body is data associated with the request, such as the content of an HTML form, UTF-8 JSON payload, or a file.
HttpRequest.Body allows the request body to be read with Stream:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/uploadstream", async (IConfiguration config, HttpContext context) =>
{
var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());
await using var writeStream = File.Create(filePath);
await context.Request.Body.CopyToAsync(writeStream);
});
app.Run();
HttpRequest.Body
can be read directly or used with other APIs that accept stream.
Note
Minimal APIs supports binding HttpRequest.Body directly to a Stream parameter.
Enable request body buffering
The request body can only be read once, from beginning to end. Forward-only reading of the request body avoids the overhead of buffering the entire request body and reduces memory usage. However, in some scenarios, there's a need to read the request body multiple times. For example, middleware might need to read the request body and then rewind it so it's available for the endpoint.
The EnableBuffering extension method enables buffering of the HTTP request body and is the recommended way to enable multiple reads. Because a request can be any size, EnableBuffering
supports options for buffering large request bodies to disk, or rejecting them entirely.
The middleware in the following example:
- Enables multiple reads with
EnableBuffering
. It must be called before reading the request body. - Reads the request body.
- Rewinds the request body to the start so other middleware or the endpoint can read it.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) =>
{
context.Request.EnableBuffering();
await ReadRequestBody(context.Request.Body);
context.Request.Body.Position = 0;
await next.Invoke();
});
app.Run();
BodyReader
An alternative way to read the request body is to use the HttpRequest.BodyReader property. The BodyReader
property exposes the request body as a PipeReader. This API is from I/O pipelines, an advanced, high-performance way to read the request body.
The reader directly accesses the request body and manages memory on the caller's behalf. Unlike HttpRequest.Body
, the reader doesn't copy request data into a buffer. However, a reader is more complicated to use than a stream and should be used with caution.
For information on how to read content from BodyReader
, see I/O pipelines PipeReader.
HttpResponse
HttpContext.Response provides access to HttpResponse. HttpResponse
is used to set information on the HTTP response sent back to the client.
Commonly used members on HttpResponse
include:
Property | Description | Example |
---|---|---|
HttpResponse.StatusCode | The response code. Must be set before writing to the response body. | 200 |
HttpResponse.ContentType | The response content-type header. Must be set before writing to the response body. |
application/json |
HttpResponse.Headers | A collection of response headers. Must be set before writing to the response body. | server=Kestrel x-custom-header=MyValue |
HttpResponse.Body | A Stream for writing the response body. | Generated web page |
Set response headers
HttpResponse.Headers provides access to the response headers sent with the HTTP response. There are two ways to access headers using this collection:
- Provide the header name to the indexer on the header collection. The header name isn't case-sensitive. The indexer can access any header value.
- The header collection also has properties for getting and setting commonly used HTTP headers. The properties provide a fast, IntelliSense driven way to access headers.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", (HttpResponse response) =>
{
response.Headers.CacheControl = "no-cache";
response.Headers["x-custom-header"] = "Custom value";
return Results.File(File.OpenRead("helloworld.txt"));
});
app.Run();
An app can't modify headers after the response has started. Once the response starts, the headers are sent to the client. A response is started by flushing the response body or calling HttpResponse.StartAsync(CancellationToken). The HttpResponse.HasStarted property indicates whether the response has started. An error is thrown when attempting to modify headers after the response has started:
System.InvalidOperationException: Headers are read-only, response has already started.
Note
Unless response buffering is enabled, all write operations (for example, WriteAsync) flush the response body internally and mark the response as started. Response buffering is disabled by default.
Write response body
An HTTP response can include a response body. The response body is data associated with the response, such as generated web page content, UTF-8 JSON payload, or a file.
HttpResponse.Body allows the response body to be written with Stream:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/downloadfile", async (IConfiguration config, HttpContext context) =>
{
var filePath = Path.Combine(config["StoredFilesPath"], "helloworld.txt");
await using var fileStream = File.OpenRead(filePath);
await fileStream.CopyToAsync(context.Response.Body);
});
app.Run();
HttpResponse.Body
can be written directly or used with other APIs that write to a stream.
BodyWriter
An alternative way to write the response body is to use the HttpResponse.BodyWriter property. The BodyWriter
property exposes the response body as a PipeWriter. This API is from I/O pipelines, and it's an advanced, high-performance way to write the response.
The writer provides direct access to the response body and manages memory on the caller's behalf. Unlike HttpResponse.Body
, the write doesn't copy request data into a buffer. However, a writer is more complicated to use than a stream and writer code should be thoroughly tested.
For information on how to write content to BodyWriter
, see I/O pipelines PipeWriter.
Set response trailers
HTTP/2 and HTTP/3 support response trailers. Trailers are headers sent with the response after the response body is complete. Because trailers are sent after the response body, trailers can be added to the response at any time.
The following code sets trailers using AppendTrailer:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", (HttpResponse response) =>
{
// Write body
response.WriteAsync("Hello world");
if (response.SupportsTrailers())
{
response.AppendTrailer("trailername", "TrailerValue");
}
});
app.Run();
RequestAborted
The HttpContext.RequestAborted cancellation token can be used to notify that the HTTP request has been aborted by the client or server. The cancellation token should be passed to long-running tasks so they can be canceled if the request is aborted. For example, aborting a database query or HTTP request to get data to return in the response.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var httpClient = new HttpClient();
app.MapPost("/books/{bookId}", async (int bookId, HttpContext context) =>
{
var stream = await httpClient.GetStreamAsync(
$"http://contoso/books/{bookId}.json", context.RequestAborted);
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
app.Run();
The RequestAborted
cancellation token doesn't need to be used for request body read operations because reads always throw immediately when the request is aborted. The RequestAborted
token is also usually unnecessary when writing response bodies, because writes immediately no-op when the request is aborted.
In some cases, passing the RequestAborted
token to write operations can be a convenient way to force a write loop to exit early with an OperationCanceledException. However, it's typically better to pass the RequestAborted
token into any asynchronous operations responsible for retrieving the response body content instead.
Note
Minimal APIs supports binding HttpContext.RequestAborted directly to a CancellationToken parameter.
Abort()
The HttpContext.Abort() method can be used to abort an HTTP request from the server. Aborting the HTTP request immediately triggers the HttpContext.RequestAborted cancellation token and sends a notification to the client that the server has aborted the request.
The middleware in the following example:
- Adds a custom check for malicious requests.
- Aborts the HTTP request if the request is malicious.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) =>
{
if (RequestAppearsMalicious(context.Request))
{
// Malicious requests don't even deserve an error response (e.g. 400).
context.Abort();
return;
}
await next.Invoke();
});
app.Run();
User
The HttpContext.User property is used to get or set the user, represented by ClaimsPrincipal, for the request. The ClaimsPrincipal is typically set by ASP.NET Core authentication.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/user/current", [Authorize] async (HttpContext context) =>
{
var user = await GetUserAsync(context.User.Identity.Name);
return Results.Ok(user);
});
app.Run();
Note
Minimal APIs supports binding HttpContext.User directly to a ClaimsPrincipal parameter.
Features
The HttpContext.Features property provides access to the collection of feature interfaces for the current request. Since the feature collection is mutable even within the context of a request, middleware can be used to modify the collection and add support for additional features. Some advanced features are only available by accessing the associated interface through the feature collection.
The following example:
- Gets IHttpMinRequestBodyDataRateFeature from the features collection.
- Sets MinDataRate to null. This removes the minimum data rate that the request body must be sent by the client for this HTTP request.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/long-running-stream", async (HttpContext context) =>
{
var feature = context.Features.Get<IHttpMinRequestBodyDataRateFeature>();
if (feature != null)
{
feature.MinDataRate = null;
}
// await and read long-running stream from request body.
await Task.Yield();
});
app.Run();
For more information about using request features and HttpContext
, see Request Features in ASP.NET Core.
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 =>
{