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
source to share
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.
source to share