3
\$\begingroup\$

I've been working on another timesaver because I really really don't like to type all those validation ifs and exceptions over an over again.

What I came up with this time is a set of validation extensions that make use of expression trees even if very simple ones.


There are three classes. The basis is the Validate class that creates the first validation context:

public static class Validate
{
    public static ValidationContext<TArg> Expression<TArg>(Expression<Func<TArg>> expression)
    {
        var memberExpression = expression.Body as MemberExpression;
        var name = memberExpression.Member.Name;
        var value = expression.Compile()();
        return new ValidationContext<TArg>()
        {
            Argument = value,
            Name = name
        };
    }
}

Then a ValidationContext passes the information about the expression and its value:

public class ValidationContext<TArg>
{
    public TArg Argument { get; internal set; }

    public string Name { get; internal set; }
}

Finally there are all the validation extensions (I removed some of them to make the list shorter):

public static class Validation
{
    public static ValidationContext<TArg> IsNotNull<TArg>(this ValidationContext<TArg> context)
    {
        if (context.Argument == null) throw new ArgumentNullException(context.Name);
        return context;
    }

    public static ValidationContext<string> IsNotNullOrEmpty(this ValidationContext<string> context)
    {
        if (string.IsNullOrEmpty(context.Argument)) throw new ArgumentNullException(context.Name);
        return context;
    }

    public static ValidationContext<TArg> IsLessThen<TArg>(this ValidationContext<TArg> context, TArg max) where TArg : IComparable
    {
        if (!(context.Argument.CompareTo(max) < 0))
            throw new ArgumentOutOfRangeException(context.Name, $"'{context.Name}' of '{context.Argument}' is not less then '{max}'.");
        return context;
    }

    public static ValidationContext<TArg> IsLessThenOrEqual<TArg>(this ValidationContext<TArg> context, TArg max) where TArg : IComparable
    {
        if (!(context.Argument.CompareTo(max) <= 0))
            throw new ArgumentOutOfRangeException(context.Name, $"'{context.Name}' of '{context.Argument}' is not less then or eqal '{max}'.");
        return context;
    }

    public static ValidationContext<TArg> IsBetween<TArg>(this ValidationContext<TArg> context, TArg min, TArg max) where TArg : IComparable
    {
        if (!(context.Argument.CompareTo(min) > 0 && context.Argument.CompareTo(max) < 0))
            throw new ArgumentOutOfRangeException(context.Name, $"'{context.Name}' of '{context.Argument}' is not between '{min}' and {max}.");
        return context;
    }

    public static ValidationContext<TArg> IsBetweenOrEqual<TArg>(this ValidationContext<TArg> context, TArg min, TArg max) where TArg : IComparable
    {
        if (!(context.Argument.CompareTo(min) >= 0 && context.Argument.CompareTo(max) <= 0))
            throw new ArgumentOutOfRangeException(context.Name, $"'{context.Name}' of '{context.Argument}' is not between or equal '{min}' and {max}.");
        return context;
    }   

    public static ValidationContext<TArg> IsEqual<TArg>(this ValidationContext<TArg> context, TArg value) where TArg : IComparable
    {
        if (context.Argument.CompareTo(value) != 0) throw new Exception($"'{context.Name}' is not equal '{value.ToString()}'.");
        return context;
    }

    public static ValidationContext<TArg> Where<TArg>(this ValidationContext<TArg> context, Func<TArg, bool> predicate)
    {
        if (!predicate(context.Argument)) throw new Exception($"'{context.Name}' is not valid.");
        return context;
    }

    public static ValidationContext<string> IsMatch(this ValidationContext<string> context, string pattern, RegexOptions options = RegexOptions.None)
    {
        if (!Regex.IsMatch(context.Argument, pattern, options)) throw new Exception($"'{context.Name}' does not match '{pattern}'.");
        return context;
    }

    public static ValidationContext<TArg2> And<TArg, TArg2>(this ValidationContext<TArg> context, Expression<Func<TArg, TArg2>> expression)
    {
        var memberExpression = expression.Body as MemberExpression;
        var name = memberExpression.Member.Name;
        var value = expression.Compile()(context.Argument);
        return new ValidationContext<TArg2>()
        {
            Argument = value,
            Name = name
        };
    }
}

The last extension in this class allows to chain another expression and more validations.

Here are some examples (the lines throwing exceptions are commented out):

var foo = (string)null;

//Validate.Expression(() => foo).IsNotNull(); // bam!
//Validate.Expression(() => foo).IsNotNullOrEmpty(); // bam!

foo = "bar";

Validate.Expression(() => foo).IsNotNull().IsMatch("[a-z]+");
//Validate.Expression(() => foo).IsNotNull().Matches("^[a-z]{2}$"); // bam!

Validate.Expression(() => foo.Length).IsEqual(3);
//Validate.Expression(() => foo.Length).IsEqual(4); // bam!

// chaining more expressions
Validate.Expression(() => foo).IsNotNull().And(s => s.Length).IsEqual(3);
//Validate.Expression(() => foo).IsNotNull().And(s => s.Length).IsEqual(4); // bam!

Validate.Expression(() => foo.Length).IsBetween(2, 4);
//Validate.Expression(() => foo.Length).IsBetween(3, 4);

Validate.Expression(() => foo.Length).IsBetweenOrEqual(3, 6);
//Validate.Expression(() => foo.Length).IsBetweenOrEqual(4, 6); // bam!

var baz = 3;

Validate.Expression(() => baz).IsLessThen(4);
Validate.Expression(() => baz).IsLessThenOrEqual(3);
//Validate.Expression(() => baz).IsLessThenOrEqual(2); // bam!

You can also return the final value:

var length = Validate.Expression(() => foo.Length).IsBetween(2, 4).Argument;

The main goals are:

  • easy to use
  • easy to extend
  • provide as much information as possible
  • simplify validation
\$\endgroup\$
4
  • \$\begingroup\$ Expressions are slow, really slow, and there is no syntactically easy way to cache the compiled one. There are though some libraries which allow to use them for validations. \$\endgroup\$ Jun 13, 2016 at 21:07
  • \$\begingroup\$ @DmitryNogin Oh! Great, I see I've reinvented the wheel ;-] About the performance... damn! They give us such cool possibilities only to make them slow :( \$\endgroup\$
    – t3chb0t
    Jun 14, 2016 at 3:43
  • \$\begingroup\$ Under those circumstances I find there is no need for the class.method(expr).extension() syntax. I'll use extensions only. \$\endgroup\$
    – t3chb0t
    Jun 14, 2016 at 3:55
  • \$\begingroup\$ @NikitaB this is v1 ;-) \$\endgroup\$
    – t3chb0t
    Jun 14, 2016 at 13:51

1 Answer 1

1
\$\begingroup\$

This time I'm going to write my own review ;-)

For most of you this isn't a great discovery but compiling an expression is a real performance hit. For 10k loops it makes a difference of at least 3sec on my machine.

I refactored my code and implemented a few optimizations. They are not very pretty but at least the difference between the expression and expression-less version is about 4milliseconds for 10k loops so actually negligible.

The new Validate class:

public static class Validate
{
    public static ValidationContext<TArg> Expression<TArg>(
        Func<TArg> lambda, 
        Func<Expression<Func<TArg>>> memberExpression)
    {
        return new ValidationContext<TArg>()
        {
            Argument = lambda(),
            Expression = memberExpression
        };
    }

    public static ValidationContext<TArg> Expression<TArg>(
        Func<TArg> lambda, 
        string memberName)
    {
        return new ValidationContext<TArg>()
        {
            Argument = lambda(),
            MemberName = memberName
        };
    }    
}

The updated context:

public class ValidationContext<TArg>
{
    private string _memberName;

    public TArg Argument { get; internal set; }

    public string MemberName
    {
        get
        {
            if (!string.IsNullOrEmpty(_memberName)) return _memberName;
            var memberExpression = Expression().Body as MemberExpression;
            return memberExpression.Member.Name;
        }
        set { _memberName = value; }
    }

    internal Func<Expression<Func<TArg>>> Expression { get; set; }
}

As you can see I either can specify the member name or evaluate it dynamicaly. I do it only in case of an exception. The Func<Expression<Func<TArg>>> prevents the expression to be build prematurely.

It's call probably wouldn't win a beauty competition but what else we can do:

var foo = "bar";
Validate.Expression(() => foo, () => () => foo).IsNotNullOrEmpty();

So after all it's not that bad, isn't it? ;-)

\$\endgroup\$

Your Answer

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

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