Operasi permintaan dan respons di ASP.NET Core

Oleh Justin Kotalik

Artikel ini menjelaskan cara membaca dari isi permintaan dan menulis ke isi respons. Kode untuk operasi ini mungkin diperlukan saat menulis middleware. Di luar penulisan middleware, kode kustom umumnya tidak diperlukan karena operasi ditangani oleh MVC dan Razor Pages.

Ada dua abstraksi untuk badan permintaan dan respons: Stream dan Pipe. Untuk pembacaan permintaan, HttpRequest.Body adalah Stream, dan HttpRequest.BodyReader merupakan PipeReader. Untuk penulisan respons, HttpResponse.Body adalah Stream, dan HttpResponse.BodyWriter merupakan PipeWriter.

Alur direkomendasikan melalui aliran. Aliran dapat lebih mudah digunakan untuk beberapa operasi sederhana, tetapi alur memiliki keunggulan performa dan lebih mudah digunakan dalam sebagian besar skenario. ASP.NET Core mulai menggunakan alur alih-alih streaming secara internal. Contoh meliputi:

  • FormReader
  • TextReader
  • TextWriter
  • HttpResponse.WriteAsync

Aliran tidak dihapus dari kerangka kerja. Aliran terus digunakan di seluruh .NET, dan banyak jenis aliran tidak memiliki setara pipa, seperti FileStreams dan ResponseCompression.

Contoh aliran

Misalkan tujuannya adalah untuk membuat middleware yang membaca seluruh isi permintaan sebagai daftar string, memisahkan pada baris baru. Implementasi aliran sederhana mungkin terlihat seperti contoh berikut:

Peringatan

Kode berikut:

  • Digunakan untuk menunjukkan masalah dengan tidak menggunakan pipa untuk membaca isi permintaan.
  • Tidak dimaksudkan untuk digunakan dalam aplikasi produksi.
private async Task<List<string>> GetListOfStringsFromStream(Stream requestBody)
{
    // Build up the request body in a string builder.
    StringBuilder builder = new StringBuilder();

    // Rent a shared buffer to write the request body into.
    byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);

    while (true)
    {
        var bytesRemaining = await requestBody.ReadAsync(buffer, offset: 0, buffer.Length);
        if (bytesRemaining == 0)
        {
            break;
        }

        // Append the encoded string into the string builder.
        var encodedString = Encoding.UTF8.GetString(buffer, 0, bytesRemaining);
        builder.Append(encodedString);
    }

    ArrayPool<byte>.Shared.Return(buffer);

    var entireRequestBody = builder.ToString();

    // Split on \n in the string.
    return new List<string>(entireRequestBody.Split("\n"));
}

Jika Anda ingin melihat komentar kode yang diterjemahkan ke bahasa selain bahasa Inggris, beri tahu kami dalam masalah diskusi GitHub ini.

Kode ini berfungsi, tetapi ada beberapa masalah:

  • Sebelum menambahkan ke StringBuilder, contoh membuat string lain (encodedString) yang segera dibuang. Proses ini terjadi untuk semua byte dalam aliran, sehingga hasilnya adalah alokasi memori ekstra ukuran seluruh isi permintaan.
  • Contoh membaca seluruh string sebelum memisahkan pada baris baru. Lebih efisien untuk memeriksa baris baru dalam array byte.

Berikut adalah contoh yang memperbaiki beberapa masalah sebelumnya:

Peringatan

Kode berikut:

  • Digunakan untuk menunjukkan solusi untuk beberapa masalah dalam kode sebelumnya sambil tidak menyelesaikan semua masalah.
  • Tidak dimaksudkan untuk digunakan dalam aplikasi produksi.
private async Task<List<string>> GetListOfStringsFromStreamMoreEfficient(Stream requestBody)
{
    StringBuilder builder = new StringBuilder();
    byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);
    List<string> results = new List<string>();

    while (true)
    {
        var bytesRemaining = await requestBody.ReadAsync(buffer, offset: 0, buffer.Length);

        if (bytesRemaining == 0)
        {
            results.Add(builder.ToString());
            break;
        }

        // Instead of adding the entire buffer into the StringBuilder
        // only add the remainder after the last \n in the array.
        var prevIndex = 0;
        int index;
        while (true)
        {
            index = Array.IndexOf(buffer, (byte)'\n', prevIndex);
            if (index == -1)
            {
                break;
            }

            var encodedString = Encoding.UTF8.GetString(buffer, prevIndex, index - prevIndex);

            if (builder.Length > 0)
            {
                // If there was a remainder in the string buffer, include it in the next string.
                results.Add(builder.Append(encodedString).ToString());
                builder.Clear();
            }
            else
            {
                results.Add(encodedString);
            }

            // Skip past last \n
            prevIndex = index + 1;
        }

        var remainingString = Encoding.UTF8.GetString(buffer, prevIndex, bytesRemaining - prevIndex);
        builder.Append(remainingString);
    }

    ArrayPool<byte>.Shared.Return(buffer);

    return results;
}

Contoh sebelumnya ini:

  • Tidak buffer seluruh isi permintaan dalam StringBuilder kecuali tidak ada karakter baris baru.
  • Tidak memanggil Split string.

Namun, masih ada beberapa masalah:

  • Jika karakter baris baru jarang, sebagian besar isi permintaan di-buffer dalam string.
  • Kode terus membuat string (remainingString) dan menambahkannya ke buffer string, yang menghasilkan alokasi tambahan.

Masalah ini dapat diperbaiki, tetapi kode menjadi semakin rumit dengan sedikit peningkatan. Alur menyediakan cara untuk menyelesaikan masalah ini dengan kompleksitas kode minimal.

Pipelines

Contoh berikut menunjukkan bagaimana skenario yang sama dapat ditangani menggunakan PipeReader:

private async Task<List<string>> GetListOfStringFromPipe(PipeReader reader)
{
    List<string> results = new List<string>();

    while (true)
    {
        ReadResult readResult = await reader.ReadAsync();
        var buffer = readResult.Buffer;

        SequencePosition? position = null;

        do
        {
            // Look for a EOL in the buffer
            position = buffer.PositionOf((byte)'\n');

            if (position != null)
            {
                var readOnlySequence = buffer.Slice(0, position.Value);
                AddStringToList(results, in readOnlySequence);

                // Skip the line + the \n character (basically position)
                buffer = buffer.Slice(buffer.GetPosition(1, position.Value));
            }
        }
        while (position != null);


        if (readResult.IsCompleted && buffer.Length > 0)
        {
            AddStringToList(results, in buffer);
        }

        reader.AdvanceTo(buffer.Start, buffer.End);

        // At this point, buffer will be updated to point one byte after the last
        // \n character.
        if (readResult.IsCompleted)
        {
            break;
        }
    }

    return results;
}

private static void AddStringToList(List<string> results, in ReadOnlySequence<byte> readOnlySequence)
{
    // Separate method because Span/ReadOnlySpan cannot be used in async methods
    ReadOnlySpan<byte> span = readOnlySequence.IsSingleSegment ? readOnlySequence.First.Span : readOnlySequence.ToArray().AsSpan();
    results.Add(Encoding.UTF8.GetString(span));
}

Contoh ini memperbaiki banyak masalah yang dimiliki implementasi stream:

  • Tidak perlu buffer string karena PipeReader byte handel yang belum digunakan.
  • String yang dikodekan langsung ditambahkan ke daftar string yang dikembalikan.
  • ToArray Selain panggilan, dan memori yang digunakan oleh string, pembuatan string bebas alokasi.

Adapter

Properti Body, BodyReader, dan BodyWriter tersedia untuk HttpRequest dan HttpResponse. Saat Anda mengatur Body ke aliran yang berbeda, set adaptor baru secara otomatis menyesuaikan setiap jenis dengan jenis lainnya. Jika Anda mengatur HttpRequest.Body ke aliran baru, HttpRequest.BodyReader secara otomatis diatur ke yang baru PipeReader yang membungkus HttpRequest.Body.

StartAsync

HttpResponse.StartAsync digunakan untuk menunjukkan bahwa header tidak dapat dimodifikasi dan untuk menjalankan OnStarting panggilan balik. Saat menggunakan Kestrel sebagai server, memanggil StartAsync sebelum menggunakan PipeReader jaminan bahwa memori yang dikembalikan oleh GetMemory milik Kestrel's internal Pipe daripada buffer eksternal.

Sumber Daya Tambahan: