6
4

More than 3 years have passed since last update.

FromBody のモデル検証が .NET Core と .NET Framework で違う

Posted at

.NET Core の場合はデフォルトでモデルの検証処理が入っています。 .NET Framework の場合は入っていません。 .NET Framework から .NET Core に移行するときなどに注意する必要があります。

.NET 環境

どちらも Web API を使用します。

  • .NET Core 3.1
  • .NET Framework 4.7.2

サンプルコード

それぞれで同じものを使えるモデルクラスと、今回の比較対象の Web API Post Method のサンプルコードです。

// Request Body に使用するモデルクラス
public class Model
{
    [Required]
    public string a { get; set; }
}
// .NET Core
// Request されてきた値を JSON 形式で返します。
[HttpPost]
public string Post([FromBody]Model value)
{
    return JsonSerializer.Serialize(value);
}
// .NET Framework
// こちらも同じように Request されてきた値を JSON 形式で返します。
public string Post([FromBody]Model value)
{
    return JsonConvert.SerializeObject(value);
}

動作確認

いくつか検証エラーになりそうなパターンで Shell から Request してみると違いがわかります。

Body に何も設定しない場合

.NET Core の場合はステータスコード 400 と RFC 7807 仕様に準拠したエラーの内容が返ってきます。 .NET Framework の場合はステータスコード 200 と "null" が返ってきます。

# .NET Core

$ curl --location --request POST 'https://localhost:44370/api/home' --header 'Content-Type: application/json' --data-raw '' -k
# {
#   "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
#   "title": "One or more validation errors occurred.",
#   "status": 400,
#   "traceId": "|cebcea05-40c27a10f8cc8ff5.",
#   "errors": {
#     "": [
#       "A non-empty request body is required."
#     ]
#   }
# }
# .NET Framework

$ curl --location --request POST 'https://localhost:44304/api/home' --header 'Content-Type: application/json' --data-raw '' -k
# "null"

Body に {} を設定した場合

何も設定しない場合と同じステータスコードが返ってきます。エラーの内容は変わっていて、モデルクラスの Required エラーになっています。

# .NET Core

$ curl --location --request POST 'https://localhost:44370/api/home' --header 'Content-Type: application/json' --data-raw '{}' -k
# {
#   "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
#   "title": "One or more validation errors occurred.",
#   "status": 400,
#   "traceId": "|cebcea05-40c27a10f8cc8ff5.",
#   "errors": {
#       "a": [
#           "The a field is required."
#       ]
#   }
# }
# .NET Framework

$ curl --location --request POST 'https://localhost:44304/api/home' --header 'Content-Type: application/json' --data-raw '{}' -k
# {\"a\":null}

Body の a に null を設定した場合

先程試した {} を設定した場合と同じステータスコードとエラー内容が返ってきます。

# .NET Core

$ curl --location --request POST 'https://localhost:44370/api/home' --header 'Content-Type: application/json' --data-raw '{"a": null}' -k
# {
#   "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
#   "title": "One or more validation errors occurred.",
#   "status": 400,
#   "traceId": "|cebcea05-40c27a10f8cc8ff5.",
#   "errors": {
#       "a": [
#           "The a field is required."
#       ]
#   }
# }
# .NET Framework

$ curl --location --request POST 'https://localhost:44304/api/home' --header 'Content-Type: application/json' --data-raw '{"a": null}' -k
# {\"a\":null}

まとめ。なんで動作が違うのか

.NET Core には ApiController に対して ModelStateInvalidFilter がデフォルト動作するようになっています。
これによって .NET Framework では自分で用意する必要があった ModelState.IsValid と null のチェックが実装不要になっています。
僕たち開発者にとっては .NET Core のほうが検証処理をデフォルト実装していて便利だと思います。恐らくそういう意見が多くて .NET Core に実装されたのだと考えています。

お互いの動作を合わせるようなコードも載せておきます。細かい複雑な部分など完全な一致ではないので、参考程度にしてください。

.NET Core のデフォルト検証を外したい場合のコード

Startup.cs で options.SuppressModelStateInvalidFilter = true を設定すれば検証しなくなります。

// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()
        .ConfigureApiBehaviorOptions(options =>
        {
            options.SuppressModelStateInvalidFilter = true;
        });
}

.NET Framework で FromBody のデフォルト検証を追加したい場合のコード

ActionFilterAttribute を継承した ModelState と null のチェックをするフィルタークラスを自作して、 WebApiConfig のフィルターに追加します。

// FromBody のリクエストパラメーターが null または ModelState.IsValid == false の場合にステータスコード 400 を返すフィルタークラス
// Note: もっと効率のよい実装や、わかりやすい実装があったら教えてください。
public class ModelStateInvalidFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var fromBodyParameterNames = actionContext.ActionDescriptor.GetParameters()
            .Where(p => p.ParameterBinderAttribute != null && p.ParameterBinderAttribute.Match(new FromBodyAttribute()))
            .Select(descriptor => descriptor.ParameterName);

        foreach (var fromBodyParameterName in fromBodyParameterNames)
        {
            foreach (var requiredArgument in actionContext.ActionArguments.Where(pair => pair.Key == fromBodyParameterName))
            {
                if (requiredArgument.Value == null)
                {
                    actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, "A non-empty request body is required.");
                }
            }
        }

        if (!actionContext.ModelState.IsValid)
        {
            actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
        }
    }
}
// WebApiConfig.cs
public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services
        config.Filters.Add(new ModelStateInvalidFilter());

        // 以下、略 ...
    }
}

参考文献

ASP.NET Core を使って Web API を作成する | Microsoft Docs

aspnetcore/ModelStateInvalidFilter.cs at master · dotnet/aspnetcore · GitHub

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4