20
\$\begingroup\$

I wrote a simple validation tool that allows me to quickly define validation rules and check my data objects against them. I know there is this another tool called FluentValidation but... I'm not a fan. So here is my small alternative helper.


The main type is the Validator<T> that allows me to define a collection of rules and their string representation for debugging purposes. It also provides a default rule that checks if the object is not null.

public class Validator<T>
{
    private readonly IEnumerable<ValidationRule<T>> _rules;

    public Validator(IEnumerable<ValidationRule<T>> rules)
    {
        _rules = rules;
    }

    public static ValidatorBuilder<T> Builder => new ValidatorBuilder<T>();

    public bool IsValid(T obj)
    {
        return _rules.All(x => x.IsMet(obj));
    }

    public IEnumerable<Validation> Validate(T obj)
    {
        if (obj == null)
        {
            yield return new Validation(false, $"Object of type {typeof(T).Name} does not meet the requirement: ({typeof(T).Name} != null)");
            yield break;
        }

        foreach (var rule in _rules)
        {            
            var isValid = rule.IsMet(obj);
            yield return new Validation(
                isValid, 
                isValid 
                    ? $"Object of type {typeof(T).Name} meets the requirement: {rule}"
                    : $"Object of type {typeof(T).Name} does not meet the requirement: {rule}");
        }
    }
}

Using the above class alone would be too difficult so it provides a Builder property to get a new ValidatorBuilder<T> that helps me to build the rules.

Rules as well as messages are compiled from Expressions because calling ToString on it will produce the actual expression as a string. Because expressions contains a lambda expression x => I use regex to remove it from the string then I replace each x. variable with the name of the T.

public class ValidatorBuilder<T>
{
    private readonly List<ValidationRule<T>> _rules = new List<ValidationRule<T>>();

    public ValidatorBuilder<T> Where(Expression<Func<T, bool>> expression)
    {
        var expressionString = expression.ToString();

        var variableName = Regex.Match(expressionString, "^([a-z0-9_]+) => ").Groups[1].Value;
        expressionString = Regex.Replace(expressionString, "^[a-z0-9_]+ => ", string.Empty);
        expressionString = Regex.Replace(expressionString, $"{variableName}\\.", $"{typeof(T).Name}.");

        _rules.Add(new ValidationRule<T>(expressionString, expression.Compile()));
        return this;
    }

    public Validator<T> Build()
    {
        return new Validator<T>(_rules);
    }                
}

Each rule is implemented as ValidationRule<T> that checks the object with its predicate and when used as a string then it outputs the expression string.

public class ValidationRule<T>
{ 
    private readonly string _expression;
    private readonly Func<T, bool> _predicate;

    public ValidationRule(string expression, Func<T, bool> predicate)
    {
        _expression = expression;
        _predicate = predicate;
    }

    public bool IsMet(T obj) => _predicate(obj);

    public override string ToString() => _expression;
}

Validation results are returned as Validation objects that have only two properties:

public class Validation
{
    public Validation(bool success, string message)
    {
        Success = success;
        Message = message;
    }
    public bool Success { get; }
    public string Message { get; }
}

Example

And this is how I use it. I first build a validator that I then use to validate the data:

var builder = Validator<Person>.Builder;

var personValidator =
    builder
        .Where(p => !string.IsNullOrEmpty(p.FirstName))
        .Where(p => p.LastName != null)
        .Where(p => !p.LastName.StartsWith("D"))
        .Build();

personValidator.Validate(new Person
{
    FirstName = "John",
    LastName = "Doe"
})
.Dump();

The output is:

Success    Message
-------    -------
True       Object of type Person meets the requirement: Not(IsNullOrEmpty(Person.FirstName)) 
True       Object of type Person meets the requirement: (Person.LastName != null) 
False      Object of type Person does not meet the requirement: Not(Person.LastName.StartsWith("D")) 
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Only a small thing but you could change the $"Object of type .. strings so you only have one and have the ternary inside to select meets or does not meet. \$\endgroup\$ Oct 17, 2017 at 16:22

6 Answers 6

17
\$\begingroup\$

Instead of using regular expressions to manipulate the expression string I prefer to do expression manipulation. While this can be a little daunting at first it turns out to be fairly simple. It handles a lot more of the oddball cases.

For instance, when someone does this:

var builder = Validator<String>.Builder;
var stringValidator = builder
    .Where(s => !string.IsNullOrEmpty(s))
    .Build();

In the Where method the following line fails to locate the variable reference:

    expressionString = Regex.Replace(expressionString, $"{variableName}\\.", $"{typeof(T).Name}.");

Likewise the following (somewhat contrived) example has unexpected results:

var builder = Validator<DateTime>.Builder;
var dateValidator = builder
    .Where(day => day.Year == DateTime.Today.Year)
    .Build();

In order to do this nicely you can use an ExpressionVisitor class to replace all references to the parameter with another parameter that you construct with the right name for your desired output.

Here's a simple ReplaceVisitor that does the trick:

public class ReplaceVisitor : ExpressionVisitor
{
    private Expression _from, _to;

    public ReplaceVisitor(Expression from, Expression to)
    {
        _from = from;
        _to = to;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        return node.Equals(_from) ? _to : base.VisitParameter(node);
    }

    public static Expression Replace(Expression target, Expression from, Expression to)
    {
        return new ReplaceVisitor(from, to).Visit(target);
    }
}

With that we can replace all the references to the parameter with a custom parameter with the typename - or anything really - as its name. We can also cut the lambda header (parameter name and =>) by simply working with the lambda expression's Body.

Your Where method then becomes:

public ValidatorBuilder<T> Where(Expression<Func<T, bool>> expression)
{
    var typeParameter = Expression.Parameter(typeof(T), typeof(T).Name);
    var expressionString = ReplaceVisitor.Replace(expression.Body, expression.Parameters[0], typeParameter).ToString();
    _rules.Add(new ValidationRule<T>(expressionString, expression.Compile()));
    return this;
}

This now handles all of the things that your regular expressions do and more, without the inconvenience of having to deal with edge cases.

\$\endgroup\$
5
  • \$\begingroup\$ This is genius ;-) String manipulation is indeed ugly and I didn't know I could modify an expression in such a cool way. \$\endgroup\$
    – t3chb0t
    Oct 17, 2017 at 15:21
  • \$\begingroup\$ I hope you don't mind me fixing the code. The Visit override didn't work well for me (got a null reference exception) but the VisitParameter seems to work fine. If this is not what you meat then I'll undo the edit. \$\endgroup\$
    – t3chb0t
    Oct 17, 2017 at 15:35
  • \$\begingroup\$ @t3chb0t The edit is fine... I was typing from memory and used the wrong code form for Visit() which can receive a null argument. It could also be simply public override Expression Visit(Expression node) => node == _from ? _to : base(node); Simple equality is all we need. And yes, this is a powerful option for working with Expressions. \$\endgroup\$
    – Corey
    Oct 17, 2017 at 21:21
  • \$\begingroup\$ No follow-up but a self-answer instead if you're curious what it became. \$\endgroup\$
    – t3chb0t
    Oct 18, 2017 at 20:06
  • \$\begingroup\$ Glad I could help :) \$\endgroup\$
    – Corey
    Oct 19, 2017 at 21:48
6
\$\begingroup\$

I just noticed a couple of small issues.

First, IsValid(obj) may return true on null, whereas Validate(obj) has a special check for null. I would rewrite IsValid this way:

public bool IsValid(T obj)
{
    bool anyErrors = Validate(obj).Any();
    return !anyErrors;
}

Second, your regex replacement might produce odd results in certain cases. Consider:

e => e.FirstName.Length > 0

The regex will match two occurrences of "e." in that lambda. You might want to beef up that regex. Alternatively, what I would do is reduce the cleverness, maybe display (Person e) => e.FirstName.Length instead.

Which reminds me, this is also a valid lambda expression:

(Person p) => p.FirstName != null

Will your string manipulations handle that also? You might want to forego the string manipulations altogether, as there are probably more corner cases (method groups come to mind, but the C# compiler might handle those).

\$\endgroup\$
2
  • \$\begingroup\$ I upvoted Corey's answer. I didn't notice he covered the string manipulation problem much better than me until after posting. \$\endgroup\$ Oct 17, 2017 at 17:55
  • \$\begingroup\$ True, I thought only about a few simple use cases I wrote it for but there may be some more tricky ones. The expression solution is the most optimal one I think but I like the idea with not changing anything but inserting the type name too. Maybe it should be an option ;-) anyways +1 \$\endgroup\$
    – t3chb0t
    Oct 17, 2017 at 18:01
5
\$\begingroup\$

Not much to say here.

Your code looks clean and is easy to read.

There is just a little bit what I would change, namely the "default" rule of the Validator<T>.

If you ever would have the need to validate that a passed T obj is null you couldn't do it with the Validator<T> in its current state.

Maybe having a "default" rule as a property would do the trick.

\$\endgroup\$
1
  • \$\begingroup\$ Not much to say is good too ;-) I was thinking the same about the null object but then I thought that checking if the actual object is null isn't actually what a validation should be doing so I might even be throwing the ArgumentNullException... I can't make my mind ;-] \$\endgroup\$
    – t3chb0t
    Oct 17, 2017 at 12:11
4
\$\begingroup\$

Don't hang on to an IEnumerable

public class Validator<T>
{
    private readonly IEnumerable<ValidationRule<T>> _rules;

    public Validator(IEnumerable<ValidationRule<T>> rules)
    {
        _rules = rules;
    }

    ...
}

It's generally recommended to immediately materialize an enumerable if you're going to keep the result around. You can't know if the enumerable is backed by a high latency resource such as a database, a file on a network share and so on.

Materializing the enumerable once can also prevent any potential errors from (bad) queries with side effects that are only meant to happen once, results changing from queries that are backed by a list that is later modified and so on.

\$\endgroup\$
1
  • 3
    \$\begingroup\$ +1, it should really be _rules = rules.ToList(); or even _rules = rules.ToList().AsReadOnly(); :-) There's another reason for materializing the enumerable immediately, which is that any errors in enumerating the results happen at that point, rather than at some seemingly-random point in the future. It makes debugging MUCH easier! \$\endgroup\$ Oct 18, 2017 at 10:53
4
\$\begingroup\$

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
\$\endgroup\$
7
  • 1
    \$\begingroup\$ I'd like to thank everyone for your ideas and improvement suggestions in addtion to all +1s ;-) \$\endgroup\$
    – t3chb0t
    Oct 18, 2017 at 16:18
  • 1
    \$\begingroup\$ This is really excellent now. I demand that you put this on github :) \$\endgroup\$ Oct 18, 2017 at 16:29
  • 1
    \$\begingroup\$ @StephenByrne nice to hear that you like it too, and it's almost there, on github ;-) I just need to write a few tests before commiting it. I'll let you know when it's done. \$\endgroup\$
    – t3chb0t
    Oct 18, 2017 at 16:31
  • \$\begingroup\$ @StephenByrne I put the code in one of my projects and the exact folder is here \$\endgroup\$
    – t3chb0t
    Oct 19, 2017 at 18:23
  • 1
    \$\begingroup\$ This single question and its answers has taught me more about validation, expressions and lambdas than dozens tutorials. \$\endgroup\$ Dec 16, 2022 at 12:27
3
\$\begingroup\$

Nice work!

One thing you could do is leverage polymorphism for the Validation class so that you have a separate type for Valid and Invalid results.

And then you can re-use the validation "loop" in the IsValid method, to make sure the two don't diverge (e.g. you don't have to have a separate null check in the IsMet method as well).

Mind you, I am not sure the IsValid method is particularly useful, since it's just as easy to call validator.Validate(obj).Any(v=>v is FailedValidation) from the client code anyway and I would imagine that a caller would want to know why something is invalid as opposed to simply the whether it is invalid or not. But of course that's just me :).

Finally, just a tiny addition to the constructor to make sure that you have at least an empty set of rules.

public class Validator<T>
{
    private readonly IEnumerable<ValidationRule<T>> _rules;

    public Validator(IEnumerable<ValidationRule<T>> rules)
    {
        _rules = rules??Enumerable.Empty<T>();
    }

    public static ValidatorBuilder<T> Builder => new ValidatorBuilder<T>();

    public bool IsValid(T obj)
    {
        return Validate(obj).Any(v=>v is FailedValidation);
    }

    public IEnumerable<Validation> Validate(T obj)
    {
       if (obj == null)
        {
            yield return new NullObjectValidation();
            yield break;
        }

        foreach (var rule in _rules)
        {            
            if(rule.IsMet(obj))
            {
                yield return new OkValidation();
            }else
            {
               yield return new FailedValidation(obj,rule);
            }           
        }
    }
}

where NullObjectValidation, OkValidation and FailedValidation are all subclasses of Validation (with NullObjectValidation being a subclass of FailedValidation).

This allows you to encapsulate the error messages (OkValidation's message should probably just be empty anyway) for each scenario.

\$\endgroup\$
1
  • \$\begingroup\$ I like the ideas and the NullObjectValidation adds one more option to how a null object could be handeled... now I have to pick one ;-) \$\endgroup\$
    – t3chb0t
    Oct 18, 2017 at 4:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.