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;








4












$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.










share|improve this question











$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

















4












$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.










share|improve this question











$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













4












4








4





$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.










share|improve this question











$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






share|improve this question















share|improve this question













share|improve this question




share|improve this question








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












  • 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










2 Answers
2






active

oldest

votes


















2












$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;






share|improve this answer









$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 to IEnumerable<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 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




    $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












$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.





share









$endgroup$















    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
    );



    );













    draft saved

    draft discarded


















    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









    2












    $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;






    share|improve this answer









    $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 to IEnumerable<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 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




      $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















    2












    $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;






    share|improve this answer









    $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 to IEnumerable<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 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




      $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













    2












    2








    2





    $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;






    share|improve this answer









    $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;







    share|improve this answer












    share|improve this answer



    share|improve this answer










    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 to IEnumerable<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 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




      $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




      $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




      $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 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




      $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













    1












    $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.





    share









    $endgroup$

















      1












      $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.





      share









      $endgroup$















        1












        1








        1





        $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.





        share









        $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.






        share











        share


        share










        answered 5 mins ago









        t3chb0tt3chb0t

        36k7 gold badges58 silver badges133 bronze badges




        36k7 gold badges58 silver badges133 bronze badges



























            draft saved

            draft discarded
















































            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.




            draft saved


            draft discarded














            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





















































            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







            Popular posts from this blog

            Invision Community Contents History See also References External links Navigation menuProprietaryinvisioncommunity.comIPS Community ForumsIPS Community Forumsthis blog entry"License Changes, IP.Board 3.4, and the Future""Interview -- Matt Mecham of Ibforums""CEO Invision Power Board, Matt Mecham Is a Liar, Thief!"IPB License Explanation 1.3, 1.3.1, 2.0, and 2.1ArchivedSecurity Fixes, Updates And Enhancements For IPB 1.3.1Archived"New Demo Accounts - Invision Power Services"the original"New Default Skin"the original"Invision Power Board 3.0.0 and Applications Released"the original"Archived copy"the original"Perpetual licenses being done away with""Release Notes - Invision Power Services""Introducing: IPS Community Suite 4!"Invision Community Release Notes

            Canceling a color specificationRandomly assigning color to Graphics3D objects?Default color for Filling in Mathematica 9Coloring specific elements of sets with a prime modified order in an array plotHow to pick a color differing significantly from the colors already in a given color list?Detection of the text colorColor numbers based on their valueCan color schemes for use with ColorData include opacity specification?My dynamic color schemes

            Tom Holland Mục lục Đầu đời và giáo dục | Sự nghiệp | Cuộc sống cá nhân | Phim tham gia | Giải thưởng và đề cử | Chú thích | Liên kết ngoài | Trình đơn chuyển hướngProfile“Person Details for Thomas Stanley Holland, "England and Wales Birth Registration Index, 1837-2008" — FamilySearch.org”"Meet Tom Holland... the 16-year-old star of The Impossible""Schoolboy actor Tom Holland finds himself in Oscar contention for role in tsunami drama"“Naomi Watts on the Prince William and Harry's reaction to her film about the late Princess Diana”lưu trữ"Holland and Pflueger Are West End's Two New 'Billy Elliots'""I'm so envious of my son, the movie star! British writer Dominic Holland's spent 20 years trying to crack Hollywood - but he's been beaten to it by a very unlikely rival"“Richard and Margaret Povey of Jersey, Channel Islands, UK: Information about Thomas Stanley Holland”"Tom Holland to play Billy Elliot""New Billy Elliot leaving the garage"Billy Elliot the Musical - Tom Holland - Billy"A Tale of four Billys: Tom Holland""The Feel Good Factor""Thames Christian College schoolboys join Myleene Klass for The Feelgood Factor""Government launches £600,000 arts bursaries pilot""BILLY's Chapman, Holland, Gardner & Jackson-Keen Visit Prime Minister""Elton John 'blown away' by Billy Elliot fifth birthday" (video with John's interview and fragments of Holland's performance)"First News interviews Arrietty's Tom Holland"“33rd Critics' Circle Film Awards winners”“National Board of Review Current Awards”Bản gốc"Ron Howard Whaling Tale 'In The Heart Of The Sea' Casts Tom Holland"“'Spider-Man' Finds Tom Holland to Star as New Web-Slinger”lưu trữ“Captain America: Civil War (2016)”“Film Review: ‘Captain America: Civil War’”lưu trữ“‘Captain America: Civil War’ review: Choose your own avenger”lưu trữ“The Lost City of Z reviews”“Sony Pictures and Marvel Studios Find Their 'Spider-Man' Star and Director”“‘Mary Magdalene’, ‘Current War’ & ‘Wind River’ Get 2017 Release Dates From Weinstein”“Lionsgate Unleashing Daisy Ridley & Tom Holland Starrer ‘Chaos Walking’ In Cannes”“PTA's 'Master' Leads Chicago Film Critics Nominations, UPDATED: Houston and Indiana Critics Nominations”“Nominaciones Goya 2013 Telecinco Cinema – ENG”“Jameson Empire Film Awards: Martin Freeman wins best actor for performance in The Hobbit”“34th Annual Young Artist Awards”Bản gốc“Teen Choice Awards 2016—Captain America: Civil War Leads Second Wave of Nominations”“BAFTA Film Award Nominations: ‘La La Land’ Leads Race”“Saturn Awards Nominations 2017: 'Rogue One,' 'Walking Dead' Lead”Tom HollandTom HollandTom HollandTom Hollandmedia.gettyimages.comWorldCat Identities300279794no20130442900000 0004 0355 42791085670554170004732cb16706349t(data)XX5557367