次の方法で共有


ASP.NET Core での要求と応答の操作

これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 9 バージョンを参照してください。

警告

このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、 .NET および .NET Core サポート ポリシーを参照してください。 現在のリリースについては、この記事の .NET 9 バージョンを参照してください。

重要

この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。

現在のリリースについては、この記事の .NET 9 バージョンを参照してください。

作成者: Justin Kotalik

この記事では、要求本文からの読み取りと、応答本文への書き込みを行う方法について説明します。 ミドルウェアを作成するときは、これらの操作のコードが必要になることがあります。 操作は MVC と Razor Pages によって処理されるため、ミドルウェアの作成以外では、通常、カスタムコードは必要ありません。

要求と応答の本文には 2 つの抽象化があります: StreamPipe です。 要求の読み取りでは、HttpRequest.BodyStream で、HttpRequest.BodyReaderPipeReader です。 応答の書き込みでは、HttpResponse.BodyStream で、HttpResponse.BodyWriterPipeWriter です。

パイプラインは、ストリームよりも推奨されます。 一部の単純な操作ではストリームの方が使いやすい場合がありますが、パイプラインの方がパフォーマンスに優れていて、ほとんどのシナリオでより簡単に使えます。 ASP.NET Core では、内部的にストリームではなくパイプラインが使われ始めています。 その例は次のとおりです。

  • FormReader
  • TextReader
  • TextWriter
  • HttpResponse.WriteAsync

ストリームがフレームワークから削除されていません。 ストリームは、.NET 全体で引き続き使用されます。

  • 多くのストリームの種類には、 FileStreamsResponseCompressionなど、パイプに相当するものがありません。
  • ストリームに圧縮を追加するのは簡単です。

ストリームの例

要求本文全体を、改行で分割した文字列のリストとして読み取るミドルウェアを作成したいとします。 単純なストリームの実装は、次の例のようになります。

警告

コード例を次に示します。

  • 要求本文の読み取りにパイプが使用されない問題を示します。
  • 実稼働アプリで使用する意図はありません。
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 ディスカッション イシューにてお知らせください。

このコードで動作しますが、いくつか問題があります。

  • 例では、StringBuilder に追加する前に、すぐに破棄される別の文字列 (encodedString) を作成しています。 このプロセスはストリーム内のすべてのバイトに対して実行されるため、要求本文全体のサイズよりも余分にメモリの割り当てが発生します。
  • 例では、改行で分割する前に文字列全体を読み取っています。 バイト配列内で改行をチェックした方がより効率的です。

これらの問題をいくつか修正した例を次に示します。

警告

コード例を次に示します。

  • 全部の問題が解消されるわけではありませんが、前のコードの一部の問題を解決する策を示します。
  • 実稼働アプリで使用する意図はありません。
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;
}

前の例の場合:

  • 要求本文全体を StringBuilder にバッファーすることがありません (改行文字がない場合を除く)。
  • 文字列の Split を呼び出しません。

ただし、まだいくつかの問題が残っています。

  • 改行文字がスパースだった場合、要求本文の多くが文字列にバッファーされます。
  • コードで依然として文字列 (remainingString) が作成され、文字列バッファーに追加されているため、余分な割り当てが発生します。

これらの問題は修正可能ですが、わずかな改善でコードがますます複雑になっていきます。 パイプラインには、これらの問題を、コードの複雑さを最小限に抑えて解決する方法が用意されています。

パイプライン

次の例は、 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));
}

この例では、ストリームの実装で発生していた多くの問題が修正されています。

  • 使われていないバイトを PipeReader で処理するため、文字列バッファーが不要です。
  • エンコードされた文字列は、返される文字列のリストに直接追加されます。
  • ToArray 呼び出しと、文字列で使用されるメモリを除き、文字列作成では割り当てが発生しません。

HttpResponse.BodyWriterに直接書き込む場合は、PipeWriter.FlushAsyncを手動で呼び出して、基になる応答本文にデータがフラッシュされるようにします。 その理由は次のとおりです。

  • HttpResponse.BodyWriter は、フラッシュ操作がトリガーされるまでデータをバッファーする PipeWriter です。
  • FlushAsyncを呼び出すと、バッファーに格納されたデータが基になる応答本文に書き込まれます。

FlushAsyncを呼び出すタイミング、バッファー サイズ、ネットワーク書き込みオーバーヘッドなどのバランス要因、データを個別のチャンクで送信するかどうかは、開発者が決定する必要があります。 詳細については、「 .NET の System.IO.Pipelines」を参照してください。

アダプター

BodyBodyReader には、BodyWriterHttpRequestHttpResponse プロパティを利用できます。 Body を別のストリームに設定すると、新しいアダプターのセットにより、各種類が別のものに自動的に適応します。 HttpRequest.Body を新しいストリームに設定した場合、HttpRequest.BodyReader は自動的に、PipeReader をラップする新しい HttpRequest.Body に設定されます。

スタート非同期

HttpResponse.StartAsync は、ヘッダーが変更不可能であり、また OnStarting コールバックを実行することを示すために使います。 サーバーとして Kestrel を使う場合、StartAsync を使う前に PipeReader を呼び出すことで、GetMemory によって返されるメモリが、外部バッファーではなく Kestrel の内部 Pipe に属するよう保証できます。

その他の技術情報