FluentValidation链属性验证问题

时间:2015-05-06 08:11:48

标签: c# wpf validation chaining fluentvalidation

我刚刚使用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扩展方法覆盖错误消息,但不想为每个验证规则执行此操作。

你有什么建议吗?感谢

1 个答案:

答案 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()方法仅适用于先前定义的规则

注意:解决方案适用于仅具有引用类型的链。