ASP.NET Core MVCでRequestとResponseのBodyをログ出力する
結論
以下の3つ実装すれば取得できる。
① Streamの読み取りを何回でも可能にするためにカスタムMiddlewareを実装する
RequestEnableBuffering.cs
public class RequestEnableBufferingMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
context.Request.EnableBuffering();
await next(context);
}
}
② ログを出力するためにActionFilterを実装する
LoggerFIlter.cs
public class LoggerFilter : IAsyncActionFilter
{
async Task IAsyncActionFilter.OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
context.HttpContext.Request.Body.Position = 0;
using var requestReader = new StreamReader(context.HttpContext.Request.Body);
var requestText = await requestReader.ReadToEndAsync();
Console.WriteLine(JsonSerializer.Serialize(requestText));
context.HttpContext.Request.Body.Position = 0;
var executedContext = await next();
if (executedContext.Result is ObjectResult objectResult)
{
Console.WriteLine(JsonSerializer.Serialize(objectResult.Value));
}
}
}
③ Global FilterとMiddlewareを使用するように設定する
builder.Services.AddControllers(
option =>
{
// Global Filters
option.Filters.Add<LoggerFilter>();
});
builder.Services.AddSingleton<RequestEnableBufferingMiddleware>();
// --- この間にその他のServiceのDI ---
// --- この間にその他のServiceのDI ---
var app = builder.Build();
// --- この間にその他のMiddlewareのUse ---
// --- この間にその他のMiddlewareのUse ---
// MapControllersの前にMiddlewareを追加
app.UseMiddleware<RequestEnableBufferingMiddleware>();
app.MapControllers();
app.Run(url);
解説
Responseに関してはResultをキャストするだけで取得できるので、Requestの取得方法について解説する。
ネットで検索すると以下の関数をLoggerFilterの中で呼び出すコードが出てくる。
context.HttpContext.Request.EnableBuffering();
HttpRequestのBodyはStreamになっておりデフォルトでは一回しかReadできない。一方で、その一回はControllerがBodyの情報を取得するために使用する。なので、ログ用にBodyの中身を取得するには、Streamを複数回Readできるようにする必要がある。この複数回Readできるようにする関数がEnableBuffering()である。EnableBuffering()を実行することでReadした後にStreamが指すポインタを以下のコードで戻せるようになる。
context.HttpContext.Request.Body.Position = 0;
では上の二行をLoggerFilter内に実装すれば良いかというと実はそれでは上手くいかない。公式ドキュメントにもあるようにActionFilterに到達するタイミングでは既にModelBindingが完了しているため、ActionFilter内で上記設定を行なっても手遅れなのだ。したがって、ModelBindingより前にEnableBuffering()を実行する必要がある。そこで、Middlewareを追加しEnableBuffering()を行うというのがこの記事の本題だ。
Middlewareの処理順は自分がProgram.csやStartup.csでUseMiddleware<T>()を呼び出した順序できまる(厳密にいうとUse()関数です)。この辺は、公式のドキュメントを読むとわかる。今回はActionFilterの前に呼ばれさえすれば良いので、MapControllersの前に呼ぶのが適切だろう。なので、RequestEnableBufferingMiddleware.csを実装しMapControllersの前でUseMiddleware()を行う。
以上それらを合わせると最終的に結論で書いたようなコードとなる。最後に、今回はBodyをReadする方法をメインで解説しているが本番のコードでログを実装する場合は以下も検討した方が良い。
- 構造化ロギング
- ILoggerの使用
- ログ処理のエラーの握りつぶし
おまけ
構造化ロギングを行うときに呼び出されたRequestに紐づくControllerの引数の型情報が必要になることがある。その場合以下のコードで取得することができる。
async Task IAsyncActionFilter.OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var controller = context.Controller as ControllerBase;
var parameters = controller.ControllerContext.ActionDescriptor.Parameters;
}