I've just implemented INotifyDataErrorInfo using JeremySkinner's FluentValidation. However I have some difficulties with validation of complex properties.
For example, I would like to validate Nationality property:
RuleFor(vm => vm.Nationality.SelectedItem.Value)
.NotEmpty()
.Length(0, 255);
However, this great looking peace of code has two major problems:
1) it throws null reference exception when SelectedItem is null.
it would be great if I could write something like this:
CustomizedRuleFor(vm => vm.Nationality.SelectedItem.Value)
.NotEmpty(); //add some stuff here
2) full property path in error message, e.g: "The specified condition was not met for 'Nationality. Selected Item. Value'
". I only need 'Nationality'
in error message.
I know I can override error message using WithMessage extension method, but don't want to do it for every validation rule.
Do you have any suggestions? Thanks
Problem 1.
You can solve getting NullReferenceException
problem by two ways, which depends on necessarity of client validation support and availablity to change model class:
Modify your model's default constructor to create SelectedItem
with null value:
public class Nationality
{
public Nationality()
{
// use proper class instead of SelectableItem
SelectedItem = new SelectableItem { Value = null };
}
}
Or you can use conditional validation instead, if SelectedItem should be null in different cases and it's normal situation for you:
RuleFor(vm => vm.Nationality.SelectedItem.Value)
.When(vm => vm.Nationality.SelectedItem != null)
.NotEmpty()
.Length(0, 255);
In this case validator will validate only when condition is true, but conditional validation doesn't support client-side validation (if you want to integrate with ASP.NET MVC).
Problem 2.
To save default error message format, add WithName
method to rule builder 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: GENERIC SOLUTION
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 usage
RuleFor(vm => vm.Nationality.SelectedItem.Value)
.NotEmpty()
.Length(0, 255)
.ApplyChainValidation(vm => vm.Nationality.SelectedItem.Value);
There is no possibility to escape redundant expression duplication, because When()
method, which used inside extension method, works for previously defined rules only.
Note: solution work for chains with reference types only.