ASP.NET Core で HttpContext にアクセスする
ASP.NET Core アプリでは、IHttpContextAccessor インターフェイスと、その既定の実装 HttpContextAccessor を介して HttpContext
にアクセスします。 IHttpContextAccessor
を使用する必要があるのは、サービス内の HttpContext
にアクセスする必要がある場合のみです。
HttpContext はスレッド セーフでない
この記事では、主に Razor Pages、コントローラー、ミドルウェアなどからの要求と応答のフローで HttpContext
を使用することについて説明します。HttpContext
を要求と応答のフロー外で使用する際には、次の点を考慮してください。
HttpContext
はスレッド セーフではありません。複数のスレッドからアクセスすると、例外、データの破損、一般的に予測できない結果が発生する可能性があります。- IHttpContextAccessor インターフェイスは注意して使用する必要があります。 いつものように、
HttpContext
は要求フロー外でキャプチャしては "なりません"。IHttpContextAccessor
:- 非同期呼び出しのパフォーマンスに悪影響を与える可能性がある AsyncLocal<T> に依存します。
- テストをより困難にする可能性がある "アンビエント状態" に対する依存関係を作成します。
- 要求フロー外でアクセスされた場合、IHttpContextAccessor.HttpContext は
null
である可能性があります。 - 要求フロー外で
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);
}
}
前のコードでは、HttpContext
が null
の場合、userAgent
文字列は "Unknown"
に設定されます。 可能であれば、HttpContext
が明示的にサービスに渡される必要があります。 HttpContext
のデータを明示的に渡すことで、次のようになります。
- サービス API が要求フロー外でより使いやすくなります。
- パフォーマンスによい影響を及ぼします。
- アンビエント状態に依存するよりも、コードの理解と推論を容易にします。
サービスで HttpContext
にアクセスする必要がある場合は、要求スレッドから呼び出されないときに、HttpContext
が null
になる可能性を考慮に入れてください。
また、アプリケーションには 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
があります。 PeriodicBranchesLoggerService
は 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 =>
{
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>();
次に例を示します。
UserRepository
はIHttpContextAccessor
に対する依存関係を宣言します。- 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
を決して渡さないでください。 代わりに必要なデータを渡してください。 次の例では、電子メールの送信を開始するために SendEmail
が SendEmailCoreAsync
を呼びします。 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 を使用する
カスタム ミドルウェア コンポーネントを使用する場合、HttpContext
は Invoke
メソッドまたは 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>();
}
次に例を示します。
UserRepository
はIHttpContextAccessor
に対する依存関係を宣言します。- 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 のその他のセキュリティ シナリオ」を参照してください。