Fluently Validation of ObjectsSimple object validator with a new APIAdditional model validationValidation check and code-savingMath expression parser in C#Return IEnumerable<KeyValuePair> from a private method; use Dictionary or anon. type?Validation for DTO using DataAnnotationsFluent method arguments validationProperty validationInitializing immutable objects with a nested builderValidation for a CQS system that throws an exceptionSimple object validator
Does a centaur PC also count as being mounted?
Was "I have the farts, again" broadcast from the Moon to the whole world?
The Puzzling Reverse and Add Sequence
What is the line crossing the Pacific Ocean that is shown on maps?
“Faire” being used to mean “avoir l’air”?
The difference between Rad1 and Rfd1
Is there a short way to check uniqueness of values without using 'if' and multiple 'and's?
Does anycast addressing add additional latency in any way?
Can I travel from Germany to England alone as an unaccompanied minor?
Zombie diet, why humans?
Do 3D printers really reach 50 micron (0.050mm) accuracy?
Why is a blank required between "[[" and "-e xxx" in ksh?
How do I spend money in Sweden and Denmark?
How well known and how commonly used was Huffman coding in 1979?
How should I behave to assure my friends that I am not after their money?
“Transitive verb” + interrupter+ “object”?
What's the point of DHS warning passengers about Manila airport?
Transitive action of a discrete group on a compact space
One folder two different locations on ubuntu 18.04
Is it bad to describe a character long after their introduction?
can’t run a function against EXEC
Why did this meteor appear cyan?
How hard is it to sell a home which is currently mortgaged?
Does the UK have a written constitution?
Fluently Validation of Objects
Simple object validator with a new APIAdditional model validationValidation check and code-savingMath expression parser in C#Return IEnumerable<KeyValuePair> from a private method; use Dictionary or anon. type?Validation for DTO using DataAnnotationsFluent method arguments validationProperty validationInitializing immutable objects with a nested builderValidation for a CQS system that throws an exceptionSimple object validator
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty,.everyoneloves__bot-mid-leaderboard:empty margin-bottom:0;
$begingroup$
Inspired by this question by t3chb0t and as an elaboration of my own answer, I have written the following solution. My goal was to reduce complexity both in implementation and use. Eventually - I have to admit - the implementation ended up being rather complex - but in my flavor; but in terms of ease of use, I think I succeeded. My original idea was inspired by Railway Oriented Programming, but I don't think I can claim to conform to that in the following.
The use case is as follows:
private static void ValidationTest()
n)e Street$", "Street Name doesn't conform to the pattern");
DoTheValidation(validator, Tester);
private static void ValidationTestDefaultErrorMessages()
n)e Street$", null);
DoTheValidation(validator, Tester);
private static void DoTheValidation<T>(Validator<T> validator, T source)
var result = source.ValidateWith(validator);
Console.WriteLine("The following Errors were found: ");
foreach (ValidateResult<T> failure in result.Where(r => (r as Success<T>) is null))
Console.WriteLine(failure);
private class Person
public string FirstName get; set;
public string LastName get; set;
public Address Address get; set;
public int Age get; set;
private class Address
public string Street get; set;
private static readonly Person Tester = new Person
FirstName = "Cookie",
LastName = "Monster",
Age = 45,
Address = new Address
Street = "Sesame Street"
;
As shown, it's possible to add validation rules in an easy fluent manner.
The ValidationStopConditions
is defined as:
public enum ValidationStopConditions
RunAll = 1,
StopOnFailure = 2,
StopOnWarning = 3
and determines if all rules should be run no matter what happens or if the validation stops on first failure or warning.
The Validator
class looks like:
public static class Validator
public static Validator<TSource> For<TSource>(ValidationStopConditions stopCondition = ValidationStopConditions.RunAll) => new Validator<TSource>(stopCondition);
public class Validator<T>
List<Func<T, ValidateResult<T>>> m_rules = new List<Func<T, ValidateResult<T>>>();
public Validator(ValidationStopConditions stopCondition)
StopCondition = stopCondition;
public ValidationStopConditions StopCondition get;
public IReadOnlyList<ValidateResult<T>> Validate(T source)
if (source == null) return Enumerable.Empty<ValidateResult<T>>().ToList();
switch (StopCondition)
case ValidationStopConditions.RunAll:
return m_rules.Select(rule => rule(source)).ToList();
case ValidationStopConditions.StopOnFailure:
List<ValidateResult<T>> results = new List<ValidateResult<T>>();
foreach (var rule in m_rules)
var result = rule(source);
results.Add(result);
if (result is Failure<T>)
return results;
return results;
case ValidationStopConditions.StopOnWarning:
List<ValidateResult<T>> results = new List<ValidateResult<T>>();
foreach (var rule in m_rules)
var result = rule(source);
results.Add(result);
if (result is Warning<T>)
return results;
return results;
default:
throw new InvalidOperationException($"Invalid Stop Condition: StopCondition");
internal void AddRule(Predicate<T> predicate, string errorMessage)
Func<T, ValidateResult<T>> rule = source =>
if (predicate(source))
return new Success<T>(source);
return new Failure<T>(source, errorMessage);
;
m_rules.Add(rule);
internal void AddWarning(Predicate<T> predicate, string warningMessage)
Func<T, ValidateResult<T>> rule = source =>
if (predicate(source))
return new Success<T>(source);
return new Warning<T>(source, warningMessage);
;
m_rules.Add(rule);
And the rules are defined as extension methods as:
public static class ValidationRules
// Helper method - not a rule
private static string GetDefaultMessage(this Expression expression, string format)
ValidateExpressionVisitor visitor = new ValidateExpressionVisitor();
visitor.Visit(expression);
return string.Format(format, visitor.Message);
public static Validator<T> NotNull<T, TMember>(this Validator<T> validator, Expression<Func<T, TMember>> expression, string errorMessage)
errorMessage = errorMessage ?? expression.GetDefaultMessage("0 is null");
var getter = expression.Compile();
Predicate<T> predicate = source => getter(source) != null;
validator.AddRule(predicate, errorMessage);
return validator;
public static Validator<T> MustBeNull<T, TMember>(this Validator<T> validator, Expression<Func<T, TMember>> expression, string errorMessage)
errorMessage = errorMessage ?? expression.GetDefaultMessage("0 is not null");
var getter = expression.Compile();
Predicate<T> predicate = source => getter(source) == null;
validator.AddRule(predicate, errorMessage);
return validator;
public static Validator<T> IsTrue<T>(this Validator<T> validator, Expression<Predicate<T>> predicate, string errorMessage)
errorMessage = errorMessage ?? predicate.GetDefaultMessage("0 is not true");
validator.AddRule(predicate.Compile(), errorMessage);
return validator;
public static Validator<T> WarnIfTrue<T>(this Validator<T> validator, Expression<Predicate<T>> predicate, string message)
message = message ?? predicate.GetDefaultMessage("0 is true");
validator.AddWarning(src => !predicate.Compile()(src), message);
return validator;
public static Validator<T> IsFalse<T>(this Validator<T> validator, Expression<Predicate<T>> predicate, string errorMessage)
errorMessage = errorMessage ?? predicate.GetDefaultMessage("0 is not false");
validator.AddRule(src => !predicate.Compile()(src), errorMessage);
return validator;
public static Validator<T> WarnIfFalse<T>(this Validator<T> validator, Expression<Predicate<T>> predicate, string message)
message = message ?? predicate.GetDefaultMessage("0 is false");
validator.AddWarning(predicate.Compile(), message);
return validator;
public static Validator<T> Match<T>(this Validator<T> validator, Expression<Func<T, string>> expression, string pattern, string errorMessage)
errorMessage = errorMessage ?? $@"expression.GetDefaultMessage("") doesn't match pattern: ""pattern""";
var getter = expression.Compile();
Predicate<T> predicate = source => Regex.IsMatch(getter(source), pattern);
validator.AddRule(predicate, errorMessage);
return validator;
New rules can easily be added when needed.
The result of each validation can either be Success<T>
, Warning<T>
or Failure<T>
:
public abstract class ValidateResult<T>
public ValidateResult(T source)
Source = source;
public T Source get;
public class Success<T> : ValidateResult<T>
public Success(T source) : base(source)
public override string ToString()
return "Everything is OK";
public class Failure<T> : ValidateResult<T>
public Failure(T source, string message) : base(source)
Message = message;
public string Message get;
public override string ToString()
return $"Error: Message";
public class Warning<T> : ValidateResult<T>
public Warning(T source, string message) : base(source)
Message = message;
public string Message get;
public override string ToString()
return $"Warning: Message";
The message member of Warning
and Failure
will be either the provided message argument to the rule or an auto generated default.
A convenient api:
public static class ValidationExtensions
public static IReadOnlyList<ValidateResult<T>> ValidateWith<T>(this T source, Validator<T> validator)
if (source == null) throw new ArgumentNullException(nameof(source));
if (validator == null) throw new ArgumentNullException(nameof(validator));
return validator.Validate(source);
The default error/warning messages are found using a simple ExpressionVisitor
:
internal class ValidateExpressionVisitor : ExpressionVisitor
public ValidateExpressionVisitor()
public string Message get; private set;
protected override Expression VisitLambda<T>(Expression<T> node)
Message = node.Body.ToString();
return base.VisitLambda(node);
This is very basic, and is intended only for test, development and debugging.
Any comments are welcome.
c# validation fluent-interface
$endgroup$
add a comment |
$begingroup$
Inspired by this question by t3chb0t and as an elaboration of my own answer, I have written the following solution. My goal was to reduce complexity both in implementation and use. Eventually - I have to admit - the implementation ended up being rather complex - but in my flavor; but in terms of ease of use, I think I succeeded. My original idea was inspired by Railway Oriented Programming, but I don't think I can claim to conform to that in the following.
The use case is as follows:
private static void ValidationTest()
n)e Street$", "Street Name doesn't conform to the pattern");
DoTheValidation(validator, Tester);
private static void ValidationTestDefaultErrorMessages()
n)e Street$", null);
DoTheValidation(validator, Tester);
private static void DoTheValidation<T>(Validator<T> validator, T source)
var result = source.ValidateWith(validator);
Console.WriteLine("The following Errors were found: ");
foreach (ValidateResult<T> failure in result.Where(r => (r as Success<T>) is null))
Console.WriteLine(failure);
private class Person
public string FirstName get; set;
public string LastName get; set;
public Address Address get; set;
public int Age get; set;
private class Address
public string Street get; set;
private static readonly Person Tester = new Person
FirstName = "Cookie",
LastName = "Monster",
Age = 45,
Address = new Address
Street = "Sesame Street"
;
As shown, it's possible to add validation rules in an easy fluent manner.
The ValidationStopConditions
is defined as:
public enum ValidationStopConditions
RunAll = 1,
StopOnFailure = 2,
StopOnWarning = 3
and determines if all rules should be run no matter what happens or if the validation stops on first failure or warning.
The Validator
class looks like:
public static class Validator
public static Validator<TSource> For<TSource>(ValidationStopConditions stopCondition = ValidationStopConditions.RunAll) => new Validator<TSource>(stopCondition);
public class Validator<T>
List<Func<T, ValidateResult<T>>> m_rules = new List<Func<T, ValidateResult<T>>>();
public Validator(ValidationStopConditions stopCondition)
StopCondition = stopCondition;
public ValidationStopConditions StopCondition get;
public IReadOnlyList<ValidateResult<T>> Validate(T source)
if (source == null) return Enumerable.Empty<ValidateResult<T>>().ToList();
switch (StopCondition)
case ValidationStopConditions.RunAll:
return m_rules.Select(rule => rule(source)).ToList();
case ValidationStopConditions.StopOnFailure:
List<ValidateResult<T>> results = new List<ValidateResult<T>>();
foreach (var rule in m_rules)
var result = rule(source);
results.Add(result);
if (result is Failure<T>)
return results;
return results;
case ValidationStopConditions.StopOnWarning:
List<ValidateResult<T>> results = new List<ValidateResult<T>>();
foreach (var rule in m_rules)
var result = rule(source);
results.Add(result);
if (result is Warning<T>)
return results;
return results;
default:
throw new InvalidOperationException($"Invalid Stop Condition: StopCondition");
internal void AddRule(Predicate<T> predicate, string errorMessage)
Func<T, ValidateResult<T>> rule = source =>
if (predicate(source))
return new Success<T>(source);
return new Failure<T>(source, errorMessage);
;
m_rules.Add(rule);
internal void AddWarning(Predicate<T> predicate, string warningMessage)
Func<T, ValidateResult<T>> rule = source =>
if (predicate(source))
return new Success<T>(source);
return new Warning<T>(source, warningMessage);
;
m_rules.Add(rule);
And the rules are defined as extension methods as:
public static class ValidationRules
// Helper method - not a rule
private static string GetDefaultMessage(this Expression expression, string format)
ValidateExpressionVisitor visitor = new ValidateExpressionVisitor();
visitor.Visit(expression);
return string.Format(format, visitor.Message);
public static Validator<T> NotNull<T, TMember>(this Validator<T> validator, Expression<Func<T, TMember>> expression, string errorMessage)
errorMessage = errorMessage ?? expression.GetDefaultMessage("0 is null");
var getter = expression.Compile();
Predicate<T> predicate = source => getter(source) != null;
validator.AddRule(predicate, errorMessage);
return validator;
public static Validator<T> MustBeNull<T, TMember>(this Validator<T> validator, Expression<Func<T, TMember>> expression, string errorMessage)
errorMessage = errorMessage ?? expression.GetDefaultMessage("0 is not null");
var getter = expression.Compile();
Predicate<T> predicate = source => getter(source) == null;
validator.AddRule(predicate, errorMessage);
return validator;
public static Validator<T> IsTrue<T>(this Validator<T> validator, Expression<Predicate<T>> predicate, string errorMessage)
errorMessage = errorMessage ?? predicate.GetDefaultMessage("0 is not true");
validator.AddRule(predicate.Compile(), errorMessage);
return validator;
public static Validator<T> WarnIfTrue<T>(this Validator<T> validator, Expression<Predicate<T>> predicate, string message)
message = message ?? predicate.GetDefaultMessage("0 is true");
validator.AddWarning(src => !predicate.Compile()(src), message);
return validator;
public static Validator<T> IsFalse<T>(this Validator<T> validator, Expression<Predicate<T>> predicate, string errorMessage)
errorMessage = errorMessage ?? predicate.GetDefaultMessage("0 is not false");
validator.AddRule(src => !predicate.Compile()(src), errorMessage);
return validator;
public static Validator<T> WarnIfFalse<T>(this Validator<T> validator, Expression<Predicate<T>> predicate, string message)
message = message ?? predicate.GetDefaultMessage("0 is false");
validator.AddWarning(predicate.Compile(), message);
return validator;
public static Validator<T> Match<T>(this Validator<T> validator, Expression<Func<T, string>> expression, string pattern, string errorMessage)
errorMessage = errorMessage ?? $@"expression.GetDefaultMessage("") doesn't match pattern: ""pattern""";
var getter = expression.Compile();
Predicate<T> predicate = source => Regex.IsMatch(getter(source), pattern);
validator.AddRule(predicate, errorMessage);
return validator;
New rules can easily be added when needed.
The result of each validation can either be Success<T>
, Warning<T>
or Failure<T>
:
public abstract class ValidateResult<T>
public ValidateResult(T source)
Source = source;
public T Source get;
public class Success<T> : ValidateResult<T>
public Success(T source) : base(source)
public override string ToString()
return "Everything is OK";
public class Failure<T> : ValidateResult<T>
public Failure(T source, string message) : base(source)
Message = message;
public string Message get;
public override string ToString()
return $"Error: Message";
public class Warning<T> : ValidateResult<T>
public Warning(T source, string message) : base(source)
Message = message;
public string Message get;
public override string ToString()
return $"Warning: Message";
The message member of Warning
and Failure
will be either the provided message argument to the rule or an auto generated default.
A convenient api:
public static class ValidationExtensions
public static IReadOnlyList<ValidateResult<T>> ValidateWith<T>(this T source, Validator<T> validator)
if (source == null) throw new ArgumentNullException(nameof(source));
if (validator == null) throw new ArgumentNullException(nameof(validator));
return validator.Validate(source);
The default error/warning messages are found using a simple ExpressionVisitor
:
internal class ValidateExpressionVisitor : ExpressionVisitor
public ValidateExpressionVisitor()
public string Message get; private set;
protected override Expression VisitLambda<T>(Expression<T> node)
Message = node.Body.ToString();
return base.VisitLambda(node);
This is very basic, and is intended only for test, development and debugging.
Any comments are welcome.
c# validation fluent-interface
$endgroup$
2
$begingroup$
the implementation ended up being rather complex - it virtually always does ;-]
$endgroup$
– t3chb0t
11 hours ago
2
$begingroup$
@t3chb0t: But I naively hope every time..
$endgroup$
– Henrik Hansen
11 hours ago
add a comment |
$begingroup$
Inspired by this question by t3chb0t and as an elaboration of my own answer, I have written the following solution. My goal was to reduce complexity both in implementation and use. Eventually - I have to admit - the implementation ended up being rather complex - but in my flavor; but in terms of ease of use, I think I succeeded. My original idea was inspired by Railway Oriented Programming, but I don't think I can claim to conform to that in the following.
The use case is as follows:
private static void ValidationTest()
n)e Street$", "Street Name doesn't conform to the pattern");
DoTheValidation(validator, Tester);
private static void ValidationTestDefaultErrorMessages()
n)e Street$", null);
DoTheValidation(validator, Tester);
private static void DoTheValidation<T>(Validator<T> validator, T source)
var result = source.ValidateWith(validator);
Console.WriteLine("The following Errors were found: ");
foreach (ValidateResult<T> failure in result.Where(r => (r as Success<T>) is null))
Console.WriteLine(failure);
private class Person
public string FirstName get; set;
public string LastName get; set;
public Address Address get; set;
public int Age get; set;
private class Address
public string Street get; set;
private static readonly Person Tester = new Person
FirstName = "Cookie",
LastName = "Monster",
Age = 45,
Address = new Address
Street = "Sesame Street"
;
As shown, it's possible to add validation rules in an easy fluent manner.
The ValidationStopConditions
is defined as:
public enum ValidationStopConditions
RunAll = 1,
StopOnFailure = 2,
StopOnWarning = 3
and determines if all rules should be run no matter what happens or if the validation stops on first failure or warning.
The Validator
class looks like:
public static class Validator
public static Validator<TSource> For<TSource>(ValidationStopConditions stopCondition = ValidationStopConditions.RunAll) => new Validator<TSource>(stopCondition);
public class Validator<T>
List<Func<T, ValidateResult<T>>> m_rules = new List<Func<T, ValidateResult<T>>>();
public Validator(ValidationStopConditions stopCondition)
StopCondition = stopCondition;
public ValidationStopConditions StopCondition get;
public IReadOnlyList<ValidateResult<T>> Validate(T source)
if (source == null) return Enumerable.Empty<ValidateResult<T>>().ToList();
switch (StopCondition)
case ValidationStopConditions.RunAll:
return m_rules.Select(rule => rule(source)).ToList();
case ValidationStopConditions.StopOnFailure:
List<ValidateResult<T>> results = new List<ValidateResult<T>>();
foreach (var rule in m_rules)
var result = rule(source);
results.Add(result);
if (result is Failure<T>)
return results;
return results;
case ValidationStopConditions.StopOnWarning:
List<ValidateResult<T>> results = new List<ValidateResult<T>>();
foreach (var rule in m_rules)
var result = rule(source);
results.Add(result);
if (result is Warning<T>)
return results;
return results;
default:
throw new InvalidOperationException($"Invalid Stop Condition: StopCondition");
internal void AddRule(Predicate<T> predicate, string errorMessage)
Func<T, ValidateResult<T>> rule = source =>
if (predicate(source))
return new Success<T>(source);
return new Failure<T>(source, errorMessage);
;
m_rules.Add(rule);
internal void AddWarning(Predicate<T> predicate, string warningMessage)
Func<T, ValidateResult<T>> rule = source =>
if (predicate(source))
return new Success<T>(source);
return new Warning<T>(source, warningMessage);
;
m_rules.Add(rule);
And the rules are defined as extension methods as:
public static class ValidationRules
// Helper method - not a rule
private static string GetDefaultMessage(this Expression expression, string format)
ValidateExpressionVisitor visitor = new ValidateExpressionVisitor();
visitor.Visit(expression);
return string.Format(format, visitor.Message);
public static Validator<T> NotNull<T, TMember>(this Validator<T> validator, Expression<Func<T, TMember>> expression, string errorMessage)
errorMessage = errorMessage ?? expression.GetDefaultMessage("0 is null");
var getter = expression.Compile();
Predicate<T> predicate = source => getter(source) != null;
validator.AddRule(predicate, errorMessage);
return validator;
public static Validator<T> MustBeNull<T, TMember>(this Validator<T> validator, Expression<Func<T, TMember>> expression, string errorMessage)
errorMessage = errorMessage ?? expression.GetDefaultMessage("0 is not null");
var getter = expression.Compile();
Predicate<T> predicate = source => getter(source) == null;
validator.AddRule(predicate, errorMessage);
return validator;
public static Validator<T> IsTrue<T>(this Validator<T> validator, Expression<Predicate<T>> predicate, string errorMessage)
errorMessage = errorMessage ?? predicate.GetDefaultMessage("0 is not true");
validator.AddRule(predicate.Compile(), errorMessage);
return validator;
public static Validator<T> WarnIfTrue<T>(this Validator<T> validator, Expression<Predicate<T>> predicate, string message)
message = message ?? predicate.GetDefaultMessage("0 is true");
validator.AddWarning(src => !predicate.Compile()(src), message);
return validator;
public static Validator<T> IsFalse<T>(this Validator<T> validator, Expression<Predicate<T>> predicate, string errorMessage)
errorMessage = errorMessage ?? predicate.GetDefaultMessage("0 is not false");
validator.AddRule(src => !predicate.Compile()(src), errorMessage);
return validator;
public static Validator<T> WarnIfFalse<T>(this Validator<T> validator, Expression<Predicate<T>> predicate, string message)
message = message ?? predicate.GetDefaultMessage("0 is false");
validator.AddWarning(predicate.Compile(), message);
return validator;
public static Validator<T> Match<T>(this Validator<T> validator, Expression<Func<T, string>> expression, string pattern, string errorMessage)
errorMessage = errorMessage ?? $@"expression.GetDefaultMessage("") doesn't match pattern: ""pattern""";
var getter = expression.Compile();
Predicate<T> predicate = source => Regex.IsMatch(getter(source), pattern);
validator.AddRule(predicate, errorMessage);
return validator;
New rules can easily be added when needed.
The result of each validation can either be Success<T>
, Warning<T>
or Failure<T>
:
public abstract class ValidateResult<T>
public ValidateResult(T source)
Source = source;
public T Source get;
public class Success<T> : ValidateResult<T>
public Success(T source) : base(source)
public override string ToString()
return "Everything is OK";
public class Failure<T> : ValidateResult<T>
public Failure(T source, string message) : base(source)
Message = message;
public string Message get;
public override string ToString()
return $"Error: Message";
public class Warning<T> : ValidateResult<T>
public Warning(T source, string message) : base(source)
Message = message;
public string Message get;
public override string ToString()
return $"Warning: Message";
The message member of Warning
and Failure
will be either the provided message argument to the rule or an auto generated default.
A convenient api:
public static class ValidationExtensions
public static IReadOnlyList<ValidateResult<T>> ValidateWith<T>(this T source, Validator<T> validator)
if (source == null) throw new ArgumentNullException(nameof(source));
if (validator == null) throw new ArgumentNullException(nameof(validator));
return validator.Validate(source);
The default error/warning messages are found using a simple ExpressionVisitor
:
internal class ValidateExpressionVisitor : ExpressionVisitor
public ValidateExpressionVisitor()
public string Message get; private set;
protected override Expression VisitLambda<T>(Expression<T> node)
Message = node.Body.ToString();
return base.VisitLambda(node);
This is very basic, and is intended only for test, development and debugging.
Any comments are welcome.
c# validation fluent-interface
$endgroup$
Inspired by this question by t3chb0t and as an elaboration of my own answer, I have written the following solution. My goal was to reduce complexity both in implementation and use. Eventually - I have to admit - the implementation ended up being rather complex - but in my flavor; but in terms of ease of use, I think I succeeded. My original idea was inspired by Railway Oriented Programming, but I don't think I can claim to conform to that in the following.
The use case is as follows:
private static void ValidationTest()
n)e Street$", "Street Name doesn't conform to the pattern");
DoTheValidation(validator, Tester);
private static void ValidationTestDefaultErrorMessages()
n)e Street$", null);
DoTheValidation(validator, Tester);
private static void DoTheValidation<T>(Validator<T> validator, T source)
var result = source.ValidateWith(validator);
Console.WriteLine("The following Errors were found: ");
foreach (ValidateResult<T> failure in result.Where(r => (r as Success<T>) is null))
Console.WriteLine(failure);
private class Person
public string FirstName get; set;
public string LastName get; set;
public Address Address get; set;
public int Age get; set;
private class Address
public string Street get; set;
private static readonly Person Tester = new Person
FirstName = "Cookie",
LastName = "Monster",
Age = 45,
Address = new Address
Street = "Sesame Street"
;
As shown, it's possible to add validation rules in an easy fluent manner.
The ValidationStopConditions
is defined as:
public enum ValidationStopConditions
RunAll = 1,
StopOnFailure = 2,
StopOnWarning = 3
and determines if all rules should be run no matter what happens or if the validation stops on first failure or warning.
The Validator
class looks like:
public static class Validator
public static Validator<TSource> For<TSource>(ValidationStopConditions stopCondition = ValidationStopConditions.RunAll) => new Validator<TSource>(stopCondition);
public class Validator<T>
List<Func<T, ValidateResult<T>>> m_rules = new List<Func<T, ValidateResult<T>>>();
public Validator(ValidationStopConditions stopCondition)
StopCondition = stopCondition;
public ValidationStopConditions StopCondition get;
public IReadOnlyList<ValidateResult<T>> Validate(T source)
if (source == null) return Enumerable.Empty<ValidateResult<T>>().ToList();
switch (StopCondition)
case ValidationStopConditions.RunAll:
return m_rules.Select(rule => rule(source)).ToList();
case ValidationStopConditions.StopOnFailure:
List<ValidateResult<T>> results = new List<ValidateResult<T>>();
foreach (var rule in m_rules)
var result = rule(source);
results.Add(result);
if (result is Failure<T>)
return results;
return results;
case ValidationStopConditions.StopOnWarning:
List<ValidateResult<T>> results = new List<ValidateResult<T>>();
foreach (var rule in m_rules)
var result = rule(source);
results.Add(result);
if (result is Warning<T>)
return results;
return results;
default:
throw new InvalidOperationException($"Invalid Stop Condition: StopCondition");
internal void AddRule(Predicate<T> predicate, string errorMessage)
Func<T, ValidateResult<T>> rule = source =>
if (predicate(source))
return new Success<T>(source);
return new Failure<T>(source, errorMessage);
;
m_rules.Add(rule);
internal void AddWarning(Predicate<T> predicate, string warningMessage)
Func<T, ValidateResult<T>> rule = source =>
if (predicate(source))
return new Success<T>(source);
return new Warning<T>(source, warningMessage);
;
m_rules.Add(rule);
And the rules are defined as extension methods as:
public static class ValidationRules
// Helper method - not a rule
private static string GetDefaultMessage(this Expression expression, string format)
ValidateExpressionVisitor visitor = new ValidateExpressionVisitor();
visitor.Visit(expression);
return string.Format(format, visitor.Message);
public static Validator<T> NotNull<T, TMember>(this Validator<T> validator, Expression<Func<T, TMember>> expression, string errorMessage)
errorMessage = errorMessage ?? expression.GetDefaultMessage("0 is null");
var getter = expression.Compile();
Predicate<T> predicate = source => getter(source) != null;
validator.AddRule(predicate, errorMessage);
return validator;
public static Validator<T> MustBeNull<T, TMember>(this Validator<T> validator, Expression<Func<T, TMember>> expression, string errorMessage)
errorMessage = errorMessage ?? expression.GetDefaultMessage("0 is not null");
var getter = expression.Compile();
Predicate<T> predicate = source => getter(source) == null;
validator.AddRule(predicate, errorMessage);
return validator;
public static Validator<T> IsTrue<T>(this Validator<T> validator, Expression<Predicate<T>> predicate, string errorMessage)
errorMessage = errorMessage ?? predicate.GetDefaultMessage("0 is not true");
validator.AddRule(predicate.Compile(), errorMessage);
return validator;
public static Validator<T> WarnIfTrue<T>(this Validator<T> validator, Expression<Predicate<T>> predicate, string message)
message = message ?? predicate.GetDefaultMessage("0 is true");
validator.AddWarning(src => !predicate.Compile()(src), message);
return validator;
public static Validator<T> IsFalse<T>(this Validator<T> validator, Expression<Predicate<T>> predicate, string errorMessage)
errorMessage = errorMessage ?? predicate.GetDefaultMessage("0 is not false");
validator.AddRule(src => !predicate.Compile()(src), errorMessage);
return validator;
public static Validator<T> WarnIfFalse<T>(this Validator<T> validator, Expression<Predicate<T>> predicate, string message)
message = message ?? predicate.GetDefaultMessage("0 is false");
validator.AddWarning(predicate.Compile(), message);
return validator;
public static Validator<T> Match<T>(this Validator<T> validator, Expression<Func<T, string>> expression, string pattern, string errorMessage)
errorMessage = errorMessage ?? $@"expression.GetDefaultMessage("") doesn't match pattern: ""pattern""";
var getter = expression.Compile();
Predicate<T> predicate = source => Regex.IsMatch(getter(source), pattern);
validator.AddRule(predicate, errorMessage);
return validator;
New rules can easily be added when needed.
The result of each validation can either be Success<T>
, Warning<T>
or Failure<T>
:
public abstract class ValidateResult<T>
public ValidateResult(T source)
Source = source;
public T Source get;
public class Success<T> : ValidateResult<T>
public Success(T source) : base(source)
public override string ToString()
return "Everything is OK";
public class Failure<T> : ValidateResult<T>
public Failure(T source, string message) : base(source)
Message = message;
public string Message get;
public override string ToString()
return $"Error: Message";
public class Warning<T> : ValidateResult<T>
public Warning(T source, string message) : base(source)
Message = message;
public string Message get;
public override string ToString()
return $"Warning: Message";
The message member of Warning
and Failure
will be either the provided message argument to the rule or an auto generated default.
A convenient api:
public static class ValidationExtensions
public static IReadOnlyList<ValidateResult<T>> ValidateWith<T>(this T source, Validator<T> validator)
if (source == null) throw new ArgumentNullException(nameof(source));
if (validator == null) throw new ArgumentNullException(nameof(validator));
return validator.Validate(source);
The default error/warning messages are found using a simple ExpressionVisitor
:
internal class ValidateExpressionVisitor : ExpressionVisitor
public ValidateExpressionVisitor()
public string Message get; private set;
protected override Expression VisitLambda<T>(Expression<T> node)
Message = node.Body.ToString();
return base.VisitLambda(node);
This is very basic, and is intended only for test, development and debugging.
Any comments are welcome.
c# validation fluent-interface
c# validation fluent-interface
edited 11 hours ago
Henrik Hansen
asked 12 hours ago
Henrik HansenHenrik Hansen
9,6181 gold badge13 silver badges34 bronze badges
9,6181 gold badge13 silver badges34 bronze badges
2
$begingroup$
the implementation ended up being rather complex - it virtually always does ;-]
$endgroup$
– t3chb0t
11 hours ago
2
$begingroup$
@t3chb0t: But I naively hope every time..
$endgroup$
– Henrik Hansen
11 hours ago
add a comment |
2
$begingroup$
the implementation ended up being rather complex - it virtually always does ;-]
$endgroup$
– t3chb0t
11 hours ago
2
$begingroup$
@t3chb0t: But I naively hope every time..
$endgroup$
– Henrik Hansen
11 hours ago
2
2
$begingroup$
the implementation ended up being rather complex - it virtually always does ;-]
$endgroup$
– t3chb0t
11 hours ago
$begingroup$
the implementation ended up being rather complex - it virtually always does ;-]
$endgroup$
– t3chb0t
11 hours ago
2
2
$begingroup$
@t3chb0t: But I naively hope every time..
$endgroup$
– Henrik Hansen
11 hours ago
$begingroup$
@t3chb0t: But I naively hope every time..
$endgroup$
– Henrik Hansen
11 hours ago
add a comment |
2 Answers
2
active
oldest
votes
$begingroup$
This API does feel fluent for consumers to use.
You have also included some features I missed in the post you were inspired by.
- various severity levels [warning, error]
- custom error messages (although t3chb0t did comment he was working on this)
What I'm still missing is a way to throw an exception if I want to. Currently, your API is a sand-box. You could foresee ThrowOnError
and ThrowOnWarning
. Perhaps also with overloads that take an exception type. If multiple errors/warnings are found, they should be wrapped in an AggregateException
.
private static void DoTheValidation<T>(Validator<T> validator, T source)
var result = source.ValidateWith(validator).ThrowOnError().Result;
$endgroup$
1
$begingroup$
OK, I understand, what you mean, and I will let it be a candidate for further development, but I intentionally try to avoid throwing - adapted from the Railway pattern. But your suggested example could easily be implemented as an extension toIEnumerable<ValidateResult<T>>
$endgroup$
– Henrik Hansen
10 hours ago
1
$begingroup$
I got the impression that there is a misunderstanding to the intention of such validators. To me, their main purpose is to validate data-objects so throwing custom exceptions is in my opinion rarely required and it's not really necessary to open this API to the end-user (although easy to code). This means that a DTO is either valid, invalid or is something inbetween where data could be corrected so the validation yields just warnings which is a new concept (to me) - but I like it very much as this is a point where possible fixing extensions could be implemented.
$endgroup$
– t3chb0t
1 hour ago
1
$begingroup$
I also think thatAggregateException
is for something else. Here, we don't produce exceptions but validation results and since they are not real exceptions, they shouldn't be aggregated. Instead, there should be only one final exception telling me that a dto is invalid. The only useful thing about exceptions is their name and message anyway so they sholdn't contain any other data (based on experience).
$endgroup$
– t3chb0t
55 mins ago
1
$begingroup$
In case of DTOs it's often even desirable to not throw; when you got hundereds of dtos where a great deal of them might be invalid it could turn out to a huge bottleck so it's good to be able to avoid exceptions.
$endgroup$
– t3chb0t
55 mins ago
1
$begingroup$
@t3chb0t You guys are on the verge of building a compiler here. At least an 'optimizer' part handling and manipulating expressions.
$endgroup$
– dfhwze
12 mins ago
|
show 4 more comments
$begingroup$
Fluent API
Fluent APIs are generally very useful but one has to be very careful with them as there is a chance of making them overfluent. This means that you try to create an API for every possible combination like:
var validator = Validator.For<Person>(ValidationStopConditions.RunAll)
.WarnIfTrue(p => p.Age > 50, "Person is older than 50")
.WarnIfFalse(p => p.Age < 50, "Person is older than 50")
.NotNull(p => p.LastName, "LastName is null")
.MustBeNull(p => p.LastName, "LastName should be null")
.IsTrue(p => p.FirstName.Length > 3, "First Name is too short")
.IsFalse(p => p.FirstName.StartsWith("Cos"), "First Name starts with Coo")
.Match(p => p.Address.Street, @"^Sesa(m|n)e Street$", "Street Name doesn't conform to the pattern");
Instead, I think it's better to make them composable so that end-users have the freedom of creating expressions not anticipated by the API creator. (I made this mistake in my utility too (by having Null
and NotNull
instead of using a modifier) so I have redesigned it since).
This would both reduce the number of available APIs and the learning curve for the end-user and make also coding and testing easier because there would be much less combinations.
Consider this:
Validator
.For<Person>()
.True(p => p.Age > 50)
// then modifiers can be chained...
.Exclude() // <- or Exclude/Not/Negate etc,
.Require() // <- upgrades this check to yield an error instead of a warning
Without such modifiers like Exclude/Not
or Warn
you would need to create these versions for each and every rule. Then you add a new one... and you can create it three or four times again. Now, what happens if you create a new modifier? You'll have to create even more versions of all existing APIs. You would end up with so many of them...
Consistency
There should be more consistency between the APIs. So, when there is MustBeNull
then there should also be MustBeTrue
instead of just IsTrue
, etc.
Validation levels
I like that idea of having results other than just black-n-white but also a gray Warning
inbetween. This opens a bunch of whole new possibilities such as fixing property values.
Handling validations
I think the first switch is (might be) dagerous:
public enum ValidationStopConditions
RunAll = 1,
StopOnFailure = 2,
StopOnWarning = 3
I haven't exactly analyzed how rules are handled but it might crash when person.FirstName
is null
and later person.FirstName > 3
is used. The idea of having Error
rule was to break here because it's pointless to check other conditions that rely on that one. This should signal an unrecoverable validation error. But I guess it just yields through all other rules (according to ROP).
Creating & compiling expressions
Expressions can be very tricky but they are at the same time super useful for generating error messages and it's nice to see that model here too. However some of them are less useful than other. Let's take a look at this one:
var getter = expression.Compile();
Predicate<T> predicate = source => Regex.IsMatch(getter(source), pattern);
The generated expression string won't show the Regex.IsMatch
because it's not part of the expression. Unless it's by design, I suggest the follwing approach (taken from my new APIs). Here, you build a new expression containing all calls so that they are rendered into the final string.
public static LambdaExpression Match<T>(Expression<Func<T, string>> expression, string pattern, RegexOptions options)
var isMatchMethod = typeof(Regex).GetMethod(nameof(Regex.IsMatch), new [] typeof(string), typeof(string), typeof(RegexOptions) );
return
Expression.Lambda(
Expression.Call(
isMatchMethod,
expression.Body,
Expression.Constant(pattern),
Expression.Constant(options)),
expression.Parameters
);
Naming
I would rename the ValidateExpressionVisitor
to something more intuitive like ValidationMessageCreator
. It doesn't have to have the Visitor
ending as it rarely fits into what a visitor is actually doing. I suggest dropping that suffix.
$endgroup$
add a comment |
Your Answer
StackExchange.ifUsing("editor", function ()
StackExchange.using("externalEditor", function ()
StackExchange.using("snippets", function ()
StackExchange.snippets.init();
);
);
, "code-snippets");
StackExchange.ready(function()
var channelOptions =
tags: "".split(" "),
id: "196"
;
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function()
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled)
StackExchange.using("snippets", function()
createEditor();
);
else
createEditor();
);
function createEditor()
StackExchange.prepareEditor(
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader:
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
,
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
);
);
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f222821%2ffluently-validation-of-objects%23new-answer', 'question_page');
);
Post as a guest
Required, but never shown
2 Answers
2
active
oldest
votes
2 Answers
2
active
oldest
votes
active
oldest
votes
active
oldest
votes
$begingroup$
This API does feel fluent for consumers to use.
You have also included some features I missed in the post you were inspired by.
- various severity levels [warning, error]
- custom error messages (although t3chb0t did comment he was working on this)
What I'm still missing is a way to throw an exception if I want to. Currently, your API is a sand-box. You could foresee ThrowOnError
and ThrowOnWarning
. Perhaps also with overloads that take an exception type. If multiple errors/warnings are found, they should be wrapped in an AggregateException
.
private static void DoTheValidation<T>(Validator<T> validator, T source)
var result = source.ValidateWith(validator).ThrowOnError().Result;
$endgroup$
1
$begingroup$
OK, I understand, what you mean, and I will let it be a candidate for further development, but I intentionally try to avoid throwing - adapted from the Railway pattern. But your suggested example could easily be implemented as an extension toIEnumerable<ValidateResult<T>>
$endgroup$
– Henrik Hansen
10 hours ago
1
$begingroup$
I got the impression that there is a misunderstanding to the intention of such validators. To me, their main purpose is to validate data-objects so throwing custom exceptions is in my opinion rarely required and it's not really necessary to open this API to the end-user (although easy to code). This means that a DTO is either valid, invalid or is something inbetween where data could be corrected so the validation yields just warnings which is a new concept (to me) - but I like it very much as this is a point where possible fixing extensions could be implemented.
$endgroup$
– t3chb0t
1 hour ago
1
$begingroup$
I also think thatAggregateException
is for something else. Here, we don't produce exceptions but validation results and since they are not real exceptions, they shouldn't be aggregated. Instead, there should be only one final exception telling me that a dto is invalid. The only useful thing about exceptions is their name and message anyway so they sholdn't contain any other data (based on experience).
$endgroup$
– t3chb0t
55 mins ago
1
$begingroup$
In case of DTOs it's often even desirable to not throw; when you got hundereds of dtos where a great deal of them might be invalid it could turn out to a huge bottleck so it's good to be able to avoid exceptions.
$endgroup$
– t3chb0t
55 mins ago
1
$begingroup$
@t3chb0t You guys are on the verge of building a compiler here. At least an 'optimizer' part handling and manipulating expressions.
$endgroup$
– dfhwze
12 mins ago
|
show 4 more comments
$begingroup$
This API does feel fluent for consumers to use.
You have also included some features I missed in the post you were inspired by.
- various severity levels [warning, error]
- custom error messages (although t3chb0t did comment he was working on this)
What I'm still missing is a way to throw an exception if I want to. Currently, your API is a sand-box. You could foresee ThrowOnError
and ThrowOnWarning
. Perhaps also with overloads that take an exception type. If multiple errors/warnings are found, they should be wrapped in an AggregateException
.
private static void DoTheValidation<T>(Validator<T> validator, T source)
var result = source.ValidateWith(validator).ThrowOnError().Result;
$endgroup$
1
$begingroup$
OK, I understand, what you mean, and I will let it be a candidate for further development, but I intentionally try to avoid throwing - adapted from the Railway pattern. But your suggested example could easily be implemented as an extension toIEnumerable<ValidateResult<T>>
$endgroup$
– Henrik Hansen
10 hours ago
1
$begingroup$
I got the impression that there is a misunderstanding to the intention of such validators. To me, their main purpose is to validate data-objects so throwing custom exceptions is in my opinion rarely required and it's not really necessary to open this API to the end-user (although easy to code). This means that a DTO is either valid, invalid or is something inbetween where data could be corrected so the validation yields just warnings which is a new concept (to me) - but I like it very much as this is a point where possible fixing extensions could be implemented.
$endgroup$
– t3chb0t
1 hour ago
1
$begingroup$
I also think thatAggregateException
is for something else. Here, we don't produce exceptions but validation results and since they are not real exceptions, they shouldn't be aggregated. Instead, there should be only one final exception telling me that a dto is invalid. The only useful thing about exceptions is their name and message anyway so they sholdn't contain any other data (based on experience).
$endgroup$
– t3chb0t
55 mins ago
1
$begingroup$
In case of DTOs it's often even desirable to not throw; when you got hundereds of dtos where a great deal of them might be invalid it could turn out to a huge bottleck so it's good to be able to avoid exceptions.
$endgroup$
– t3chb0t
55 mins ago
1
$begingroup$
@t3chb0t You guys are on the verge of building a compiler here. At least an 'optimizer' part handling and manipulating expressions.
$endgroup$
– dfhwze
12 mins ago
|
show 4 more comments
$begingroup$
This API does feel fluent for consumers to use.
You have also included some features I missed in the post you were inspired by.
- various severity levels [warning, error]
- custom error messages (although t3chb0t did comment he was working on this)
What I'm still missing is a way to throw an exception if I want to. Currently, your API is a sand-box. You could foresee ThrowOnError
and ThrowOnWarning
. Perhaps also with overloads that take an exception type. If multiple errors/warnings are found, they should be wrapped in an AggregateException
.
private static void DoTheValidation<T>(Validator<T> validator, T source)
var result = source.ValidateWith(validator).ThrowOnError().Result;
$endgroup$
This API does feel fluent for consumers to use.
You have also included some features I missed in the post you were inspired by.
- various severity levels [warning, error]
- custom error messages (although t3chb0t did comment he was working on this)
What I'm still missing is a way to throw an exception if I want to. Currently, your API is a sand-box. You could foresee ThrowOnError
and ThrowOnWarning
. Perhaps also with overloads that take an exception type. If multiple errors/warnings are found, they should be wrapped in an AggregateException
.
private static void DoTheValidation<T>(Validator<T> validator, T source)
var result = source.ValidateWith(validator).ThrowOnError().Result;
answered 10 hours ago
dfhwzedfhwze
3,3961 gold badge6 silver badges32 bronze badges
3,3961 gold badge6 silver badges32 bronze badges
1
$begingroup$
OK, I understand, what you mean, and I will let it be a candidate for further development, but I intentionally try to avoid throwing - adapted from the Railway pattern. But your suggested example could easily be implemented as an extension toIEnumerable<ValidateResult<T>>
$endgroup$
– Henrik Hansen
10 hours ago
1
$begingroup$
I got the impression that there is a misunderstanding to the intention of such validators. To me, their main purpose is to validate data-objects so throwing custom exceptions is in my opinion rarely required and it's not really necessary to open this API to the end-user (although easy to code). This means that a DTO is either valid, invalid or is something inbetween where data could be corrected so the validation yields just warnings which is a new concept (to me) - but I like it very much as this is a point where possible fixing extensions could be implemented.
$endgroup$
– t3chb0t
1 hour ago
1
$begingroup$
I also think thatAggregateException
is for something else. Here, we don't produce exceptions but validation results and since they are not real exceptions, they shouldn't be aggregated. Instead, there should be only one final exception telling me that a dto is invalid. The only useful thing about exceptions is their name and message anyway so they sholdn't contain any other data (based on experience).
$endgroup$
– t3chb0t
55 mins ago
1
$begingroup$
In case of DTOs it's often even desirable to not throw; when you got hundereds of dtos where a great deal of them might be invalid it could turn out to a huge bottleck so it's good to be able to avoid exceptions.
$endgroup$
– t3chb0t
55 mins ago
1
$begingroup$
@t3chb0t You guys are on the verge of building a compiler here. At least an 'optimizer' part handling and manipulating expressions.
$endgroup$
– dfhwze
12 mins ago
|
show 4 more comments
1
$begingroup$
OK, I understand, what you mean, and I will let it be a candidate for further development, but I intentionally try to avoid throwing - adapted from the Railway pattern. But your suggested example could easily be implemented as an extension toIEnumerable<ValidateResult<T>>
$endgroup$
– Henrik Hansen
10 hours ago
1
$begingroup$
I got the impression that there is a misunderstanding to the intention of such validators. To me, their main purpose is to validate data-objects so throwing custom exceptions is in my opinion rarely required and it's not really necessary to open this API to the end-user (although easy to code). This means that a DTO is either valid, invalid or is something inbetween where data could be corrected so the validation yields just warnings which is a new concept (to me) - but I like it very much as this is a point where possible fixing extensions could be implemented.
$endgroup$
– t3chb0t
1 hour ago
1
$begingroup$
I also think thatAggregateException
is for something else. Here, we don't produce exceptions but validation results and since they are not real exceptions, they shouldn't be aggregated. Instead, there should be only one final exception telling me that a dto is invalid. The only useful thing about exceptions is their name and message anyway so they sholdn't contain any other data (based on experience).
$endgroup$
– t3chb0t
55 mins ago
1
$begingroup$
In case of DTOs it's often even desirable to not throw; when you got hundereds of dtos where a great deal of them might be invalid it could turn out to a huge bottleck so it's good to be able to avoid exceptions.
$endgroup$
– t3chb0t
55 mins ago
1
$begingroup$
@t3chb0t You guys are on the verge of building a compiler here. At least an 'optimizer' part handling and manipulating expressions.
$endgroup$
– dfhwze
12 mins ago
1
1
$begingroup$
OK, I understand, what you mean, and I will let it be a candidate for further development, but I intentionally try to avoid throwing - adapted from the Railway pattern. But your suggested example could easily be implemented as an extension to
IEnumerable<ValidateResult<T>>
$endgroup$
– Henrik Hansen
10 hours ago
$begingroup$
OK, I understand, what you mean, and I will let it be a candidate for further development, but I intentionally try to avoid throwing - adapted from the Railway pattern. But your suggested example could easily be implemented as an extension to
IEnumerable<ValidateResult<T>>
$endgroup$
– Henrik Hansen
10 hours ago
1
1
$begingroup$
I got the impression that there is a misunderstanding to the intention of such validators. To me, their main purpose is to validate data-objects so throwing custom exceptions is in my opinion rarely required and it's not really necessary to open this API to the end-user (although easy to code). This means that a DTO is either valid, invalid or is something inbetween where data could be corrected so the validation yields just warnings which is a new concept (to me) - but I like it very much as this is a point where possible fixing extensions could be implemented.
$endgroup$
– t3chb0t
1 hour ago
$begingroup$
I got the impression that there is a misunderstanding to the intention of such validators. To me, their main purpose is to validate data-objects so throwing custom exceptions is in my opinion rarely required and it's not really necessary to open this API to the end-user (although easy to code). This means that a DTO is either valid, invalid or is something inbetween where data could be corrected so the validation yields just warnings which is a new concept (to me) - but I like it very much as this is a point where possible fixing extensions could be implemented.
$endgroup$
– t3chb0t
1 hour ago
1
1
$begingroup$
I also think that
AggregateException
is for something else. Here, we don't produce exceptions but validation results and since they are not real exceptions, they shouldn't be aggregated. Instead, there should be only one final exception telling me that a dto is invalid. The only useful thing about exceptions is their name and message anyway so they sholdn't contain any other data (based on experience).$endgroup$
– t3chb0t
55 mins ago
$begingroup$
I also think that
AggregateException
is for something else. Here, we don't produce exceptions but validation results and since they are not real exceptions, they shouldn't be aggregated. Instead, there should be only one final exception telling me that a dto is invalid. The only useful thing about exceptions is their name and message anyway so they sholdn't contain any other data (based on experience).$endgroup$
– t3chb0t
55 mins ago
1
1
$begingroup$
In case of DTOs it's often even desirable to not throw; when you got hundereds of dtos where a great deal of them might be invalid it could turn out to a huge bottleck so it's good to be able to avoid exceptions.
$endgroup$
– t3chb0t
55 mins ago
$begingroup$
In case of DTOs it's often even desirable to not throw; when you got hundereds of dtos where a great deal of them might be invalid it could turn out to a huge bottleck so it's good to be able to avoid exceptions.
$endgroup$
– t3chb0t
55 mins ago
1
1
$begingroup$
@t3chb0t You guys are on the verge of building a compiler here. At least an 'optimizer' part handling and manipulating expressions.
$endgroup$
– dfhwze
12 mins ago
$begingroup$
@t3chb0t You guys are on the verge of building a compiler here. At least an 'optimizer' part handling and manipulating expressions.
$endgroup$
– dfhwze
12 mins ago
|
show 4 more comments
$begingroup$
Fluent API
Fluent APIs are generally very useful but one has to be very careful with them as there is a chance of making them overfluent. This means that you try to create an API for every possible combination like:
var validator = Validator.For<Person>(ValidationStopConditions.RunAll)
.WarnIfTrue(p => p.Age > 50, "Person is older than 50")
.WarnIfFalse(p => p.Age < 50, "Person is older than 50")
.NotNull(p => p.LastName, "LastName is null")
.MustBeNull(p => p.LastName, "LastName should be null")
.IsTrue(p => p.FirstName.Length > 3, "First Name is too short")
.IsFalse(p => p.FirstName.StartsWith("Cos"), "First Name starts with Coo")
.Match(p => p.Address.Street, @"^Sesa(m|n)e Street$", "Street Name doesn't conform to the pattern");
Instead, I think it's better to make them composable so that end-users have the freedom of creating expressions not anticipated by the API creator. (I made this mistake in my utility too (by having Null
and NotNull
instead of using a modifier) so I have redesigned it since).
This would both reduce the number of available APIs and the learning curve for the end-user and make also coding and testing easier because there would be much less combinations.
Consider this:
Validator
.For<Person>()
.True(p => p.Age > 50)
// then modifiers can be chained...
.Exclude() // <- or Exclude/Not/Negate etc,
.Require() // <- upgrades this check to yield an error instead of a warning
Without such modifiers like Exclude/Not
or Warn
you would need to create these versions for each and every rule. Then you add a new one... and you can create it three or four times again. Now, what happens if you create a new modifier? You'll have to create even more versions of all existing APIs. You would end up with so many of them...
Consistency
There should be more consistency between the APIs. So, when there is MustBeNull
then there should also be MustBeTrue
instead of just IsTrue
, etc.
Validation levels
I like that idea of having results other than just black-n-white but also a gray Warning
inbetween. This opens a bunch of whole new possibilities such as fixing property values.
Handling validations
I think the first switch is (might be) dagerous:
public enum ValidationStopConditions
RunAll = 1,
StopOnFailure = 2,
StopOnWarning = 3
I haven't exactly analyzed how rules are handled but it might crash when person.FirstName
is null
and later person.FirstName > 3
is used. The idea of having Error
rule was to break here because it's pointless to check other conditions that rely on that one. This should signal an unrecoverable validation error. But I guess it just yields through all other rules (according to ROP).
Creating & compiling expressions
Expressions can be very tricky but they are at the same time super useful for generating error messages and it's nice to see that model here too. However some of them are less useful than other. Let's take a look at this one:
var getter = expression.Compile();
Predicate<T> predicate = source => Regex.IsMatch(getter(source), pattern);
The generated expression string won't show the Regex.IsMatch
because it's not part of the expression. Unless it's by design, I suggest the follwing approach (taken from my new APIs). Here, you build a new expression containing all calls so that they are rendered into the final string.
public static LambdaExpression Match<T>(Expression<Func<T, string>> expression, string pattern, RegexOptions options)
var isMatchMethod = typeof(Regex).GetMethod(nameof(Regex.IsMatch), new [] typeof(string), typeof(string), typeof(RegexOptions) );
return
Expression.Lambda(
Expression.Call(
isMatchMethod,
expression.Body,
Expression.Constant(pattern),
Expression.Constant(options)),
expression.Parameters
);
Naming
I would rename the ValidateExpressionVisitor
to something more intuitive like ValidationMessageCreator
. It doesn't have to have the Visitor
ending as it rarely fits into what a visitor is actually doing. I suggest dropping that suffix.
$endgroup$
add a comment |
$begingroup$
Fluent API
Fluent APIs are generally very useful but one has to be very careful with them as there is a chance of making them overfluent. This means that you try to create an API for every possible combination like:
var validator = Validator.For<Person>(ValidationStopConditions.RunAll)
.WarnIfTrue(p => p.Age > 50, "Person is older than 50")
.WarnIfFalse(p => p.Age < 50, "Person is older than 50")
.NotNull(p => p.LastName, "LastName is null")
.MustBeNull(p => p.LastName, "LastName should be null")
.IsTrue(p => p.FirstName.Length > 3, "First Name is too short")
.IsFalse(p => p.FirstName.StartsWith("Cos"), "First Name starts with Coo")
.Match(p => p.Address.Street, @"^Sesa(m|n)e Street$", "Street Name doesn't conform to the pattern");
Instead, I think it's better to make them composable so that end-users have the freedom of creating expressions not anticipated by the API creator. (I made this mistake in my utility too (by having Null
and NotNull
instead of using a modifier) so I have redesigned it since).
This would both reduce the number of available APIs and the learning curve for the end-user and make also coding and testing easier because there would be much less combinations.
Consider this:
Validator
.For<Person>()
.True(p => p.Age > 50)
// then modifiers can be chained...
.Exclude() // <- or Exclude/Not/Negate etc,
.Require() // <- upgrades this check to yield an error instead of a warning
Without such modifiers like Exclude/Not
or Warn
you would need to create these versions for each and every rule. Then you add a new one... and you can create it three or four times again. Now, what happens if you create a new modifier? You'll have to create even more versions of all existing APIs. You would end up with so many of them...
Consistency
There should be more consistency between the APIs. So, when there is MustBeNull
then there should also be MustBeTrue
instead of just IsTrue
, etc.
Validation levels
I like that idea of having results other than just black-n-white but also a gray Warning
inbetween. This opens a bunch of whole new possibilities such as fixing property values.
Handling validations
I think the first switch is (might be) dagerous:
public enum ValidationStopConditions
RunAll = 1,
StopOnFailure = 2,
StopOnWarning = 3
I haven't exactly analyzed how rules are handled but it might crash when person.FirstName
is null
and later person.FirstName > 3
is used. The idea of having Error
rule was to break here because it's pointless to check other conditions that rely on that one. This should signal an unrecoverable validation error. But I guess it just yields through all other rules (according to ROP).
Creating & compiling expressions
Expressions can be very tricky but they are at the same time super useful for generating error messages and it's nice to see that model here too. However some of them are less useful than other. Let's take a look at this one:
var getter = expression.Compile();
Predicate<T> predicate = source => Regex.IsMatch(getter(source), pattern);
The generated expression string won't show the Regex.IsMatch
because it's not part of the expression. Unless it's by design, I suggest the follwing approach (taken from my new APIs). Here, you build a new expression containing all calls so that they are rendered into the final string.
public static LambdaExpression Match<T>(Expression<Func<T, string>> expression, string pattern, RegexOptions options)
var isMatchMethod = typeof(Regex).GetMethod(nameof(Regex.IsMatch), new [] typeof(string), typeof(string), typeof(RegexOptions) );
return
Expression.Lambda(
Expression.Call(
isMatchMethod,
expression.Body,
Expression.Constant(pattern),
Expression.Constant(options)),
expression.Parameters
);
Naming
I would rename the ValidateExpressionVisitor
to something more intuitive like ValidationMessageCreator
. It doesn't have to have the Visitor
ending as it rarely fits into what a visitor is actually doing. I suggest dropping that suffix.
$endgroup$
add a comment |
$begingroup$
Fluent API
Fluent APIs are generally very useful but one has to be very careful with them as there is a chance of making them overfluent. This means that you try to create an API for every possible combination like:
var validator = Validator.For<Person>(ValidationStopConditions.RunAll)
.WarnIfTrue(p => p.Age > 50, "Person is older than 50")
.WarnIfFalse(p => p.Age < 50, "Person is older than 50")
.NotNull(p => p.LastName, "LastName is null")
.MustBeNull(p => p.LastName, "LastName should be null")
.IsTrue(p => p.FirstName.Length > 3, "First Name is too short")
.IsFalse(p => p.FirstName.StartsWith("Cos"), "First Name starts with Coo")
.Match(p => p.Address.Street, @"^Sesa(m|n)e Street$", "Street Name doesn't conform to the pattern");
Instead, I think it's better to make them composable so that end-users have the freedom of creating expressions not anticipated by the API creator. (I made this mistake in my utility too (by having Null
and NotNull
instead of using a modifier) so I have redesigned it since).
This would both reduce the number of available APIs and the learning curve for the end-user and make also coding and testing easier because there would be much less combinations.
Consider this:
Validator
.For<Person>()
.True(p => p.Age > 50)
// then modifiers can be chained...
.Exclude() // <- or Exclude/Not/Negate etc,
.Require() // <- upgrades this check to yield an error instead of a warning
Without such modifiers like Exclude/Not
or Warn
you would need to create these versions for each and every rule. Then you add a new one... and you can create it three or four times again. Now, what happens if you create a new modifier? You'll have to create even more versions of all existing APIs. You would end up with so many of them...
Consistency
There should be more consistency between the APIs. So, when there is MustBeNull
then there should also be MustBeTrue
instead of just IsTrue
, etc.
Validation levels
I like that idea of having results other than just black-n-white but also a gray Warning
inbetween. This opens a bunch of whole new possibilities such as fixing property values.
Handling validations
I think the first switch is (might be) dagerous:
public enum ValidationStopConditions
RunAll = 1,
StopOnFailure = 2,
StopOnWarning = 3
I haven't exactly analyzed how rules are handled but it might crash when person.FirstName
is null
and later person.FirstName > 3
is used. The idea of having Error
rule was to break here because it's pointless to check other conditions that rely on that one. This should signal an unrecoverable validation error. But I guess it just yields through all other rules (according to ROP).
Creating & compiling expressions
Expressions can be very tricky but they are at the same time super useful for generating error messages and it's nice to see that model here too. However some of them are less useful than other. Let's take a look at this one:
var getter = expression.Compile();
Predicate<T> predicate = source => Regex.IsMatch(getter(source), pattern);
The generated expression string won't show the Regex.IsMatch
because it's not part of the expression. Unless it's by design, I suggest the follwing approach (taken from my new APIs). Here, you build a new expression containing all calls so that they are rendered into the final string.
public static LambdaExpression Match<T>(Expression<Func<T, string>> expression, string pattern, RegexOptions options)
var isMatchMethod = typeof(Regex).GetMethod(nameof(Regex.IsMatch), new [] typeof(string), typeof(string), typeof(RegexOptions) );
return
Expression.Lambda(
Expression.Call(
isMatchMethod,
expression.Body,
Expression.Constant(pattern),
Expression.Constant(options)),
expression.Parameters
);
Naming
I would rename the ValidateExpressionVisitor
to something more intuitive like ValidationMessageCreator
. It doesn't have to have the Visitor
ending as it rarely fits into what a visitor is actually doing. I suggest dropping that suffix.
$endgroup$
Fluent API
Fluent APIs are generally very useful but one has to be very careful with them as there is a chance of making them overfluent. This means that you try to create an API for every possible combination like:
var validator = Validator.For<Person>(ValidationStopConditions.RunAll)
.WarnIfTrue(p => p.Age > 50, "Person is older than 50")
.WarnIfFalse(p => p.Age < 50, "Person is older than 50")
.NotNull(p => p.LastName, "LastName is null")
.MustBeNull(p => p.LastName, "LastName should be null")
.IsTrue(p => p.FirstName.Length > 3, "First Name is too short")
.IsFalse(p => p.FirstName.StartsWith("Cos"), "First Name starts with Coo")
.Match(p => p.Address.Street, @"^Sesa(m|n)e Street$", "Street Name doesn't conform to the pattern");
Instead, I think it's better to make them composable so that end-users have the freedom of creating expressions not anticipated by the API creator. (I made this mistake in my utility too (by having Null
and NotNull
instead of using a modifier) so I have redesigned it since).
This would both reduce the number of available APIs and the learning curve for the end-user and make also coding and testing easier because there would be much less combinations.
Consider this:
Validator
.For<Person>()
.True(p => p.Age > 50)
// then modifiers can be chained...
.Exclude() // <- or Exclude/Not/Negate etc,
.Require() // <- upgrades this check to yield an error instead of a warning
Without such modifiers like Exclude/Not
or Warn
you would need to create these versions for each and every rule. Then you add a new one... and you can create it three or four times again. Now, what happens if you create a new modifier? You'll have to create even more versions of all existing APIs. You would end up with so many of them...
Consistency
There should be more consistency between the APIs. So, when there is MustBeNull
then there should also be MustBeTrue
instead of just IsTrue
, etc.
Validation levels
I like that idea of having results other than just black-n-white but also a gray Warning
inbetween. This opens a bunch of whole new possibilities such as fixing property values.
Handling validations
I think the first switch is (might be) dagerous:
public enum ValidationStopConditions
RunAll = 1,
StopOnFailure = 2,
StopOnWarning = 3
I haven't exactly analyzed how rules are handled but it might crash when person.FirstName
is null
and later person.FirstName > 3
is used. The idea of having Error
rule was to break here because it's pointless to check other conditions that rely on that one. This should signal an unrecoverable validation error. But I guess it just yields through all other rules (according to ROP).
Creating & compiling expressions
Expressions can be very tricky but they are at the same time super useful for generating error messages and it's nice to see that model here too. However some of them are less useful than other. Let's take a look at this one:
var getter = expression.Compile();
Predicate<T> predicate = source => Regex.IsMatch(getter(source), pattern);
The generated expression string won't show the Regex.IsMatch
because it's not part of the expression. Unless it's by design, I suggest the follwing approach (taken from my new APIs). Here, you build a new expression containing all calls so that they are rendered into the final string.
public static LambdaExpression Match<T>(Expression<Func<T, string>> expression, string pattern, RegexOptions options)
var isMatchMethod = typeof(Regex).GetMethod(nameof(Regex.IsMatch), new [] typeof(string), typeof(string), typeof(RegexOptions) );
return
Expression.Lambda(
Expression.Call(
isMatchMethod,
expression.Body,
Expression.Constant(pattern),
Expression.Constant(options)),
expression.Parameters
);
Naming
I would rename the ValidateExpressionVisitor
to something more intuitive like ValidationMessageCreator
. It doesn't have to have the Visitor
ending as it rarely fits into what a visitor is actually doing. I suggest dropping that suffix.
answered 5 mins ago
t3chb0tt3chb0t
36k7 gold badges58 silver badges133 bronze badges
36k7 gold badges58 silver badges133 bronze badges
add a comment |
add a comment |
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f222821%2ffluently-validation-of-objects%23new-answer', 'question_page');
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
2
$begingroup$
the implementation ended up being rather complex - it virtually always does ;-]
$endgroup$
– t3chb0t
11 hours ago
2
$begingroup$
@t3chb0t: But I naively hope every time..
$endgroup$
– Henrik Hansen
11 hours ago