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

# はじめに

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

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

```csharp: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`になっているのが確認できます。

```csharp: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`にあるため、名前空間のインポートが必要になります。

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

```csharp
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`にしておきますが、引数順として最後に用意されているため、それまでの引数も全て設定する必要があります。

```csharp
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`メソッドを呼び出します。

```csharp
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回読み込んでみる

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

```csharp: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でちゃんと値が取得できているのが確認できます。

# まとめ

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

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

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