Nguon: Microsoft Learn · .NET 8.0

Các thao tác Request và Response trong ASP.NET Core

Nguồn: Request and response operations in ASP.NET Core

Bởi Justin Kotalik

Bài viết này giải thích cách đọc từ request body (thân yêu cầu) và ghi vào response body (thân phản hồi). Code cho các thao tác này có thể cần thiết khi viết middleware (phần mềm trung gian). Ngoài việc viết middleware, code tùy chỉnh thường không cần thiết vì các thao tác được xử lý bởi MVC và Razor Pages.

Có hai abstraction (trừu tượng hóa) cho request body và response body: StreamPipe. Để đọc request, HttpRequest.Body là một Stream, và HttpRequest.BodyReader là một PipeReader. Để ghi response, HttpResponse.Body là một Stream, và HttpResponse.BodyWriter là một PipeWriter.

Pipelines được khuyến nghị hơn streams. Streams có thể dễ sử dụng hơn cho một số thao tác đơn giản, nhưng pipelines có lợi thế về hiệu suất và dễ sử dụng hơn trong hầu hết các tình huống. ASP.NET Core đang bắt đầu sử dụng pipelines thay vì streams trong nội bộ. Ví dụ bao gồm:

Streams không bị loại bỏ khỏi framework. Streams tiếp tục được sử dụng trong .NET:

Ví dụ với Stream

Giả sử mục tiêu là tạo một middleware đọc toàn bộ request body dưới dạng danh sách chuỗi, tách theo dòng mới. Một cài đặt stream đơn giản có thể trông như ví dụ sau:

Code sau: - Được dùng để minh họa các vấn đề khi không dùng pipe để đọc request body. - Không nhằm mục đích sử dụng trong ứng dụng production.

csharp
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"));
}

Code này hoạt động, nhưng có một số vấn đề:

Đây là một ví dụ sửa một số vấn đề trước:

Code sau: - Được dùng để minh họa giải pháp cho một số vấn đề trong code trước trong khi không giải quyết tất cả các vấn đề. - Không nhằm mục đích sử dụng trong ứng dụng production.

csharp
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;
}

Ví dụ trước:

Tuy nhiên, vẫn còn một số vấn đề:

Các vấn đề này có thể sửa được, nhưng code ngày càng phức tạp hơn với ít cải thiện. Pipelines cung cấp cách giải quyết những vấn đề này với độ phức tạp code tối thiểu.

Pipelines

Ví dụ sau cho thấy cách tình huống stream trước có thể được xử lý bằng PipeReader:

csharp
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));
}

Ví dụ này sửa nhiều vấn đề mà các cài đặt stream gặp phải:

Khi ghi trực tiếp vào HttpResponse.BodyWriter, hãy gọi PipeWriter.FlushAsync thủ công để đảm bảo dữ liệu được flush vào response body bên dưới. Đây là lý do:

Developer tự quyết định khi nào gọi FlushAsync, cân bằng các yếu tố như kích thước buffer, overhead ghi mạng, và liệu dữ liệu có nên được gửi theo từng khối riêng biệt. Để biết thêm thông tin, xem System.IO.Pipelines in .NET.

Adapters (Bộ chuyển đổi)

Các thuộc tính Body, BodyReader, và BodyWriter có sẵn cho HttpRequestHttpResponse. Khi bạn đặt Body thành một stream khác, một tập hợp adapters mới tự động thích ứng mỗi loại với các loại khác. Nếu bạn đặt HttpRequest.Body thành một stream mới, HttpRequest.BodyReader tự động được đặt thành một PipeReader mới bao gồm HttpRequest.Body.

StartAsync

HttpResponse.StartAsync được dùng để chỉ ra rằng headers không thể sửa đổi và chạy các callbacks OnStarting. Khi sử dụng Kestrel làm server, gọi StartAsync trước khi sử dụng PipeReader đảm bảo rằng bộ nhớ được trả về bởi GetMemory thuộc về Pipe nội bộ của Kestrel thay vì một buffer bên ngoài.