FluentValidation 链属性验证问题

FluentValidation Chain Properties Validation Issue

我刚刚使用 JeremySkinner's FluentValidation 实现了 INotifyDataErrorInfo。但是我在验证复杂属性时遇到了一些困难。

例如,我想验证国籍 属性:

RuleFor(vm => vm.Nationality.SelectedItem.Value)
  .NotEmpty()
  .Length(0, 255);

然而,这种看起来很平静的代码有两个主要问题:

1) SelectedItem为空时抛出空引用异常。

要是能写成这样就好了:

CustomizedRuleFor(vm => vm.Nationality.SelectedItem.Value)
   .NotEmpty(); //add some stuff here

2) 错误消息中的完整 属性 路径,例如:"The specified condition was not met for 'Nationality. Selected Item. Value'"。我只需要 'Nationality' 在错误消息中。

我知道我可以使用 WithMessage 扩展方法覆盖错误消息,但不想对每个验证规则都这样做。

你有什么建议吗?谢谢

问题一

您可以通过两种方式解决获取 NullReferenceException 的问题,这取决于客户端验证支持的必要性和更改模型的可用性 class:

修改模型的默认构造函数以创建具有空值的 SelectedItem

public class Nationality
{
    public Nationality()
    {
        // use proper class instead of SelectableItem 
        SelectedItem = new SelectableItem { Value = null };
    }
}

您可以改用条件验证,如果 SelectedItem 在不同情况下应该为空并且这对您来说是正常情况:

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

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