ASP.NET Core で HttpContext にアクセスする

ASP.NET Core アプリでは、IHttpContextAccessor インターフェイスと、その既定の実装 HttpContextAccessor を介して HttpContext にアクセスします。 IHttpContextAccessor を使用する必要があるのは、サービス内の HttpContext にアクセスする必要がある場合のみです。

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

Razor Pages から HttpContext を使用する

Razor Pages PageModel では、PageModel.HttpContext プロパティが公開されます。

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

        // ...
    }
}

同じプロパティを対応する Razor ページ ビューで使用できます。

@page
@model IndexModel

@{
    var message = HttpContext.Request.PathBase;

    // ...
}

MVC で Razor ビューから HttpContext を使用する

MVC パターンの Razor ビューには、ビューの RazorPage.Context プロパティを使用して HttpContext が公開されます。 次の例では、Windows 認証を使用して、イントラネット アプリで現在のユーザー名を取得します。

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

    // ...
}

コントローラーから HttpContext を使用する

コントローラーは ControllerBase.HttpContext プロパティを公開します。

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

        // ...

        return View();
    }
}

最小限の API から HttpContext を使用する

最小限の API から HttpContext を使うには、HttpContext パラメーターを追加します。

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

ミドルウェアから HttpContext を使用する

カスタム ミドルウェア コンポーネントから HttpContext を使うには、Invoke または InvokeAsync メソッドに渡された HttpContext パラメーターを使います。

public class MyCustomMiddleware
{
    // ...

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

SignalR から HttpContext を使用する

SignalR から HttpContext を使うには、Hub.Context に対して GetHttpContext メソッドを呼び出します。

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

        // ...
    }
}

gRPC メソッドから HttpContext を使用する

gRPC メソッドから HttpContext を使う方法については、「gRPC メソッドで HttpContext を解決する」を参照してください。

カスタム コンポーネントから HttpContext を使用する

HttpContext へのアクセスを必要とするその他のフレームワークおよびカスタム コンポーネントに対して推奨される方法は、組み込みの依存関係の挿入 (DI) コンテナーを使用して依存関係を登録することです。 DI コンテナーは、それぞれのコンストラクター内で IHttpContextAccessor を依存関係として宣言するすべてのクラスに、これを提供します。

var builder = WebApplication.CreateBuilder(args);

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

次に例を示します。

  • UserRepositoryIHttpContextAccessor に対する依存関係を宣言します。
  • DI で依存関係のチェーンが解決され、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 にアクセスする

HttpContext はスレッド セーフではありません。 要求の処理以外で HttpContext のプロパティを読み書きすると、結果的に NullReferenceException になることがあります。

Note

アプリで NullReferenceException エラーが散発的に生成される場合、コードの中で、バックグラウンド処理を開始する部分や要求完了後に処理を続行する部分を見直してください。 コントローラー メソッドを async void として定義するなどの間違いを探します。

HttpContext データでバックグラウンド作業を安全に行うには:

  • 要求処理中に必要なデータをコピーします。
  • コピーしたデータをバックグラウンド タスクに渡します。
  • 並列タスクで HttpContext データを参照 "しないで" ください。 必要なデータは、並列タスクを開始する前にコンテキストから抽出してください。

アンセーフ コードを避けるために、バックグラウンド処理を行わないメソッドには HttpContext を決して渡さないでください。 代わりに必要なデータを渡してください。 次の例では、電子メールの送信を開始するために SendEmailSendEmailCoreAsync を呼びします。 X-Correlation-Id ヘッダーの値は、HttpContext ではなく SendEmailCoreAsync に渡されます。 コードの実行では、SendEmailCoreAsync が完了するのを待機しません。

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 と共有状態

Blazor サーバー アプリはサーバー メモリに存在します。 これは、同じプロセス内で複数のアプリがホストされていることを意味します。 Blazor では各アプリ セッションに対して、独自の DI コンテナー スコープで回線が開始されます。 つまり、スコープが指定されたサービスは、Blazor セッションごとに一意になるということです。

警告

特別な注意を払っていない限り、同じサーバーの共有状態のアプリがシングルトン サービスを使用することはお勧めできません。これにより、回線をまたいだユーザー状態のリークなど、セキュリティ上の脆弱性が生じる可能性があります。

Blazor アプリでは、ステートフル シングルトン サービスを使用できます (特に専用に設計されている場合)。 たとえば、メモリ キャッシュをシングルトンとして使用するのは問題ありません。これは、使用されるキャッシュ キーをユーザーが制御できない場合に、特定のエントリにアクセスするためにキーが必要になるためです。

また、セキュリティ上の理由から、Blazor アプリ内で IHttpContextAccessor を使用することはできません。 Blazor アプリは、ASP.NET Core パイプラインのコンテキストの外部で実行されます。 HttpContext は、IHttpContextAccessor 内で使用できるとは限りません。また、Blazor アプリが開始されたコンテキストが保持されることも保証されません。

Blazor アプリに要求状態を渡す方法としては、アプリの初期レンダリングでルート コンポーネントのパラメーターを使用することをお勧めします。

  • Blazor アプリに渡すデータをすべて含むクラスを定義します。
  • その時点で利用可能な HttpContext を使用して、Razor ページからそのデータを設定します。
  • ルート コンポーネント (アプリ) へのパラメーターとして Blazor アプリにデータを渡します。
  • アプリに渡されるデータを保持するために、ルート コンポーネントでパラメーターを定義します。
  • アプリ内のユーザー固有のデータを使用します。または、アプリ全体で使用できるようにそのデータを OnInitializedAsync 内のスコープ サービスにコピーします。

詳細とコード例については、「ASP.NET Core Blazor Server のその他のセキュリティ シナリオ」を参照してください。

ASP.NET Core アプリでは、IHttpContextAccessor インターフェイスと、その既定の実装 HttpContextAccessor を介して HttpContext にアクセスします。 IHttpContextAccessor を使用する必要があるのは、サービス内の HttpContext にアクセスする必要がある場合のみです。

Razor Pages から HttpContext を使用する

Razor Pages PageModel では、PageModel.HttpContext プロパティが公開されます。

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

        // ...
    }
}

同じプロパティを対応する Razor ページ ビューで使用できます。

@page
@model IndexModel

@{
    var message = HttpContext.Request.PathBase;

    // ...
}

MVC で Razor ビューから HttpContext を使用する

MVC パターンの Razor ビューには、ビューの RazorPage.Context プロパティを使用して HttpContext が公開されます。 次の例では、Windows 認証を使用して、イントラネット アプリで現在のユーザー名を取得します。

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

    // ...
}

コントローラーから HttpContext を使用する

コントローラーは ControllerBase.HttpContext プロパティを公開します。

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

        // ...

        return View();
    }
}

ミドルウェアから HttpContext を使用する

カスタム ミドルウェア コンポーネントを使用する場合、HttpContextInvoke メソッドまたは InvokeAsync メソッドに渡されます。

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

カスタム コンポーネントから HttpContext を使用する

HttpContext へのアクセスを必要とするその他のフレームワークおよびカスタム コンポーネントに対して推奨される方法は、組み込みの依存関係の挿入 (DI) コンテナーを使用して依存関係を登録することです。 DI コンテナーは、それぞれのコンストラクター内で IHttpContextAccessor を依存関係として宣言するすべてのクラスに、これを提供します。

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

次に例を示します。

  • UserRepositoryIHttpContextAccessor に対する依存関係を宣言します。
  • DI で依存関係のチェーンが解決され、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 にアクセスする

HttpContext はスレッド セーフではありません。 要求の処理以外で HttpContext のプロパティを読み書きすると、結果的に NullReferenceException になることがあります。

Note

アプリで NullReferenceException エラーが散発的に生成される場合、コードの中で、バックグラウンド処理を開始する部分や要求完了後に処理を続行する部分を見直してください。 コントローラー メソッドを async void として定義するなどの間違いを探します。

HttpContext データでバックグラウンド作業を安全に行うには:

  • 要求処理中に必要なデータをコピーします。
  • コピーしたデータをバックグラウンド タスクに渡します。
  • 並列タスクで HttpContext データを参照 "しないで" ください。 必要なデータは、並列タスクを開始する前にコンテキストから抽出してください。

アンセーフ コードを避けるために、バックグラウンド処理を行わないメソッドには HttpContext を決して渡さないでください。 代わりに必要なデータを渡してください。 次の例では、電子メールの送信を開始するために SendEmailCore が呼び出されます。 correlationId は、HttpContext ではなく SendEmailCore に渡されます。 コードの実行では、SendEmailCore が完了するのを待機しません。

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 と共有状態

Blazor サーバー アプリはサーバー メモリに存在します。 これは、同じプロセス内で複数のアプリがホストされていることを意味します。 Blazor では各アプリ セッションに対して、独自の DI コンテナー スコープで回線が開始されます。 つまり、スコープが指定されたサービスは、Blazor セッションごとに一意になるということです。

警告

特別な注意を払っていない限り、同じサーバーの共有状態のアプリがシングルトン サービスを使用することはお勧めできません。これにより、回線をまたいだユーザー状態のリークなど、セキュリティ上の脆弱性が生じる可能性があります。

Blazor アプリでは、ステートフル シングルトン サービスを使用できます (特に専用に設計されている場合)。 たとえば、メモリ キャッシュをシングルトンとして使用するのは問題ありません。これは、使用されるキャッシュ キーをユーザーが制御できない場合に、特定のエントリにアクセスするためにキーが必要になるためです。

また、セキュリティ上の理由から、Blazor アプリ内で IHttpContextAccessor を使用することはできません。 Blazor アプリは、ASP.NET Core パイプラインのコンテキストの外部で実行されます。 HttpContext は、IHttpContextAccessor 内で使用できるとは限りません。また、Blazor アプリが開始されたコンテキストが保持されることも保証されません。

Blazor アプリに要求状態を渡す方法としては、アプリの初期レンダリングでルート コンポーネントのパラメーターを使用することをお勧めします。

  • Blazor アプリに渡すデータをすべて含むクラスを定義します。
  • その時点で利用可能な HttpContext を使用して、Razor ページからそのデータを設定します。
  • ルート コンポーネント (アプリ) へのパラメーターとして Blazor アプリにデータを渡します。
  • アプリに渡されるデータを保持するために、ルート コンポーネントでパラメーターを定義します。
  • アプリ内のユーザー固有のデータを使用します。または、アプリ全体で使用できるようにそのデータを OnInitializedAsync 内のスコープ サービスにコピーします。

詳細とコード例については、「ASP.NET Core Blazor Server のその他のセキュリティ シナリオ」を参照してください。