初めに
この記事は、NEXTSCAPE クラウドインテグレーション事業本部 Advent Calendar 2018 の
12月22日分の記事となります。
そもそもモデルバインドとは
おおざっぱに言ってしまうとHTTPリクエストのデータから
アクションメソッドを呼び出すときの引数オブジェクトにマッピングしてくれる機能です。
例えば
public class LocalGovCodeController : ControllerBase
{
public ActionResult<string> Get(string code) => Ok(code);
}
上記のようなコントローラのアクションメソッドがあって、
http://foo.com/api/LocalGovCode?code=12345
上記のようなURLでアクセスしたときに 12345
が code
に代入されるのはこの機能のおかげです。
このモデルバインディングの機能はカスタマイズがすることができて、
例えば
- Actionメソッドのパラメータにデフォルトではバインド不可能なアプリケーション独自の型をバインドするようにできる。
- URLのパラメータに半角/全角が混在していてもどちらかに統一してアクションメソッドの引数に渡すことができる。
-
1,234,567
のような値でもint型の引数として受け取ることができる。
以上のようなことができます。
今回はアプリケーション独自の型にバインドできるようにしてみましょう。
例として、とある、5-6桁の文字列からなるコードを表すクラスを利用します。
クラス定義としてはこんな感じでしょうか。
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 メソッドはこんな感じに実装したいとします。
public ActionResult<string> Find(LocalCode code)
{
if(!ModelState.IsValid)
{
return BadRequest(ModelState);
}
return Ok(code);
}
このまま実行すると…
バインダがないとエラーになってしまいます。
ではそのバインダーを作成しましょう。
ただし、モデルバインダ本体だけでなくモデルバインダを提供するファクトリが必要になります。
まずはモデルバインダーの本体から。
IModelBinder
インターフェースを実装するだけです。
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
インターフェースを実装するだけです。
実装も至って単純で先に作ったモデルバインダのインスタンスを返すように設定します。
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属性を付与するだけです。
[ModelBinder(BinderType = typeof(LocalCodeBinder))]
public class LocalCode
{
// 省略
}
ブラウザでもちゃんとデータが返却されていることが確認できます。
今回はアプリケーション独自の型にバインドできるように対応しましたが、
この対応をすることによってアクションメソッドもすっきりしたかと思います。
この記事を読んだ機会にぜひ使ってみてはいかがでしょうか。