.NETで自前で入力チェックを行う際に便利なFluentValidationの使い方

# はじめに

ASP.NETにて入力チェックをするには**DataAnnotation**による標準のValidationフレームワークを利用することができますが、標準機能だけでは業務要件的に全てのケースに対応できない場合には結局自前で検証コードを書くことになる為、**検証コードが複数個所に分散する**ことを嫌って標準のValidationを使っていない方もいるかと思います。

また、いろいろな理由でDataAnnotationが使えず、**コントローラや業務ロジック側で入力検証をする**必要があるケースもあるでしょう。

その際に使用できるシンプルな入力検証フレームワークの一つが**FluentValidation**です。
https://docs.fluentvalidation.net/

これを使うと、フレームワークでできる部分はフレームワークに任せ、複雑な検証は自前のコードで追加で記述することが容易に可能です。

また、業務システムにおいて重要な**カスタムエラーメッセージの指定も柔軟な方法で可能**です。
併せて、**項目名をモデルクラスのDisplay属性から取得してエラーメッセージに利用する方法**もまとめておきます。

# 個人的なFluentValidationのお気に入りポイント

- **検証実行条件**を指定できる（Whenメソッド）
- 検証対象のプロパティ名（文字列）ではなくプロパティそのものを**ラムダ式**で渡せる（強い型付け）
- エラーメッセージの項目名をプロパティの**Display属性**から取ってこれる（自前でコーディングが必要 ※この記事で解説しています）
- エラー結果から**発生元プロパティ名**を取得できる
- 検証用の**個別クラス**を書かなくても「コード形式チェック」等の独自の検証処理を**ライブラリ化**できる（Must/Customメソッド、拡張メソッドを使用）
- **エラーメッセージの差し替え**が可能（自前でコーディングが必要 ※この記事で解説しています）
- **メッセージテンプレート**機能が使いやすい

# 大まかな使い方の説明
## 基本
事前にモデルクラスに対する各プロパティの入力検証ルールを定義し、Validate()でそれらを実行します。すると結果がValidationResultで返ってきます。

```C#
Customer customer = new Customer();
CustomerValidator validator = new CustomerValidator();

ValidationResult results = validator.Validate(customer);

if(! results.IsValid) {
  foreach(var failure in results.Errors) {
    Console.WriteLine("Property " + failure.PropertyName + " failed validation. Error was: " + failure.ErrorMessage);
  }
}
```

## 検証結果の取得（ValidationResult）

```C#
ValidationResult results = validator.Validate(customer);
```

上記の結果取得できる``ValidationResult``の主なプロパティは以下の通りです。

|プロパティ|説明|
|:---|:---|
|Errors| List<ValidationFailure> が返ります。|
|IsValid| Errors.Count == 0 の結果が真偽値で返ります。結果がエラーを含むかどうかをシンプルに記述できます。|

``Errors``の中は``ValidationFailure``型のリストです。主なプロパティは以下の通りです。

| プロパティ | 説明 |
|:-----|:-|
|PropertyName | モデルのプロパティ名です。|
|ErrorMessage | エラーメッセージテンプレートに各種埋め込みが行われた結果のメッセージです。|
|AttemptedValue | エラーの元となったプロパティの値です。object型です。|
|CustomState | WithState(x)で指定した値が入ります。これにより、ユーザーデータを管理できます。|
|Severity | WithSeverity(x)で指定したエラーレベルが入ります。規定値はSeverity.Errorです。FluentValidation自身はこの値を意識しません（Errorを返そうがInfoを返そうがIsValidはFalseを返します）。|
|ErrorCode | WithErrorCode(x)で指定した値か、指定していなければビルトイン検証クラス名が入ります(MustやCustomメソッドを使った場合はnullが入ります）。|
|FormattedMessagePlaceholderValues | Dictionary<string, object>です。エラーメッセージに埋め込んだ元の値が入っています。|

## 検証ルールの定義
入力検証ルールの定義は次のように、``AbstractValidator<T>``を継承してモデル毎に専用のクラスを定義します（クラスを定義しないやり方も後述）。

ただ、やることはシンプルで、コンストラクタで``RuleFor``（検証ルール定義）を並べて初期化するだけです。

```C#
using FluentValidation;

public class CustomerValidator : AbstractValidator<Customer> {
  public CustomerValidator() {
    RuleFor(customer => customer.Surname).NotNull().MaximumLength(30);
    
    //Greater than a particular value
    RuleFor(customer => customer.CreditLimit).GreaterThan(0);

    //Greater than another property
    RuleFor(customer => customer.CreditLimit).GreaterThan(customer => customer.MinimumCreditLimit);
  }
}
```

``RuleFor``メソッドにプロパティメンバを返すラムダ式を渡す、というのが非常にスマートです。
最初、これは何をやっているんだろうかと不思議でしたが、これによって、``RuleFor``内部ではラムダ式の右辺から``Reflection.MemberInfo``を取得し、そこからプロパティ名やプロパティ属性値なども取得可能になっています。

## ビルトインの検証メソッド
ビルトインの検証メソッドは次のものが用意されています。
https://docs.fluentvalidation.net/en/latest/built-in-validators.html

- NotNull Validator
- NotEmpty Validator
- NotEqual Validator
- Equal Validator
- Length Validator
- MaxLength Validator
- MinLength Validator
- Less Than Validator
- Less Than Or Equal Validator
- Greater Than Validator
- Greater Than Or Equal Validator
- Predicate Validator
- Regular Expression Validator
- Email Validator
- Credit Card Validator
- Enum Validator
- Enum Name Validator
- Empty Validator
- Null Validator
- ExclusiveBetween Validator
- InclusiveBetween Validator
- ScalePrecision Validator


## カスタム入力検証（Mustメソッド）
ビルトインの検証メソッドでは対応できない部分は、次のように``Must``メソッドを使って記述することもできます。
プロパティ値を受け取って真偽値を返す関数を渡すと、その関数の結果が``True``になる時以外は検証エラーとなります。

```C#
public class PersonValidator : AbstractValidator<Person> {
  public PersonValidator() {
    RuleFor(x => x.Pets)
      .Must(list => list.Count < 10)
      .WithMessage("The list must contain fewer than 10 items");
  }
}
```

## カスタム入力検証のライブラリ化
この検証ルールを再利用可能な形で定義したい場合、次のように ``IRuleBuilder<T,TProperty>``型の拡張メソッドを定義すれば、``NotNull``や``MaximumLength``のようなビルトイン検証メソッドと同様に``RuleFor``の後のメソッドチェーンで呼び出すことができます。
ちょっとこの辺は、拡張メソッドの作り方やジェネリックの書き方に親しんでいないとギョッとしてしまうかもしれませんね。

```C#
public static class MyCustomValidators {
  public static IRuleBuilderOptions<T, IList<TElement>> ListMustContainFewerThan<T, TElement>(this IRuleBuilder<T, IList<TElement>> ruleBuilder, int num) {
    return ruleBuilder.Must(list => list.Count < num).WithMessage("The list contains too many items");
  }
}
```

```C#
RuleFor(x => x.Pets).ListMustContainFewerThan(10);
```

## カスタム入力検証のエラーメッセージ
このように自前で作った検証ルールには自前のエラーメッセージが必要になりますが、FluentValidationはメッセージの整形機能についても公開されており、次のように``{placeholdername}``の形でエラーメッセージをテンプレート化することができます。

```C#
public static IRuleBuilderOptions<T, IList<TElement>> ListMustContainFewerThan<T, TElement>(this IRuleBuilder<T, IList<TElement>> ruleBuilder, int num) {

  return ruleBuilder.Must((rootObject, list, context) => {
    context.MessageFormatter.AppendArgument("MaxElements", num);
    return list.Count < num;
  })
  .WithMessage("{PropertyName} must contain fewer than {MaxElements} items.");
}
```

尚、``{PropertyName}``はFluentValidationの標準のプレースホルダーで、プロパティ名が入ります。これを日本語の項目名に自動的に置き換えるやり方は後程説明します。

## カスタム入力検証のエラーコード
``Must``を使った場合、返される``ValidationFailure``の``ErrorCode``には``null``が入っています。
``ErrorCode``を使ってエラー種別を判定したい場合には、``WithErrorCode``を使って独自のエラーコードを指定することができます。

```C#
public static IRuleBuilderOptions<T, IList<TElement>> ListMustContainFewerThan<T, TElement>(this IRuleBuilder<T, IList<TElement>> ruleBuilder, int num) {

  return ruleBuilder.Must((rootObject, list, context) => {
    context.MessageFormatter.AppendArgument("MaxElements", num);
    return list.Count < num;
  })
  .WithMessage("{PropertyName} must contain fewer than {MaxElements} items.")
  .WithErrorCode("PetsLimitOver");
}
```

``ErrorCode``はメッセージテンプレートを探すキーにもなっている為、以下のように事前に``"PetsLimitOver"``に対するメッセージテンプレートを定義しておけば、``WithMessage``を省略できます。エラーメッセージの管理方法については後程詳しく説明します。

```C#:Startup.cs
var languageManager = ValidatorOptions.Global.LanguageManager as Resources.LanguageManager;
languageManager.AddTranslation( "ja-JP", "PetsLimitOver", "{PropertyName} は少なくとも{MaxElements}件以上でなければなりません" );
```

```C#
public static IRuleBuilderOptions<T, IList<TElement>> ListMustContainFewerThan<T, TElement>(this IRuleBuilder<T, IList<TElement>> ruleBuilder, int num) {

  return ruleBuilder.Must((rootObject, list, context) => {
    context.MessageFormatter.AppendArgument("MaxElements", num);
    return list.Count < num;
  })
  .WithErrorCode("PetsLimitOver");
}
```

## 複雑なカスタム検証（Customメソッド）
``Must``だけでは対応できないような複雑な検証には、``Custom``メソッドで対応可能です。このやり方だと、``AddFailure``を複数回呼んで、複数のエラーを同時に返すことも可能です。

但しこの方法は直接``ResultFailure``オブジェクトを生成する（フレームワークに結果の生成を任せない）やり方なので、メッセージテンプレート機能が使えません。

```C#
public class PersonValidator : AbstractValidator<Person> {
  public PersonValidator() {
   RuleFor(x => x.Pets).Custom((list, context) => {
     if(list.Count > 10) {
       context.AddFailure("The list must contain 10 items or fewer");
     }
   });
  }
}
```

もし``Custom``メソッドでもメッセージテンプレート機能が必要な場合は、組み込みのメッセージ整形機構である``MessageFormatter``を直接利用することができます。

```C#
public class PersonValidator : AbstractValidator<Person> {
  public PersonValidator() {
   RuleFor(x => x.Pets).Custom((list, context) => {
     if(list.Count > 10) {
       context.MessageFormatter.AppendPropertyName(context.DisplayName);
       var failure = new Results.ValidationFailure(
           context.PropertyName, 
           context.MessageFormatter.BuildMessage("{PropertyName} must contain 10 items or fewer")
       );
       failure.ErrorCode = "PetsLimitOver";
       
       context.AddFailure(failure);
     }
   });
  }
}
```

また、もし事前定義したメッセージテンプレートを``ErrorCode``で取得したい場合には、``LanguageManager``を直接参照して次のように書くこともできます。
ただ、どうしても煩雑ですので、基本的には``Must``メソッドと``WithErrorCode``を使って検証コードを書いた方が無難でしょう。

```C#
public class PersonValidator : AbstractValidator<Person> {
  public PersonValidator() {
   RuleFor(x => x.Pets).Custom((list, context) => {
     if(list.Count > 10) {
       string errorCode = "PetsLimitOver";
       string messageTemplate = ValidatorOptions.Global.LanguageManager.GetString(errorCode);
       context.MessageFormatter.AppendPropertyName(context.DisplayName);
       var failure = new Results.ValidationFailure(
           context.PropertyName, 
           context.MessageFormatter.BuildMessage(messageTemplate)
       );
       failure.ErrorCode = errorCode;
       
       context.AddFailure(failure);
     }
   });
  }
}
```

さらに再利用可能な検証コードを書くために、``PropertyValidator<T, TProperty>``クラスを継承したプロパティバリデータを書く事もできますが、ここまでくると少し高度になってくるので、通常は不要であると書かれています（この記事でも紹介しません）。

## クラス定義不要なInlineValidator

作者的にはあまり推奨していないようですが、検証用のクラスを``AbstractValidator<T>``を継承して別途作らずとも、``InlineValidator``クラスを用いて以下のような使い捨ての検証コードを書いて実行することもできます。個人的にはとても便利だと思います。

```C#
Customer customer = new Customer();
var v = new InlineValidator<Customer>();

v.RuleFor(x => x.Name).NotNull();
v.RuleFor(x => x.Address).MaximumLength(100);

ValidationResult results = v.Validate(customer);

```

ちなみにこれをこう書くこともできます（作者推奨の書き方のようです）。

```C#
Customer customer = new Customer();
var validator = new InlineValidator<Customer> {
    v => v.RuleFor(x => x.Name).NotNull()
};

ValidationResult results = validator.Validate(customer);

```

最初「えっ？ C#にこんな初期化構文あったっけ？」と驚いたのですが、よく見るとこれはC#のコレクション初期化子構文です。

``InlineValidator``は``IEnumerable``を実装しており、``Add(Func<InlineValidator<T>, IRuleBuilderOptions<T, TProperty>> ruleCreator)``を持っています。``IEnumerable``かつ``Add``を持っているとコレクション初期化子で初期化できるというC#の仕様を利用しているわけです。

``Add``内部で``ruleCreator(this)``を実行しているので、上記のようにインスタンス初期化用のラムダ式や匿名関数をコレクション初期化子を通して渡せるようになっています。

中括弧の中に初期化処理が書かれているように見えるのは、実は単なる匿名関数のリストだったわけです。

ですので、次のようにカンマで複数並べることもできますし、

```C#
var validator = new InlineValidator<Customer> {
    v => v.RuleFor(x => x.Name).NotNull(),
    v => v.RuleFor(x => x.Address).MaximumLength(100)
};
```

次のように１つの匿名関数の中でいろいろやっても大丈夫です。

```C#
var validator = new InlineValidator<Customer> {
    v => {
        v.RuleFor(x => x.Name).NotNull();
        v.RuleFor(x => x.Address).MaximumLength(100);
        return null;
    }
};
```

ただ、なんというかこれは、だいぶマニアックな気がしますね（汗）。C#によほど慣れている人でないと、裏で何が起こるか理解できないのではないでしょうか。

無難に最初のやり方で書くか、変数スコープが気になるのであれば、普通に検証クラスを作った方が良い気がします。


## 独自の検証コード作成サンプル

例えばあなたのプロジェクトに既に入力チェック用のシンプルな社内ライブラリが存在し、以下のような数値チェックのプログラムがあったとします。

```C#
class SyanaiValidation
{
    public static bool IsNumber(string value)
    {
        ...
    }
}
```

これをFluentValidationに組み込むには、次のようにします。

```C#
class SyanaiFluentValidationExtentions
{
    public static IRuleBuilderOptions<T, string> IsNumber<T>(this IRuleBuilder<T, string> ruleBuilder) {
        return ruleBuilder.Must((T rootObject, string propertyValue, ValidationContext<T> context) => {
            return SyanaiValidation.IsNumber(propertyValue);
        }).WithErrorCode("Syanai_IsNumberError");
    }
}
```

``propertyValue``が``TProperty``型のままだと``SayanaiValidation.IsNumber(string)``を呼び出せないので、``TProperty``の型が``string``に置き換えられている点に注目して下さい。これにより、``string``型のプロパティ以外ではこの拡張メソッドを使用できなくなります。

使う際には、以下のようにメソッドチェーンで使用できます。

```C#
RuleFor(x => x.Age).NotNull().IsNumber();
```

``"Syanai_IsNumberError"``というエラーコードに対するエラーメッセージの定義方法は、後程説明していきます。

## 階層構造のあるモデルの検証

例えば次のような階層構造を持つモデルクラスがあったとします。
CustomerがAddressを持っている構造です。

```C#
public class Customer {
  public string Name { get; set; }
  public Address Address { get; set; }
}

public class Address {
  public string Line1 { get; set; }
  public string Line2 { get; set; }
  public string Town { get; set; }
  public string County { get; set; }
  public string Postcode { get; set; }
}
```

このような場合には、先にAddress用の検証クラスAddressValidatorを作っておいて、Customer側の検証ルール作成のところでは``SetValidator``を使って次のように書きます。

```C#
public class CustomerValidator : AbstractValidator<Customer> {
  public CustomerValidator() {
    RuleFor(customer => customer.Name).NotNull();
    RuleFor(customer => customer.Address).SetValidator(new AddressValidator());
  }
}
```

めちゃくちゃわかりやすくてシンプルですね。

## リスト構造のあるモデルの検証

文字列型や整数型などのプリミティブ型のリストについては、``RuleForEach``を使って次のように書きます。

```C#
public class Person {
  public List<string> AddressLines { get; set; } = new List<string>();
}

public class PersonValidator : AbstractValidator<Person> {
  public PersonValidator() {
    RuleForEach(x => x.AddressLines).NotNull();
  }
}
```

とてもわかりやすいですね！

また、クラス型のオブジェクトのリストについては、``SetValidator``と``RuleForEach``を組み合わせて次のように書きます。

```C#
public class Customer {
  public List<Order> Orders { get; set; } = new List<Order>();
}

public class Order {
  public double Total { get; set; }
}
```

```C#
public class OrderValidator : AbstractValidator<Order> {
  public OrderValidator() {
    RuleFor(x => x.Total).GreaterThan(0);
  }
}

public class CustomerValidator : AbstractValidator<Customer> {
  public CustomerValidator() {
    RuleForEach(x => x.Orders).SetValidator(new OrderValidator());
  }
}
```

非常に直感的です。

# FluentValidationのエラーメッセージの管理方法

業務で入力検証フレームワークを使うにあたって最初に考えるのは、エラーメッセージをどうカスタマイズできるかだと思います。

FluentValidationでは、プレースホルダーが指定されたメッセージテンプレートを元にエラーメッセージを生成します。
プレースホルダーをユーザーが作ることも可能で、規定では次のプレースホルダーが有効です。

|プレースホルダー|説明|
|:---|:---|
|{PropertyName}|エラーの原因となったプロパティ名。DisplayNameが指定されていた場合、そちらが優先される。|
|{PropertyValue}|エラーの原因となったプロパティ値。|

ビルトインの検証クラスが生成するエラーメッセージは、以下の流れで生成されます。

1. エラーメッセージテンプレート（例"{PropertyName}は必須です"）を取得する。
2. PropertyName等の埋め込みデータを用意する。
3. エラーメッセージテンプレートに埋め込みデータを適用する。

エラーメッセージテンプレートは、以下の流れで取得されます（``LengthValidator``の場合）。

1. ``RulesFor(xx).NotNull().WithMesage(string)``のような形で外部から``WithMessage``や``WithCode``を使って指定された場合、そちらが優先される。
2. ``LengthValidator.Name = "LengthValidator"`` これが``LengthValidator``のエラーメッセージIDになる。
3. エラーが起こると、``ValidatorOptions.Global.LanguageManager.GetString(Name, culture)`` が呼ばれる。``errorCode``が設定されていた場合、``.GetString(errorCode, culture)`` が呼ばれる。
4. ``LanguageManager``が``FluentValidation.Resources.JapaneseLanguage.GetTranslation(key)``を呼び出す。
5. ``JapaneseLanguage``クラス内に``switch``を使ったリテラルでメッセージが記述されており、``"LengthValidator"``というキーに対する文字列``"'{PropertyName}' は {MinLength} から {MaxLength} 文字の間で入力する必要があります。 {TotalLength} 文字入力されています。"``が返される。但し、もし内部ディクショナリ ``_languages``にキーに対応するメッセージが存在すればそれを優先して返す。

こうして決定されたエラーメッセージテンプレートは、``MessageFormatter``クラスの``BuildMessage(string messageTemplate)``によって、メッセージテンプレートの``{PropertyName}``や``{MinLength}``等のプレースホルダーキーを実際の値に変換されます。この値は``AppendArgument``を用いて各検証クラスが指定したものです。
そして、この``MessageFormatter``クラスは、``ValidatorOptions.Global.MessageFormatterFactory``によって生成されます。

正直だいぶ複雑で、上記を分析するのにひたすらソースコードとドキュメントを追いましたが、要約すると次の通りとなります。

1. ビルトイン検証クラスのデフォルトメッセージテンプレートは国際化対応されているものの、ソースコード直書きである。
2. その場限りでエラーメッセージを変更するには``WithMesage``や``WithErrorCode``を使う方法がある。
3. 特定のビルトイン検証クラスのメッセージをグローバルに差し替えたい場合には、``LanguageManager.AddTransration(culture, key, message)``を使って上書き指定する。
4. 全てをグローバルに差し替えたい場合には、``LanguageManager``をカスタムで作って差し替える。

それぞれ詳細は次の通りとなります。

## WithMessage を使って差し替える

```C#
RuleFor(customer => customer.Surname).NotNull().WithMessage("{PropertyName}は必須です");

```

``WithMessage``メソッドには``messageProvider``関数を渡す事もできます。``messageProvider``関数の引数xには、検証対象のモデルオブジェクトが渡されます。
これを用いて以下のようなローカライズ対応をすることも可能ですが、``NotNull``検証を使うたびに個別の``WithMessage``を指定するのはかなりの手間だと思われますし、間違いも起こりやすいでしょう。
尚、``messageProvider``関数は必要になったタイミングで遅延実行されるようですので、DBなどから読み込む場合には注意が必要です。

```C#
RuleFor(x => x.Surname).NotNull().WithMessage(x => MyLocalizedMessages.SurnameRequired);

```

## WithErrorCodeを使って差し替える

メッセージ本文ではなく、エラーコードを指定して差し替えることができます。エラーコードとは、メッセージテンプレートを取得するためのキーです。ビルトインの検証クラスでは、エラーコードは検証クラス名が設定されています。

``WithErrorCode``を使って独自のエラーコードが指定されていた場合、それがメッセージキーとして優先され、``LanguageManager.GetString``に渡されます。
よって、指定したエラーコードに対するエラーメッセージはあらかじめ``LanguageManager``に登録しておく必要があります。

```C#:Startup.cs
var languageManager = ValidatorOptions.Global.LanguageManager as Resources.LanguageManager;
languageManager.AddTranslation( "ja-JP", "E101", "{PropertyName}は必須です" );
```

```C#:検証ルール
RuleFor(x => x.Surname).NotNull().WithErrorCode("E101");
```

カスタムメッセージを一元管理したいだけならこのやり方が最もシンプルでしょう。
但し、この方法で登録する場合、システムで利用する全てのメッセージを事前に``LanguageManager``に読み込んでおく必要があります。
例えばASP.NET Coreの場合Startup.csに（ASP.NET無印の場合、Global.asaxに）記述しておけば問題ありません。
もしメッセージ量が多すぎるとか、リアルタイムにメッセージDBから読み込みたいというような場合には、この方法は使えません。

## 特定のビルトイン検証クラスのメッセージをグローバルに差し替える

``LanguageManager.GetString``が受け取るメッセージキーは基本的に``WithErrorCode``で渡されるものと同等で、``WithErrorCode``が指定されなかった場合には、単にその検証クラスの名称（例えば``LengthValidator``の場合、``"LengthValidator"``）がキーとして渡されるようです。

よって、``NotNullValidator``のエラーメッセージをグローバルに差し替えるには、次のようにします。

```C#:Startup.cs
var languageManager = ValidatorOptions.Global.LanguageManager as Resources.LanguageManager;
languageManager.AddTranslation( "ja-JP", "NotNullValidator", "{PropertyName}は必須です" );
```

## 全てのビルトイン検証クラスのメッセージをまとめて差し替える

``LanguageManager``のサブクラスを作って差し替えることが可能です。

```C#:CustomLanguageManager.cs
class CustomLanguageManager : FluentValidation.Resources.LanguageManager
{
    public CustomLanguageManager() 
    {
        this.AddTranslation("ja-JP", "NotNullValidator", "{PropertyName}は必須です");
        this.AddTranslation("ja-JP", "MinimumLengthValidator", "{PropertyName}が{MaxLength}を超えています");
    }
}
```

```C#:Startup.cs
ValidatorOptions.Global.LanguageManager = new CustomLanguageManager()
```

公式サイトでは上記のようにコンストラクタでメッセージを一括で設定するやり方が紹介されていますが、この内容ですと、Startup.csで``AddTranslation``するのと大して変わりません。

そこで以下のように``GetString``をオーバーライドする方法を使うと、メッセージテンプレートの取得処理を完全に一元管理＆コントロールできるようになります。例えば「エラーが発生したタイミングでDBからエラーメッセージテンプレートを取得したい」というような事も可能になります。

インタフェースを介することで、DIにも対応可能となります。

```C#:CustomLanguageManager.cs
interface IMesasgeResolver
{
    public string GetString(string key);
}

class DBMessageResolver : IMessageResolver
{
    private MyDbContext _db;
    
    public DBMessageResolver(MyDbContext db)
    {
        _db = db;
    }

    public string GetString(string key)
    {
        return (
            from m 
            in this._db.Messages 
            where m.MessageKey == key 
            select m.Message
        ).FirstOrDefault();
    }
}

class CustomLanguageManager : FluentValidation.Resources.LanguageManager
{
    private IMessageResolver _messageResolver;  // IMessageResolverは自前で定義するインタフェース

    public CustomLanguageManager(IMessageResolver messageResolver)
    {
        _messageResolver = messageResolver;
    }

    public override string GetString(string key, CultureInfo culture = null) 
    {
        string message = this._messageResolver.GetString(key);
        return message ?? base.GetString(key, calture);
    }
}
```

**（追記）
　注意：このやり方で生成したCustomeLanguageManagerは、GlobalなSingletonとなります。よって、ASP.NETのような並列処理前提のアーキテクチャで動作させる際にはマルチスレッド対応が必要になります（この例のMyDbContextはリクエスト毎に生成される事を前提にしていますが、現状、それをValidatorOptions.Global.LanguageManagerにDIする方法を見つけられていません）。
　埋め込みリソースやハードコーディングなリテラル文字列を読み込むLanguageManagerならばマルチスレッドを考慮する必要は通常ほとんどありませんが、この例のようにリアルタイムにDBから読み込むようなケースでは重大な問題になるでしょう。解決策が見つかるまでは、DBからのメッセージリソース読み込みはリアルタイムに行うのではなく、Startup時に一度行うだけにすべきです。**

**（追記2）**
**上記の件について、作者に質問してみました。**
https://github.com/FluentValidation/FluentValidation/issues/1841
回答としては、**「LanguageManagerはビルトイン検証クラス用のメッセージを差し替える為のものであり、グローバルなシングルトンとして使われることを前提としている為、このようなDBからメッセージを取得するような使い方はできない」**というものでした。
そのような使い方をしたければValidatorにコンストラクタインジェクションでリソースを渡して、WithMessageを使って差し替えて下さいという回答だったのですが、ビルトイン検証クラスのメッセージにまで毎回WithMessageを使って個別メッセージを指定していたら大変面倒なので、それらのメッセージについてのみ、事前にDBから読み込む形にしようと思います。

**（追記3）**
思いつきですが、DBMessageResolverに Func\<MyDbContext\>を渡して、その関数でDIコンテナからリクエスト単位のDbContextインスタンスを取得して返すようにすれば、いけそうですね…（悪い顔）。ただ、作者は「そういうのはやめてくれ」と言いそうです。個人的には、確かにリアルタイムにメッセージリソースをDBから取得するのはパフォーマンスが良くないのですが、業務要件上そうせざるをえないパターンもあるのですよね…。

# プロパティ名の日本語化

規定では、``{PropertyName}``はモデルのプロパティ名がそのまま使われます。``customer.Surname``プロパティの``NotNull``エラーならば、

```
Surnameは必須です
```

のようになります。

これを差し替えるには2つの方法があります。

## WithNameを使ってプロパティ名を差し替える

``WithName``メソッドを使うと、その場限りでプロパティ名を差し替えることができます。

```C#
RuleFor(customer => customer.Surname).NotNull().WithMessage("{PropertyName}は必須です").WithName("苗字");
```

```
 苗字は必須です
```

尚、``WithMessage``の引数には遅延実行の為の関数を指定することも可能です。

## DisplayNameResolverを使ってグローバルに差し替える

以下の方法で、プロパティ名→任意の文字列 への変換関数をグローバルに指定することができます。
``resolver``関数の型は``Func<Type, Reflection.MemberInfo, Expressions.LambdaExpression, string>``です。

```C#:Startup.cs
ValidatorOptions.Global.DisplayNameResolver = (modelType, memberInfo, expression) => resolver;
```

この方法を使って、``Display``属性を読み取るやり方をご紹介します。

## Display属性を読み取ってエラーメッセージのプロパティ名表示に使う。
DataAnnotationsの``Display``属性をクラス定義から読み取った結果をプロパティ名と差し替えるには、先ほどの``DisplayNameResolver``を使って次のようにします。
（以前のFluentValidationのバージョンはこれを規定でやってくれたようですが、ASP.NET Coreになって不整合が起き、規定では何もしなくなったようです）

```C#
public class Customer
{
    [Display(Name="苗字")]
    public string Surname { get; set; }
    :
}
```

```C#:Startup.cs
ValidatorOptions.Global.DisplayNameResolver = (modelType, memberInfo, expression) => 
{
    var displayName = memberInfo.GetCustomAttribute<System.ComponentModel.DataAnnotations.DisplayAttribute>()?.GetName();
    return displayName ?? memberInfo.Name;
};
```

エラーメッセージの``{PropertyName}``プレースホルダーには基本的には``PropertyName``が入りますが、``DisplayNameResolver``が指定されていた場合はそちらが使われます。

但し、``ValidationContext``の``PropertyName``には、常に実際のプロパティ名が入っています。``ValidationFailure``の``PropertyName``も同様です。``DisplayName``はエラーメッセージの生成時にのみ、``PropertyName``の代わりに利用されます。

これによって、エラーメッセージの生成を完全にFluentValidationに任せることができるようになります。

#その他

## 入力エラー1件目で残りはスキップする（Cascade modeの設定）

規定では、FluentValidationは一つのエラーが起きた後も残りの検証も全て実施します。

例えば次のような検証ルールにおいて、入力値が空で``NotNull``でエラーを検出した場合でも、続けて``NotEqual("foo")``の検証も行います。
結果、2つのエラーが``ValidationResult.Errors``に返ります。

```C#
RuleFor(x => x.Surname).NotNull().NotEqual("foo");
```

これが不要な場合、以下のように指定すると、1件目のエラーでやめてValidationResultを返します。

```C#
RuleFor(x => x.Surname).Cascade(CascadeMode.Stop).NotNull().NotEqual("foo");
```

もし、システムの全ての検証において同じ設定を行いたい場合には、スタートアップにて以下のグローバル設定を行います。

```C#
ValidatorOptions.Global.CascadeMode = CascadeMode.Stop;
```

## Whenメソッドで「検証を実行する前提条件」を指定

例えばあるコード項目があり、Code Subcode のような2つの項目に入力欄が分かれていたとします。
そしてこれらは2つとも入力されている場合にのみ入力検証を行い、全て空ならば入力エラーとしない、とします。

単純に``NotNull``条件を付けてしまうと、両方空にすることができなくなってしまいます。
そこで、``When``メソッドを使って、この入力検証に対する条件として「もう一つの入力欄が空でなければ」を追加します。

```C#
RuleFor(x => x.Code).NotNull().When(x => ! String.IsNullOrEmpty(x.Subcode));
RuleFor(x => x.Subode).NotNull().When(x => ! String.IsNullOrEmpty(x.Code));
```

尚、``When``の指定は、それ以前に指定した検証に対してのみ影響します。具体的に``When``の範囲を指定する方法もあります。

個別のプロパティにそれぞれ``When``をつけて整合性を考えるのが複雑すぎる場合、複数のルールに対する``When``をまとめて指定することもできます。

```C#
When(x => ! (String.IsNullOrEmpty(x.Code) && String.IsNullOrEmpty(x.Subcode)), () => {
   RuleFor(x => x.Code).NotNull();
   RuleFor(x => x.Subcode).NotNull();
});
```

詳しくはConditionsの項目を参照してください。
https://docs.fluentvalidation.net/en/latest/conditions.html#

FluentValidationには``DependentRules``というメソッドもあり、依存関係にある検証を関連づけることができますが、作者としては「読みにくくなるので``When``と``CascadeMode``を組み合わせて使った方が良い。明らかに``DependentRules``を使った方が見通しがよくなる場合のみ使うと良い」とのことです。
https://docs.fluentvalidation.net/en/latest/conditions.html#dependent-rules


# FluentValidationのあまり好きではないポイント

個人的に、FluentValidationのあまり好きではない点を挙げておきます。

## 頻繁なアップデートに伴う仕様変更

アップデートが今でも頻繁に行われており、この1年間でも約2回のメジャーアップデートが行われています。
それ自体は良いのですが、その際に、後方互換性のない破壊的な変更が毎回行われており、最新版にアップデートしようとするとコードの修正が必要になっています。

## エラーメッセージに依存したValidationResult

設計思想として、「エラー結果はエラーメッセージで返す」というのがベースになっているようで、エラーコードはエラーメッセージを生成する為のキーでしかないような扱いです。

その為、エラー結果を「このエラーの場合は〇〇をして、このエラーの場合は△△をして…」のように処理したい場合には、この記事で解説しているように、エラーコードを都度指定する少し面倒な管理が必要になります。

例えば``Must``メソッドを使って定義した検証ルールは、基本的に``ErrorCode``を何も返しません。それを返すには、別途``WithErrorCode``を指定しなければなりません。

``Custom``メソッドの場合はもう少し面倒で、生成する``ValidationFailure``に``ErrorCode``を別途指定して返す必要があります。

ビルトインクラスの場合は、規定値としてErrorCodeに検証クラス名（NotNullの場合、"NotNullValidator"）が設定されますので、それを元に処理の振分を行うことは可能です。

``Exception``クラスのように、検証結果を表す``ValidationFailure``自体に検証ごとの結果型を持たせ、戻り値の型でエラー種別を判断しつつ、細かいエラー結果を追加で取得できる形になっていたら便利だったのにと思いますが、それはそれで面倒だと感じたかもしれません。

## 使うのに少々高度な知識を要する

少なくとも、ジェネリクスやラムダ式、匿名関数、拡張メソッドについて理解していないと完全には使いこなせません。
``InlineValidator``のコレクション初期化子を使ったルール定義とか、ちょっとマニアックすぎます。

実はこれが一番大きな欠点であるようにも思います。もし、そこまでC#に習熟していない人員しか集められないような場合だと、このフレームワークを採用しづらいようにも思います。

ただ、事前に決められた``NotNull``とか``MaximumLength``とかのメソッドだけを使い、``Must``なんかは基本使わせない、``When``は基本的なケースのみ使い方を教える、とかにすれば、パターン化して運用できそうな気もします。それ以外はFluentValidationを使わずに自前で書くようにすれば、必須チェックやフォーマットチェック、範囲チェックなどの定型チェックはフレームワークに任せて、あとは各自でってできるかもしれませんね。