ASP.NET Core Razor PagesでIAsyncPageFilterを実装する

## はじめに

ASP.NET Core MVC には、IActionFilterというインターフェースがあり、これを実装することで、要求処理パイプラインのアクション メソッドが呼び出される直前と直後に独自のコードを実行させることができます。
Razor Pagesでも似たようなIAsyncPageFilterインターフェース、 IPageFilterインターフェースが用意されています。

ここでは、非同期版の`IAsyncPageFilter`インターフェースの簡単な使い方を説明します。

なおこの記事で作成したソースコードはGitHuibで公開しています。
https://github.com/gushwell/IAsyncPageFilterSample


## IAsyncPageFilterインターフェース

`IAsyncPageFilter`インターフェースには、以下の2つのメソッドがあります。

```cs
public interface IAsyncPageFilter : IFilterMetadata
{
   Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context);
   Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next);
}
```

`OnPageHandlerSelectionAsync`メソッドは、Pageハンドラー メソッドが選択された後、モデル バインドが発生する前に非同期的に呼び出されます。

`OnPageHandlerExecutionAsync`は、モデル バインドが完了した後、ハンドラー メソッドが呼び出される前に非同期的に呼び出されます。

## IAsyncPageFilterを実装する

ここでは、簡単なアクセスログを出力するコードを書いてみます。

モデルバインディングでエラーになった場合もログを出力するように`OnPageHandlerSelectionAsync`メソッドでログを出力しています。


```cs
using Microsoft.AspNetCore.Mvc.Filters;

namespace WebAppSample;

public class MyAsyncPageFilter : IAsyncPageFilter
{
    private readonly ILogger _logger;
    public MyAsyncPageFilter(ILogger logger)
    {
        _logger = logger;
    }

    public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
    {
        await next.Invoke();
    }

    public Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context)
    {
        var httpContext = context.HttpContext;
        var message = $"HttpType: {httpContext.Request?.Method}|Path :{httpContext.Request?.Path}\tUserAgent: {httpContext.Request?.Headers["User-Agent"].ToString()}\tPage: {context.HandlerInstance?.GetType().Name}\tHandler: {context.HandlerMethod?.MethodInfo?.Name}";
        _logger.LogTrace(message);
        return Task.CompletedTask;
    }

}
```


## MyAsyncPageFilterをグローバルに組み込む

Program.csで、先ほど作成した`MyAsyncPageFilter`を組み込みます。

```cs
using WebAppSample;

var builder = WebApplication.CreateBuilder(args);

var logger = LoggerFactory.Create(config =>
{
    config.AddConsole();
    config.SetMinimumLevel(LogLevel.Trace);
}).CreateLogger("WebAppSample");

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.Filters.Add(new MyAsyncPageFilter(logger));
    });


var app = builder.Build();

... 以下省略 ...

```

`MyAsyncPageFilter`は、`ILogger`のインスタンスをコンストラクタで受け取りたいのですが、残念ながらDIで自動で渡すことができません。

そのため、独自に`LoggerFactory.Create`を使って、Loggerインスタンスを生成しています。

`MyAsyncPageFilter`内では、

```
_logger.LogTrace（...);
```

とTraceログを出力していますので、`LoggerFactory.Create`時に、ログレベルを指定しています。

```
 config.SetMinimumLevel(LogLevel.Trace);
```


## 実行してみる

これで、`MyAsyncPageFilter`がグローバルで組み込まれたので、ページにアクセスがあるたびにログが出力されることになります。

テストをわかりやすくするために、`appsettings.Development.json` を書き換えて、フレームワーク側が出すログレベルを変更します。

```json
{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning"
    }
  }
}
```

それでは実行してみます。
「ASP.NET Core Web アプリ」プロジェクトを新規作成した時にできるサンプルプロジェクトに前述のコードを組み込んだ状態で実行しています。プロジェクトの名前を WebAppSample としています。

コンソールにログが出力されていることを確認できるように、デバッグでは、「IIS Express」ではなく、プロジェクト名の「WebAppSample」を選んでデバッグします。

![2022-11-23 11_33_13-WebAppSample - Microsoft Visual Studio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29415/484abeaf-1410-9374-64e2-c9e3fd0cc7aa.png)

以下、Homeページを開いた後に、Privacyページにアクセスした結果です。IndexページのOnGetメソッド、PrivacyページのOnGetメソッドが呼び出されているのが確認できます。

![2022-11-23 11_35_22-Privacy Policy - WebAppSample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29415/fc2ca1f0-177c-fe99-60f4-cd35e5482011.png)

![2022-11-23 11_34_35-E__repos_WebAppSample_WebAppSample_bin_Debug_net6.0_WebAppSample.exe.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29415/619c354a-9ecb-c9d5-0623-5619545aca60.png)