Bagikan melalui


Menggunakan HttpContext di ASP.NET Core

HttpContext merangkum semua informasi tentang permintaan dan respons HTTP individual. Instans HttpContext diinisialisasi ketika permintaan HTTP diterima. HttpContext Instans ini dapat diakses oleh middleware dan kerangka kerja aplikasi seperti pengontrol API Web, Razor Pages, SignalR, gRPC, dan banyak lagi.

Untuk informasi selengkapnya tentang mengakses HttpContext, lihat Mengakses HttpContext di ASP.NET Core.

HttpRequest

HttpContext.Request menyediakan akses ke HttpRequest. HttpRequest memiliki informasi tentang permintaan HTTP masuk, dan diinisialisasi ketika permintaan HTTP diterima oleh server. HttpRequest bukan baca-saja, dan middleware dapat mengubah nilai permintaan di alur middleware.

Anggota yang umum digunakan pada HttpRequest meliputi:

Properti Deskripsi Contoh
HttpRequest.Path Jalur permintaan. /en/article/getstarted
HttpRequest.Method Metode permintaan. GET
HttpRequest.Headers Kumpulan header permintaan. user-agent=Edge
x-custom-header=MyValue
HttpRequest.RouteValues Kumpulan nilai rute. Koleksi diatur ketika permintaan dicocokkan dengan rute. language=en
article=getstarted
HttpRequest.Query Kumpulan nilai kueri yang diurai dari QueryString. filter=hello
page=1
HttpRequest.ReadFormAsync() Metode yang membaca isi permintaan sebagai formulir dan mengembalikan kumpulan nilai formulir. Untuk informasi tentang mengapa ReadFormAsync harus digunakan untuk mengakses data formulir, lihat Memilih ReadFormAsync daripada Request.Form. email=user@contoso.com
password=TNkt4taM
HttpRequest.Body A Stream untuk membaca isi permintaan. Payload UTF-8 JSON

Mendapatkan header permintaan

HttpRequest.Headers menyediakan akses ke header permintaan yang dikirim dengan permintaan HTTP. Ada dua cara untuk mengakses header menggunakan koleksi ini:

  • Berikan nama header ke pengindeks pada koleksi header. Nama header tidak peka huruf besar/kecil. Pengindeks dapat mengakses nilai header apa pun.
  • Koleksi header juga memiliki properti untuk mendapatkan dan mengatur header HTTP yang umum digunakan. Properti ini menyediakan cara yang cepat dan didorong IntelliSense untuk mengakses header.
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();

Untuk informasi tentang menangani header yang muncul lebih dari sekali secara efisien, lihat Tampilan singkat StringValues.

Membaca isi permintaan

Permintaan HTTP dapat menyertakan isi permintaan. Isi permintaan adalah data yang terkait dengan permintaan, seperti konten formulir HTML, payload ON UTF-8 JS, atau file.

HttpRequest.Body memungkinkan isi permintaan dibaca dengan 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 dapat dibaca secara langsung atau digunakan dengan API lain yang menerima aliran.

Catatan

API minimal mendukung pengikatan HttpRequest.Body langsung ke Stream parameter.

Mengaktifkan buffering isi permintaan

Isi permintaan hanya dapat dibaca sekali, dari awal hingga akhir. Pembacaan isi permintaan khusus ke depan menghindari overhead buffering seluruh isi permintaan dan mengurangi penggunaan memori. Namun, dalam beberapa skenario, ada kebutuhan untuk membaca isi permintaan beberapa kali. Misalnya, middleware mungkin perlu membaca isi permintaan lalu memutarnya kembali sehingga tersedia untuk titik akhir.

Metode EnableBuffering ekstensi memungkinkan buffering isi permintaan HTTP dan merupakan cara yang disarankan untuk mengaktifkan beberapa bacaan. Karena permintaan dapat berukuran berapa pun, EnableBuffering mendukung opsi untuk menyangga badan permintaan besar ke disk, atau menolaknya sepenuhnya.

Middleware dalam contoh berikut:

  • Mengaktifkan beberapa bacaan dengan EnableBuffering. Ini harus dipanggil sebelum membaca isi permintaan.
  • Membaca isi permintaan.
  • Memutar balik isi permintaan ke awal sehingga middleware lain atau titik akhir dapat membacanya.
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

Cara alternatif untuk membaca isi permintaan adalah dengan menggunakan HttpRequest.BodyReader properti . Properti BodyReader mengekspos isi permintaan sebagai PipeReader. API ini berasal dari alur I/O, cara canggih berkinerja tinggi untuk membaca isi permintaan.

Pembaca langsung mengakses isi permintaan dan mengelola memori atas nama pemanggil. Tidak seperti HttpRequest.Body, pembaca tidak menyalin data permintaan ke dalam buffer. Namun, pembaca lebih rumit untuk digunakan daripada aliran dan harus digunakan dengan hati-hati.

Untuk informasi tentang cara membaca konten dari BodyReader, lihat Alur I/O PipeReader.

HttpResponse

HttpContext.Response menyediakan akses ke HttpResponse. HttpResponse digunakan untuk mengatur informasi tentang respons HTTP yang dikirim kembali ke klien.

Anggota yang umum digunakan pada HttpResponse meliputi:

Properti Deskripsi Contoh
HttpResponse.StatusCode Kode respons. Harus diatur sebelum menulis ke isi respons. 200
HttpResponse.ContentType Header respons content-type . Harus diatur sebelum menulis ke isi respons. application/json
HttpResponse.Headers Kumpulan header respons. Harus diatur sebelum menulis ke isi respons. server=Kestrel
x-custom-header=MyValue
HttpResponse.Body A Stream untuk menulis isi respons. Halaman web yang dihasilkan

Mengatur header respons

HttpResponse.Headers menyediakan akses ke header respons yang dikirim dengan respons HTTP. Ada dua cara untuk mengakses header menggunakan koleksi ini:

  • Berikan nama header ke pengindeks pada koleksi header. Nama header tidak peka huruf besar/kecil. Pengindeks dapat mengakses nilai header apa pun.
  • Koleksi header juga memiliki properti untuk mendapatkan dan mengatur header HTTP yang umum digunakan. Properti ini menyediakan cara yang cepat dan didorong IntelliSense untuk mengakses header.
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();

Aplikasi tidak dapat mengubah header setelah respons dimulai. Setelah respons dimulai, header dikirim ke klien. Respons dimulai dengan membersihkan isi respons atau memanggil HttpResponse.StartAsync(CancellationToken). Properti HttpResponse.HasStarted menunjukkan apakah respons telah dimulai. Kesalahan dilemparkan saat mencoba mengubah header setelah respons dimulai:

System.InvalidOperationException: Header bersifat baca-saja, respons telah dimulai.

Catatan

Kecuali buffering respons diaktifkan, semua operasi tulis (misalnya, WriteAsync) bersihkan isi respons secara internal dan tandai respons sebagai dimulai. Buffering respons dinonaktifkan secara default.

Isi respons tulis

Respons HTTP dapat menyertakan isi respons. Isi respons adalah data yang terkait dengan respons, seperti konten halaman web yang dihasilkan, payload ON UTF-8 JS, atau file.

HttpResponse.Body memungkinkan isi respons ditulis dengan 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 dapat ditulis secara langsung atau digunakan dengan API lain yang menulis ke aliran.

BodyWriter

Cara alternatif untuk menulis isi respons adalah dengan menggunakan HttpResponse.BodyWriter properti . Properti BodyWriter mengekspos isi respons sebagai PipeWriter. API ini berasal dari alur I/O, dan merupakan cara canggih dan berkinerja tinggi untuk menulis respons.

Penulis menyediakan akses langsung ke isi respons dan mengelola memori atas nama pemanggil. Tidak seperti HttpResponse.Body, tulisan tidak menyalin data permintaan ke dalam buffer. Namun, penulis lebih rumit untuk digunakan daripada aliran dan kode penulis harus diuji secara menyeluruh.

Untuk informasi tentang cara menulis konten ke BodyWriter, lihat PipeWriter alur I/O.

Mengatur trailer respons

HTTP/2 dan HTTP/3 mendukung trailer respons. Trailer adalah header yang dikirim dengan respons setelah isi respons selesai. Karena trailer dikirim setelah isi respons, trailer dapat ditambahkan ke respons kapan saja.

Kode berikut menetapkan trailer menggunakan 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

Token HttpContext.RequestAborted pembatalan dapat digunakan untuk memberi tahu bahwa permintaan HTTP telah dibatalkan oleh klien atau server. Token pembatalan harus diteruskan ke tugas yang berjalan lama sehingga dapat dibatalkan jika permintaan dibatalkan. Misalnya, membatalkan kueri database atau permintaan HTTP untuk mendapatkan data agar kembali dalam respons.

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

Token RequestAborted pembatalan tidak perlu digunakan untuk operasi baca isi permintaan karena bacaan selalu segera dilemparkan ketika permintaan dibatalkan. Token RequestAborted juga biasanya tidak perlu saat menulis badan respons, karena segera menulis no-op ketika permintaan dibatalkan.

Dalam beberapa kasus, meneruskan RequestAborted token ke operasi tulis dapat menjadi cara mudah untuk memaksa perulangan tulis keluar lebih awal dengan OperationCanceledException. Namun, biasanya lebih baik untuk meneruskan RequestAborted token ke operasi asinkron apa pun yang bertanggung jawab untuk mengambil konten isi respons sebagai gantinya.

Catatan

API minimal mendukung pengikatan HttpContext.RequestAborted langsung ke CancellationToken parameter.

Abort()

Metode HttpContext.Abort() ini dapat digunakan untuk membatalkan permintaan HTTP dari server. Membatalkan permintaan HTTP segera memicu HttpContext.RequestAborted token pembatalan dan mengirim pemberitahuan ke klien bahwa server telah membatalkan permintaan.

Middleware dalam contoh berikut:

  • Menambahkan pemeriksaan kustom untuk permintaan berbahaya.
  • Membatalkan permintaan HTTP jika permintaan berbahaya.
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

Properti HttpContext.User digunakan untuk mendapatkan atau mengatur pengguna, yang diwakili oleh ClaimsPrincipal, untuk permintaan. ClaimsPrincipal biasanya diatur oleh autentikasi 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();

Catatan

API minimal mendukung pengikatan HttpContext.User langsung ke ClaimsPrincipal parameter.

Features

Properti HttpContext.Features menyediakan akses ke kumpulan antarmuka fitur untuk permintaan saat ini. Karena koleksi fitur dapat diubah bahkan dalam konteks permintaan, middleware dapat digunakan untuk memodifikasi koleksi dan menambahkan dukungan untuk fitur tambahan. Beberapa fitur canggih hanya tersedia dengan mengakses antarmuka terkait melalui koleksi fitur.

Lihat contoh berikut:

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

Untuk informasi selengkapnya tentang menggunakan fitur permintaan dan HttpContext, lihat Fitur Permintaan di ASP.NET Core.

HttpContext tidak aman dari utas

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

  • HttpContextTIDAK aman utas, 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:
    • Bergantung pada AsyncLocal<T> yang dapat memiliki dampak performa negatif pada panggilan asinkron.
    • Membuat 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 menangkap IHttpContextAccessor.HttpContext dalam 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. Secara eksplisit meneruskan HttpContext data:

  • 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 memperhitungkan kemungkinan HttpContext 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);
    }
}

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