Validation validation

I just implemented INotifyDataErrorInfo using JeremySkinner FluentValidation . However, I have some difficulty in checking complex properties.

For example, I would like to check the citizenship property:

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

      

However, this great world of code has two main problems:

1) it throws a null reference exception when the SelectedItem is invalid.

it would be great if i could write something like this:

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

      

2) full path of the property in the error message, for example: "The specified condition was not met for 'Nationality. Selected Item. Value'

". I only need 'Nationality'

the error message.

I know I can override the error message using the WithMessage extension method, but I don't want to do this for every validation rule.

Do you have any suggestions? Thanks to

+3


source to share


1 answer


Problem 1.

You can solve the problem in NullReferenceException

two ways, depending on the need to support client validation and the availability to change the model class:

Modify the default constructor for the model to create SelectedItem

with a null value:

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

      

Or, instead, you can use a conditional check if the SelectedItem needs to be empty in different cases and it's okay for you:

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

      

In this case, the validator will only check when the condition is true, but the conditional check does not support client side validation (if you want to integrate with ASP.NET MVC).

Problem 2.

To keep the default error message format, add a method WithName

to the rule building method chain:



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);

      

UPDATE: GENERAL DECISION

Extension Method for Rule Builder

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));
    }
}

      

And use

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

      

There is no way to avoid duplication of duplicate expressions because the method When()

that is used inside the extension method only works for previously defined rules.

Note. The solution only works for chains with reference types.

+3


source







All Articles