I managed to rewrite most parts of it and I think it's much better now. It shouldn't be anything fancy, just a simple data validation helper that I guess most of the time will just check if something is not null
. Thus no async
stuff etc. because it should not contain any business-logic.
The Validator<T>
class became a collection of rules and is now immutable. Adding new rules results in a new validator. This should allow to add new rules ad-hoc if necessary without breaking the old ones. This time it also calls .ToList
on the rules
collection.
public class Validator<T> : IEnumerable<ValidationRule<T>>
{
private readonly List<ValidationRule<T>> _rules;
public Validator([NotNull] IEnumerable<ValidationRule<T>> rules)
{
if (rules == null) throw new ArgumentNullException(nameof(rules));
_rules = rules.ToList();
}
public static Validator<T> Empty => new Validator<T>(Enumerable.Empty<ValidationRule<T>>());
public Validator<T> Add([NotNull] ValidationRule<T> rule)
{
if (rule == null) throw new ArgumentNullException(nameof(rule));
return new Validator<T>(_rules.Concat(new[] { rule }));
}
public IEnumerable<IValidation<T>> Validate(T obj)
{
foreach (var rule in _rules)
{
if (rule.IsMet(obj))
{
yield return PassedValidation<T>.Create(rule);
}
else
{
yield return FailedValidation<T>.Create(rule);
if (rule.Options.HasFlag(ValidationOptions.StopOnFailure))
{
yield break;
}
}
}
}
public IEnumerator<ValidationRule<T>> GetEnumerator()
{
return _rules.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public static Validator<T> operator +(Validator<T> validator, ValidationRule<T> rule)
{
return validator.Add(rule);
}
}
The ValidationRule<T>
class went lazy and got new parameters. It now takes care of the expression itself. It compiles it and creates the expression-string only if requested.
public class ValidationRule<T>
{
private readonly Lazy<string> _expressionString;
private readonly Lazy<Func<T, bool>> _predicate;
public ValidationRule(Expression<Func<T, bool>> expression, ValidationOptions options)
{
if (expression == null) throw new ArgumentNullException(nameof(expression));
_predicate = new Lazy<Func<T, bool>>(() => expression.Compile());
_expressionString = new Lazy<string>(() => CreateExpressionString(expression));
Options = options;
}
public ValidationOptions Options { get; }
private static string CreateExpressionString(Expression<Func<T, bool>> expression)
{
var typeParameterReplacement = Expression.Parameter(typeof(T), $"<{typeof(T).Name}>");
return ReplaceVisitor.Replace(expression.Body, expression.Parameters[0], typeParameterReplacement).ToString();
}
public bool IsMet(T obj) => _predicate.Value(obj);
public override string ToString() => _expressionString.Value;
public static implicit operator string(ValidationRule<T> rule) => rule?.ToString();
}
There are now new ValidationOptions
- with just two values - as I didn't need more - but I wanted to have a clean call without simply true
. The validator checks this after a rule has failed to see if it can continue.
[Flags]
public enum ValidationOptions
{
None = 0,
StopOnFailure = 1 << 0,
}
The ReplaceVisitor
class does not only replace the parameter name but it also can replace constants with its name, remove the DisplayClass
closure and retrieve the field name and remove the Convert
expression that is created when checking a T
against null
.
public class ReplaceVisitor : ExpressionVisitor
{
private readonly ParameterExpression _fromParameter;
private readonly ParameterExpression _toParameter;
private ReplaceVisitor(ParameterExpression fromParameter, ParameterExpression toParameter)
{
_fromParameter = fromParameter;
_toParameter = toParameter;
}
protected override Expression VisitParameter(ParameterExpression node)
{
return node.Equals(_fromParameter) ? _toParameter : base.VisitParameter(node);
}
protected override Expression VisitMember(MemberExpression node)
{
// Extract member name from closures.
if (node.Expression is ConstantExpression)
{
return Expression.Parameter(node.Type, node.Member.Name);
}
return base.VisitMember(node);
}
protected override Expression VisitUnary(UnaryExpression node)
{
// Remove type conversion, this is change (Convert(<T>) != null) to (<T> != null)
if (node.Operand.Type == _fromParameter.Type)
{
return Expression.Parameter(node.Operand.Type, _toParameter.Name);
}
return base.VisitUnary(node);
}
public static Expression Replace([NotNull] Expression target, [NotNull] ParameterExpression from, [NotNull] ParameterExpression to)
{
if (target == null) throw new ArgumentNullException(nameof(target));
if (from == null) throw new ArgumentNullException(nameof(from));
if (to == null) throw new ArgumentNullException(nameof(to));
return new ReplaceVisitor(from, to).Visit(target);
}
}
The Validation
class has now descendants. One for each of the two possible outcomes. I created an interface for it but I'm not sure whether I actually need it. It got however a T
parameter that I need later to be able to chain the new extensions.
public interface IValidation<T>
{
bool Success { get; }
string Expression { get; }
}
public abstract class Validation<T> : IValidation<T>
{
protected Validation(bool success, string expression)
{
Success = success;
Expression = expression;
}
public bool Success { get; }
public string Expression { get; }
}
internal class PassedValidation<T> : Validation<T>
{
private PassedValidation(string rule) : base(true, rule) { }
public static IValidation<T> Create(string rule) => new PassedValidation<T>(rule);
public override string ToString() => $"{Expression}: Passed";
}
internal class FailedValidation<T> : Validation<T>
{
private FailedValidation(string rule) : base(false, rule) { }
public static IValidation<T> Create(string rule) => new FailedValidation<T>(rule);
public override string ToString() => $"{Expression}: Failed";
}
public class ValidationRule<T>
{
private readonly Lazy<string> _expressionString;
private readonly Lazy<Func<T, bool>> _predicate;
public ValidationRule(Expression<Func<T, bool>> expression, ValidationOptions options)
{
if (expression == null) throw new ArgumentNullException(nameof(expression));
_predicate = new Lazy<Func<T, bool>>(() => expression.Compile());
_expressionString = new Lazy<string>(() => CreateExpressionString(expression));
Options = options;
}
public ValidationOptions Options { get; }
private static string CreateExpressionString(Expression<Func<T, bool>> expression)
{
var typeParameterReplacement = Expression.Parameter(typeof(T), $"<{typeof(T).Name}>");
return ReplaceVisitor.Replace(expression.Body, expression.Parameters[0], typeParameterReplacement).ToString();
}
public bool IsMet(T obj) => _predicate.Value(obj);
public override string ToString() => _expressionString.Value;
public static implicit operator string(ValidationRule<T> rule) => rule?.ToString();
}
In order to be able to build validation-rules more easily I created this ValidationComposer
that provides two extension methods so I can pick one that seems to be easier to read for a specific condition. There is no ValidationBuilder
anymore.
public static class ValidatorComposer
{
public static Validator<T> IsValidWhen<T>(this Validator<T> validator, Expression<Func<T, bool>> expression, ValidationOptions options = ValidationOptions.None)
{
return validator + new ValidationRule<T>(expression, options);
}
public static Validator<T> IsNotValidWhen<T>(this Validator<T> validator, Expression<Func<T, bool>> expression, ValidationOptions options = ValidationOptions.None)
{
var notExpression = Expression.Lambda<Func<T, bool>>(Expression.Not(expression.Body), expression.Parameters[0]);
return validator.IsValidWhen(notExpression, options);
}
}
The last component is the ValidationExtensions
class that provides even more helpers so that a data object can be validated more fluently or so that a failed validtion can throw an exception. Exeptions are generated dynamically and are made of the name of the type that failed the validation so there is no ValidationException
but for example a PersonValidationException
can be thrown.
public static class ValidatorExtensions
{
public static IEnumerable<IValidation<T>> ValidateWith<T>([NotNull] this T obj, [NotNull] Validator<T> validator)
{
return validator.Validate(obj);
}
public static bool AllSuccess<T>([NotNull] this IEnumerable<IValidation<T>> validations)
{
if (validations == null) throw new ArgumentNullException(nameof(validations));
return validations.All(v => v.Success);
}
public static void ThrowIfInvalid<T>([NotNull] this IEnumerable<IValidation<T>> validations)
{
if (validations.AllSuccess())
{
return;
}
var requriements = validations.Aggregate(
new StringBuilder(),
(result, validation) => result.AppendLine($"{validation.Expression} == {validation.Success}")
).ToString();
throw DynamicException.Factory.CreateDynamicException
(
name: $"{typeof(T).Name}Validation{nameof(Exception)}",
message: $"Object of type '{typeof(T).Name}' does not meet one or more requirements.{Environment.NewLine}{Environment.NewLine}{requriements}",
innerException: null
);
}
}
I still need to write a few unit-tests for it but for now I'm satisfied with the result (I'm pretty sure there are still a few cases where the expression-string isn't optimal but I'll implement them when I come across them).
In closing a few examples:
var age = 5;
var lastName = "Doe";
var personValidator =
Validator<Person>.Empty
.IsNotValidWhen(p => p == null, ValidationOptions.StopOnFailure)
.IsValidWhen(p => !string.IsNullOrEmpty(p.FirstName))
.IsNotValidWhen(p => p.LastName == null)
.IsNotValidWhen(p => p.LastName.StartsWith("D"))
.IsValidWhen(p => p.LastName != null)
.IsValidWhen(p => p.LastName == lastName)
.IsValidWhen(p => p.DayOfBirth == DateTime.Today)
.IsValidWhen(p => p.Age > age);
var person = new Person
{
FirstName = "John",
LastName = "Doe"
};
Various validation calls:
personValidator.Validate(person).Dump();
person.ValidateWith(personValidator).AllSuccess().Dump();
default(Person).ValidateWith(personValidator).Dump();
person.ValidateWith(personValidator).ThrowIfInvalid();
The result of Exception.ToString();
is:
PersonValidationException: Object of type 'Person' does not meet one or more requirements.
Not((<Person> == null)) == True
Not(IsNullOrEmpty(<Person>.FirstName)) == True
Not((<Person>.LastName == null)) == True
Not(<Person>.LastName.StartsWith("D")) == False
(<Person>.LastName != null) == True
(<Person>.LastName == lastName) == True
$"Object of type ..
strings so you only have one and have the ternary inside to selectmeets
ordoes not meet
. \$\endgroup\$