I've been working on another timesaver because I really really don't like to type all those validation if
s 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
class.method(expr).extension()
syntax. I'll use extensions only. \$\endgroup\$