FluentValidationを使用してオブジェクトを検証していますが、このオブジェクトにはコレクションメンバーがあるため、RuleForEach
を使用しようとしています。たとえば、Customer
とOrders
があり、顧客の注文でその顧客に許可されている最大値を超える合計値がないことを確認するとします。
this.RuleForEach(customer => customer.Orders)
.Must((customer, orders) => orders.Max(order => order.TotalValue) <= customer.MaxOrderValue)
ここまでは順調ですね。ただし、エラーのコンテキストに関する追加情報も記録する必要があります(たとえば、データファイルのどこでエラーが見つかったかなど)。これをFluentValidationで実現するのは非常に難しいことがわかりました。これまでのところ、私の最善の解決策はWithState
メソッドを使用することです。たとえば、お客様の住所の詳細が正しくない場合は、次のようにします。
this.RuleFor(customer => customer.Address)
.Must(...)
.WithState(customer => GetErrorContext(customer.Address))
(GetErrorContext
は、関連する詳細を抽出するための私の方法です。
今私が抱えている問題は、RuleForEach
を使用する場合、メソッドシグネチャがCustomer
を参照する式を提供すると想定していることであり、検証エラーの原因となった特定のOrder
ではありません。そして、私はどの注文に問題があったかを知る方法がないようです。したがって、適切なコンテキスト情報を保存できません。
言い換えれば、私はこれしかできないのです。
this.RuleForEach(customer => customer.Orders)
.Must((customer, orders) => orders.Max(order => order.TotalValue) <= customer.MaxOrderValue)
.WithState(customer => ...)
...私が本当にこれをやりたかったとき:
this.RuleForEach(customer => customer.Orders)
.Must((customer, orders) => orders.Max(order => order.TotalValue) <= customer.MaxOrderValue)
.WithState(order => ...)
失敗したコレクションアイテムの詳細(またはインデックス)にアクセスする方法は本当にありませんか?
もう1つの見方は、WithState
に同等のWithStateForEach
...を指定することです。
現在、FluentValidationには、検証状態を希望どおりに設定できる機能はありません。 RuleForEach
は、単純なコレクションアイテムの簡単なバリデーターの作成を防ぐように設計されており、その実装はすべての可能なユースケースをカバーしていませんでした。
Order
の個別のバリデータークラスを作成し、SetCollectionValidator
メソッドを使用して適用できます。アクセスするために Customer.MaxOrderValue
in Must
メソッド— Order
に後方参照するプロパティをCustomer
に追加します。
public class CustomerValidator
{
public CustomerValidator()
{
RuleFor(customer => customer.Orders).SetCollectionValidator(new OrderValidator());
}
}
public class OrderValidator
{
public OrderValidator()
{
RuleFor(order => order.TotalValue)
.Must((order, total) => total <= order.Customer.MaxOrderValue)
.WithState(order => GetErrorInfo(order)); // pass order info into state
}
}
RuleForEach
メソッドを引き続き使用する場合は、カスタム状態の代わりにエラーメッセージを使用できます。これは、オーバーロードの1つで親と子の両方のアイテムエンティティオブジェクトにアクセスできるためです。
public class CustomerValidator
{
public CustomerValidator()
{
RuleForEach(customer => customer.Orders)
.Must((customer, order) => order.TotalValue) <= customer.MaxOrderValue)
.WithMessage("order with Id = {0} have error. It's total value exceeds {1}, that is maximum for {2}",
(customer, order) => order.Id,
(customer, order) => customer.MaxOrderValue,
(customer, order) => customer.Name);
}
}
失敗した注文のすべてのインデックス(または識別子)を収集する必要がある場合— Custom
ルールを使用して、次のように収集できます。
public CustomerValidator()
{
Custom((customer, validationContext) =>
{
var isValid = true;
var failedOrders = new List<int>();
for (var i = 0; i < customer.Orders.Count; i++)
{
if (customer.Orders[i].TotalValue > customer.MaxOrderValue)
{
isValid = false;
failedOrders.Add(i);
}
}
if (!isValid){
var errorMessage = string.Format("Error: {0} orders TotalValue exceed maximum TotalValue allowed", string.Join(",", failedOrders));
return new ValidationFailure("", errorMessage) // return indexes of orders through error message
{
CustomState = GetOrdersErrorInfo(failedOrders) // set state object for parent model here
};
}
return null;
});
}
P.S。
それを忘れないでくださいあなたの目標は検証を実装することであり、FluentValidationをどこでも使用することではありません。検証ロジックを個別のメソッドとして実装する場合があります。これは、ASP.NET MVCでViewModel
と連携してModelState
を埋めます。
要件に一致する解決策が見つからない場合は、ライブラリを使用したcrutchful実装よりも手動実装の方が優れています。