我刚刚使用JeremySkinner's FluentValidation实现了INotifyDataErrorInfo。但是我在验证复杂属性方面遇到了一些困难。
例如,我想验证国籍财产:
RuleFor(vm => vm.Nationality.SelectedItem.Value)
.NotEmpty()
.Length(0, 255);
然而,这种看起来很棒的代码和平有两个主要问题:
1)当SelectedItem为null时,它抛出空引用异常。
如果我能写出这样的话会很棒:CustomizedRuleFor(vm => vm.Nationality.SelectedItem.Value)
.NotEmpty(); //add some stuff here
2)错误消息中的完整属性路径,例如:“'Nationality. Selected Item. Value'
”未满足指定的条件。我在错误消息中只需要'Nationality'
。
我知道我可以使用WithMessage扩展方法覆盖错误消息,但不想为每个验证规则执行此操作。
你有什么建议吗?感谢
答案 0 :(得分:3)
问题1。
您可以通过两种方式解决获取NullReferenceException
问题,这取决于客户端验证支持的必要性和更改模型类的可用性:
修改模型的默认构造函数以创建具有空值的SelectedItem
:
public class Nationality
{
public Nationality()
{
// use proper class instead of SelectableItem
SelectedItem = new SelectableItem { Value = null };
}
}
或您可以使用条件验证,如果SelectedItem在不同情况下应该为null,并且这是正常情况:
RuleFor(vm => vm.Nationality.SelectedItem.Value)
.When(vm => vm.Nationality.SelectedItem != null)
.NotEmpty()
.Length(0, 255);
在这种情况下,验证器仅在条件为真时才会验证,但条件验证不支持客户端验证(如果要与ASP.NET MVC集成)。
问题2。
要保存默认错误消息格式,请将WithName
方法添加到规则构建器方法链:
RuleFor(vm => vm.Nationality.SelectedItem.Value)
.WithName("Nationality") // replace "Nationality.SelectedItem.Value" string with "Nationality" in error messages for both rules
.NotEmpty()
.Length(0, 255);
更新:通用解决方案
规则构建器的扩展方法
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using FluentValidation;
using FluentValidation.Attributes;
using FluentValidation.Internal;
public static class FluentValidationExtensions
{
public static IRuleBuilderOptions<TModel, TProperty> ApplyChainValidation<TModel, TProperty>(this IRuleBuilderOptions<TModel, TProperty> builder, Expression<Func<TModel, TProperty>> expr)
{
// with name string
var firstMember = PropertyChain.FromExpression(expr).ToString().Split('.')[0]; // PropertyChain is internal FluentValidation class
// create stack to collect model properties from property chain since parents to childs to check for null in appropriate order
var reversedExpressions = new Stack<Expression>();
var getMemberExp = new Func<Expression, MemberExpression>(toUnwrap =>
{
if (toUnwrap is UnaryExpression)
{
return ((UnaryExpression)toUnwrap).Operand as MemberExpression;
}
return toUnwrap as MemberExpression;
}); // lambda from PropertyChain implementation
var memberExp = getMemberExp(expr.Body);
var firstSkipped = false;
// check only parents of property to validate
while (memberExp != null)
{
if (firstSkipped)
{
reversedExpressions.Push(memberExp); // don't check target property for null
}
firstSkipped = true;
memberExp = getMemberExp(memberExp.Expression);
}
// build expression that check parent properties for null
var currentExpr = reversedExpressions.Pop();
var whenExpr = Expression.NotEqual(currentExpr, Expression.Constant(null));
while (reversedExpressions.Count > 0)
{
whenExpr = Expression.AndAlso(whenExpr, Expression.NotEqual(currentExpr, Expression.Constant(null)));
currentExpr = reversedExpressions.Pop();
}
var parameter = expr.Parameters.First();
var lambda = Expression.Lambda<Func<TModel, bool>>(whenExpr, parameter); // use parameter of source expression
var compiled = lambda.Compile();
return builder
.WithName(firstMember)
.When(model => compiled.Invoke(model));
}
}
使用
RuleFor(vm => vm.Nationality.SelectedItem.Value)
.NotEmpty()
.Length(0, 255)
.ApplyChainValidation(vm => vm.Nationality.SelectedItem.Value);
没有可能逃避冗余表达式重复,因为使用内部扩展方法的When()
方法仅适用于先前定义的规则。
注意:解决方案适用于仅具有引用类型的链。