ASP.NETのモデル検証

# はじめに
ASP.NET/ASP.NET Coreでは、.NETライブラリの[System.ComponentModel.DataAnnotations](https://docs.microsoft.com/ja-jp/dotnet/api/system.componentmodel.dataannotations?view=netframework-4.6.2)名前空間を用いたのモデル検証機能を提供している。

本記事では、以下について取り扱います。
- モデル検証とは
- System.ComponentModel.DataAnnotationsに定義された検証属性の一部を紹介
- モデル検証結果を確認する方法
- モデル検証をカスタマイズする方法
- モデル検証を単体テストする方法

# モデル検証とは

モデルクラス内に検証ルールを指定することで、アプリケーションのあらゆる場所に検証ルールを適用することができる機能。

参考
[CodeZine ASP.NET 4.5の「モデル検証」を活用する](https://codezine.jp/article/detail/7412)
[Microsoft Doc:Adding Validation to the Model](https://docs.microsoft.com/en-us/aspnet/mvc/overview/older-versions/getting-started-with-aspnet-mvc4/adding-validation-to-the-model)
CodeZineの解説はとてもわかりやすいです。


# 検証属性

System.ComponentModel.DataAnnotations名前空間に定義された検証属性を用いることで、モデル検証ができます。

~~~C#
using System.ComponentModel.DataAnnotations;

namespace WebApiValidation.Models
{
    public class Customer
    {
        [Required]
        [Range(1,100)]
        public int? Id { get; set; }
        [Required]
        public string Surname { get; set; }
        [Required]
        public string Forename { get; set; }
    }
}
~~~

[Required属性](https://docs.microsoft.com/ja-jp/dotnet/api/system.componentmodel.dataannotations.requiredattribute?view=netframework-4.6.2)をつけると、プロパティを空にできなくなります(nullを指定できない。文字列の場合は空白文字もNG)。
[Range属性](https://docs.microsoft.com/ja-jp/dotnet/api/system.componentmodel.dataannotations.rangeattribute?view=netframework-4.6.2)では、範囲に制限を設けることができます。上記例だと、1から100までの間に制限する。

この他にも、Email形式の検証属性、正規表現の検証属性、文字列長の検証属性などが存在する。
詳細は、[System.ComponentModel.DataAnnotations](https://docs.microsoft.com/ja-jp/dotnet/api/system.componentmodel.dataannotations?view=netframework-4.6.2)名前空間を参照して下さい。

# モデル検証結果の判定

モデル検証結果はModelState.IsValidで確認できます。
ASP.NET/ASP.NET Coreでも同様です（下記リンクを参照）。

[Microsoft Doc: ASP.NET WebAPIでのモデル検証](https://docs.microsoft.com/ja-jp/aspnet/web-api/overview/formats-and-model-binding/model-validation-in-aspnet-web-api)
[Microsoft Doc: ASP.NET Core MVC および Razor Pages でのモデルの検証](https://docs.microsoft.com/ja-jp/aspnet/core/mvc/models/validation?view=aspnetcore-3.0)
[C# Corner: Creating Your Own Validation Attribute In MVC And Web API 2.0](https://www.c-sharpcorner.com/article/creating-your-own-validation-attribute-in-web-api-and-mvc/
)

ここでは、ASP.NET Web APIの実装例を以下に示します。

~~~C#
using System.Web.Http;
using WebApiValidation.Models;

namespace WebApiValidation.Controllers
{
    [RoutePrefix("Customer")]
    public class CustomerController : ApiController
    {
        [Route("")]
        [HttpPost]
        public IHttpActionResult Post([FromBody]Customer customer)
        {
            if (ModelState.IsValid)
            {
                return Ok();
            }
            return BadRequest();
        }
    }
}
~~~


# モデル検証のカスタマイズ

##  検証属性のカスタマイズ

[System.ComponentModel.DataAnnotations](https://docs.microsoft.com/ja-jp/dotnet/api/system.componentmodel.dataannotations?view=netframework-4.6.2)名前空間に定義された検証属性で処理できない場合などに、カスタム検証属性を作成します。

実装のポイントは下記の通り。
- ValidationAttributeを継承します。
- bool IsValid(object value)をオーバーライドします。

ここではすでに紹介した[Required属性](https://docs.microsoft.com/ja-jp/dotnet/api/system.componentmodel.dataannotations.requiredattribute?view=netframework-4.6.2)を自作したいと思います。

~~~C#
using System.ComponentModel.DataAnnotations;

namespace WebApiValidation.Models
{
    public class RequiredCustomAttribute : ValidationAttribute
    {
        public override bool IsValid(object value)
        {
            if (value == null)
                return false;
            return !(value is string str) ||  str.Trim().Length != 0;
        }
    }
}
~~~

使用方法は下記の通り。
~~~C#
[RequiredCustom]
public string Surname { get; set; }
~~~

参考
[Microsoft　Doc: How to Customize Data Field Validation in the Data Model Using Custom Attributes](https://docs.microsoft.com/ja-jp/previous-versions/aspnet/cc668224(v=vs.100))

## IValidatableObjectを用いた検証

[IValidatableObject](https://docs.microsoft.com/ja-jp/dotnet/api/system.componentmodel.dataannotations.ivalidatableobject?view=netframework-4.6.2)を用いた場合、クラスレベルで検証するができるようになります。標準搭載の検証属性だと難しい複数のプロパティの値を比較するなどの検証が可能になります。

実装のポイントは下記の通り。
- IValidatableObjectを継承します。
- IValidatableObject.Validate(ValidationContext)を実装します。

ここでは[検証属性](#検証属性)で示したサンプルを、IValidatableObjectで書き直してみます。

~~~C#
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace WebApiValidation.Models
{
    public class CustomerValidateObject: IValidatableObject
    {
        private const string ErrorPrefix = "Invalid";
        public int? Id { get; set; }
        public string Surname { get; set; }
        public string Forename { get; set; }
        
        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            if (Id == null)
            {
                yield return new ValidationResult($"{ErrorPrefix} {nameof(Id)}");
            }
            if (1 > Id || Id  > 100)
            {
                yield return new ValidationResult($"{ErrorPrefix} {nameof(Id)}");
            }
            if (string.IsNullOrEmpty(Surname))
            {
                yield return new ValidationResult($"{ErrorPrefix} {nameof(Surname)}");
            }
            if (string.IsNullOrEmpty(Forename))
            {
                yield return new ValidationResult($"{ErrorPrefix} {nameof(Forename)}");
            }
        }
    }
}
~~~

参考
[C# Corner: Web API Validation](https://www.c-sharpcorner.com/article/learn-about-web-api-validation/)


# モデル検証の単体テスト

[Validator](https://docs.microsoft.com/ja-jp/dotnet/api/system.componentmodel.dataannotations.validator?view=netframework-4.6.2)クラスを使用します。
[IValidatableObjectを用いた検証](#IValidatableObjectを用いた検証)の単体テスト方法を示します。

~~~C#
using System.Collections.Generic;
using WebApiValidation.Models;
using System.ComponentModel.DataAnnotations;
using NUnit.Framework;

namespace UnitTestProject
{
    [TestFixture]
    public class CustomerValidateObjectTest
    {
        [TestCase(null, null,null)]
        [TestCase(-1, null,null)]
        [TestCase(0, null,null)]
        [TestCase(1, null,null)]
        [TestCase(1, "",null)]
        [TestCase(1, null,"")]
        [TestCase(1, "","")]
        [TestCase(101, "","")]
        public void TestCustomerValidateObject_Validate_Failed(int? id , string surName, string foreName)
        {
            var model = new CustomerValidateObject
            {
                Id = id,
                Surname = surName,
                Forename = foreName
            };
            var context = new ValidationContext(model);
            Assert.IsFalse(Validator.TryValidateObject(model, context, new List<ValidationResult>()));
        }

        [TestCase(1, "Yamamoto","Taro")]
        [TestCase(100, "Yamamoto","Taro")]
        public void TestCustomerValidateObject_Validate_Success(int id , string surName, string foreName)
        {
            var model = new CustomerValidateObject
            {
                Id = id,
                Surname = surName,
                Forename = foreName
            };
            var context = new ValidationContext(model);
            Assert.IsTrue(Validator.TryValidateObject(model, context, new List<ValidationResult>()));
        }
    }
}
~~~

<details>
<summary>テストの実行環境</summary>
<div>
.NET Framework4.6.2<br>
NUnit 3.13.2<br>
NUnit3 TestAdapter 3.17.0<br>
System.ComponentModel.Annotations 5.0.0<br>
Microsoft.AspNet.WebApi.Core 5.2.7<br>
</div>
</details>


参考
[C# Corner:Model Class Validation Testing Using Nunit](https://www.c-sharpcorner.com/article/model-class-validation-testing-using-nunit/)
[INTERTECH: How to Unit Test .NET Entity Validation](https://www.intertech.com/unit-test-net-entity-validation/)
[StackOverFlow: How to write Unit Test for IValidatableObject Model](https://stackoverflow.com/questions/24593005/how-to-write-unit-test-for-ivalidatableobject-model/24593113)


# まとめ

System.ComponentModel.DataAnnotationsを用いて、モデル検証を実施する方法（実装・単体テスト）を示した。
今回は触れませんでしたが、FluentValidationという検証ルールを構築する便利なOSSライブラリをASP.NET/ASP.NET COREに組み込む方法もあります。

参考
[FluentValidation 公式ドキュメント](https://docs.fluentvalidation.net/en/9.x/index.html)
[Qiita:.NETで自前で入力チェックを行う際に便利なFluentValidationの使い方](https://qiita.com/jun1s/items/e291fd7a1f3b991eaf48)