モデルバインディング機能を拡張してつかう。(Custom Model Binding)

初めに
===
この記事は、[NEXTSCAPE クラウドインテグレーション事業本部 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/ns-cloud) の
12月22日分の記事となります。


そもそもモデルバインドとは
===

おおざっぱに言ってしまうとHTTPリクエストのデータから
アクションメソッドを呼び出すときの引数オブジェクトにマッピングしてくれる機能です。

例えば 

```csharp
    public class LocalGovCodeController : ControllerBase
    {
        public ActionResult<string> Get(string code) => Ok(code);
    }
```

上記のようなコントローラのアクションメソッドがあって、

```text
   http://foo.com/api/LocalGovCode?code=12345
```

上記のようなURLでアクセスしたときに `12345` が `code` に代入されるのはこの機能のおかげです。


このモデルバインディングの機能はカスタマイズがすることができて、
例えば

* Actionメソッドのパラメータにデフォルトではバインド不可能なアプリケーション独自の型をバインドするようにできる。
* URLのパラメータに半角/全角が混在していてもどちらかに統一してアクションメソッドの引数に渡すことができる。
* `1,234,567` のような値でもint型の引数として受け取ることができる。

以上のようなことができます。
今回はアプリケーション独自の型にバインドできるようにしてみましょう。

例として、とある、5-6桁の文字列からなるコードを表すクラスを利用します。
クラス定義としてはこんな感じでしょうか。

```csharp
    public class LocalCode
    {

        string _code = string.Empty;
        private LocalCode(string source)
        {
            _code = source;
        }

        public static bool TryParse(string source, out LocalGovCode result)
        {
            bool ret = false;
            result = null;

            if(string.IsNullOrWhiteSpace(source))
            {
                // 引数指定なし
                return ret;
            }

            if(source.Length < 5 || 6 < source.Length)
            {
                // コード長不正
                return ret;
            }

            result = new LocalGovCode(source);

            return true;
        }

        public override string ToString() => _code;

    }
```

Action メソッドはこんな感じに実装したいとします。

```csharp
        public ActionResult<string> Find(LocalCode code)
        {
            if(!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            return Ok(code);
        }
```

このまま実行すると…
![Binderがないエラー.png](https://qiita-image-store.s3.amazonaws.com/0/200114/004c158d-f6d9-85fb-3f9e-30aa1cf084c4.png)
バインダがないとエラーになってしまいます。

ではそのバインダーを作成しましょう。
ただし、モデルバインダ本体だけでなくモデルバインダを提供するファクトリが必要になります。

まずはモデルバインダーの本体から。
`IModelBinder` インターフェースを実装するだけです。

```csharp
    public class LocalCodeBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }

            var modelName = bindingContext.ModelName;
            
            var valueProviderResult =
                bindingContext.ValueProvider.GetValue(modelName);

            if (valueProviderResult == ValueProviderResult.None)
            {
                return Task.CompletedTask;
            }

            bindingContext.ModelState.SetModelValue(modelName,
                valueProviderResult);

            var value = valueProviderResult.FirstValue;

            if (string.IsNullOrEmpty(value))
            {
                return Task.CompletedTask;
            }

            if (!LocalCode.TryParse(value, out LocalCode result))
            {
                // 入力チェックエラーがある場合
                bindingContext.ModelState.TryAddModelError(
                                        modelName,
                                        "コードの指定が不正です。");
                return Task.CompletedTask;
            }

            bindingContext.Result = ModelBindingResult.Success(result);
            return Task.CompletedTask;
        }
    }
```

大きな流れとしては、

* リクエストデータの取得
* 入力チェック
* 独自のクラスのインスタンスの生成

以上を行えばモデルバインダの実装が完了です。
モデルバインダのファクトリは `IModelBinderProvider`インターフェースを実装するだけです。
実装も至って単純で先に作ったモデルバインダのインスタンスを返すように設定します。

```csharp
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (context.Metadata.ModelType == typeof(LocalCode))
            {
                return new BinderTypeModelBinder(typeof(LocalCodeBinder));
            }

            return null;
        }
```

それでは、あともう一息。
`LocalCode`型の引数が指定された場合は今回つくったバインダを利用するように設定しましょう。
`LocalCode`クラスにModelBinder属性を付与するだけです。

```csharp
    [ModelBinder(BinderType = typeof(LocalCodeBinder))]
    public class LocalCode
    {
        // 省略
    }
```

デバッグ実行すれば…
![できました.png](https://qiita-image-store.s3.amazonaws.com/0/200114/ee78f967-40a4-43e4-8bdd-c33ad235d607.png)

ブラウザでもちゃんとデータが返却されていることが確認できます。

今回はアプリケーション独自の型にバインドできるように対応しましたが、
この対応をすることによってアクションメソッドもすっきりしたかと思います。

この記事を読んだ機会にぜひ使ってみてはいかがでしょうか。