ASP.NET CoreでValidationAttributeを集約した属性を作る

ASP.NET Core MVC でリクエストの検証をするために 属性を用いる場合の話です。

「検証用属性の集約をしたい場合は [ValidationProviderAttribute](https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.aspnetcore.mvc.dataannotations.validationproviderattribute?view=aspnetcore-3.1) を使えば可能です。」
というお話になります。

## 集約しない場合のコード
例として以下のようなコードを考えます。
- 登録、削除の処理が存在する。
- それぞれ、「得意先コード」という項目があり、属性を用いて検証を行っている。

コントローラークラス

``` cs:Sample01Controller.cs

    [ApiController]
    [Route("api/sample01")]
    public class Sample01Controller : ControllerBase
    {
        /// <summary>
        /// 登録
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        [HttpPost("register")]
        public IActionResult Register([FromBody] RegisterRequest request)
        {
            //具体的な処理は省略
            return Ok();
        }

        /// <summary>
        /// 削除
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        [HttpPost("delete")]
        public IActionResult Delete([FromBody] DeleteRequest request)
        {
            //具体的な処理は省略
            return Ok();
        }
    }
```

登録用のリクエストクラス

``` cs:RegisterRequest.cs
    public class RegisterRequest
    {
        /// <summary>
        /// 得意先コード
        /// </summary>
        [DisplayName("得意先コード")]
        [MaxLength(8, ErrorMessage = "{0}は8文字以内にしてください")]
        [RegularExpression("^[a-zA-Z]*$", ErrorMessage = "{0}は半角英字にしてください。")]
        public string TokuiCode { get; set; }

        //その他の項目は省略...
    }
```

削除用のリクエストクラス

``` cs:DeleteRequest.cs

    public class DeleteRequest
    {
        /// <summary>
        /// 得意先コード
        /// </summary>
        [DisplayName("得意先コード")]
        [MaxLength(8, ErrorMessage = "{0}は8文字以内にしてください")]
        [RegularExpression("^[a-zA-Z]*$", ErrorMessage = "{0}は半角英字にしてください。")]
        public string TokuiCode { get; set; }

        //その他の項目は省略...
    }
```

この場合 `RegisterRequest`, `DeleteRequest` にそれぞれ`[MaxLength]`, `[RegularExpression]` が定義されているため、以下のような問題が発生することが考えられます。

- 別の処理でも得意先コードが必要になり、繰り返して作っているうちに人為的ミスにより誤った長さ、正規表現が記入されてしまった。
- 開発途中で長さが変わったり、許容する文字のパターンが増えたため一括で修正しなければならなくなった。

上記のような問題に前もって対応しておくため 「得意先コード」を表す属性を作ります。


## 集約後のコード

コントローラークラスは同じなので省略

得意先コードを表す属性

```cs:TokuiCodeAttribute.cs
    /// <summary>
    /// 得意先コードのチェックを集約した属性
    /// </summary>
    public class TokuiCodeAttribute : ValidationProviderAttribute
    {
        public override IEnumerable<ValidationAttribute> GetValidationAttributes()
        {
            return new ValidationAttribute[] {
                new MaxLengthAttribute(8){ ErrorMessage = "{0}は8文字以内にしてください"},
                new RegularExpressionAttribute("^[a-zA-Z]*$"){ ErrorMessage = "{0}は半角英字にしてください。"},
            };
        }
    }
```

登録用のリクエストクラス

```cs:RegisterRequest.cs
    public class RegisterRequest
    {
        /// <summary>
        /// 得意先コード
        /// </summary>
        [DisplayName("得意先コード")]
        [TokuiCode]//←集約した属性
        public string TokuiCode { get; set; }

        //その他の項目は省略...
    }
```

削除用のリクエストクラス

```cs:DeleteRequest.cs
    public class DeleteRequest
    {
        /// <summary>
        /// 得意先コード
        /// </summary>
        [DisplayName("得意先コード")]
        [TokuiCode]//←集約した属性
        public string TokuiCode { get; set; }

        //その他の項目は省略...
    }
```

`ValidationProviderAttribute` を継承した属性 `TokuiCodeAttribute` を用意しておくことで、こちらに定義を集約することが可能です。

`ValidationProviderAttribute`の説明や [DataAnnotationsMetadataProvider.cs](https://github.com/dotnet/aspnetcore/blob/v3.1.3/src/Mvc/Mvc.DataAnnotations/src/DataAnnotationsMetadataProvider.cs#L340) を見る限り、上記のように使うものだと思います。