Mengakses HttpContext di ASP.NET Core

ASP.NET Akses aplikasi HttpContext Core melalui IHttpContextAccessor antarmuka dan implementasi HttpContextAccessordefaultnya. Anda hanya perlu menggunakan IHttpContextAccessor saat Anda memerlukan akses ke HttpContext dalam layanan.

HttpContext tidak aman untuk utas

Artikel ini terutama membahas penggunaan HttpContext dalam alur permintaan dan respons dari Razor Pages, pengontrol, middleware, dll. Pertimbangkan hal berikut saat menggunakan HttpContext di luar alur permintaan dan respons:

  • HttpContextBUKAN utas yang aman, mengaksesnya dari beberapa utas dapat mengakibatkan pengecualian, kerusakan data, dan hasil yang umumnya tidak dapat diprediksi.
  • Antarmuka IHttpContextAccessor harus digunakan dengan hati-hati. Seperti biasa, HttpContexttidak boleh ditangkap di luar alur permintaan. IHttpContextAccessor:
    • Mengandalkan AsyncLocal<T> yang dapat memiliki dampak performa negatif pada panggilan asinkron.
    • Menciptakan dependensi pada "status sekitar" yang dapat membuat pengujian lebih sulit.
  • IHttpContextAccessor.HttpContext mungkin null jika diakses di luar alur permintaan.
  • Untuk mengakses informasi dari HttpContext luar alur permintaan, salin informasi di dalam alur permintaan. Berhati-hatilah untuk menyalin data aktual dan bukan hanya referensi. Misalnya, daripada menyalin referensi ke , salin nilai header yang IHeaderDictionaryrelevan atau salin seluruh kunci kamus dengan kunci sebelum meninggalkan alur permintaan.
  • Jangan ambil IHttpContextAccessor.HttpContext di konstruktor.

Contoh berikut mencatat cabang GitHub saat diminta dari /branch titik akhir:

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

API GitHub memerlukan dua header. Header User-Agent ditambahkan secara dinamis oleh 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);
    }
}

Dalam kode sebelumnya, ketika HttpContext adalah null, userAgent string diatur ke "Unknown". Jika memungkinkan, HttpContext harus secara eksplisit diteruskan ke layanan. Meneruskan HttpContext data secara eksplisit:

  • Membuat API layanan lebih dapat digunakan di luar alur permintaan.
  • Lebih baik untuk performa.
  • Membuat kode lebih mudah dipahami dan beralasan tentang daripada mengandalkan status sekitar.

Ketika layanan harus mengakses HttpContext, layanan harus mempertanyakan kemungkinan HttpContext menjadi ketika tidak dipanggil null dari utas permintaan.

Aplikasi ini juga mencakup PeriodicBranchesLoggerService, yang mencatat cabang GitHub terbuka dari repositori yang ditentukan setiap 30 detik:

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 adalah layanan yang dihosting, yang berjalan di luar alur permintaan dan respons. Pengelogan PeriodicBranchesLoggerService dari memiliki null HttpContext. PeriodicBranchesLoggerService ditulis untuk tidak bergantung pada 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 =>
{

Gunakan HttpContext dari Razor Halaman

Razor Halaman PageModel mengekspos PageModel.HttpContext properti :

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

        // ...
    }
}

Properti yang sama dapat digunakan dalam Tampilan Halaman terkait Razor :

@page
@model IndexModel

@{
    var message = HttpContext.Request.PathBase;

    // ...
}

Menggunakan HttpContext dari Razor tampilan di MVC

Razor tampilan dalam pola MVC mengekspos HttpContext melalui RazorPage.Context properti pada tampilan. Contoh berikut mengambil nama pengguna saat ini di aplikasi intranet menggunakan Autentikasi Windows:

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

    // ...
}

Menggunakan HttpContext dari pengontrol

Pengontrol mengekspos ControllerBase.HttpContext properti :

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

        // ...

        return View();
    }
}

Menggunakan HttpContext dari API minimal

Untuk menggunakan HttpContext dari API minimal, tambahkan HttpContext parameter:

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

Menggunakan HttpContext dari middleware

Untuk menggunakan HttpContext dari komponen middleware kustom, gunakan parameter yang HttpContext diteruskan ke Invoke metode atau InvokeAsync :

public class MyCustomMiddleware
{
    // ...

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

Gunakan HttpContext dari SignalR

Untuk menggunakan HttpContext dari SignalR, panggil GetHttpContext metode pada Hub.Context:

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

        // ...
    }
}

Menggunakan HttpContext dari metode gRPC

Untuk menggunakan HttpContext dari metode gRPC, lihat Mengatasi HttpContext dalam metode gRPC.

Menggunakan HttpContext dari komponen kustom

Untuk kerangka kerja lain dan komponen kustom yang memerlukan akses ke HttpContext, pendekatan yang disarankan adalah mendaftarkan dependensi menggunakan kontainer Injeksi Dependensi (DI) bawaan. Kontainer DI memasok IHttpContextAccessor ke kelas apa pun yang menyatakannya sebagai dependensi dalam konstruktor mereka:

var builder = WebApplication.CreateBuilder(args);

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

Lihat contoh berikut:

  • UserRepository menyatakan dependensinya pada IHttpContextAccessor.
  • Dependensi disediakan ketika DI menyelesaikan rantai dependensi dan membuat instans 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;

        // ...
    }
}

Akses HttpContext dari utas latar belakang

HttpContext tidak aman untuk utas. Membaca atau menulis properti di HttpContext luar pemrosesan permintaan dapat menghasilkan NullReferenceException.

Catatan

Jika aplikasi Anda menghasilkan kesalahan sporadis NullReferenceException , tinjau bagian kode yang memulai pemrosesan latar belakang atau yang melanjutkan pemrosesan setelah permintaan selesai. Cari kesalahan, seperti mendefinisikan metode pengontrol sebagai async void.

Untuk melakukan pekerjaan latar belakang dengan aman dengan HttpContext data:

  • Salin data yang diperlukan selama pemrosesan permintaan.
  • Teruskan data yang disalin ke tugas latar belakang.
  • Jangan mereferensikan HttpContext data dalam tugas paralel. Ekstrak data yang diperlukan dari konteks sebelum memulai tugas paralel.

Untuk menghindari kode yang tidak aman, jangan pernah meneruskan HttpContext ke metode yang melakukan pekerjaan latar belakang. Teruskan data yang diperlukan sebagai gantinya. Dalam contoh berikut, SendEmail panggilan SendEmailCoreAsync untuk mulai mengirim email. Nilai X-Correlation-Id header diteruskan ke SendEmailCoreAsync alih-alih HttpContext. Eksekusi kode tidak menunggu SendEmailCoreAsync untuk selesai:

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 dan status bersama

Blazor aplikasi server hidup dalam memori server. Itu berarti bahwa ada beberapa aplikasi yang dihosting dalam proses yang sama. Untuk setiap sesi aplikasi, Blazor memulai sirkuit dengan cakupan kontainer DI sendiri. Itu berarti bahwa layanan terlingkup unik per Blazor sesi.

Peringatan

Kami tidak merekomendasikan aplikasi pada status berbagi server yang sama menggunakan layanan singleton kecuali perawatan ekstrem diambil, karena ini dapat memperkenalkan kerentanan keamanan, seperti membocorkan status pengguna di seluruh sirkuit.

Anda dapat menggunakan layanan singleton stateful di Blazor aplikasi jika dirancang khusus untuk itu. Misalnya, tidak masalah untuk menggunakan cache memori sebagai singleton karena memerlukan kunci untuk mengakses entri tertentu, dengan asumsi pengguna tidak memiliki kontrol atas kunci cache apa yang digunakan.

Selain itu, sekali lagi karena alasan keamanan, Anda tidak boleh menggunakan IHttpContextAccessor dalam Blazor aplikasi. Blazor aplikasi berjalan di luar konteks alur ASP.NET Core. HttpContext tidak dijamin tersedia dalam IHttpContextAccessor, juga tidak dijamin akan menyimpan konteks yang memulai Blazor aplikasi.

Cara yang disarankan untuk meneruskan status permintaan ke Blazor aplikasi adalah melalui parameter ke komponen akar dalam penyajian awal aplikasi:

  • Tentukan kelas dengan semua data yang ingin Anda berikan ke Blazor aplikasi.
  • Isi data tersebut Razor dari halaman menggunakan yang tersedia pada saat itu HttpContext .
  • Teruskan data ke Blazor aplikasi sebagai parameter ke komponen akar (Aplikasi).
  • Tentukan parameter di komponen akar untuk menyimpan data yang diteruskan ke aplikasi.
  • Gunakan data khusus pengguna dalam aplikasi; atau sebagai alternatif, salin data tersebut ke dalam layanan terlingkup di dalamnya OnInitializedAsync sehingga dapat digunakan di seluruh aplikasi.

Untuk informasi selengkapnya dan contoh kode, lihat skenario keamanan tambahan ASP.NET CoreBlazor Server.

ASP.NET Akses aplikasi HttpContext Core melalui IHttpContextAccessor antarmuka dan implementasi HttpContextAccessordefaultnya. Anda hanya perlu menggunakan IHttpContextAccessor saat Anda memerlukan akses ke HttpContext dalam layanan.

Gunakan HttpContext dari Razor Halaman

Razor Halaman PageModel mengekspos PageModel.HttpContext properti :

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

        // ...
    }
}

Properti yang sama dapat digunakan dalam Tampilan Halaman terkait Razor :

@page
@model IndexModel

@{
    var message = HttpContext.Request.PathBase;

    // ...
}

Menggunakan HttpContext dari Razor tampilan di MVC

Razor tampilan dalam pola MVC mengekspos HttpContext melalui RazorPage.Context properti pada tampilan. Contoh berikut mengambil nama pengguna saat ini di aplikasi intranet menggunakan Autentikasi Windows:

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

    // ...
}

Menggunakan HttpContext dari pengontrol

Pengontrol mengekspos ControllerBase.HttpContext properti :

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

        // ...

        return View();
    }
}

Menggunakan HttpContext dari middleware

Saat bekerja dengan komponen middleware kustom, HttpContext diteruskan ke Invoke metode atau InvokeAsync :

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

Menggunakan HttpContext dari komponen kustom

Untuk kerangka kerja lain dan komponen kustom yang memerlukan akses ke HttpContext, pendekatan yang disarankan adalah mendaftarkan dependensi menggunakan kontainer Injeksi Dependensi (DI) bawaan. Kontainer DI memasok IHttpContextAccessor ke kelas apa pun yang menyatakannya sebagai dependensi dalam konstruktor mereka:

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

Lihat contoh berikut:

  • UserRepository menyatakan dependensinya pada IHttpContextAccessor.
  • Dependensi disediakan ketika DI menyelesaikan rantai dependensi dan membuat instans 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);
    }
}

Akses HttpContext dari utas latar belakang

HttpContext tidak aman untuk utas. Membaca atau menulis properti di HttpContext luar pemrosesan permintaan dapat menghasilkan NullReferenceException.

Catatan

Jika aplikasi Anda menghasilkan kesalahan sporadis NullReferenceException , tinjau bagian kode yang memulai pemrosesan latar belakang atau yang melanjutkan pemrosesan setelah permintaan selesai. Cari kesalahan, seperti mendefinisikan metode pengontrol sebagai async void.

Untuk melakukan pekerjaan latar belakang dengan aman dengan HttpContext data:

  • Salin data yang diperlukan selama pemrosesan permintaan.
  • Teruskan data yang disalin ke tugas latar belakang.
  • Jangan mereferensikan HttpContext data dalam tugas paralel. Ekstrak data yang diperlukan dari konteks sebelum memulai tugas paralel.

Untuk menghindari kode yang tidak aman, jangan pernah meneruskan HttpContext ke metode yang melakukan pekerjaan latar belakang. Teruskan data yang diperlukan sebagai gantinya. Dalam contoh berikut, SendEmailCore dipanggil untuk mulai mengirim email. diteruskan correlationId ke SendEmailCore, bukan HttpContext. Eksekusi kode tidak menunggu SendEmailCore untuk selesai:

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 dan status bersama

Blazor aplikasi server hidup dalam memori server. Itu berarti bahwa ada beberapa aplikasi yang dihosting dalam proses yang sama. Untuk setiap sesi aplikasi, Blazor memulai sirkuit dengan cakupan kontainer DI sendiri. Itu berarti bahwa layanan terlingkup unik per Blazor sesi.

Peringatan

Kami tidak merekomendasikan aplikasi pada status berbagi server yang sama menggunakan layanan singleton kecuali perawatan ekstrem diambil, karena ini dapat memperkenalkan kerentanan keamanan, seperti membocorkan status pengguna di seluruh sirkuit.

Anda dapat menggunakan layanan singleton stateful di Blazor aplikasi jika dirancang khusus untuk itu. Misalnya, tidak masalah untuk menggunakan cache memori sebagai singleton karena memerlukan kunci untuk mengakses entri tertentu, dengan asumsi pengguna tidak memiliki kontrol atas kunci cache apa yang digunakan.

Selain itu, sekali lagi karena alasan keamanan, Anda tidak boleh menggunakan IHttpContextAccessor dalam Blazor aplikasi. Blazor aplikasi berjalan di luar konteks alur ASP.NET Core. HttpContext tidak dijamin tersedia dalam IHttpContextAccessor, juga tidak dijamin akan menyimpan konteks yang memulai Blazor aplikasi.

Cara yang disarankan untuk meneruskan status permintaan ke Blazor aplikasi adalah melalui parameter ke komponen akar dalam penyajian awal aplikasi:

  • Tentukan kelas dengan semua data yang ingin Anda berikan ke Blazor aplikasi.
  • Isi data tersebut Razor dari halaman menggunakan yang tersedia pada saat itu HttpContext .
  • Teruskan data ke Blazor aplikasi sebagai parameter ke komponen akar (Aplikasi).
  • Tentukan parameter di komponen akar untuk menyimpan data yang diteruskan ke aplikasi.
  • Gunakan data khusus pengguna dalam aplikasi; atau sebagai alternatif, salin data tersebut ke dalam layanan terlingkup di dalamnya OnInitializedAsync sehingga dapat digunakan di seluruh aplikasi.

Untuk informasi selengkapnya dan contoh kode, lihat skenario keamanan tambahan ASP.NET CoreBlazor Server.