Share via


ASP.NET Core で HttpContext を使用する

HttpContext を指定すると、個々の HTTP 要求と応答に関するすべての情報がカプセル化されます。 HttpContext インスタンスは、HTTP 要求の受信時に初期化されます。 HttpContext インスタンスには、ミドルウェアや Web API コントローラー、Razor Pages、SignalR、gRPC などのアプリ フレームワークからアクセスできます。

HttpContext にアクセスする方法の詳細については、「ASP.NET Core で HttpContext にアクセスする」を参照してください。

HttpRequest

HttpContext.Request を指定すると、HttpRequest にアクセスできます。 HttpRequest には受信 HTTP 要求に関する情報があり、サーバーで HTTP 要求を受け取ったタイミングで初期化されます。 HttpRequest は読み取り専用ではなく、ミドルウェアによってミドルウェア パイプラインの要求値が変更されます。

HttpRequest で一般的に使用されるメンバーは、次のとおりです。

プロパティ 説明
HttpRequest.Path 要求パス。 /en/article/getstarted
HttpRequest.Method 要求メソッド。 GET
HttpRequest.Headers 要求ヘッダーのコレクション。 user-agent=Edge
x-custom-header=MyValue
HttpRequest.RouteValues ルート値のコレクション。 要求がルートと一致すると、コレクションが設定されます。 language=en
article=getstarted
HttpRequest.Query QueryString から解析されたクエリ値のコレクション。 filter=hello
page=1
HttpRequest.ReadFormAsync() 要求本文をフォームとして読み取り、フォーム値のコレクションを返すメソッド。 フォーム データへのアクセスに ReadFormAsync を使用する理由については、「Request.Form よりも ReadFormAsync を優先する」を参照してください。 email=user@contoso.com
password=TNkt4taM
HttpRequest.Body 要求本文を読み取るための Stream UTF-8 JSON ペイロード

要求ヘッダーを取得する

HttpRequest.Headers を指定すると、HTTP 要求で送信された要求ヘッダーにアクセスできます。 このコレクションを使用してヘッダーにアクセスする方法は 2 つあります。

  • ヘッダー コレクションのインデクサーにヘッダー名を指定します。 ヘッダー名の大文字と小文字は区別されません。 インデクサーから任意のヘッダー値にアクセスできます。
  • ヘッダー コレクションには、一般的に使用される HTTP ヘッダーを取得および設定するためのプロパティもあります。 これらのプロパティにより、高速かつ IntelliSense 駆動型の方法でヘッダーにアクセスできます。
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();

複数回出現するヘッダーを効率的に処理する方法については、「A brief look at StringValues (StringValues の概要)」を参照してください。

要求本文を読み取る

HTTP 要求には要求本文が含まれる場合があります。 要求本文とは、HTML フォームの内容、UTF-8 JSON ペイロード、ファイルなど、要求に関連付けられているデータのことです。

HttpRequest.Body では、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 は、直接読み取るか、ストリームを受け入れる他の API と共に使用することができます。

Note

Minimal API を使用すると、HttpRequest.BodyStream パラメーターに直接バインドできます。

要求本文のバッファーリングを有効にする

要求本文は、最初から最後まで 1 回のみ読み取ることができます。 要求本文の順方向専用読み取りにより、要求本文全体に対してバッファーリングを行うオーバーヘッドを回避し、メモリ使用量を減らします。 ただし、一部のシナリオでは、要求本文を複数回読み取る必要があります。 たとえば、ミドルウェアでは、要求本文を読み取り、それを巻き戻してエンドポイントで使用できるようにする必要がある場合があります。

EnableBuffering 拡張メソッドは、HTTP 要求本文のバッファーリングを有効にし、複数の読み取りを有効にする推奨される方法です。 要求は任意のサイズになる可能性があるため、EnableBuffering では大きな要求本文のディスクへのバッファーリングや、それを完全に拒否するオプションがサポートされています。

次の例のミドルウェアでは、次のことが行われます。

  • EnableBuffering を指定して複数回の読み取りを有効にする。 要求本文を読み取る前に呼び出す必要があります。
  • 要求本文を読み取る。
  • 要求本文を先頭に巻き戻して、他のミドルウェアまたはエンドポイントで読み取ることができるようにします。
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

要求本文を読み取るもう 1 つの方法は、HttpRequest.BodyReader プロパティを使用する方法です。 BodyReader プロパティを指定すると、要求本文が PipeReader として公開されます。 これは、要求本文を読み取る高度かつパフォーマンスの高い方法である I/O パイプラインからの API です。

リーダーが要求本文に直接アクセスし、呼び出し元の代わりにメモリを管理します。 HttpRequest.Body とは異なり、リーダーによって要求データがバッファーにコピーされることはありません。 ただし、リーダーはストリームよりも使用が複雑であり、注意して使用する必要があります。

BodyReader からコンテンツを読み取る方法の詳細については、I/O パイプラインの PipeReader に関するセクションを参照してください。

HttpResponse

HttpContext.Response を指定すると、HttpResponse にアクセスできます。 HttpResponse は、クライアントに返される HTTP 応答に関する情報を設定するために使用されます。

HttpResponse で一般的に使用されるメンバーは、次のとおりです。

プロパティ 説明
HttpResponse.StatusCode 応答コード。 応答本文に書き込む前に設定する必要があります。 200
HttpResponse.ContentType 応答の content-type ヘッダー。 応答本文に書き込む前に設定する必要があります。 application/json
HttpResponse.Headers 応答ヘッダーのコレクション。 応答本文に書き込む前に設定する必要があります。 server=Kestrel
x-custom-header=MyValue
HttpResponse.Body 応答本文を書き込むための Stream 生成された Web ページ

応答ヘッダーを設定する

HttpResponse.Headers を指定すると、HTTP 応答で送信された応答ヘッダーにアクセスできます。 このコレクションを使用してヘッダーにアクセスする方法は 2 つあります。

  • ヘッダー コレクションのインデクサーにヘッダー名を指定します。 ヘッダー名の大文字と小文字は区別されません。 インデクサーから任意のヘッダー値にアクセスできます。
  • ヘッダー コレクションには、一般的に使用される HTTP ヘッダーを取得および設定するためのプロパティもあります。 これらのプロパティにより、高速かつ IntelliSense 駆動型の方法でヘッダーにアクセスできます。
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();

応答が開始された後は、アプリでヘッダーを変更することはできません。 応答が開始されると、ヘッダーがクライアントに送信されます。 応答は、応答本文をフラッシュするか、HttpResponse.StartAsync(CancellationToken) を呼び出すことによって開始されます。 HttpResponse.HasStarted プロパティは、応答が開始されているかどうかを示します。 応答が開始された後にヘッダーを変更しようとすると、次のエラーがスローされます。

System.InvalidOperationException: ヘッダーは読み取り専用であり、応答は既に開始されています。

Note

応答バッファリングが有効になっていない限り、すべての書き込み操作 (WriteAsync など) は応答本文を内部でフラッシュし、応答に開始済みのマークを付けます。 応答バッファリングは既定で無効になっています。

応答本文を書き込む

HTTP 応答には、応答本文が含まれる場合があります。 応答本文とは、生成された Web ページコンテンツ、UTF-8 JSON ペイロード、ファイルなど、応答に関連付けられたデータのことです。

HttpResponse.Body では、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 は、直接書き込むか、ストリームに書き込む他の API と共に使用することができます。

BodyWriter

応答本文を書き込むもう 1 つの方法は、HttpResponse.BodyWriter プロパティを使用する方法です。 BodyWriter プロパティを指定すると、応答本文が PipeWriter として公開されます。 これは、応答を書き込む高度かつパフォーマンスの高い方法である I/O パイプラインの API です。

ライターによって応答本文への直接アクセスが提供され、呼び出し元の代わりにメモリを管理します。 HttpResponse.Body とは異なり、書き込みによって要求データがバッファーにコピーされることはありません。 ただし、ライターはストリームを使用するよりも複雑で、ライター コードを徹底的にテストする必要があります。

コンテンツを BodyWriter に書き込む方法については、I/O パイプラインの PipeWriter に関するページを参照してください。

応答トレーラーを設定する

HTTP/2 と HTTP/3 は応答トレーラーに対応しています。 トレーラーとは、応答本文が完了した後に応答と共に送信されるヘッダーのことです。 トレーラーは応答本文の後に送信されるため、トレーラーはいつでも応答に追加できます。

次のコードでは、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

HttpContext.RequestAborted キャンセル トークンは、HTTP 要求がクライアントまたはサーバーによって中止されたことを通知するために使用できます。 要求が中止された場合に取り消すことができるように、キャンセル トークンは実行時間の長いタスクに渡すことをお勧めします。 たとえば、データベース クエリまたは HTTP 要求を中止して、データが応答に返されるようにします。

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();

読み取りは要求が中止されたときに常に直ちにスローされるため、RequestAborted キャンセル トークンを要求本文の読み取り操作に使用する必要はありません。 また、書き込みは要求が中止されると即座に実行されなくなるため、一般的に応答本文を書き込むときにも RequestAborted トークンは不要です。

場合によっては、書き込み操作に RequestAborted トークンを渡すことは、OperationCanceledException によって書き込みループを早い段階で強制的に終了させる便利な方法となります。 ただし、一般的には、代わりにその応答本文コンテンツを取得することを担う非同期操作に RequestAborted トークンを渡すことをお勧めします。

Note

Minimal API を使用すると、HttpContext.RequestAbortedCancellationToken パラメーターに直接バインドできます。

Abort()

HttpContext.Abort() メソッドを使用すると、サーバーからの HTTP 要求を中止することができます。 HTTP 要求を中止すると、すぐに HttpContext.RequestAborted キャンセル トークンがトリガーされ、サーバーで要求が中止されたことを示す通知がクライアントに送信されます。

次の例のミドルウェアでは、次のことが行われます。

  • 悪意のある要求に対するカスタム チェックを追加する。
  • HTTP 要求に悪意がある場合に、その要求を中止する。
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

HttpContext.User プロパティは、その要求のユーザー (ClaimsPrincipal で表現される) を取得または設定するために使用されます。 ClaimsPrincipal は通常、ASP.NET Core 認証によって設定されます。

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 API を使用すると、HttpContext.UserClaimsPrincipal パラメーターに直接バインドできます。

Features

HttpContext.Features プロパティを指定すると、現在の要求の機能インターフェイスのコレクションにアクセスできます。 機能のコレクションは要求のコンテキスト内でも変更可能であるため、コレクションの変更と、その他の機能のサポートの追加にはミドルウェアを使用できます。 一部の高度な機能は、関連付けられているインターフェイスに機能コレクションを通じてアクセスすることによってのみ使用できます。

次のような例です。

  • 機能コレクションから IHttpMinRequestBodyDataRateFeature を取得する。
  • MinDataRate に null を設定します。 これにより、この HTTP 要求に対してクライアントから要求本文が送信される必要がある最小データ レートが削除されます。
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();

要求機能と HttpContext の使用の詳細については、「ASP.NET Core での要求機能」を参照してください。

HttpContext はスレッド セーフでない

この記事では、主に Razor Pages、コントローラー、ミドルウェアなどからの要求と応答のフローで HttpContext を使用することについて説明します。HttpContext を要求と応答のフロー外で使用する際には、次の点を考慮してください。

  • HttpContext はスレッド セーフではありません。複数のスレッドからアクセスすると、例外、データの破損、一般的に予測できない結果が発生する可能性があります。
  • IHttpContextAccessor インターフェイスは注意して使用する必要があります。 いつものように、HttpContext は要求フロー外でキャプチャしては "なりません"。 IHttpContextAccessor:
    • 非同期呼び出しのパフォーマンスに悪影響を与える可能性がある AsyncLocal<T> に依存します。
    • テストをより困難にする可能性がある "アンビエント状態" に対する依存関係を作成します。
  • 要求フロー外でアクセスされた場合、IHttpContextAccessor.HttpContextnull である可能性があります。
  • 要求フロー外で HttpContext からの情報にアクセスするには、要求フロー内で情報をコピーします。 参照だけでなく、実際のデータをコピーするように注意してください。 たとえば、IHeaderDictionary への参照をコピーするのではなく、関連するヘッダー値をコピーするか、要求フローを終了する前にキーごとにディクショナリ キー全体をコピーします。
  • コンストラクターで IHttpContextAccessor.HttpContext をキャプチャしてはなりません。

次のサンプルでは、/branch エンドポイントから要求されると、GitHub ブランチをログに記録します。

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();

GitHub API には 2 つのヘッダーが必要です。 User-Agent ヘッダーは、次のように 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();

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);
    }
}

前のコードでは、HttpContextnull の場合、userAgent 文字列は "Unknown" に設定されます。 可能であれば、HttpContext が明示的にサービスに渡される必要があります。 HttpContext のデータを明示的に渡すことで、次のようになります。

  • サービス API が要求フロー外でより使いやすくなります。
  • パフォーマンスによい影響を及ぼします。
  • アンビエント状態に依存するよりも、コードの理解と推論を容易にします。

サービスで HttpContext にアクセスする必要がある場合は、要求スレッドから呼び出されないときに、HttpContextnull になる可能性を考慮に入れてください。

また、アプリケーションには PeriodicBranchesLoggerService が含まれており、指定したリポジトリの開かれている GitHub ブランチが 30 秒ごとにログに記録されます。

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 は、要求と応答のフロー外で実行されるホステッド サービスです。 PeriodicBranchesLoggerService からのログには null の HttpContext があります。 PeriodicBranchesLoggerServiceHttpContext に依存しないように書き込まれました。

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 =>
{