Help us understand the problem. What is going on with this article?

ASP.NET CoreでリクエストBodyを複数回読み込めるようにする

More than 1 year has passed since last update.

はじめに

ASP.NET Coreアプリケーションを開発していて、メインの処理に行く前にHTTPミドルウェアやフィルターなどでHTTPリクエストのBodyを読み込みたいなんてことがあるとき、注意して開発しないとメインの処理でBodyが受け取れなくなります。

例えば以下のようなコードを実行してみたとき、2回目にリクエストBodyを読み込んだ時に空になっているのがわかります。

Startup.cs
public void Configure(IApplicationBuilder app)
{
    app.Use(async (context, next) =>
    {
        // ここでリクエストBodyを読み込むと…
        using (var reader = new StreamReader(context.Request.Body))
        {
            var body = await reader.ReadToEndAsync();
        }

        await next.Invoke();
    });

    app.Run(async context =>
    {
        // ここでは空になっている
        using (var reader = new StreamReader(context.Request.Body))
        {
            var body = await reader.ReadToEndAsync();
        }
    });
}

上記では簡単なASP.NET Coreアプリケーションで試してみましたが、ASP.NET Core MVCアプリケーションで以下のようなアクションメソッドを追加して事前にリクエストBodyを読み込んでおくと、モデルバインディングの結果がnullになっているのが確認できます。

SampleController.cs
[Route("[controller]")]
public class SampleController : Controller
{
    [HttpPost("[action]")]
    public IActionResult Do([FromBody]Request request)
    {
        // requestインスタンスがnullになっている。

        return Ok();
    }
}

[DataContract]
public class Request
{
    [DataMember(Name = "id")]
    public string Id { get; set; }
}

対処法

リクエストBodyを複数回読み込むためには、いくつか設定しておかなければいけないことがあります。

それを順を追って説明していきます。

リクエストBodyの複数回読み込みを許可する

最初はHTTPリクエストに対して複数回リクエストBodyを読み込ませられるようにします。

これはHttpRequestクラスの拡張メソッドであるEnableRewindメソッドを呼び出すだけで可能になります。

HttpRequestクラスはMicrosoft.AspNetCore.Http名前空間にありますが、この拡張メソッドはMicrosoft.AspNetCore.Http.Internalにあるため、名前空間のインポートが必要になります。

このメソッドはドキュメントコメントがないため詳細は分からないのですが、読み込み時のバッファーの設定が出来るオプション引数があるようですが、特に設定しなくても大丈夫そうなので引数なしで呼び出しておきます。

context.Request.EnableRewind();

このメソッドは1回呼び出しておけば、以降複数回リクエストBodyを読み込むときは呼び出さなくても大丈夫なようになっています。

リクエストBodyのStreamをDisposeしない

2つ目はリクエストBodyのStreamをStreamReaderを使用して読み込むときに、破棄しないようにしておく必要があります。

.NETのライブラリではIDisposableを実装したクラスがIDisposableを実装したインスタンスを受け取る時に、受け取ったインスタンスを破棄するかどうかを設定できる引数が大抵用意されていて、StreamReaderもコンストラクタで読み込むStreamを受け取れるようになっているのですが、何も設定せずにStreamReaderを破棄すると受け取ったStreamを破棄するようになっています。

リクエストBodyのStreamを一旦破棄してしまうとそれ以降読み込むことができなくなってしまうため、StreamReaderに渡すときに破棄しないように設定するにはStreamReaderのコンストラクタでleaveOpen引数をtrueにしておきますが、引数順として最後に用意されているため、それまでの引数も全て設定する必要があります。

using (var reader = new StreamReader(
    context.Request.Body, // 読み込むStream
    Encoding.UTF8, // Streamの文字コード
    true, // バイト順マークのチェックを入れるかどうか
    1024, // 読み込むときのバッファサイズ
    true // 読み込むStreamを破棄しないかどうか
    ))
{
}

この設定はEnableRewindメソッドの呼び出しと違って、リクエストBodyを読み込むときに毎回設定する必要があります。

リクエストBodyのStreamの読み込み開始位置を先頭にする

リクエストBodyのStreamを読み込むと読み込み位置が最後まで到達してしまうため、再度読み込めるようにするには読み込み位置を先頭にする必要があります。

方法は2つあってPositionプロパティを0にするか、Seekメソッドを呼び出します。

using (var reader = new StreamReader(context.Request.Body, Encoding.UTF8, true, 1024, true))
{
    // リクエストBodyを読み込む
    var body = reader.ReadToEnd();

    // 読み込み開始位置を先頭にする(どっちの方法でやっても大丈夫)
    reader.BaseStream.Position = 0;
    reader.BaseStream.Seek(0, SeekOrigin.Begin);
}

この設定も読み込むときに毎回呼び出しておく必要があります。

実際にリクエストBodyを2回読み込んでみる

これまでの設定を反映すると最初のコードは以下のようになります。

Startup.cs
public void Configure(IApplicationBuilder app)
{
    app.Use(async (context, next) =>
    {
        // リクエストBodyを複数回読み込めるようにする
        context.Request.EnableRewind();

        // リクエストBodyのStreamを破棄しないようにする
        using (var reader = new StreamReader(context.Request.Body, Encoding.UTF8, true, 1024, true))
        {
            // リクエストBody読み込み
            var body = await reader.ReadToEndAsync();

            // リクエストBodyの読み込み位置を先頭にする
            reader.BaseStream.Position = 0;
        }

        await next.Invoke();
    });

    app.Run(async context =>
    {
        using (var reader = new StreamReader(context.Request.Body))
        {
            // リクエストBody再読み込み
            var body = await reader.ReadToEndAsync();
        }
    });
}

このコードを実行してみると2回目のリクエストBodyでちゃんと値が取得できているのが確認できます。

まとめ

  • HttpRequestEnableRewindメソッドを呼び出しておく
  • StreamReaderにリクエストBodyのStreamを渡すときにleaveOpen引数をtrueにする
  • リクエストBodyの読み込みが完了したらStreamの読み込み位置を先頭にする

この3つを設定しておけばリクエストBodyを複数回読み込むことができるようになります。

本文でASP.NET Core MVCのコードでは試しませんでしたが、上記3つを設定したうえで最初に掲載したASP.NET Core MVCのコードを実行すると、アクションメソッドで引数がちゃんとモデルバインディングされて値が入ってきていることが確認できます。

duo
総合 ITベンチャーです。バーチャルライブ配信アプリ「IRIAM」、「ミライアカリ」をはじめとするバーチャルタレント事業、アミューズメント事業を展開しています。
https://zizai.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした