by WebSurfer
7. July 2022 16:30
ASP.NET Core Web アプリの応答ボディの文字列を、サーバー側でのログなどの目的で、取得する方法を書きます。先の記事「サーバー側で応答コンテンツの取得」の ASP.NET Core 版です。

.NET Framework 版 ASP.NET で利用した HttpModule、HttpResponse.Filter プロパティ、HttpResponse.OutputStream プロパティは ASP.NET Core ではサポートされていません。
代わりにカスタム Middleware と HttpResponse.Body プロパティを使って同様なことを行います。
ただし、HttpResponse.Body プロパティで取得できる Stream は CanRead, CanSeek が false になっており読むことができないという問題があります。
その解決に、先の記事「HttpRequest.Body から読み取る方法 (CORE)」で書いた EnableBuffering メソッドが使えるかと思ったのですが、応答側 HttpResponse ではサポートされていませんでした。
やむを得ず、Middleware で next.Invoke の前に応答ボディ用の Stream を MemoryStream に差し替えて、next.Invoke の後で MemoryStream に書き込まれた応答ボディを取得するという手段を取りました。
そのサンプルコードは以下の通りです。デバッグ実行すると上の画像ように「出力」ウィンドウに応答ボディの文字列が取得できます。
namespace MvcCore6App3.Middleware
{
public class ResponseContentLogMiddleware
{
private readonly RequestDelegate _next;
public ResponseContentLogMiddleware(RequestDelegate next)
{
this._next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// この例では Home/Privacy のみログを取るという前提
if (context.Request.Path.ToString().Contains("/Home/Privacy"))
{
// 応答ボディの Stream を取得して保持
Stream responseStream = context.Response.Body;
// 応答ボディの文字列を取得する
string bodyContent = "";
try
{
using (var memoryStream = new MemoryStream())
{
// 応答ストリームを MemoryStream に差し替えて、それに
// 応答ボディを取得。_next.Invoke の前でないとダメ
context.Response.Body = memoryStream;
await _next.Invoke(context);
// この時点ではすでに MemoryStream には応答ボディは
// 書き込まれている
if (memoryStream.Length > 0L &&
memoryStream.CanRead &&
memoryStream.CanSeek)
{
memoryStream.Position = 0L;
var reader = new StreamReader(memoryStream);
bodyContent = await reader.ReadToEndAsync();
// 確認用(デバッグ実行で Visual Studio の「出力」
// ウィンドウに表示される)
System.Diagnostics.Debug.Write(bodyContent);
memoryStream.Position = 0L;
// MemoryStream に取得した応答ボディのバイト列を
// responseStream にコピー
await memoryStream.CopyToAsync(responseStream);
}
}
}
finally
{
// 上のコードで MemoryStream に差し替えた応答ボディの
// Stream を元に戻す
context.Response.Body = responseStream;
}
}
else
{
// 何もしない場合でも以下のコードは必須。これが無いとミドル
// ウェアのチェーンの途中で止まってしまう
await _next.Invoke(context);
}
}
}
}
上の Middleware が動くようにするには Program.cs (.NET 5.0 以前では Startup.cs) での設定が必要です。以下のコードで「追記」とコメントした一行を追加します。
// ・・・前略・・・
app.UseAuthentication();
app.UseAuthorization();
// 追記
app.UseMiddleware<ResponseContentLogMiddleware>();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.Run();
HttpResponse.Body プロパティで取得できる Stream が読めないのは、HttpRequest.Body と同様に、"lightweight and performant as possible" ということを狙ってのことではないかと思います。
なので、上記のようなことをすると性能上の劣化が生じるかもしれません。どうしてもログが必要と言うような場合にとどめておいた方が良さそうな気がします。