Quite some time ago I have created the Simple object validator (see also self-answer). The more I used it the more I thougt its API could be better so I have heavily refactored it and would like to you take another look at the new version.
Requirements
I'd like my validator to be:
- intuitive
- easy to use
- extendable
- testable
- helpful by providing precise error messages
- immutable so that predefined rules cannot be manipulated
In order to meet these criteria I have removed a couple of classes and built it on top of System.Collections.Immutable
. Usually, you should not notice that and be able to just use the provided extensions.
How it works
You start with an empty collection of rules for the specified type and use one of the Add
extensions to add validation rules. There are two types of them:
Require
- which means it cannot continue when this fails (e.g. something isnull
)Ensure
- the validator can continue with the next rule
Validation rules are compiled from expressions and use two parameters:
T
- the object being validatedTContext
- optional context with additional data
Expressions are also used for generating error messages that are prettyfied with an expression visitor that replaces ugly closure classes with pretty type names like <param:Person>.FirstName
.
The main extensibility point of this framework are the two properties Require
and Ensure
that return a builder that lets the user chain extensions such as True
, False
, NotNull
etc.
There is no classic validator but an extension (ValidateWith
), for an IImutableList<>
that executes the rules. It returns a tuple with the object being validated and a lookup with results. Its key is bool
where true
returns successul rules and false
failed ones. When the execution should be interrupted because of validation errors, the user can chain the ThrowIfValidationFailed
extension.
With the currently available APIs it's also possible to create shortcuts to reduce the verbosity. See the Simplified
test below. I think it still could be better.
In general, a set of rules would be a static
field. It's supposed to be build once and reused many times as compiling expressions might otherwise become a bottleneck.
Example
These tests show it in action:
public class ValidationTest
{
private static readonly Person Tester = new Person
{
FirstName = "Cookie",
LastName = "Monster",
Address = new Address
{
Street = "Sesame Street"
}
};
[Fact]
public void Can_validate_rules()
{
var rules =
ValidationRuleCollection
.For<Person>()
.Add(x =>
ValidationRule
.Require
.NotNull(x))
.Add(x =>
ValidationRule
.Require
.NotNull(() => x.FirstName))
.Add(x =>
ValidationRule
.Ensure
.True(() => x.FirstName.Length > 3))
.Add(x =>
ValidationRule
.Require
.NotNull(() => x.Address))
.Add(x =>
ValidationRule
.Ensure
.False(() => x.Address.Street.Length > 100));
var (person, results) = Tester.ValidateWith(rules);
Assert.Equal(5, results[true].Count());
Assert.Equal(0, results[false].Count());
Tester.ValidateWith(rules).ThrowIfValidationFailed();
}
[Fact]
public void Can_throw_if_validation_failed()
{
var rules =
ValidationRuleCollection
.For<Person>()
.Add(x =>
ValidationRule
.Require
.NotNull(x))
.Add(x =>
ValidationRule
.Require
.NotNull(() => x.FirstName))
.Add(x =>
ValidationRule
.Ensure
.True(() => x.FirstName.Length > 3));
var (person, results) = default(Person).ValidateWith(rules);
Assert.Equal(0, results[true].Count());
Assert.Equal(1, results[false].Count());
Assert.ThrowsAny<DynamicException>(() => default(Person).ValidateWith(rules).ThrowIfValidationFailed());
}
[Fact]
public void Simplified()
{
var rules =
ValidationRuleCollection
.For<Person>()
.Require((b, x) => b.NotNull(() => x))
.Ensure((b, x) => b.NotNull(() => x.FirstName))
.Ensure((b, x) => b.True(() => x.FirstName.Length > 3));
var (person, results) = default(Person).ValidateWith(rules);
Assert.Equal(0, results[true].Count());
Assert.Equal(1, results[false].Count());
Assert.ThrowsAny<DynamicException>(() => default(Person).ValidateWith(rules).ThrowIfValidationFailed());
}
private class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public Address Address { get; set; }
}
private class Address
{
public string Street { get; set; }
}
}
Code
ValidationRuleCollection
and convenience extensions for working with immutable collections so that I don't have to create my own immutables.
public static class ValidationRuleCollection
{
public static IImmutableList<IValidationRule<T, TContext>> For<T, TContext>() => ImmutableList<IValidationRule<T, TContext>>.Empty;
public static IImmutableList<IValidationRule<T, object>> For<T>() => ImmutableList<IValidationRule<T, object>>.Empty;
}
public static class ValidationRuleCollectionExtensions
{
public static IImmutableList<IValidationRule<T, TContext>> Add<T, TContext>(this IImmutableList<IValidationRule<T, TContext>> rules, Func<T, TContext, ValidationRuleBuilder> builder)
{
return rules.Add(builder(default, default).Build<T, TContext>());
}
public static IImmutableList<IValidationRule<T, object>> Add<T>(this IImmutableList<IValidationRule<T, object>> rules, Func<T, ValidationRuleBuilder> builder)
{
return rules.Add(builder(default).Build<T, object>());
}
public static IImmutableList<IValidationRule<T, object>> Require<T>(this IImmutableList<IValidationRule<T, object>> rules, Func<ValidationRuleBuilder, T, ValidationRuleBuilder> builder)
{
return rules.Add(builder(ValidationRule.Require, default).Build<T, object>());
}
public static IImmutableList<IValidationRule<T, object>> Ensure<T>(this IImmutableList<IValidationRule<T, object>> rules, Func<ValidationRuleBuilder, T, ValidationRuleBuilder> builder)
{
return rules.Add(builder(ValidationRule.Ensure, default).Build<T, object>());
}
public static (T Value, ILookup<bool, IValidationResult<T>> Results) ValidateWith<T, TContext>(this T obj, IImmutableList<IValidationRule<T, TContext>> rules, TContext context)
{
return
(
obj,
rules
.Evaluate(obj, context)
.ToLookup(r => r.Success)
);
}
public static (T Value, ILookup<bool, IValidationResult<T>> Results) ValidateWith<T>(this T obj, IImmutableList<IValidationRule<T, object>> rules)
{
return obj.ValidateWith(rules, default);
}
private static IEnumerable<IValidationResult<T>> Evaluate<T, TContext>(this IImmutableList<IValidationRule<T, TContext>> rules, T obj, TContext context)
{
var result = default(IValidationResult<T>);
foreach (var rule in rules)
{
yield return result = rule.Evaluate(obj, context);
if (!result.Success && rule.Option == ValidationRuleOption.Require) yield break;
}
}
}
ValidationRule
, its callbacks and helpers.
public delegate bool ValidationPredicate<in T, in TContext>(T obj, TContext context);
public delegate string MessageCallback<in T, in TContext>(T obj, TContext context);
public interface IValidationRule<T, in TContext>
{
ValidationRuleOption Option { get; }
IValidationResult<T> Evaluate([CanBeNull] T obj, TContext context);
}
public enum ValidationRuleOption
{
Ensure,
Require
}
internal class ValidationRule<T, TContext> : IValidationRule<T, TContext>
{
private readonly ValidationPredicate<T, TContext> _predicate;
private readonly MessageCallback<T, TContext> _message;
private readonly string _expressionString;
public ValidationRule
(
[NotNull] Expression<ValidationPredicate<T, TContext>> predicate,
[NotNull] Expression<MessageCallback<T, TContext>> message,
[NotNull] ValidationRuleOption option
)
{
if (predicate == null) throw new ArgumentNullException(nameof(predicate));
_predicate = predicate.Compile();
_message = message.Compile();
_expressionString = ValidationParameterPrettifier.Prettify<T>(predicate).ToString();
Option = option;
}
public ValidationRuleOption Option { get; }
public IValidationResult<T> Evaluate(T obj, TContext context)
{
return new ValidationResult<T>(ToString(), _predicate(obj, context), _message(obj, context));
}
public override string ToString() => _expressionString;
public static implicit operator string(ValidationRule<T, TContext> rule) => rule?.ToString();
}
public static class ValidationRule
{
public static ValidationRuleBuilder Ensure => new ValidationRuleBuilder(ValidationRuleOption.Ensure);
public static ValidationRuleBuilder Require => new ValidationRuleBuilder(ValidationRuleOption.Require);
}
ValidtionBuilder
...
public class ValidationRuleBuilder
{
private readonly ValidationRuleOption _option;
private LambdaExpression _predicate;
private LambdaExpression _message;
public ValidationRuleBuilder(ValidationRuleOption option)
{
_option = option;
}
public ValidationRuleBuilder Predicate(LambdaExpression expression)
{
_predicate = expression;
return this;
}
public ValidationRuleBuilder Message(Expression<Func<string>> message)
{
_message = message;
return this;
}
[NotNull]
public IValidationRule<T, TContext> Build<T, TContext>()
{
if (_predicate is null || _message is null) throw new InvalidOperationException("Validation-rule requires you to set rule and message first.");
var parameters = new[]
{
_predicate.Parameters.ElementAtOrDefault(0) ?? ValidationParameterPrettifier.CreatePrettyParameter<T>(),
_predicate.Parameters.ElementAtOrDefault(1) ?? ValidationParameterPrettifier.CreatePrettyParameter<TContext>()
};
var expressionWithParameter = parameters.Aggregate(_predicate.Body, ValidationParameterInjector.InjectParameter);
var predicate = Expression.Lambda<ValidationPredicate<T, TContext>>(expressionWithParameter, parameters);
var messageWithParameter = parameters.Aggregate(_message.Body, ValidationParameterInjector.InjectParameter);
var message = Expression.Lambda<MessageCallback<T, TContext>>(messageWithParameter, parameters);
return new ValidationRule<T, TContext>(predicate, message, _option);
}
}
...and its extensions.
using static ValidationExpressionFactory;
public static class ValidationRuleBuilderExtension
{
public static ValidationRuleBuilder True(this ValidationRuleBuilder builder, Expression<Func<bool>> expression)
{
return
builder
.Predicate(expression)
.Message(() => "The specified expression must be 'true'.");
}
public static ValidationRuleBuilder Null<TMember>(this ValidationRuleBuilder builder, Expression<Func<TMember>> expression)
{
return
builder
.Predicate(ReferenceEqualNull(expression))
.Message(() => $"{typeof(TMember).ToPrettyString(false)} must be null.");
}
public static ValidationRuleBuilder Null<T>(this ValidationRuleBuilder builder, T value)
{
return
builder
.Predicate(ReferenceEqualNull<T>())
.Message(() => $"{typeof(T).ToPrettyString(false)} must be null.");
}
public static ValidationRuleBuilder False(this ValidationRuleBuilder builder, Expression<Func<bool>> expression)
{
return
builder
.Predicate(Negate(expression))
.Message(() => "The specified expression must be 'false'.");
}
public static ValidationRuleBuilder NotNull<TMember>(this ValidationRuleBuilder builder, Expression<Func<TMember>> expression)
{
return
builder
.Predicate(Negate(ReferenceEqualNull(expression)))
.Message(() => $"{typeof(TMember).ToPrettyString(false)} must not be null.");
}
public static ValidationRuleBuilder NotNull<T>(this ValidationRuleBuilder builder, T value)
{
return
builder
.Predicate(Negate(ReferenceEqualNull<T>()))
.Message(() => $"{typeof(T).ToPrettyString(false)} must not be null.");
}
}
ValidationResult
with its extensions
using static ValidationResult;
// ReSharper disable once UnusedTypeParameter - T is required for chaining extensions.
public interface IValidationResult<T>
{
string Expression { get; }
bool Success { get; }
string Message { get; }
}
internal static class ValidationResult
{
public static readonly IDictionary<bool, string> Strings = new Dictionary<bool, string>
{
[true] = "Success",
[false] = "Failed"
};
}
internal class ValidationResult<T> : IValidationResult<T>
{
public ValidationResult([NotNull] string expression, bool success, [NotNull] string message)
{
Expression = expression;
Success = success;
Message = message;
}
public string Expression { get; }
public bool Success { get; }
public string Message { get; }
public override string ToString() => $"{Strings[Success]} | {Message} | {Expression}";
public static implicit operator bool(ValidationResult<T> result) => result.Success;
}
public static class ValidationResultExtensions
{
/// <summary>
/// Throws validation-exception when validation failed.
/// </summary>
public static T ThrowIfValidationFailed<T>(this (T Value, ILookup<bool, IValidationResult<T>> Results) lookup)
{
return
lookup.Results[false].Any()
? throw DynamicException.Create
(
$"{typeof(T).ToPrettyString()}Validation",
$"Object does not meet one or more requirements.{Environment.NewLine}{Environment.NewLine}" +
$"{lookup.Results[false].Select(Func.ToString).Join(Environment.NewLine)}"
)
: default(T);
}
}
Helpers
To check wheter a type is a closure, I use this extension:
internal static class TypeExtensions
{
public static bool IsClosure(this Type type)
{
return
type.Name.StartsWith("<>c__DisplayClass") &&
type.IsDefined(typeof(CompilerGeneratedAttribute));
}
}
And a couple more for creating expressions:
internal static class ValidationExpressionFactory
{
public static LambdaExpression ReferenceEqualNull<T>()
{
return ReferenceEqualNull<T>(Expression.Parameter(typeof(T)));
}
public static LambdaExpression ReferenceEqualNull<T>(Expression<Func<T>> expression)
{
// x => object.ReferenceEqual(x.Member, null)
// This is tricky because the original expression is () => (<>c__DisplayClass).x.y.z
// We first need to the closure and inject out parameter there.
var member = ValidationClosureSearch.FindParameter(expression);
var parameter = Expression.Parameter(member.Type);
var expressionWithParameter = ValidationParameterInjector.InjectParameter(expression.Body, parameter);
return ReferenceEqualNull<T>(parameter, expressionWithParameter);
}
private static LambdaExpression ReferenceEqualNull<T>(ParameterExpression parameter, Expression value = default)
{
// x => object.ReferenceEqual(x, null)
return
Expression.Lambda(
Expression.ReferenceEqual(
value ?? parameter,
Expression.Constant(default(T))),
parameter
);
}
public static LambdaExpression Negate(LambdaExpression expression)
{
// !x
return
Expression.Lambda(
Expression.Not(expression.Body),
expression.Parameters
);
}
}
Expression visitors
With this one I search for closures to replace them with a parameter as validation expression don't have them, e.g: .NotNull(() => x.FirstName))
/// <summary>
/// Searches for the member of the closure class.
/// </summary>
internal class ValidationClosureSearch : ExpressionVisitor
{
private MemberExpression _closure;
public static MemberExpression FindParameter(Expression expression)
{
var parameterSearch = new ValidationClosureSearch();
parameterSearch.Visit(expression);
return parameterSearch._closure;
}
protected override Expression VisitMember(MemberExpression node)
{
if (node.Expression.Type.IsClosure())
{
_closure = node;
}
return base.VisitMember(node);
}
}
Once I've found it, I use this one to replace that closures with actual parameters:
/// <summary>
/// Injects the specified parameter to replace the closure.
/// </summary>
public class ValidationParameterInjector : ExpressionVisitor
{
private readonly ParameterExpression _parameter;
private ValidationParameterInjector(ParameterExpression parameter) => _parameter = parameter;
public static Expression InjectParameter(Expression expression, ParameterExpression parameter)
{
return new ValidationParameterInjector(parameter).Visit(expression is LambdaExpression lambda ? lambda.Body : expression);
}
protected override Expression VisitMember(MemberExpression node)
{
var isClosure =
node.Type == _parameter.Type &&
node.Expression.Type.IsClosure();
return
isClosure
? _parameter
: base.VisitMember(node);
}
}
The last one is used to prettify validation expressions for display by injecting good looking type names.
- before:
"Param_0.FirstName"
- after:
"<param:Person>.FirstName>"
// We don't want to show the exact same expression as the condition
// because there are variables and closures that don't look pretty.
// We replace them with more friendly names.
internal class ValidationParameterPrettifier : ExpressionVisitor
{
private readonly ParameterExpression _originalParameter;
private readonly ParameterExpression _prettyParameter;
private ValidationParameterPrettifier(ParameterExpression originalParameter, ParameterExpression prettyParameter)
{
_originalParameter = originalParameter;
_prettyParameter = prettyParameter;
}
protected override Expression VisitParameter(ParameterExpression node)
{
return node.Equals(_originalParameter) ? _prettyParameter : base.VisitParameter(node);
}
protected override Expression VisitMember(MemberExpression node)
{
// Extract member name from closures.
return
node.Expression is ConstantExpression
? Expression.Parameter(node.Type, node.Member.Name)
: base.VisitMember(node);
}
protected override Expression VisitUnary(UnaryExpression node)
{
// Remove type conversion, this is change (Convert(<T>) != null) to (<T> != null)
return
node.Operand.Type == _originalParameter.Type
? Expression.Parameter(node.Operand.Type, _prettyParameter.Name)
: base.VisitUnary(node);
}
public static Expression Prettify<T>([NotNull] LambdaExpression expression)
{
if (expression == null) throw new ArgumentNullException(nameof(expression));
return
expression
.Parameters
.Aggregate(expression.Body, (e, p) => new ValidationParameterPrettifier(expression.Parameters[0], CreatePrettyParameter<T>()).Visit(expression.Body));
}
public static ParameterExpression CreatePrettyParameter<T>()
{
return Expression.Parameter(typeof(T), $"<param:{typeof(T).ToPrettyString()}>");
}
}
That's it.
Questions
- would you say it meets my own requirements?
- would you say any requirements or features are missing?
- is there anything else I can improve?