ASP.NET Core での要求と応答の操作Request and response operations in ASP.NET Core

作成者: Justin KotalikBy Justin Kotalik

この記事では、要求本文からの読み取りと、応答本文への書き込みを行う方法について説明します。This article explains how to read from the request body and write to the response body. ミドルウェアを作成するときは、これらの操作のコードが必要になることがあります。Code for these operations might be required when writing middleware. 操作は MVC と Razor Pages によって処理されるため、ミドルウェアの作成以外では、通常、カスタムコードは必要ありません。Outside of writing middleware, custom code isn't generally required because the operations are handled by MVC and Razor Pages.

要求と応答の本文には 2 つの抽象化があります: StreamPipe です。There are two abstractions for the request and response bodies: Stream and Pipe. 要求の読み取りでは、HttpRequest.BodyStream で、HttpRequest.BodyReaderPipeReader です。For request reading, HttpRequest.Body is a Stream, and HttpRequest.BodyReader is a PipeReader. 応答の書き込みでは、HttpResponse.BodyStream で、HttpResponse.BodyWriterPipeWriter です。For response writing, HttpResponse.Body is a Stream, and HttpResponse.BodyWriter is a PipeWriter.

パイプラインは、ストリームよりも推奨されます。Pipelines are recommended over streams. 一部の単純な操作ではストリームの方が使いやすい場合がありますが、パイプラインの方がパフォーマンスに優れていて、ほとんどのシナリオでより簡単に使えます。Streams can be easier to use for some simple operations, but pipelines have a performance advantage and are easier to use in most scenarios. ASP.NET Core では、内部的にストリームではなくパイプラインが使われ始めています。ASP.NET Core is starting to use pipelines instead of streams internally. その例は次のとおりです。Examples include:

  • FormReader
  • TextReader
  • TextWriter
  • HttpResponse.WriteAsync

ストリームがフレームワークから削除されていません。Streams aren't being removed from the framework. ストリームは .NET 全体で使われ続けます。また、ストリームの種類の多くにはパイプラインに相当するものがありません (FileStreamsResponseCompression など)。Streams continue to be used throughout .NET, and many stream types don't have pipe equivalents, such as FileStreams and ResponseCompression.

ストリームの例Stream examples

要求本文全体を、改行で分割した文字列のリストとして読み取るミドルウェアを作成したいとします。Suppose the goal is to create a middleware that reads the entire request body as a list of strings, splitting on new lines. 単純なストリームの実装は、次の例のようになります。A simple stream implementation might look like the following example:

警告

コード例を次に示します。The following code:

  • 要求本文の読み取りにパイプが使用されない問題を示します。Is used to demonstrate the problems with not using a pipe to read the request body.
  • 実稼働アプリで使用する意図はありません。Is not intended to be used in production apps.
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"));
}

コードのコメントを英語以外の言語に翻訳し表示したい場合、こちらの GitHub ディスカッション イシューにてお知らせください。If you would like to see code comments translated to languages other than English, let us know in this GitHub discussion issue.

このコードで動作しますが、いくつか問題があります。This code works, but there are some issues:

  • 例では、StringBuilder に追加する前に、すぐに破棄される別の文字列 (encodedString) を作成しています。Before appending to the StringBuilder, the example creates another string (encodedString) that is thrown away immediately. このプロセスはストリーム内のすべてのバイトに対して実行されるため、要求本文全体のサイズよりも余分にメモリの割り当てが発生します。This process occurs for all bytes in the stream, so the result is extra memory allocation the size of the entire request body.
  • 例では、改行で分割する前に文字列全体を読み取っています。The example reads the entire string before splitting on new lines. バイト配列内で改行をチェックした方がより効率的です。It's more efficient to check for new lines in the byte array.

これらの問題をいくつか修正した例を次に示します。Here's an example that fixes some of the preceding issues:

警告

コード例を次に示します。The following code:

  • 全部の問題が解消されるわけではありませんが、前のコードの一部の問題を解決する策を示します。Is used to demonstrate the solutions to some problems in the preceding code while not solving all the problems.
  • 実稼働アプリで使用する意図はありません。Is not intended to be used in production apps.
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;
}

前の例の場合:This preceding example:

  • 要求本文全体を StringBuilder にバッファーすることがありません (改行文字がない場合を除く)。Doesn't buffer the entire request body in a StringBuilder unless there aren't any newline characters.
  • 文字列の Split を呼び出しません。Doesn't call Split on the string.

ただし、まだいくつかの問題が残っています。However, there are still are a few issues:

  • 改行文字がスパースだった場合、要求本文の多くが文字列にバッファーされます。If newline characters are sparse, much of the request body is buffered in the string.
  • コードで依然として文字列 (remainingString) が作成され、文字列バッファーに追加されているため、余分な割り当てが発生します。The code continues to create strings (remainingString) and adds them to the string buffer, which results in an extra allocation.

これらの問題は修正可能ですが、わずかな改善でコードがますます複雑になっていきます。These issues are fixable, but the code is becoming progressively more complicated with little improvement. パイプラインには、これらの問題を、コードの複雑さを最小限に抑えて解決する方法が用意されています。Pipelines provide a way to solve these problems with minimal code complexity.

パイプラインPipelines

同じシナリオを、PipeReader を使って処理する方法を次の例に示します。The following example shows how the same scenario can be handled using a 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));
}

この例では、ストリームの実装で発生していた多くの問題が修正されています。This example fixes many issues that the streams implementations had:

  • 使われていないバイトを PipeReader で処理するため、文字列バッファーが不要です。There's no need for a string buffer because the PipeReader handles bytes that haven't been used.
  • エンコードされた文字列は、返される文字列のリストに直接追加されます。Encoded strings are directly added to the list of returned strings.
  • ToArray 呼び出しと、文字列で使用されるメモリを除き、文字列作成では割り当てが発生しません。Other than the ToArray call, and the memory used by the string, string creation is allocation free.

アダプターAdapters

HttpRequestHttpResponse には、BodyBodyReaderBodyWriter プロパティを利用できます。The Body, BodyReader, and BodyWriter properties are available for HttpRequest and HttpResponse. Body を別のストリームに設定すると、新しいアダプターのセットにより、各種類が別のものに自動的に適応します。When you set Body to a different stream, a new set of adapters automatically adapt each type to the other. HttpRequest.Body を新しいストリームに設定した場合、HttpRequest.BodyReader は自動的に、HttpRequest.Body をラップする新しい PipeReader に設定されます。If you set HttpRequest.Body to a new stream, HttpRequest.BodyReader is automatically set to a new PipeReader that wraps HttpRequest.Body.

StartAsyncStartAsync

HttpResponse.StartAsync は、ヘッダーが変更不可能であり、また OnStarting コールバックを実行することを示すために使います。HttpResponse.StartAsync is used to indicate that headers are unmodifiable and to run OnStarting callbacks. サーバーとして Kestrel を使う場合、PipeReader を使う前に StartAsync を呼び出すことで、GetMemory によって返されるメモリが、外部バッファーではなく Kestrel の内部 Pipe に属するよう保証できます。When using Kestrel as a server, calling StartAsync before using the PipeReader guarantees that memory returned by GetMemory belongs to Kestrel's internal Pipe rather than an external buffer.

その他の技術情報Additional resources