Simple object validator with a new APISimple object validatorAm I coding Java in C#?Refactor a selection validatorValidator part 2ISBN validation with Hibernate ValidatorValidator class for PHPValidator Class in PHPValidating each process object from its own validatorSimple object validatorExtension “With” for immutable typesSwedish 'Personnummer' validator

Why isn’t the tax system continuous rather than bracketed?

How to append a matrix element by element?

Impossible darts scores

Is there a maximum distance from a planet that a moon can orbit?

Does the Distant Spell metamagic apply to the Sword Burst cantrip?

What would Earth look like at night in medieval times?

Should I include salary information on my CV?

Plata or Dinero

Is my Rep in Stack-Exchange Form?

STM Microcontroller burns every time

Why does adding parentheses prevent an error?

Why does the A-4 Skyhawk sit nose-up when on ground?

The impact of an intelligent and (mostly) hostile flying race on weapons and armor

Ending: accusative or not?

Mount a folder with a space on Linux

How often can a PC check with passive perception during a combat turn?

In the Marvel universe, can a human have a baby with any non-human?

How can I convince my reader that I will not use a certain trope?

Does the Paladin's Aura of Protection affect only either her or ONE ally in range?

What do you call the action of someone tackling a stronger person?

"It will become the talk of Paris" - translation into French

Are neural networks the wrong tool to solve this 2D platformer/shooter game? Is there a proven way to frame this problem to a neural network?

Going to get married soon, should I do it on Dec 31 or Jan 1?

How can I repair scratches on a painted French door?



Simple object validator with a new API


Simple object validatorAm I coding Java in C#?Refactor a selection validatorValidator part 2ISBN validation with Hibernate ValidatorValidator class for PHPValidator Class in PHPValidating each process object from its own validatorSimple object validatorExtension “With” for immutable typesSwedish 'Personnummer' validator






.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty,.everyoneloves__bot-mid-leaderboard:empty margin-bottom:0;








4












$begingroup$


Quite some time ago I have created the Simple object validator (see also self-answer). The more I used it the more I thougt its API could be better so I have heavily refactored it and would like to you take another look at the new version.



Requirements



I'd like my validator to be:



  • intuitive

  • easy to use

  • extendable

  • testable

  • helpful by providing precise error messages

  • immutable so that predefined rules cannot be manipulated

In order to meet these criteria I have removed a couple of classes and built it on top of System.Collections.Immutable. Usually, you should not notice that and be able to just use the provided extensions.



How it works



You start with an empty collection of rules for the specified type and use one of the Add extensions to add validation rules. There are two types of them:




  • Require - which means it cannot continue when this fails (e.g. something is null)


  • Ensure - the validator can continue with the next rule

Validation rules are compiled from expressions and use two parameters:




  • T - the object being validated


  • TContext - optional context with additional data

Expressions are also used for generating error messages that are prettyfied with an expression visitor that replaces ugly closure classes with pretty type names like <param:Person>.FirstName.



The main extensibility point of this framework are the two properties Require and Ensure that return a builder that lets the user chain extensions such as True, False, NotNull etc.



There is no classic validator but an extension (ValidateWith), for an IImutableList<> that executes the rules. It returns a tuple with the object being validated and a lookup with results. Its key is bool where true returns successul rules and false failed ones. When the execution should be interrupted because of validation errors, the user can chain the ThrowIfValidationFailed extension.



With the currently available APIs it's also possible to create shortcuts to reduce the verbosity. See the Simplified test below. I think it still could be better.



In general, a set of rules would be a static field. It's supposed to be build once and reused many times as compiling expressions might otherwise become a bottleneck.



Example



These tests show it in action:



public class ValidationTest

private static readonly Person Tester = new Person

FirstName = "Cookie",
LastName = "Monster",
Address = new Address

Street = "Sesame Street"

;

[Fact]
public void Can_validate_rules()

var rules =
ValidationRuleCollection
.For<Person>()
.Add(x =>
ValidationRule
.Require
.NotNull(x))
.Add(x =>
ValidationRule
.Require
.NotNull(() => x.FirstName))
.Add(x =>
ValidationRule
.Ensure
.True(() => x.FirstName.Length > 3))
.Add(x =>
ValidationRule
.Require
.NotNull(() => x.Address))
.Add(x =>
ValidationRule
.Ensure
.False(() => x.Address.Street.Length > 100));

var (person, results) = Tester.ValidateWith(rules);

Assert.Equal(5, results[true].Count());
Assert.Equal(0, results[false].Count());

Tester.ValidateWith(rules).ThrowIfValidationFailed();


[Fact]
public void Can_throw_if_validation_failed()

var rules =
ValidationRuleCollection
.For<Person>()
.Add(x =>
ValidationRule
.Require
.NotNull(x))
.Add(x =>
ValidationRule
.Require
.NotNull(() => x.FirstName))
.Add(x =>
ValidationRule
.Ensure
.True(() => x.FirstName.Length > 3));

var (person, results) = default(Person).ValidateWith(rules);

Assert.Equal(0, results[true].Count());
Assert.Equal(1, results[false].Count());
Assert.ThrowsAny<DynamicException>(() => default(Person).ValidateWith(rules).ThrowIfValidationFailed());


[Fact]
public void Simplified()

var rules =
ValidationRuleCollection
.For<Person>()
.Require((b, x) => b.NotNull(() => x))
.Ensure((b, x) => b.NotNull(() => x.FirstName))
.Ensure((b, x) => b.True(() => x.FirstName.Length > 3));

var (person, results) = default(Person).ValidateWith(rules);

Assert.Equal(0, results[true].Count());
Assert.Equal(1, results[false].Count());
Assert.ThrowsAny<DynamicException>(() => default(Person).ValidateWith(rules).ThrowIfValidationFailed());


private class Person

public string FirstName get; set;

public string LastName get; set;

public Address Address get; set;


private class Address

public string Street get; set;





Code



ValidationRuleCollection and convenience extensions for working with immutable collections so that I don't have to create my own immutables.



public static class ValidationRuleCollection

public static IImmutableList<IValidationRule<T, TContext>> For<T, TContext>() => ImmutableList<IValidationRule<T, TContext>>.Empty;

public static IImmutableList<IValidationRule<T, object>> For<T>() => ImmutableList<IValidationRule<T, object>>.Empty;


public static class ValidationRuleCollectionExtensions

public static IImmutableList<IValidationRule<T, TContext>> Add<T, TContext>(this IImmutableList<IValidationRule<T, TContext>> rules, Func<T, TContext, ValidationRuleBuilder> builder)

return rules.Add(builder(default, default).Build<T, TContext>());


public static IImmutableList<IValidationRule<T, object>> Add<T>(this IImmutableList<IValidationRule<T, object>> rules, Func<T, ValidationRuleBuilder> builder)

return rules.Add(builder(default).Build<T, object>());


public static IImmutableList<IValidationRule<T, object>> Require<T>(this IImmutableList<IValidationRule<T, object>> rules, Func<ValidationRuleBuilder, T, ValidationRuleBuilder> builder)


return rules.Add(builder(ValidationRule.Require, default).Build<T, object>());


public static IImmutableList<IValidationRule<T, object>> Ensure<T>(this IImmutableList<IValidationRule<T, object>> rules, Func<ValidationRuleBuilder, T, ValidationRuleBuilder> builder)


return rules.Add(builder(ValidationRule.Ensure, default).Build<T, object>());



public static (T Value, ILookup<bool, IValidationResult<T>> Results) ValidateWith<T, TContext>(this T obj, IImmutableList<IValidationRule<T, TContext>> rules, TContext context)

return
(
obj,
rules
.Evaluate(obj, context)
.ToLookup(r => r.Success)
);


public static (T Value, ILookup<bool, IValidationResult<T>> Results) ValidateWith<T>(this T obj, IImmutableList<IValidationRule<T, object>> rules)

return obj.ValidateWith(rules, default);


private static IEnumerable<IValidationResult<T>> Evaluate<T, TContext>(this IImmutableList<IValidationRule<T, TContext>> rules, T obj, TContext context)

var result = default(IValidationResult<T>);
foreach (var rule in rules)

yield return result = rule.Evaluate(obj, context);
if (!result.Success && rule.Option == ValidationRuleOption.Require) yield break;





ValidationRule, its callbacks and helpers.



public delegate bool ValidationPredicate<in T, in TContext>(T obj, TContext context);

public delegate string MessageCallback<in T, in TContext>(T obj, TContext context);

public interface IValidationRule<T, in TContext>

ValidationRuleOption Option get;

IValidationResult<T> Evaluate([CanBeNull] T obj, TContext context);


public enum ValidationRuleOption

Ensure,
Require


internal class ValidationRule<T, TContext> : IValidationRule<T, TContext>

private readonly ValidationPredicate<T, TContext> _predicate;
private readonly MessageCallback<T, TContext> _message;
private readonly string _expressionString;

public ValidationRule
(
[NotNull] Expression<ValidationPredicate<T, TContext>> predicate,
[NotNull] Expression<MessageCallback<T, TContext>> message,
[NotNull] ValidationRuleOption option
)

if (predicate == null) throw new ArgumentNullException(nameof(predicate));

_predicate = predicate.Compile();
_message = message.Compile();
_expressionString = ValidationParameterPrettifier.Prettify<T>(predicate).ToString();
Option = option;


public ValidationRuleOption Option get;

public IValidationResult<T> Evaluate(T obj, TContext context)

return new ValidationResult<T>(ToString(), _predicate(obj, context), _message(obj, context));


public override string ToString() => _expressionString;

public static implicit operator string(ValidationRule<T, TContext> rule) => rule?.ToString();


public static class ValidationRule

public static ValidationRuleBuilder Ensure => new ValidationRuleBuilder(ValidationRuleOption.Ensure);

public static ValidationRuleBuilder Require => new ValidationRuleBuilder(ValidationRuleOption.Require);



ValidtionBuilder...



public class ValidationRuleBuilder

private readonly ValidationRuleOption _option;

private LambdaExpression _predicate;
private LambdaExpression _message;

public ValidationRuleBuilder(ValidationRuleOption option)

_option = option;


public ValidationRuleBuilder Predicate(LambdaExpression expression)

_predicate = expression;
return this;


public ValidationRuleBuilder Message(Expression<Func<string>> message)

_message = message;
return this;


[NotNull]
public IValidationRule<T, TContext> Build<T, TContext>()




...and its extensions.



using static ValidationExpressionFactory;

public static class ValidationRuleBuilderExtension

public static ValidationRuleBuilder True(this ValidationRuleBuilder builder, Expression<Func<bool>> expression)

return
builder
.Predicate(expression)
.Message(() => "The specified expression must be 'true'.");


public static ValidationRuleBuilder Null<TMember>(this ValidationRuleBuilder builder, Expression<Func<TMember>> expression)

return
builder
.Predicate(ReferenceEqualNull(expression))
.Message(() => $"typeof(TMember).ToPrettyString(false) must be null.");


public static ValidationRuleBuilder Null<T>(this ValidationRuleBuilder builder, T value)

return
builder
.Predicate(ReferenceEqualNull<T>())
.Message(() => $"typeof(T).ToPrettyString(false) must be null.");


public static ValidationRuleBuilder False(this ValidationRuleBuilder builder, Expression<Func<bool>> expression)

return
builder
.Predicate(Negate(expression))
.Message(() => "The specified expression must be 'false'.");


public static ValidationRuleBuilder NotNull<TMember>(this ValidationRuleBuilder builder, Expression<Func<TMember>> expression)

return
builder
.Predicate(Negate(ReferenceEqualNull(expression)))
.Message(() => $"typeof(TMember).ToPrettyString(false) must not be null.");


public static ValidationRuleBuilder NotNull<T>(this ValidationRuleBuilder builder, T value)

return
builder
.Predicate(Negate(ReferenceEqualNull<T>()))
.Message(() => $"typeof(T).ToPrettyString(false) must not be null.");




ValidationResult with its extensions



using static ValidationResult;

// ReSharper disable once UnusedTypeParameter - T is required for chaining extensions.
public interface IValidationResult<T>

string Expression get;

bool Success get;

string Message get;


internal static class ValidationResult

public static readonly IDictionary<bool, string> Strings = new Dictionary<bool, string>

[true] = "Success",
[false] = "Failed"
;


internal class ValidationResult<T> : IValidationResult<T>

public ValidationResult([NotNull] string expression, bool success, [NotNull] string message)

Expression = expression;
Success = success;
Message = message;


public string Expression get;

public bool Success get;

public string Message get;

public override string ToString() => $"Strings[Success]

public static class ValidationResultExtensions

/// <summary>
/// Throws validation-exception when validation failed.
/// </summary>
public static T ThrowIfValidationFailed<T>(this (T Value, ILookup<bool, IValidationResult<T>> Results) lookup)

return
lookup.Results[false].Any()
? throw DynamicException.Create
(
$"typeof(T).ToPrettyString()Validation",
$"Object does not meet one or more requirements.Environment.NewLineEnvironment.NewLine" +
$"lookup.Results[false].Select(Func.ToString).Join(Environment.NewLine)"
)
: default(T);




Helpers



To check wheter a type is a closure, I use this extension:



internal static class TypeExtensions

public static bool IsClosure(this Type type)

return
type.Name.StartsWith("<>c__DisplayClass") &&
type.IsDefined(typeof(CompilerGeneratedAttribute));




And a couple more for creating expressions:



internal static class ValidationExpressionFactory

public static LambdaExpression ReferenceEqualNull<T>()

return ReferenceEqualNull<T>(Expression.Parameter(typeof(T)));


public static LambdaExpression ReferenceEqualNull<T>(Expression<Func<T>> expression)

// x => object.ReferenceEqual(x.Member, null)

// This is tricky because the original expression is () => (<>c__DisplayClass).x.y.z
// We first need to the closure and inject out parameter there.
var member = ValidationClosureSearch.FindParameter(expression);
var parameter = Expression.Parameter(member.Type);
var expressionWithParameter = ValidationParameterInjector.InjectParameter(expression.Body, parameter);
return ReferenceEqualNull<T>(parameter, expressionWithParameter);


private static LambdaExpression ReferenceEqualNull<T>(ParameterExpression parameter, Expression value = default)

// x => object.ReferenceEqual(x, null)
return
Expression.Lambda(
Expression.ReferenceEqual(
value ?? parameter,
Expression.Constant(default(T))),
parameter
);


public static LambdaExpression Negate(LambdaExpression expression)

// !x
return
Expression.Lambda(
Expression.Not(expression.Body),
expression.Parameters
);




Expression visitors



With this one I search for closures to replace them with a parameter as validation expression don't have them, e.g: .NotNull(() => x.FirstName))



/// <summary>
/// Searches for the member of the closure class.
/// </summary>
internal class ValidationClosureSearch : ExpressionVisitor

private MemberExpression _closure;

public static MemberExpression FindParameter(Expression expression)

var parameterSearch = new ValidationClosureSearch();
parameterSearch.Visit(expression);
return parameterSearch._closure;


protected override Expression VisitMember(MemberExpression node)

if (node.Expression.Type.IsClosure())

_closure = node;


return base.VisitMember(node);




Once I've found it, I use this one to replace that closures with actual parameters:



/// <summary>
/// Injects the specified parameter to replace the closure.
/// </summary>
public class ValidationParameterInjector : ExpressionVisitor

private readonly ParameterExpression _parameter;

private ValidationParameterInjector(ParameterExpression parameter) => _parameter = parameter;

public static Expression InjectParameter(Expression expression, ParameterExpression parameter)

return new ValidationParameterInjector(parameter).Visit(expression is LambdaExpression lambda ? lambda.Body : expression);


protected override Expression VisitMember(MemberExpression node)

var isClosure =
node.Type == _parameter.Type &&
node.Expression.Type.IsClosure();

return
isClosure
? _parameter
: base.VisitMember(node);




The last one is used to prettify validation expressions for display by injecting good looking type names.



  • before: "Param_0.FirstName"

  • after: "<param:Person>.FirstName>"

// We don't want to show the exact same expression as the condition
// because there are variables and closures that don't look pretty.
// We replace them with more friendly names.
internal class ValidationParameterPrettifier : ExpressionVisitor

private readonly ParameterExpression _originalParameter;
private readonly ParameterExpression _prettyParameter;

private ValidationParameterPrettifier(ParameterExpression originalParameter, ParameterExpression prettyParameter)

_originalParameter = originalParameter;
_prettyParameter = prettyParameter;


protected override Expression VisitParameter(ParameterExpression node)

return node.Equals(_originalParameter) ? _prettyParameter : base.VisitParameter(node);


protected override Expression VisitMember(MemberExpression node)

// Extract member name from closures.
return
node.Expression is ConstantExpression
? Expression.Parameter(node.Type, node.Member.Name)
: base.VisitMember(node);


protected override Expression VisitUnary(UnaryExpression node)

// Remove type conversion, this is change (Convert(<T>) != null) to (<T> != null)
return
node.Operand.Type == _originalParameter.Type
? Expression.Parameter(node.Operand.Type, _prettyParameter.Name)
: base.VisitUnary(node);


public static Expression Prettify<T>([NotNull] LambdaExpression expression)

if (expression == null) throw new ArgumentNullException(nameof(expression));

return
expression
.Parameters
.Aggregate(expression.Body, (e, p) => new ValidationParameterPrettifier(expression.Parameters[0], CreatePrettyParameter<T>()).Visit(expression.Body));


public static ParameterExpression CreatePrettyParameter<T>()

return Expression.Parameter(typeof(T), $"<param:typeof(T).ToPrettyString()>");




That's it.




Questions



  • would you say it meets my own requirements?

  • would you say any requirements or features are missing?

  • is there anything else I can improve?









share|improve this question











$endgroup$











  • $begingroup$
    Current commit: Flawless (it's how I call it)
    $endgroup$
    – t3chb0t
    8 hours ago











  • $begingroup$
    Which use cases you see for this API? End-user validation or easy validation logic for application and API developers?
    $endgroup$
    – dfhwze
    8 hours ago










  • $begingroup$
    @dfhwze for now just application and API developers (as there is no localization for messages).
    $endgroup$
    – t3chb0t
    8 hours ago










  • $begingroup$
    But you would like this to be a framework for end-users eventually, right?
    $endgroup$
    – dfhwze
    8 hours ago










  • $begingroup$
    @dfhwze that'd be cool...
    $endgroup$
    – t3chb0t
    8 hours ago

















4












$begingroup$


Quite some time ago I have created the Simple object validator (see also self-answer). The more I used it the more I thougt its API could be better so I have heavily refactored it and would like to you take another look at the new version.



Requirements



I'd like my validator to be:



  • intuitive

  • easy to use

  • extendable

  • testable

  • helpful by providing precise error messages

  • immutable so that predefined rules cannot be manipulated

In order to meet these criteria I have removed a couple of classes and built it on top of System.Collections.Immutable. Usually, you should not notice that and be able to just use the provided extensions.



How it works



You start with an empty collection of rules for the specified type and use one of the Add extensions to add validation rules. There are two types of them:




  • Require - which means it cannot continue when this fails (e.g. something is null)


  • Ensure - the validator can continue with the next rule

Validation rules are compiled from expressions and use two parameters:




  • T - the object being validated


  • TContext - optional context with additional data

Expressions are also used for generating error messages that are prettyfied with an expression visitor that replaces ugly closure classes with pretty type names like <param:Person>.FirstName.



The main extensibility point of this framework are the two properties Require and Ensure that return a builder that lets the user chain extensions such as True, False, NotNull etc.



There is no classic validator but an extension (ValidateWith), for an IImutableList<> that executes the rules. It returns a tuple with the object being validated and a lookup with results. Its key is bool where true returns successul rules and false failed ones. When the execution should be interrupted because of validation errors, the user can chain the ThrowIfValidationFailed extension.



With the currently available APIs it's also possible to create shortcuts to reduce the verbosity. See the Simplified test below. I think it still could be better.



In general, a set of rules would be a static field. It's supposed to be build once and reused many times as compiling expressions might otherwise become a bottleneck.



Example



These tests show it in action:



public class ValidationTest

private static readonly Person Tester = new Person

FirstName = "Cookie",
LastName = "Monster",
Address = new Address

Street = "Sesame Street"

;

[Fact]
public void Can_validate_rules()

var rules =
ValidationRuleCollection
.For<Person>()
.Add(x =>
ValidationRule
.Require
.NotNull(x))
.Add(x =>
ValidationRule
.Require
.NotNull(() => x.FirstName))
.Add(x =>
ValidationRule
.Ensure
.True(() => x.FirstName.Length > 3))
.Add(x =>
ValidationRule
.Require
.NotNull(() => x.Address))
.Add(x =>
ValidationRule
.Ensure
.False(() => x.Address.Street.Length > 100));

var (person, results) = Tester.ValidateWith(rules);

Assert.Equal(5, results[true].Count());
Assert.Equal(0, results[false].Count());

Tester.ValidateWith(rules).ThrowIfValidationFailed();


[Fact]
public void Can_throw_if_validation_failed()

var rules =
ValidationRuleCollection
.For<Person>()
.Add(x =>
ValidationRule
.Require
.NotNull(x))
.Add(x =>
ValidationRule
.Require
.NotNull(() => x.FirstName))
.Add(x =>
ValidationRule
.Ensure
.True(() => x.FirstName.Length > 3));

var (person, results) = default(Person).ValidateWith(rules);

Assert.Equal(0, results[true].Count());
Assert.Equal(1, results[false].Count());
Assert.ThrowsAny<DynamicException>(() => default(Person).ValidateWith(rules).ThrowIfValidationFailed());


[Fact]
public void Simplified()

var rules =
ValidationRuleCollection
.For<Person>()
.Require((b, x) => b.NotNull(() => x))
.Ensure((b, x) => b.NotNull(() => x.FirstName))
.Ensure((b, x) => b.True(() => x.FirstName.Length > 3));

var (person, results) = default(Person).ValidateWith(rules);

Assert.Equal(0, results[true].Count());
Assert.Equal(1, results[false].Count());
Assert.ThrowsAny<DynamicException>(() => default(Person).ValidateWith(rules).ThrowIfValidationFailed());


private class Person

public string FirstName get; set;

public string LastName get; set;

public Address Address get; set;


private class Address

public string Street get; set;





Code



ValidationRuleCollection and convenience extensions for working with immutable collections so that I don't have to create my own immutables.



public static class ValidationRuleCollection

public static IImmutableList<IValidationRule<T, TContext>> For<T, TContext>() => ImmutableList<IValidationRule<T, TContext>>.Empty;

public static IImmutableList<IValidationRule<T, object>> For<T>() => ImmutableList<IValidationRule<T, object>>.Empty;


public static class ValidationRuleCollectionExtensions

public static IImmutableList<IValidationRule<T, TContext>> Add<T, TContext>(this IImmutableList<IValidationRule<T, TContext>> rules, Func<T, TContext, ValidationRuleBuilder> builder)

return rules.Add(builder(default, default).Build<T, TContext>());


public static IImmutableList<IValidationRule<T, object>> Add<T>(this IImmutableList<IValidationRule<T, object>> rules, Func<T, ValidationRuleBuilder> builder)

return rules.Add(builder(default).Build<T, object>());


public static IImmutableList<IValidationRule<T, object>> Require<T>(this IImmutableList<IValidationRule<T, object>> rules, Func<ValidationRuleBuilder, T, ValidationRuleBuilder> builder)


return rules.Add(builder(ValidationRule.Require, default).Build<T, object>());


public static IImmutableList<IValidationRule<T, object>> Ensure<T>(this IImmutableList<IValidationRule<T, object>> rules, Func<ValidationRuleBuilder, T, ValidationRuleBuilder> builder)


return rules.Add(builder(ValidationRule.Ensure, default).Build<T, object>());



public static (T Value, ILookup<bool, IValidationResult<T>> Results) ValidateWith<T, TContext>(this T obj, IImmutableList<IValidationRule<T, TContext>> rules, TContext context)

return
(
obj,
rules
.Evaluate(obj, context)
.ToLookup(r => r.Success)
);


public static (T Value, ILookup<bool, IValidationResult<T>> Results) ValidateWith<T>(this T obj, IImmutableList<IValidationRule<T, object>> rules)

return obj.ValidateWith(rules, default);


private static IEnumerable<IValidationResult<T>> Evaluate<T, TContext>(this IImmutableList<IValidationRule<T, TContext>> rules, T obj, TContext context)

var result = default(IValidationResult<T>);
foreach (var rule in rules)

yield return result = rule.Evaluate(obj, context);
if (!result.Success && rule.Option == ValidationRuleOption.Require) yield break;





ValidationRule, its callbacks and helpers.



public delegate bool ValidationPredicate<in T, in TContext>(T obj, TContext context);

public delegate string MessageCallback<in T, in TContext>(T obj, TContext context);

public interface IValidationRule<T, in TContext>

ValidationRuleOption Option get;

IValidationResult<T> Evaluate([CanBeNull] T obj, TContext context);


public enum ValidationRuleOption

Ensure,
Require


internal class ValidationRule<T, TContext> : IValidationRule<T, TContext>

private readonly ValidationPredicate<T, TContext> _predicate;
private readonly MessageCallback<T, TContext> _message;
private readonly string _expressionString;

public ValidationRule
(
[NotNull] Expression<ValidationPredicate<T, TContext>> predicate,
[NotNull] Expression<MessageCallback<T, TContext>> message,
[NotNull] ValidationRuleOption option
)

if (predicate == null) throw new ArgumentNullException(nameof(predicate));

_predicate = predicate.Compile();
_message = message.Compile();
_expressionString = ValidationParameterPrettifier.Prettify<T>(predicate).ToString();
Option = option;


public ValidationRuleOption Option get;

public IValidationResult<T> Evaluate(T obj, TContext context)

return new ValidationResult<T>(ToString(), _predicate(obj, context), _message(obj, context));


public override string ToString() => _expressionString;

public static implicit operator string(ValidationRule<T, TContext> rule) => rule?.ToString();


public static class ValidationRule

public static ValidationRuleBuilder Ensure => new ValidationRuleBuilder(ValidationRuleOption.Ensure);

public static ValidationRuleBuilder Require => new ValidationRuleBuilder(ValidationRuleOption.Require);



ValidtionBuilder...



public class ValidationRuleBuilder

private readonly ValidationRuleOption _option;

private LambdaExpression _predicate;
private LambdaExpression _message;

public ValidationRuleBuilder(ValidationRuleOption option)

_option = option;


public ValidationRuleBuilder Predicate(LambdaExpression expression)

_predicate = expression;
return this;


public ValidationRuleBuilder Message(Expression<Func<string>> message)

_message = message;
return this;


[NotNull]
public IValidationRule<T, TContext> Build<T, TContext>()




...and its extensions.



using static ValidationExpressionFactory;

public static class ValidationRuleBuilderExtension

public static ValidationRuleBuilder True(this ValidationRuleBuilder builder, Expression<Func<bool>> expression)

return
builder
.Predicate(expression)
.Message(() => "The specified expression must be 'true'.");


public static ValidationRuleBuilder Null<TMember>(this ValidationRuleBuilder builder, Expression<Func<TMember>> expression)

return
builder
.Predicate(ReferenceEqualNull(expression))
.Message(() => $"typeof(TMember).ToPrettyString(false) must be null.");


public static ValidationRuleBuilder Null<T>(this ValidationRuleBuilder builder, T value)

return
builder
.Predicate(ReferenceEqualNull<T>())
.Message(() => $"typeof(T).ToPrettyString(false) must be null.");


public static ValidationRuleBuilder False(this ValidationRuleBuilder builder, Expression<Func<bool>> expression)

return
builder
.Predicate(Negate(expression))
.Message(() => "The specified expression must be 'false'.");


public static ValidationRuleBuilder NotNull<TMember>(this ValidationRuleBuilder builder, Expression<Func<TMember>> expression)

return
builder
.Predicate(Negate(ReferenceEqualNull(expression)))
.Message(() => $"typeof(TMember).ToPrettyString(false) must not be null.");


public static ValidationRuleBuilder NotNull<T>(this ValidationRuleBuilder builder, T value)

return
builder
.Predicate(Negate(ReferenceEqualNull<T>()))
.Message(() => $"typeof(T).ToPrettyString(false) must not be null.");




ValidationResult with its extensions



using static ValidationResult;

// ReSharper disable once UnusedTypeParameter - T is required for chaining extensions.
public interface IValidationResult<T>

string Expression get;

bool Success get;

string Message get;


internal static class ValidationResult

public static readonly IDictionary<bool, string> Strings = new Dictionary<bool, string>

[true] = "Success",
[false] = "Failed"
;


internal class ValidationResult<T> : IValidationResult<T>

public ValidationResult([NotNull] string expression, bool success, [NotNull] string message)

Expression = expression;
Success = success;
Message = message;


public string Expression get;

public bool Success get;

public string Message get;

public override string ToString() => $"Strings[Success]

public static class ValidationResultExtensions

/// <summary>
/// Throws validation-exception when validation failed.
/// </summary>
public static T ThrowIfValidationFailed<T>(this (T Value, ILookup<bool, IValidationResult<T>> Results) lookup)

return
lookup.Results[false].Any()
? throw DynamicException.Create
(
$"typeof(T).ToPrettyString()Validation",
$"Object does not meet one or more requirements.Environment.NewLineEnvironment.NewLine" +
$"lookup.Results[false].Select(Func.ToString).Join(Environment.NewLine)"
)
: default(T);




Helpers



To check wheter a type is a closure, I use this extension:



internal static class TypeExtensions

public static bool IsClosure(this Type type)

return
type.Name.StartsWith("<>c__DisplayClass") &&
type.IsDefined(typeof(CompilerGeneratedAttribute));




And a couple more for creating expressions:



internal static class ValidationExpressionFactory

public static LambdaExpression ReferenceEqualNull<T>()

return ReferenceEqualNull<T>(Expression.Parameter(typeof(T)));


public static LambdaExpression ReferenceEqualNull<T>(Expression<Func<T>> expression)

// x => object.ReferenceEqual(x.Member, null)

// This is tricky because the original expression is () => (<>c__DisplayClass).x.y.z
// We first need to the closure and inject out parameter there.
var member = ValidationClosureSearch.FindParameter(expression);
var parameter = Expression.Parameter(member.Type);
var expressionWithParameter = ValidationParameterInjector.InjectParameter(expression.Body, parameter);
return ReferenceEqualNull<T>(parameter, expressionWithParameter);


private static LambdaExpression ReferenceEqualNull<T>(ParameterExpression parameter, Expression value = default)

// x => object.ReferenceEqual(x, null)
return
Expression.Lambda(
Expression.ReferenceEqual(
value ?? parameter,
Expression.Constant(default(T))),
parameter
);


public static LambdaExpression Negate(LambdaExpression expression)

// !x
return
Expression.Lambda(
Expression.Not(expression.Body),
expression.Parameters
);




Expression visitors



With this one I search for closures to replace them with a parameter as validation expression don't have them, e.g: .NotNull(() => x.FirstName))



/// <summary>
/// Searches for the member of the closure class.
/// </summary>
internal class ValidationClosureSearch : ExpressionVisitor

private MemberExpression _closure;

public static MemberExpression FindParameter(Expression expression)

var parameterSearch = new ValidationClosureSearch();
parameterSearch.Visit(expression);
return parameterSearch._closure;


protected override Expression VisitMember(MemberExpression node)

if (node.Expression.Type.IsClosure())

_closure = node;


return base.VisitMember(node);




Once I've found it, I use this one to replace that closures with actual parameters:



/// <summary>
/// Injects the specified parameter to replace the closure.
/// </summary>
public class ValidationParameterInjector : ExpressionVisitor

private readonly ParameterExpression _parameter;

private ValidationParameterInjector(ParameterExpression parameter) => _parameter = parameter;

public static Expression InjectParameter(Expression expression, ParameterExpression parameter)

return new ValidationParameterInjector(parameter).Visit(expression is LambdaExpression lambda ? lambda.Body : expression);


protected override Expression VisitMember(MemberExpression node)

var isClosure =
node.Type == _parameter.Type &&
node.Expression.Type.IsClosure();

return
isClosure
? _parameter
: base.VisitMember(node);




The last one is used to prettify validation expressions for display by injecting good looking type names.



  • before: "Param_0.FirstName"

  • after: "<param:Person>.FirstName>"

// We don't want to show the exact same expression as the condition
// because there are variables and closures that don't look pretty.
// We replace them with more friendly names.
internal class ValidationParameterPrettifier : ExpressionVisitor

private readonly ParameterExpression _originalParameter;
private readonly ParameterExpression _prettyParameter;

private ValidationParameterPrettifier(ParameterExpression originalParameter, ParameterExpression prettyParameter)

_originalParameter = originalParameter;
_prettyParameter = prettyParameter;


protected override Expression VisitParameter(ParameterExpression node)

return node.Equals(_originalParameter) ? _prettyParameter : base.VisitParameter(node);


protected override Expression VisitMember(MemberExpression node)

// Extract member name from closures.
return
node.Expression is ConstantExpression
? Expression.Parameter(node.Type, node.Member.Name)
: base.VisitMember(node);


protected override Expression VisitUnary(UnaryExpression node)

// Remove type conversion, this is change (Convert(<T>) != null) to (<T> != null)
return
node.Operand.Type == _originalParameter.Type
? Expression.Parameter(node.Operand.Type, _prettyParameter.Name)
: base.VisitUnary(node);


public static Expression Prettify<T>([NotNull] LambdaExpression expression)

if (expression == null) throw new ArgumentNullException(nameof(expression));

return
expression
.Parameters
.Aggregate(expression.Body, (e, p) => new ValidationParameterPrettifier(expression.Parameters[0], CreatePrettyParameter<T>()).Visit(expression.Body));


public static ParameterExpression CreatePrettyParameter<T>()

return Expression.Parameter(typeof(T), $"<param:typeof(T).ToPrettyString()>");




That's it.




Questions



  • would you say it meets my own requirements?

  • would you say any requirements or features are missing?

  • is there anything else I can improve?









share|improve this question











$endgroup$











  • $begingroup$
    Current commit: Flawless (it's how I call it)
    $endgroup$
    – t3chb0t
    8 hours ago











  • $begingroup$
    Which use cases you see for this API? End-user validation or easy validation logic for application and API developers?
    $endgroup$
    – dfhwze
    8 hours ago










  • $begingroup$
    @dfhwze for now just application and API developers (as there is no localization for messages).
    $endgroup$
    – t3chb0t
    8 hours ago










  • $begingroup$
    But you would like this to be a framework for end-users eventually, right?
    $endgroup$
    – dfhwze
    8 hours ago










  • $begingroup$
    @dfhwze that'd be cool...
    $endgroup$
    – t3chb0t
    8 hours ago













4












4








4





$begingroup$


Quite some time ago I have created the Simple object validator (see also self-answer). The more I used it the more I thougt its API could be better so I have heavily refactored it and would like to you take another look at the new version.



Requirements



I'd like my validator to be:



  • intuitive

  • easy to use

  • extendable

  • testable

  • helpful by providing precise error messages

  • immutable so that predefined rules cannot be manipulated

In order to meet these criteria I have removed a couple of classes and built it on top of System.Collections.Immutable. Usually, you should not notice that and be able to just use the provided extensions.



How it works



You start with an empty collection of rules for the specified type and use one of the Add extensions to add validation rules. There are two types of them:




  • Require - which means it cannot continue when this fails (e.g. something is null)


  • Ensure - the validator can continue with the next rule

Validation rules are compiled from expressions and use two parameters:




  • T - the object being validated


  • TContext - optional context with additional data

Expressions are also used for generating error messages that are prettyfied with an expression visitor that replaces ugly closure classes with pretty type names like <param:Person>.FirstName.



The main extensibility point of this framework are the two properties Require and Ensure that return a builder that lets the user chain extensions such as True, False, NotNull etc.



There is no classic validator but an extension (ValidateWith), for an IImutableList<> that executes the rules. It returns a tuple with the object being validated and a lookup with results. Its key is bool where true returns successul rules and false failed ones. When the execution should be interrupted because of validation errors, the user can chain the ThrowIfValidationFailed extension.



With the currently available APIs it's also possible to create shortcuts to reduce the verbosity. See the Simplified test below. I think it still could be better.



In general, a set of rules would be a static field. It's supposed to be build once and reused many times as compiling expressions might otherwise become a bottleneck.



Example



These tests show it in action:



public class ValidationTest

private static readonly Person Tester = new Person

FirstName = "Cookie",
LastName = "Monster",
Address = new Address

Street = "Sesame Street"

;

[Fact]
public void Can_validate_rules()

var rules =
ValidationRuleCollection
.For<Person>()
.Add(x =>
ValidationRule
.Require
.NotNull(x))
.Add(x =>
ValidationRule
.Require
.NotNull(() => x.FirstName))
.Add(x =>
ValidationRule
.Ensure
.True(() => x.FirstName.Length > 3))
.Add(x =>
ValidationRule
.Require
.NotNull(() => x.Address))
.Add(x =>
ValidationRule
.Ensure
.False(() => x.Address.Street.Length > 100));

var (person, results) = Tester.ValidateWith(rules);

Assert.Equal(5, results[true].Count());
Assert.Equal(0, results[false].Count());

Tester.ValidateWith(rules).ThrowIfValidationFailed();


[Fact]
public void Can_throw_if_validation_failed()

var rules =
ValidationRuleCollection
.For<Person>()
.Add(x =>
ValidationRule
.Require
.NotNull(x))
.Add(x =>
ValidationRule
.Require
.NotNull(() => x.FirstName))
.Add(x =>
ValidationRule
.Ensure
.True(() => x.FirstName.Length > 3));

var (person, results) = default(Person).ValidateWith(rules);

Assert.Equal(0, results[true].Count());
Assert.Equal(1, results[false].Count());
Assert.ThrowsAny<DynamicException>(() => default(Person).ValidateWith(rules).ThrowIfValidationFailed());


[Fact]
public void Simplified()

var rules =
ValidationRuleCollection
.For<Person>()
.Require((b, x) => b.NotNull(() => x))
.Ensure((b, x) => b.NotNull(() => x.FirstName))
.Ensure((b, x) => b.True(() => x.FirstName.Length > 3));

var (person, results) = default(Person).ValidateWith(rules);

Assert.Equal(0, results[true].Count());
Assert.Equal(1, results[false].Count());
Assert.ThrowsAny<DynamicException>(() => default(Person).ValidateWith(rules).ThrowIfValidationFailed());


private class Person

public string FirstName get; set;

public string LastName get; set;

public Address Address get; set;


private class Address

public string Street get; set;





Code



ValidationRuleCollection and convenience extensions for working with immutable collections so that I don't have to create my own immutables.



public static class ValidationRuleCollection

public static IImmutableList<IValidationRule<T, TContext>> For<T, TContext>() => ImmutableList<IValidationRule<T, TContext>>.Empty;

public static IImmutableList<IValidationRule<T, object>> For<T>() => ImmutableList<IValidationRule<T, object>>.Empty;


public static class ValidationRuleCollectionExtensions

public static IImmutableList<IValidationRule<T, TContext>> Add<T, TContext>(this IImmutableList<IValidationRule<T, TContext>> rules, Func<T, TContext, ValidationRuleBuilder> builder)

return rules.Add(builder(default, default).Build<T, TContext>());


public static IImmutableList<IValidationRule<T, object>> Add<T>(this IImmutableList<IValidationRule<T, object>> rules, Func<T, ValidationRuleBuilder> builder)

return rules.Add(builder(default).Build<T, object>());


public static IImmutableList<IValidationRule<T, object>> Require<T>(this IImmutableList<IValidationRule<T, object>> rules, Func<ValidationRuleBuilder, T, ValidationRuleBuilder> builder)


return rules.Add(builder(ValidationRule.Require, default).Build<T, object>());


public static IImmutableList<IValidationRule<T, object>> Ensure<T>(this IImmutableList<IValidationRule<T, object>> rules, Func<ValidationRuleBuilder, T, ValidationRuleBuilder> builder)


return rules.Add(builder(ValidationRule.Ensure, default).Build<T, object>());



public static (T Value, ILookup<bool, IValidationResult<T>> Results) ValidateWith<T, TContext>(this T obj, IImmutableList<IValidationRule<T, TContext>> rules, TContext context)

return
(
obj,
rules
.Evaluate(obj, context)
.ToLookup(r => r.Success)
);


public static (T Value, ILookup<bool, IValidationResult<T>> Results) ValidateWith<T>(this T obj, IImmutableList<IValidationRule<T, object>> rules)

return obj.ValidateWith(rules, default);


private static IEnumerable<IValidationResult<T>> Evaluate<T, TContext>(this IImmutableList<IValidationRule<T, TContext>> rules, T obj, TContext context)

var result = default(IValidationResult<T>);
foreach (var rule in rules)

yield return result = rule.Evaluate(obj, context);
if (!result.Success && rule.Option == ValidationRuleOption.Require) yield break;





ValidationRule, its callbacks and helpers.



public delegate bool ValidationPredicate<in T, in TContext>(T obj, TContext context);

public delegate string MessageCallback<in T, in TContext>(T obj, TContext context);

public interface IValidationRule<T, in TContext>

ValidationRuleOption Option get;

IValidationResult<T> Evaluate([CanBeNull] T obj, TContext context);


public enum ValidationRuleOption

Ensure,
Require


internal class ValidationRule<T, TContext> : IValidationRule<T, TContext>

private readonly ValidationPredicate<T, TContext> _predicate;
private readonly MessageCallback<T, TContext> _message;
private readonly string _expressionString;

public ValidationRule
(
[NotNull] Expression<ValidationPredicate<T, TContext>> predicate,
[NotNull] Expression<MessageCallback<T, TContext>> message,
[NotNull] ValidationRuleOption option
)

if (predicate == null) throw new ArgumentNullException(nameof(predicate));

_predicate = predicate.Compile();
_message = message.Compile();
_expressionString = ValidationParameterPrettifier.Prettify<T>(predicate).ToString();
Option = option;


public ValidationRuleOption Option get;

public IValidationResult<T> Evaluate(T obj, TContext context)

return new ValidationResult<T>(ToString(), _predicate(obj, context), _message(obj, context));


public override string ToString() => _expressionString;

public static implicit operator string(ValidationRule<T, TContext> rule) => rule?.ToString();


public static class ValidationRule

public static ValidationRuleBuilder Ensure => new ValidationRuleBuilder(ValidationRuleOption.Ensure);

public static ValidationRuleBuilder Require => new ValidationRuleBuilder(ValidationRuleOption.Require);



ValidtionBuilder...



public class ValidationRuleBuilder

private readonly ValidationRuleOption _option;

private LambdaExpression _predicate;
private LambdaExpression _message;

public ValidationRuleBuilder(ValidationRuleOption option)

_option = option;


public ValidationRuleBuilder Predicate(LambdaExpression expression)

_predicate = expression;
return this;


public ValidationRuleBuilder Message(Expression<Func<string>> message)

_message = message;
return this;


[NotNull]
public IValidationRule<T, TContext> Build<T, TContext>()




...and its extensions.



using static ValidationExpressionFactory;

public static class ValidationRuleBuilderExtension

public static ValidationRuleBuilder True(this ValidationRuleBuilder builder, Expression<Func<bool>> expression)

return
builder
.Predicate(expression)
.Message(() => "The specified expression must be 'true'.");


public static ValidationRuleBuilder Null<TMember>(this ValidationRuleBuilder builder, Expression<Func<TMember>> expression)

return
builder
.Predicate(ReferenceEqualNull(expression))
.Message(() => $"typeof(TMember).ToPrettyString(false) must be null.");


public static ValidationRuleBuilder Null<T>(this ValidationRuleBuilder builder, T value)

return
builder
.Predicate(ReferenceEqualNull<T>())
.Message(() => $"typeof(T).ToPrettyString(false) must be null.");


public static ValidationRuleBuilder False(this ValidationRuleBuilder builder, Expression<Func<bool>> expression)

return
builder
.Predicate(Negate(expression))
.Message(() => "The specified expression must be 'false'.");


public static ValidationRuleBuilder NotNull<TMember>(this ValidationRuleBuilder builder, Expression<Func<TMember>> expression)

return
builder
.Predicate(Negate(ReferenceEqualNull(expression)))
.Message(() => $"typeof(TMember).ToPrettyString(false) must not be null.");


public static ValidationRuleBuilder NotNull<T>(this ValidationRuleBuilder builder, T value)

return
builder
.Predicate(Negate(ReferenceEqualNull<T>()))
.Message(() => $"typeof(T).ToPrettyString(false) must not be null.");




ValidationResult with its extensions



using static ValidationResult;

// ReSharper disable once UnusedTypeParameter - T is required for chaining extensions.
public interface IValidationResult<T>

string Expression get;

bool Success get;

string Message get;


internal static class ValidationResult

public static readonly IDictionary<bool, string> Strings = new Dictionary<bool, string>

[true] = "Success",
[false] = "Failed"
;


internal class ValidationResult<T> : IValidationResult<T>

public ValidationResult([NotNull] string expression, bool success, [NotNull] string message)

Expression = expression;
Success = success;
Message = message;


public string Expression get;

public bool Success get;

public string Message get;

public override string ToString() => $"Strings[Success]

public static class ValidationResultExtensions

/// <summary>
/// Throws validation-exception when validation failed.
/// </summary>
public static T ThrowIfValidationFailed<T>(this (T Value, ILookup<bool, IValidationResult<T>> Results) lookup)

return
lookup.Results[false].Any()
? throw DynamicException.Create
(
$"typeof(T).ToPrettyString()Validation",
$"Object does not meet one or more requirements.Environment.NewLineEnvironment.NewLine" +
$"lookup.Results[false].Select(Func.ToString).Join(Environment.NewLine)"
)
: default(T);




Helpers



To check wheter a type is a closure, I use this extension:



internal static class TypeExtensions

public static bool IsClosure(this Type type)

return
type.Name.StartsWith("<>c__DisplayClass") &&
type.IsDefined(typeof(CompilerGeneratedAttribute));




And a couple more for creating expressions:



internal static class ValidationExpressionFactory

public static LambdaExpression ReferenceEqualNull<T>()

return ReferenceEqualNull<T>(Expression.Parameter(typeof(T)));


public static LambdaExpression ReferenceEqualNull<T>(Expression<Func<T>> expression)

// x => object.ReferenceEqual(x.Member, null)

// This is tricky because the original expression is () => (<>c__DisplayClass).x.y.z
// We first need to the closure and inject out parameter there.
var member = ValidationClosureSearch.FindParameter(expression);
var parameter = Expression.Parameter(member.Type);
var expressionWithParameter = ValidationParameterInjector.InjectParameter(expression.Body, parameter);
return ReferenceEqualNull<T>(parameter, expressionWithParameter);


private static LambdaExpression ReferenceEqualNull<T>(ParameterExpression parameter, Expression value = default)

// x => object.ReferenceEqual(x, null)
return
Expression.Lambda(
Expression.ReferenceEqual(
value ?? parameter,
Expression.Constant(default(T))),
parameter
);


public static LambdaExpression Negate(LambdaExpression expression)

// !x
return
Expression.Lambda(
Expression.Not(expression.Body),
expression.Parameters
);




Expression visitors



With this one I search for closures to replace them with a parameter as validation expression don't have them, e.g: .NotNull(() => x.FirstName))



/// <summary>
/// Searches for the member of the closure class.
/// </summary>
internal class ValidationClosureSearch : ExpressionVisitor

private MemberExpression _closure;

public static MemberExpression FindParameter(Expression expression)

var parameterSearch = new ValidationClosureSearch();
parameterSearch.Visit(expression);
return parameterSearch._closure;


protected override Expression VisitMember(MemberExpression node)

if (node.Expression.Type.IsClosure())

_closure = node;


return base.VisitMember(node);




Once I've found it, I use this one to replace that closures with actual parameters:



/// <summary>
/// Injects the specified parameter to replace the closure.
/// </summary>
public class ValidationParameterInjector : ExpressionVisitor

private readonly ParameterExpression _parameter;

private ValidationParameterInjector(ParameterExpression parameter) => _parameter = parameter;

public static Expression InjectParameter(Expression expression, ParameterExpression parameter)

return new ValidationParameterInjector(parameter).Visit(expression is LambdaExpression lambda ? lambda.Body : expression);


protected override Expression VisitMember(MemberExpression node)

var isClosure =
node.Type == _parameter.Type &&
node.Expression.Type.IsClosure();

return
isClosure
? _parameter
: base.VisitMember(node);




The last one is used to prettify validation expressions for display by injecting good looking type names.



  • before: "Param_0.FirstName"

  • after: "<param:Person>.FirstName>"

// We don't want to show the exact same expression as the condition
// because there are variables and closures that don't look pretty.
// We replace them with more friendly names.
internal class ValidationParameterPrettifier : ExpressionVisitor

private readonly ParameterExpression _originalParameter;
private readonly ParameterExpression _prettyParameter;

private ValidationParameterPrettifier(ParameterExpression originalParameter, ParameterExpression prettyParameter)

_originalParameter = originalParameter;
_prettyParameter = prettyParameter;


protected override Expression VisitParameter(ParameterExpression node)

return node.Equals(_originalParameter) ? _prettyParameter : base.VisitParameter(node);


protected override Expression VisitMember(MemberExpression node)

// Extract member name from closures.
return
node.Expression is ConstantExpression
? Expression.Parameter(node.Type, node.Member.Name)
: base.VisitMember(node);


protected override Expression VisitUnary(UnaryExpression node)

// Remove type conversion, this is change (Convert(<T>) != null) to (<T> != null)
return
node.Operand.Type == _originalParameter.Type
? Expression.Parameter(node.Operand.Type, _prettyParameter.Name)
: base.VisitUnary(node);


public static Expression Prettify<T>([NotNull] LambdaExpression expression)

if (expression == null) throw new ArgumentNullException(nameof(expression));

return
expression
.Parameters
.Aggregate(expression.Body, (e, p) => new ValidationParameterPrettifier(expression.Parameters[0], CreatePrettyParameter<T>()).Visit(expression.Body));


public static ParameterExpression CreatePrettyParameter<T>()

return Expression.Parameter(typeof(T), $"<param:typeof(T).ToPrettyString()>");




That's it.




Questions



  • would you say it meets my own requirements?

  • would you say any requirements or features are missing?

  • is there anything else I can improve?









share|improve this question











$endgroup$




Quite some time ago I have created the Simple object validator (see also self-answer). The more I used it the more I thougt its API could be better so I have heavily refactored it and would like to you take another look at the new version.



Requirements



I'd like my validator to be:



  • intuitive

  • easy to use

  • extendable

  • testable

  • helpful by providing precise error messages

  • immutable so that predefined rules cannot be manipulated

In order to meet these criteria I have removed a couple of classes and built it on top of System.Collections.Immutable. Usually, you should not notice that and be able to just use the provided extensions.



How it works



You start with an empty collection of rules for the specified type and use one of the Add extensions to add validation rules. There are two types of them:




  • Require - which means it cannot continue when this fails (e.g. something is null)


  • Ensure - the validator can continue with the next rule

Validation rules are compiled from expressions and use two parameters:




  • T - the object being validated


  • TContext - optional context with additional data

Expressions are also used for generating error messages that are prettyfied with an expression visitor that replaces ugly closure classes with pretty type names like <param:Person>.FirstName.



The main extensibility point of this framework are the two properties Require and Ensure that return a builder that lets the user chain extensions such as True, False, NotNull etc.



There is no classic validator but an extension (ValidateWith), for an IImutableList<> that executes the rules. It returns a tuple with the object being validated and a lookup with results. Its key is bool where true returns successul rules and false failed ones. When the execution should be interrupted because of validation errors, the user can chain the ThrowIfValidationFailed extension.



With the currently available APIs it's also possible to create shortcuts to reduce the verbosity. See the Simplified test below. I think it still could be better.



In general, a set of rules would be a static field. It's supposed to be build once and reused many times as compiling expressions might otherwise become a bottleneck.



Example



These tests show it in action:



public class ValidationTest

private static readonly Person Tester = new Person

FirstName = "Cookie",
LastName = "Monster",
Address = new Address

Street = "Sesame Street"

;

[Fact]
public void Can_validate_rules()

var rules =
ValidationRuleCollection
.For<Person>()
.Add(x =>
ValidationRule
.Require
.NotNull(x))
.Add(x =>
ValidationRule
.Require
.NotNull(() => x.FirstName))
.Add(x =>
ValidationRule
.Ensure
.True(() => x.FirstName.Length > 3))
.Add(x =>
ValidationRule
.Require
.NotNull(() => x.Address))
.Add(x =>
ValidationRule
.Ensure
.False(() => x.Address.Street.Length > 100));

var (person, results) = Tester.ValidateWith(rules);

Assert.Equal(5, results[true].Count());
Assert.Equal(0, results[false].Count());

Tester.ValidateWith(rules).ThrowIfValidationFailed();


[Fact]
public void Can_throw_if_validation_failed()

var rules =
ValidationRuleCollection
.For<Person>()
.Add(x =>
ValidationRule
.Require
.NotNull(x))
.Add(x =>
ValidationRule
.Require
.NotNull(() => x.FirstName))
.Add(x =>
ValidationRule
.Ensure
.True(() => x.FirstName.Length > 3));

var (person, results) = default(Person).ValidateWith(rules);

Assert.Equal(0, results[true].Count());
Assert.Equal(1, results[false].Count());
Assert.ThrowsAny<DynamicException>(() => default(Person).ValidateWith(rules).ThrowIfValidationFailed());


[Fact]
public void Simplified()

var rules =
ValidationRuleCollection
.For<Person>()
.Require((b, x) => b.NotNull(() => x))
.Ensure((b, x) => b.NotNull(() => x.FirstName))
.Ensure((b, x) => b.True(() => x.FirstName.Length > 3));

var (person, results) = default(Person).ValidateWith(rules);

Assert.Equal(0, results[true].Count());
Assert.Equal(1, results[false].Count());
Assert.ThrowsAny<DynamicException>(() => default(Person).ValidateWith(rules).ThrowIfValidationFailed());


private class Person

public string FirstName get; set;

public string LastName get; set;

public Address Address get; set;


private class Address

public string Street get; set;





Code



ValidationRuleCollection and convenience extensions for working with immutable collections so that I don't have to create my own immutables.



public static class ValidationRuleCollection

public static IImmutableList<IValidationRule<T, TContext>> For<T, TContext>() => ImmutableList<IValidationRule<T, TContext>>.Empty;

public static IImmutableList<IValidationRule<T, object>> For<T>() => ImmutableList<IValidationRule<T, object>>.Empty;


public static class ValidationRuleCollectionExtensions

public static IImmutableList<IValidationRule<T, TContext>> Add<T, TContext>(this IImmutableList<IValidationRule<T, TContext>> rules, Func<T, TContext, ValidationRuleBuilder> builder)

return rules.Add(builder(default, default).Build<T, TContext>());


public static IImmutableList<IValidationRule<T, object>> Add<T>(this IImmutableList<IValidationRule<T, object>> rules, Func<T, ValidationRuleBuilder> builder)

return rules.Add(builder(default).Build<T, object>());


public static IImmutableList<IValidationRule<T, object>> Require<T>(this IImmutableList<IValidationRule<T, object>> rules, Func<ValidationRuleBuilder, T, ValidationRuleBuilder> builder)


return rules.Add(builder(ValidationRule.Require, default).Build<T, object>());


public static IImmutableList<IValidationRule<T, object>> Ensure<T>(this IImmutableList<IValidationRule<T, object>> rules, Func<ValidationRuleBuilder, T, ValidationRuleBuilder> builder)


return rules.Add(builder(ValidationRule.Ensure, default).Build<T, object>());



public static (T Value, ILookup<bool, IValidationResult<T>> Results) ValidateWith<T, TContext>(this T obj, IImmutableList<IValidationRule<T, TContext>> rules, TContext context)

return
(
obj,
rules
.Evaluate(obj, context)
.ToLookup(r => r.Success)
);


public static (T Value, ILookup<bool, IValidationResult<T>> Results) ValidateWith<T>(this T obj, IImmutableList<IValidationRule<T, object>> rules)

return obj.ValidateWith(rules, default);


private static IEnumerable<IValidationResult<T>> Evaluate<T, TContext>(this IImmutableList<IValidationRule<T, TContext>> rules, T obj, TContext context)

var result = default(IValidationResult<T>);
foreach (var rule in rules)

yield return result = rule.Evaluate(obj, context);
if (!result.Success && rule.Option == ValidationRuleOption.Require) yield break;





ValidationRule, its callbacks and helpers.



public delegate bool ValidationPredicate<in T, in TContext>(T obj, TContext context);

public delegate string MessageCallback<in T, in TContext>(T obj, TContext context);

public interface IValidationRule<T, in TContext>

ValidationRuleOption Option get;

IValidationResult<T> Evaluate([CanBeNull] T obj, TContext context);


public enum ValidationRuleOption

Ensure,
Require


internal class ValidationRule<T, TContext> : IValidationRule<T, TContext>

private readonly ValidationPredicate<T, TContext> _predicate;
private readonly MessageCallback<T, TContext> _message;
private readonly string _expressionString;

public ValidationRule
(
[NotNull] Expression<ValidationPredicate<T, TContext>> predicate,
[NotNull] Expression<MessageCallback<T, TContext>> message,
[NotNull] ValidationRuleOption option
)

if (predicate == null) throw new ArgumentNullException(nameof(predicate));

_predicate = predicate.Compile();
_message = message.Compile();
_expressionString = ValidationParameterPrettifier.Prettify<T>(predicate).ToString();
Option = option;


public ValidationRuleOption Option get;

public IValidationResult<T> Evaluate(T obj, TContext context)

return new ValidationResult<T>(ToString(), _predicate(obj, context), _message(obj, context));


public override string ToString() => _expressionString;

public static implicit operator string(ValidationRule<T, TContext> rule) => rule?.ToString();


public static class ValidationRule

public static ValidationRuleBuilder Ensure => new ValidationRuleBuilder(ValidationRuleOption.Ensure);

public static ValidationRuleBuilder Require => new ValidationRuleBuilder(ValidationRuleOption.Require);



ValidtionBuilder...



public class ValidationRuleBuilder

private readonly ValidationRuleOption _option;

private LambdaExpression _predicate;
private LambdaExpression _message;

public ValidationRuleBuilder(ValidationRuleOption option)

_option = option;


public ValidationRuleBuilder Predicate(LambdaExpression expression)

_predicate = expression;
return this;


public ValidationRuleBuilder Message(Expression<Func<string>> message)

_message = message;
return this;


[NotNull]
public IValidationRule<T, TContext> Build<T, TContext>()




...and its extensions.



using static ValidationExpressionFactory;

public static class ValidationRuleBuilderExtension

public static ValidationRuleBuilder True(this ValidationRuleBuilder builder, Expression<Func<bool>> expression)

return
builder
.Predicate(expression)
.Message(() => "The specified expression must be 'true'.");


public static ValidationRuleBuilder Null<TMember>(this ValidationRuleBuilder builder, Expression<Func<TMember>> expression)

return
builder
.Predicate(ReferenceEqualNull(expression))
.Message(() => $"typeof(TMember).ToPrettyString(false) must be null.");


public static ValidationRuleBuilder Null<T>(this ValidationRuleBuilder builder, T value)

return
builder
.Predicate(ReferenceEqualNull<T>())
.Message(() => $"typeof(T).ToPrettyString(false) must be null.");


public static ValidationRuleBuilder False(this ValidationRuleBuilder builder, Expression<Func<bool>> expression)

return
builder
.Predicate(Negate(expression))
.Message(() => "The specified expression must be 'false'.");


public static ValidationRuleBuilder NotNull<TMember>(this ValidationRuleBuilder builder, Expression<Func<TMember>> expression)

return
builder
.Predicate(Negate(ReferenceEqualNull(expression)))
.Message(() => $"typeof(TMember).ToPrettyString(false) must not be null.");


public static ValidationRuleBuilder NotNull<T>(this ValidationRuleBuilder builder, T value)

return
builder
.Predicate(Negate(ReferenceEqualNull<T>()))
.Message(() => $"typeof(T).ToPrettyString(false) must not be null.");




ValidationResult with its extensions



using static ValidationResult;

// ReSharper disable once UnusedTypeParameter - T is required for chaining extensions.
public interface IValidationResult<T>

string Expression get;

bool Success get;

string Message get;


internal static class ValidationResult

public static readonly IDictionary<bool, string> Strings = new Dictionary<bool, string>

[true] = "Success",
[false] = "Failed"
;


internal class ValidationResult<T> : IValidationResult<T>

public ValidationResult([NotNull] string expression, bool success, [NotNull] string message)

Expression = expression;
Success = success;
Message = message;


public string Expression get;

public bool Success get;

public string Message get;

public override string ToString() => $"Strings[Success]

public static class ValidationResultExtensions

/// <summary>
/// Throws validation-exception when validation failed.
/// </summary>
public static T ThrowIfValidationFailed<T>(this (T Value, ILookup<bool, IValidationResult<T>> Results) lookup)

return
lookup.Results[false].Any()
? throw DynamicException.Create
(
$"typeof(T).ToPrettyString()Validation",
$"Object does not meet one or more requirements.Environment.NewLineEnvironment.NewLine" +
$"lookup.Results[false].Select(Func.ToString).Join(Environment.NewLine)"
)
: default(T);




Helpers



To check wheter a type is a closure, I use this extension:



internal static class TypeExtensions

public static bool IsClosure(this Type type)

return
type.Name.StartsWith("<>c__DisplayClass") &&
type.IsDefined(typeof(CompilerGeneratedAttribute));




And a couple more for creating expressions:



internal static class ValidationExpressionFactory

public static LambdaExpression ReferenceEqualNull<T>()

return ReferenceEqualNull<T>(Expression.Parameter(typeof(T)));


public static LambdaExpression ReferenceEqualNull<T>(Expression<Func<T>> expression)

// x => object.ReferenceEqual(x.Member, null)

// This is tricky because the original expression is () => (<>c__DisplayClass).x.y.z
// We first need to the closure and inject out parameter there.
var member = ValidationClosureSearch.FindParameter(expression);
var parameter = Expression.Parameter(member.Type);
var expressionWithParameter = ValidationParameterInjector.InjectParameter(expression.Body, parameter);
return ReferenceEqualNull<T>(parameter, expressionWithParameter);


private static LambdaExpression ReferenceEqualNull<T>(ParameterExpression parameter, Expression value = default)

// x => object.ReferenceEqual(x, null)
return
Expression.Lambda(
Expression.ReferenceEqual(
value ?? parameter,
Expression.Constant(default(T))),
parameter
);


public static LambdaExpression Negate(LambdaExpression expression)

// !x
return
Expression.Lambda(
Expression.Not(expression.Body),
expression.Parameters
);




Expression visitors



With this one I search for closures to replace them with a parameter as validation expression don't have them, e.g: .NotNull(() => x.FirstName))



/// <summary>
/// Searches for the member of the closure class.
/// </summary>
internal class ValidationClosureSearch : ExpressionVisitor

private MemberExpression _closure;

public static MemberExpression FindParameter(Expression expression)

var parameterSearch = new ValidationClosureSearch();
parameterSearch.Visit(expression);
return parameterSearch._closure;


protected override Expression VisitMember(MemberExpression node)

if (node.Expression.Type.IsClosure())

_closure = node;


return base.VisitMember(node);




Once I've found it, I use this one to replace that closures with actual parameters:



/// <summary>
/// Injects the specified parameter to replace the closure.
/// </summary>
public class ValidationParameterInjector : ExpressionVisitor

private readonly ParameterExpression _parameter;

private ValidationParameterInjector(ParameterExpression parameter) => _parameter = parameter;

public static Expression InjectParameter(Expression expression, ParameterExpression parameter)

return new ValidationParameterInjector(parameter).Visit(expression is LambdaExpression lambda ? lambda.Body : expression);


protected override Expression VisitMember(MemberExpression node)

var isClosure =
node.Type == _parameter.Type &&
node.Expression.Type.IsClosure();

return
isClosure
? _parameter
: base.VisitMember(node);




The last one is used to prettify validation expressions for display by injecting good looking type names.



  • before: "Param_0.FirstName"

  • after: "<param:Person>.FirstName>"

// We don't want to show the exact same expression as the condition
// because there are variables and closures that don't look pretty.
// We replace them with more friendly names.
internal class ValidationParameterPrettifier : ExpressionVisitor

private readonly ParameterExpression _originalParameter;
private readonly ParameterExpression _prettyParameter;

private ValidationParameterPrettifier(ParameterExpression originalParameter, ParameterExpression prettyParameter)

_originalParameter = originalParameter;
_prettyParameter = prettyParameter;


protected override Expression VisitParameter(ParameterExpression node)

return node.Equals(_originalParameter) ? _prettyParameter : base.VisitParameter(node);


protected override Expression VisitMember(MemberExpression node)

// Extract member name from closures.
return
node.Expression is ConstantExpression
? Expression.Parameter(node.Type, node.Member.Name)
: base.VisitMember(node);


protected override Expression VisitUnary(UnaryExpression node)

// Remove type conversion, this is change (Convert(<T>) != null) to (<T> != null)
return
node.Operand.Type == _originalParameter.Type
? Expression.Parameter(node.Operand.Type, _prettyParameter.Name)
: base.VisitUnary(node);


public static Expression Prettify<T>([NotNull] LambdaExpression expression)

if (expression == null) throw new ArgumentNullException(nameof(expression));

return
expression
.Parameters
.Aggregate(expression.Body, (e, p) => new ValidationParameterPrettifier(expression.Parameters[0], CreatePrettyParameter<T>()).Visit(expression.Body));


public static ParameterExpression CreatePrettyParameter<T>()

return Expression.Parameter(typeof(T), $"<param:typeof(T).ToPrettyString()>");




That's it.




Questions



  • would you say it meets my own requirements?

  • would you say any requirements or features are missing?

  • is there anything else I can improve?






c# validation extension-methods framework expression-trees






share|improve this question















share|improve this question













share|improve this question




share|improve this question








edited 6 hours ago







t3chb0t

















asked 8 hours ago









t3chb0tt3chb0t

36k7 gold badges58 silver badges133 bronze badges




36k7 gold badges58 silver badges133 bronze badges











  • $begingroup$
    Current commit: Flawless (it's how I call it)
    $endgroup$
    – t3chb0t
    8 hours ago











  • $begingroup$
    Which use cases you see for this API? End-user validation or easy validation logic for application and API developers?
    $endgroup$
    – dfhwze
    8 hours ago










  • $begingroup$
    @dfhwze for now just application and API developers (as there is no localization for messages).
    $endgroup$
    – t3chb0t
    8 hours ago










  • $begingroup$
    But you would like this to be a framework for end-users eventually, right?
    $endgroup$
    – dfhwze
    8 hours ago










  • $begingroup$
    @dfhwze that'd be cool...
    $endgroup$
    – t3chb0t
    8 hours ago
















  • $begingroup$
    Current commit: Flawless (it's how I call it)
    $endgroup$
    – t3chb0t
    8 hours ago











  • $begingroup$
    Which use cases you see for this API? End-user validation or easy validation logic for application and API developers?
    $endgroup$
    – dfhwze
    8 hours ago










  • $begingroup$
    @dfhwze for now just application and API developers (as there is no localization for messages).
    $endgroup$
    – t3chb0t
    8 hours ago










  • $begingroup$
    But you would like this to be a framework for end-users eventually, right?
    $endgroup$
    – dfhwze
    8 hours ago










  • $begingroup$
    @dfhwze that'd be cool...
    $endgroup$
    – t3chb0t
    8 hours ago















$begingroup$
Current commit: Flawless (it's how I call it)
$endgroup$
– t3chb0t
8 hours ago





$begingroup$
Current commit: Flawless (it's how I call it)
$endgroup$
– t3chb0t
8 hours ago













$begingroup$
Which use cases you see for this API? End-user validation or easy validation logic for application and API developers?
$endgroup$
– dfhwze
8 hours ago




$begingroup$
Which use cases you see for this API? End-user validation or easy validation logic for application and API developers?
$endgroup$
– dfhwze
8 hours ago












$begingroup$
@dfhwze for now just application and API developers (as there is no localization for messages).
$endgroup$
– t3chb0t
8 hours ago




$begingroup$
@dfhwze for now just application and API developers (as there is no localization for messages).
$endgroup$
– t3chb0t
8 hours ago












$begingroup$
But you would like this to be a framework for end-users eventually, right?
$endgroup$
– dfhwze
8 hours ago




$begingroup$
But you would like this to be a framework for end-users eventually, right?
$endgroup$
– dfhwze
8 hours ago












$begingroup$
@dfhwze that'd be cool...
$endgroup$
– t3chb0t
8 hours ago




$begingroup$
@dfhwze that'd be cool...
$endgroup$
– t3chb0t
8 hours ago










2 Answers
2






active

oldest

votes


















3












$begingroup$

As developer consuming your API ..



Usability



I find this a verbose way of constructing validation rules.




var rules = ValidationRuleCollection
.For<Person>()
.Add(x =>
ValidationRule
.Require
.NotNull(x))
.Add(x =>
ValidationRule
.Require
.NotNull(() => x.FirstName))
.Add(x =>
ValidationRule
.Ensure
.True(() => x.FirstName.Length > 3));

var (person, results) = default(Person).ValidateWith(rules);



I would like to able to call this like:



Tester.Require()
.NotNull("I want to be able to provide my own error message")
.NotNull(x => x.FirstName)
.Ensure(x => x.FirstName.Length > 3)
.Validate();


Extensibility



  • I would like to provide my own error messages and fallback to default messages if I don't specity any

  • I would like to be able to not only define pass/fail - true/false validations, but I would also like to provide a severity (error, warning, alert, ..)

General Issues



  • I feel your APIs are always well written, but also pretty complex/verbose. This is a small setback in intuitive use.





share|improve this answer









$endgroup$








  • 1




    $begingroup$
    I think I could add a couple of shortcut extensions to reduce the verbosity; about the custom message... sorry, I fogot to mention it in the description :-( there are default ones that the user can override with Message. It's sometimes hard to find the balance between verbose and extendable and at the same time to avoid creating one hundred builders so that you can write Rule.For.This.Crazy.Property.NotNull.And.Whatever ;-) from now on, I'll call it a flood-API in contrast to just fluent-one :-P
    $endgroup$
    – t3chb0t
    7 hours ago







  • 1




    $begingroup$
    The problem you are describing trying to find a balance is what i call making a sophisticated API. This means, simple and most common use cases should be very easy to write, while allowing for complex usages if the user should decide to require so. This is a very hard exercise to get right :) Indeed, if you would add shortcut extensions, this would help tremendously for the most common use cases.
    $endgroup$
    – dfhwze
    7 hours ago







  • 1




    $begingroup$
    From the available APIs I can add two more extensions and zip it to ValidationRuleCollection.For<Person>().Require((r, x) => r.NotNull(() => x)).Ensure((r, x) => r.NotNull(() => x.FirstName)). I'll see how I can rar it ;-]
    $endgroup$
    – t3chb0t
    7 hours ago











  • $begingroup$
    Since I did not make a thorough review and no other answers are available yet, you can still edit the question if you want to.
    $endgroup$
    – dfhwze
    7 hours ago










  • $begingroup$
    If I add this new API to the question it'll invalidate your answer because they are very similar.
    $endgroup$
    – t3chb0t
    7 hours ago



















2












$begingroup$

I like the idea, but I'm in line with dfhwze meaning it's a little too verbose and complicated to follow, especially when unable to debug.



I would prefer a more simple pattern like the one dfhwze suggests:



 var result =
Tester // the person
.Validate()
.NotNull(p => p.LastName, "LastName is Null")
.IsTrue(p => p.FirstName.Length > 3, "FirstName is too short")
.Match(p => p.Address.Street, @"^Sesa(m|n)e Street$", "Street name is invalid");

Console.WriteLine(result);


This can be implemented in a lightweight way like the below, where I use a Railway Orientend Programming-ish pattern:



 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 static class Validation

public static ValidateResult<T> Validate<T>(this T source)

return new Success<T>(source);


private static ValidateResult<T> Validate<T>(this ValidateResult<T> result, Predicate<T> predicate, string errorMessage)

if (result is Success<T> success)

if (!predicate(success.Source))
return new Failure<T>(success.Source, errorMessage);


return result;


public static ValidateResult<T> NotNull<T, TMember>(this ValidateResult<T> result, Expression<Func<T, TMember>> expression, string errorMessage)

var getter = expression.Compile();
Predicate<T> predicate = source => getter(source) != null;
return Validate(result, predicate, errorMessage);


public static ValidateResult<T> IsTrue<T>(this ValidateResult<T> result, Expression<Func<T, bool>> expression, string errorMessage)

var predicate = new Predicate<T>(expression.Compile());
return Validate(result, predicate, errorMessage);


public static ValidateResult<T> Match<T>(this ValidateResult<T> result, Expression<Func<T, string>> expression, string pattern, string errorMessage)

var getter = expression.Compile();
Predicate<T> predicate = source => Regex.IsMatch(getter(source), pattern);
return Validate(result, predicate, errorMessage);




The idea of the ROP pattern is that the first failure stops any further validation, but without throwing or any other error handling mechanism. You end up in the same place as if everything were OK, and can evaluate the result in one place. If you want to collect all possible failures, you can easily extent ValidateResult<T> with a collection of ValidateResult<T>s and then validate through the chain no matter what each result is.



IMO it's easy to follow, maintain and extent - for instance with the ability to be able to distinguish between degrees of failure. You could for instance implement a Warning<T> : ValdiateResult<T>.




Update



As t3chb0t (kindly I believe) emphasizes in his comment, I missed that he wants to have predefined validation rules. The above pattern can easily accommodate that requirement:



 public class Validator<T>

List<Func<ValidateResult<T>, ValidateResult<T>>> m_rules = new List<Func<ValidateResult<T>, ValidateResult<T>>>();

public ValidateResult<T> Validate(T source)

ValidateResult<T> result = source.Validate();
foreach (var rule in m_rules)

result = rule(result);


return result;


internal void AddRule(Predicate<T> predicate, string errorMessage)

Func<ValidateResult<T>, ValidateResult<T>> rule = result =>

if (result is Success<T> success)

if (!predicate(success.Source))
return new Failure<T>(success.Source, errorMessage);


return result;
;
m_rules.Add(rule);




Extended with validation rules:



 public static class Validation

public static ValidateResult<T> ValidateWith<T>(this T source, Validator<T> validator)

return validator.Validate(source);



public static Validator<T> NotNull<T, TMember>(this Validator<T> validator, Expression<Func<T, TMember>> expression, string errorMessage)

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<Func<T, bool>> expression, string errorMessage)

var predicate = new Predicate<T>(expression.Compile());
validator.AddRule(predicate, errorMessage);
return validator;


public static Validator<T> Match<T>(this Validator<T> validator, Expression<Func<T, string>> expression, string pattern, string errorMessage)

var getter = expression.Compile();
Predicate<T> predicate = source => Regex.IsMatch(getter(source), pattern);
validator.AddRule(predicate, errorMessage);
return validator;




And the same use case:



 Validator<Person> validator = new Validator<Person>();

validator
.NotNull(p => p.LastName, "LastName is Null")
.IsTrue(p => p.FirstName.Length > 3, "FirstName is too short")
.Match(p => p.Address.Street, @"^Sesa(m|n)e Street$", "Street name is invalid");

var result = Tester.ValidateWith(validator);

if (result is Success<Person> success)

Console.WriteLine(success);

else if (result is Failure<Person> failure)

Console.WriteLine(failure);






share|improve this answer











$endgroup$








  • 1




    $begingroup$
    nice! I've heard about ROP once here... but then forgot about it and spent like 2h today thinking about how to solve failures of preconditions and break the chain.
    $endgroup$
    – t3chb0t
    4 hours ago














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%2f222773%2fsimple-object-validator-with-a-new-api%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









3












$begingroup$

As developer consuming your API ..



Usability



I find this a verbose way of constructing validation rules.




var rules = ValidationRuleCollection
.For<Person>()
.Add(x =>
ValidationRule
.Require
.NotNull(x))
.Add(x =>
ValidationRule
.Require
.NotNull(() => x.FirstName))
.Add(x =>
ValidationRule
.Ensure
.True(() => x.FirstName.Length > 3));

var (person, results) = default(Person).ValidateWith(rules);



I would like to able to call this like:



Tester.Require()
.NotNull("I want to be able to provide my own error message")
.NotNull(x => x.FirstName)
.Ensure(x => x.FirstName.Length > 3)
.Validate();


Extensibility



  • I would like to provide my own error messages and fallback to default messages if I don't specity any

  • I would like to be able to not only define pass/fail - true/false validations, but I would also like to provide a severity (error, warning, alert, ..)

General Issues



  • I feel your APIs are always well written, but also pretty complex/verbose. This is a small setback in intuitive use.





share|improve this answer









$endgroup$








  • 1




    $begingroup$
    I think I could add a couple of shortcut extensions to reduce the verbosity; about the custom message... sorry, I fogot to mention it in the description :-( there are default ones that the user can override with Message. It's sometimes hard to find the balance between verbose and extendable and at the same time to avoid creating one hundred builders so that you can write Rule.For.This.Crazy.Property.NotNull.And.Whatever ;-) from now on, I'll call it a flood-API in contrast to just fluent-one :-P
    $endgroup$
    – t3chb0t
    7 hours ago







  • 1




    $begingroup$
    The problem you are describing trying to find a balance is what i call making a sophisticated API. This means, simple and most common use cases should be very easy to write, while allowing for complex usages if the user should decide to require so. This is a very hard exercise to get right :) Indeed, if you would add shortcut extensions, this would help tremendously for the most common use cases.
    $endgroup$
    – dfhwze
    7 hours ago







  • 1




    $begingroup$
    From the available APIs I can add two more extensions and zip it to ValidationRuleCollection.For<Person>().Require((r, x) => r.NotNull(() => x)).Ensure((r, x) => r.NotNull(() => x.FirstName)). I'll see how I can rar it ;-]
    $endgroup$
    – t3chb0t
    7 hours ago











  • $begingroup$
    Since I did not make a thorough review and no other answers are available yet, you can still edit the question if you want to.
    $endgroup$
    – dfhwze
    7 hours ago










  • $begingroup$
    If I add this new API to the question it'll invalidate your answer because they are very similar.
    $endgroup$
    – t3chb0t
    7 hours ago
















3












$begingroup$

As developer consuming your API ..



Usability



I find this a verbose way of constructing validation rules.




var rules = ValidationRuleCollection
.For<Person>()
.Add(x =>
ValidationRule
.Require
.NotNull(x))
.Add(x =>
ValidationRule
.Require
.NotNull(() => x.FirstName))
.Add(x =>
ValidationRule
.Ensure
.True(() => x.FirstName.Length > 3));

var (person, results) = default(Person).ValidateWith(rules);



I would like to able to call this like:



Tester.Require()
.NotNull("I want to be able to provide my own error message")
.NotNull(x => x.FirstName)
.Ensure(x => x.FirstName.Length > 3)
.Validate();


Extensibility



  • I would like to provide my own error messages and fallback to default messages if I don't specity any

  • I would like to be able to not only define pass/fail - true/false validations, but I would also like to provide a severity (error, warning, alert, ..)

General Issues



  • I feel your APIs are always well written, but also pretty complex/verbose. This is a small setback in intuitive use.





share|improve this answer









$endgroup$








  • 1




    $begingroup$
    I think I could add a couple of shortcut extensions to reduce the verbosity; about the custom message... sorry, I fogot to mention it in the description :-( there are default ones that the user can override with Message. It's sometimes hard to find the balance between verbose and extendable and at the same time to avoid creating one hundred builders so that you can write Rule.For.This.Crazy.Property.NotNull.And.Whatever ;-) from now on, I'll call it a flood-API in contrast to just fluent-one :-P
    $endgroup$
    – t3chb0t
    7 hours ago







  • 1




    $begingroup$
    The problem you are describing trying to find a balance is what i call making a sophisticated API. This means, simple and most common use cases should be very easy to write, while allowing for complex usages if the user should decide to require so. This is a very hard exercise to get right :) Indeed, if you would add shortcut extensions, this would help tremendously for the most common use cases.
    $endgroup$
    – dfhwze
    7 hours ago







  • 1




    $begingroup$
    From the available APIs I can add two more extensions and zip it to ValidationRuleCollection.For<Person>().Require((r, x) => r.NotNull(() => x)).Ensure((r, x) => r.NotNull(() => x.FirstName)). I'll see how I can rar it ;-]
    $endgroup$
    – t3chb0t
    7 hours ago











  • $begingroup$
    Since I did not make a thorough review and no other answers are available yet, you can still edit the question if you want to.
    $endgroup$
    – dfhwze
    7 hours ago










  • $begingroup$
    If I add this new API to the question it'll invalidate your answer because they are very similar.
    $endgroup$
    – t3chb0t
    7 hours ago














3












3








3





$begingroup$

As developer consuming your API ..



Usability



I find this a verbose way of constructing validation rules.




var rules = ValidationRuleCollection
.For<Person>()
.Add(x =>
ValidationRule
.Require
.NotNull(x))
.Add(x =>
ValidationRule
.Require
.NotNull(() => x.FirstName))
.Add(x =>
ValidationRule
.Ensure
.True(() => x.FirstName.Length > 3));

var (person, results) = default(Person).ValidateWith(rules);



I would like to able to call this like:



Tester.Require()
.NotNull("I want to be able to provide my own error message")
.NotNull(x => x.FirstName)
.Ensure(x => x.FirstName.Length > 3)
.Validate();


Extensibility



  • I would like to provide my own error messages and fallback to default messages if I don't specity any

  • I would like to be able to not only define pass/fail - true/false validations, but I would also like to provide a severity (error, warning, alert, ..)

General Issues



  • I feel your APIs are always well written, but also pretty complex/verbose. This is a small setback in intuitive use.





share|improve this answer









$endgroup$



As developer consuming your API ..



Usability



I find this a verbose way of constructing validation rules.




var rules = ValidationRuleCollection
.For<Person>()
.Add(x =>
ValidationRule
.Require
.NotNull(x))
.Add(x =>
ValidationRule
.Require
.NotNull(() => x.FirstName))
.Add(x =>
ValidationRule
.Ensure
.True(() => x.FirstName.Length > 3));

var (person, results) = default(Person).ValidateWith(rules);



I would like to able to call this like:



Tester.Require()
.NotNull("I want to be able to provide my own error message")
.NotNull(x => x.FirstName)
.Ensure(x => x.FirstName.Length > 3)
.Validate();


Extensibility



  • I would like to provide my own error messages and fallback to default messages if I don't specity any

  • I would like to be able to not only define pass/fail - true/false validations, but I would also like to provide a severity (error, warning, alert, ..)

General Issues



  • I feel your APIs are always well written, but also pretty complex/verbose. This is a small setback in intuitive use.






share|improve this answer












share|improve this answer



share|improve this answer










answered 7 hours ago









dfhwzedfhwze

3,3211 gold badge6 silver badges32 bronze badges




3,3211 gold badge6 silver badges32 bronze badges







  • 1




    $begingroup$
    I think I could add a couple of shortcut extensions to reduce the verbosity; about the custom message... sorry, I fogot to mention it in the description :-( there are default ones that the user can override with Message. It's sometimes hard to find the balance between verbose and extendable and at the same time to avoid creating one hundred builders so that you can write Rule.For.This.Crazy.Property.NotNull.And.Whatever ;-) from now on, I'll call it a flood-API in contrast to just fluent-one :-P
    $endgroup$
    – t3chb0t
    7 hours ago







  • 1




    $begingroup$
    The problem you are describing trying to find a balance is what i call making a sophisticated API. This means, simple and most common use cases should be very easy to write, while allowing for complex usages if the user should decide to require so. This is a very hard exercise to get right :) Indeed, if you would add shortcut extensions, this would help tremendously for the most common use cases.
    $endgroup$
    – dfhwze
    7 hours ago







  • 1




    $begingroup$
    From the available APIs I can add two more extensions and zip it to ValidationRuleCollection.For<Person>().Require((r, x) => r.NotNull(() => x)).Ensure((r, x) => r.NotNull(() => x.FirstName)). I'll see how I can rar it ;-]
    $endgroup$
    – t3chb0t
    7 hours ago











  • $begingroup$
    Since I did not make a thorough review and no other answers are available yet, you can still edit the question if you want to.
    $endgroup$
    – dfhwze
    7 hours ago










  • $begingroup$
    If I add this new API to the question it'll invalidate your answer because they are very similar.
    $endgroup$
    – t3chb0t
    7 hours ago













  • 1




    $begingroup$
    I think I could add a couple of shortcut extensions to reduce the verbosity; about the custom message... sorry, I fogot to mention it in the description :-( there are default ones that the user can override with Message. It's sometimes hard to find the balance between verbose and extendable and at the same time to avoid creating one hundred builders so that you can write Rule.For.This.Crazy.Property.NotNull.And.Whatever ;-) from now on, I'll call it a flood-API in contrast to just fluent-one :-P
    $endgroup$
    – t3chb0t
    7 hours ago







  • 1




    $begingroup$
    The problem you are describing trying to find a balance is what i call making a sophisticated API. This means, simple and most common use cases should be very easy to write, while allowing for complex usages if the user should decide to require so. This is a very hard exercise to get right :) Indeed, if you would add shortcut extensions, this would help tremendously for the most common use cases.
    $endgroup$
    – dfhwze
    7 hours ago







  • 1




    $begingroup$
    From the available APIs I can add two more extensions and zip it to ValidationRuleCollection.For<Person>().Require((r, x) => r.NotNull(() => x)).Ensure((r, x) => r.NotNull(() => x.FirstName)). I'll see how I can rar it ;-]
    $endgroup$
    – t3chb0t
    7 hours ago











  • $begingroup$
    Since I did not make a thorough review and no other answers are available yet, you can still edit the question if you want to.
    $endgroup$
    – dfhwze
    7 hours ago










  • $begingroup$
    If I add this new API to the question it'll invalidate your answer because they are very similar.
    $endgroup$
    – t3chb0t
    7 hours ago








1




1




$begingroup$
I think I could add a couple of shortcut extensions to reduce the verbosity; about the custom message... sorry, I fogot to mention it in the description :-( there are default ones that the user can override with Message. It's sometimes hard to find the balance between verbose and extendable and at the same time to avoid creating one hundred builders so that you can write Rule.For.This.Crazy.Property.NotNull.And.Whatever ;-) from now on, I'll call it a flood-API in contrast to just fluent-one :-P
$endgroup$
– t3chb0t
7 hours ago





$begingroup$
I think I could add a couple of shortcut extensions to reduce the verbosity; about the custom message... sorry, I fogot to mention it in the description :-( there are default ones that the user can override with Message. It's sometimes hard to find the balance between verbose and extendable and at the same time to avoid creating one hundred builders so that you can write Rule.For.This.Crazy.Property.NotNull.And.Whatever ;-) from now on, I'll call it a flood-API in contrast to just fluent-one :-P
$endgroup$
– t3chb0t
7 hours ago





1




1




$begingroup$
The problem you are describing trying to find a balance is what i call making a sophisticated API. This means, simple and most common use cases should be very easy to write, while allowing for complex usages if the user should decide to require so. This is a very hard exercise to get right :) Indeed, if you would add shortcut extensions, this would help tremendously for the most common use cases.
$endgroup$
– dfhwze
7 hours ago





$begingroup$
The problem you are describing trying to find a balance is what i call making a sophisticated API. This means, simple and most common use cases should be very easy to write, while allowing for complex usages if the user should decide to require so. This is a very hard exercise to get right :) Indeed, if you would add shortcut extensions, this would help tremendously for the most common use cases.
$endgroup$
– dfhwze
7 hours ago





1




1




$begingroup$
From the available APIs I can add two more extensions and zip it to ValidationRuleCollection.For<Person>().Require((r, x) => r.NotNull(() => x)).Ensure((r, x) => r.NotNull(() => x.FirstName)). I'll see how I can rar it ;-]
$endgroup$
– t3chb0t
7 hours ago





$begingroup$
From the available APIs I can add two more extensions and zip it to ValidationRuleCollection.For<Person>().Require((r, x) => r.NotNull(() => x)).Ensure((r, x) => r.NotNull(() => x.FirstName)). I'll see how I can rar it ;-]
$endgroup$
– t3chb0t
7 hours ago













$begingroup$
Since I did not make a thorough review and no other answers are available yet, you can still edit the question if you want to.
$endgroup$
– dfhwze
7 hours ago




$begingroup$
Since I did not make a thorough review and no other answers are available yet, you can still edit the question if you want to.
$endgroup$
– dfhwze
7 hours ago












$begingroup$
If I add this new API to the question it'll invalidate your answer because they are very similar.
$endgroup$
– t3chb0t
7 hours ago





$begingroup$
If I add this new API to the question it'll invalidate your answer because they are very similar.
$endgroup$
– t3chb0t
7 hours ago














2












$begingroup$

I like the idea, but I'm in line with dfhwze meaning it's a little too verbose and complicated to follow, especially when unable to debug.



I would prefer a more simple pattern like the one dfhwze suggests:



 var result =
Tester // the person
.Validate()
.NotNull(p => p.LastName, "LastName is Null")
.IsTrue(p => p.FirstName.Length > 3, "FirstName is too short")
.Match(p => p.Address.Street, @"^Sesa(m|n)e Street$", "Street name is invalid");

Console.WriteLine(result);


This can be implemented in a lightweight way like the below, where I use a Railway Orientend Programming-ish pattern:



 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 static class Validation

public static ValidateResult<T> Validate<T>(this T source)

return new Success<T>(source);


private static ValidateResult<T> Validate<T>(this ValidateResult<T> result, Predicate<T> predicate, string errorMessage)

if (result is Success<T> success)

if (!predicate(success.Source))
return new Failure<T>(success.Source, errorMessage);


return result;


public static ValidateResult<T> NotNull<T, TMember>(this ValidateResult<T> result, Expression<Func<T, TMember>> expression, string errorMessage)

var getter = expression.Compile();
Predicate<T> predicate = source => getter(source) != null;
return Validate(result, predicate, errorMessage);


public static ValidateResult<T> IsTrue<T>(this ValidateResult<T> result, Expression<Func<T, bool>> expression, string errorMessage)

var predicate = new Predicate<T>(expression.Compile());
return Validate(result, predicate, errorMessage);


public static ValidateResult<T> Match<T>(this ValidateResult<T> result, Expression<Func<T, string>> expression, string pattern, string errorMessage)

var getter = expression.Compile();
Predicate<T> predicate = source => Regex.IsMatch(getter(source), pattern);
return Validate(result, predicate, errorMessage);




The idea of the ROP pattern is that the first failure stops any further validation, but without throwing or any other error handling mechanism. You end up in the same place as if everything were OK, and can evaluate the result in one place. If you want to collect all possible failures, you can easily extent ValidateResult<T> with a collection of ValidateResult<T>s and then validate through the chain no matter what each result is.



IMO it's easy to follow, maintain and extent - for instance with the ability to be able to distinguish between degrees of failure. You could for instance implement a Warning<T> : ValdiateResult<T>.




Update



As t3chb0t (kindly I believe) emphasizes in his comment, I missed that he wants to have predefined validation rules. The above pattern can easily accommodate that requirement:



 public class Validator<T>

List<Func<ValidateResult<T>, ValidateResult<T>>> m_rules = new List<Func<ValidateResult<T>, ValidateResult<T>>>();

public ValidateResult<T> Validate(T source)

ValidateResult<T> result = source.Validate();
foreach (var rule in m_rules)

result = rule(result);


return result;


internal void AddRule(Predicate<T> predicate, string errorMessage)

Func<ValidateResult<T>, ValidateResult<T>> rule = result =>

if (result is Success<T> success)

if (!predicate(success.Source))
return new Failure<T>(success.Source, errorMessage);


return result;
;
m_rules.Add(rule);




Extended with validation rules:



 public static class Validation

public static ValidateResult<T> ValidateWith<T>(this T source, Validator<T> validator)

return validator.Validate(source);



public static Validator<T> NotNull<T, TMember>(this Validator<T> validator, Expression<Func<T, TMember>> expression, string errorMessage)

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<Func<T, bool>> expression, string errorMessage)

var predicate = new Predicate<T>(expression.Compile());
validator.AddRule(predicate, errorMessage);
return validator;


public static Validator<T> Match<T>(this Validator<T> validator, Expression<Func<T, string>> expression, string pattern, string errorMessage)

var getter = expression.Compile();
Predicate<T> predicate = source => Regex.IsMatch(getter(source), pattern);
validator.AddRule(predicate, errorMessage);
return validator;




And the same use case:



 Validator<Person> validator = new Validator<Person>();

validator
.NotNull(p => p.LastName, "LastName is Null")
.IsTrue(p => p.FirstName.Length > 3, "FirstName is too short")
.Match(p => p.Address.Street, @"^Sesa(m|n)e Street$", "Street name is invalid");

var result = Tester.ValidateWith(validator);

if (result is Success<Person> success)

Console.WriteLine(success);

else if (result is Failure<Person> failure)

Console.WriteLine(failure);






share|improve this answer











$endgroup$








  • 1




    $begingroup$
    nice! I've heard about ROP once here... but then forgot about it and spent like 2h today thinking about how to solve failures of preconditions and break the chain.
    $endgroup$
    – t3chb0t
    4 hours ago
















2












$begingroup$

I like the idea, but I'm in line with dfhwze meaning it's a little too verbose and complicated to follow, especially when unable to debug.



I would prefer a more simple pattern like the one dfhwze suggests:



 var result =
Tester // the person
.Validate()
.NotNull(p => p.LastName, "LastName is Null")
.IsTrue(p => p.FirstName.Length > 3, "FirstName is too short")
.Match(p => p.Address.Street, @"^Sesa(m|n)e Street$", "Street name is invalid");

Console.WriteLine(result);


This can be implemented in a lightweight way like the below, where I use a Railway Orientend Programming-ish pattern:



 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 static class Validation

public static ValidateResult<T> Validate<T>(this T source)

return new Success<T>(source);


private static ValidateResult<T> Validate<T>(this ValidateResult<T> result, Predicate<T> predicate, string errorMessage)

if (result is Success<T> success)

if (!predicate(success.Source))
return new Failure<T>(success.Source, errorMessage);


return result;


public static ValidateResult<T> NotNull<T, TMember>(this ValidateResult<T> result, Expression<Func<T, TMember>> expression, string errorMessage)

var getter = expression.Compile();
Predicate<T> predicate = source => getter(source) != null;
return Validate(result, predicate, errorMessage);


public static ValidateResult<T> IsTrue<T>(this ValidateResult<T> result, Expression<Func<T, bool>> expression, string errorMessage)

var predicate = new Predicate<T>(expression.Compile());
return Validate(result, predicate, errorMessage);


public static ValidateResult<T> Match<T>(this ValidateResult<T> result, Expression<Func<T, string>> expression, string pattern, string errorMessage)

var getter = expression.Compile();
Predicate<T> predicate = source => Regex.IsMatch(getter(source), pattern);
return Validate(result, predicate, errorMessage);




The idea of the ROP pattern is that the first failure stops any further validation, but without throwing or any other error handling mechanism. You end up in the same place as if everything were OK, and can evaluate the result in one place. If you want to collect all possible failures, you can easily extent ValidateResult<T> with a collection of ValidateResult<T>s and then validate through the chain no matter what each result is.



IMO it's easy to follow, maintain and extent - for instance with the ability to be able to distinguish between degrees of failure. You could for instance implement a Warning<T> : ValdiateResult<T>.




Update



As t3chb0t (kindly I believe) emphasizes in his comment, I missed that he wants to have predefined validation rules. The above pattern can easily accommodate that requirement:



 public class Validator<T>

List<Func<ValidateResult<T>, ValidateResult<T>>> m_rules = new List<Func<ValidateResult<T>, ValidateResult<T>>>();

public ValidateResult<T> Validate(T source)

ValidateResult<T> result = source.Validate();
foreach (var rule in m_rules)

result = rule(result);


return result;


internal void AddRule(Predicate<T> predicate, string errorMessage)

Func<ValidateResult<T>, ValidateResult<T>> rule = result =>

if (result is Success<T> success)

if (!predicate(success.Source))
return new Failure<T>(success.Source, errorMessage);


return result;
;
m_rules.Add(rule);




Extended with validation rules:



 public static class Validation

public static ValidateResult<T> ValidateWith<T>(this T source, Validator<T> validator)

return validator.Validate(source);



public static Validator<T> NotNull<T, TMember>(this Validator<T> validator, Expression<Func<T, TMember>> expression, string errorMessage)

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<Func<T, bool>> expression, string errorMessage)

var predicate = new Predicate<T>(expression.Compile());
validator.AddRule(predicate, errorMessage);
return validator;


public static Validator<T> Match<T>(this Validator<T> validator, Expression<Func<T, string>> expression, string pattern, string errorMessage)

var getter = expression.Compile();
Predicate<T> predicate = source => Regex.IsMatch(getter(source), pattern);
validator.AddRule(predicate, errorMessage);
return validator;




And the same use case:



 Validator<Person> validator = new Validator<Person>();

validator
.NotNull(p => p.LastName, "LastName is Null")
.IsTrue(p => p.FirstName.Length > 3, "FirstName is too short")
.Match(p => p.Address.Street, @"^Sesa(m|n)e Street$", "Street name is invalid");

var result = Tester.ValidateWith(validator);

if (result is Success<Person> success)

Console.WriteLine(success);

else if (result is Failure<Person> failure)

Console.WriteLine(failure);






share|improve this answer











$endgroup$








  • 1




    $begingroup$
    nice! I've heard about ROP once here... but then forgot about it and spent like 2h today thinking about how to solve failures of preconditions and break the chain.
    $endgroup$
    – t3chb0t
    4 hours ago














2












2








2





$begingroup$

I like the idea, but I'm in line with dfhwze meaning it's a little too verbose and complicated to follow, especially when unable to debug.



I would prefer a more simple pattern like the one dfhwze suggests:



 var result =
Tester // the person
.Validate()
.NotNull(p => p.LastName, "LastName is Null")
.IsTrue(p => p.FirstName.Length > 3, "FirstName is too short")
.Match(p => p.Address.Street, @"^Sesa(m|n)e Street$", "Street name is invalid");

Console.WriteLine(result);


This can be implemented in a lightweight way like the below, where I use a Railway Orientend Programming-ish pattern:



 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 static class Validation

public static ValidateResult<T> Validate<T>(this T source)

return new Success<T>(source);


private static ValidateResult<T> Validate<T>(this ValidateResult<T> result, Predicate<T> predicate, string errorMessage)

if (result is Success<T> success)

if (!predicate(success.Source))
return new Failure<T>(success.Source, errorMessage);


return result;


public static ValidateResult<T> NotNull<T, TMember>(this ValidateResult<T> result, Expression<Func<T, TMember>> expression, string errorMessage)

var getter = expression.Compile();
Predicate<T> predicate = source => getter(source) != null;
return Validate(result, predicate, errorMessage);


public static ValidateResult<T> IsTrue<T>(this ValidateResult<T> result, Expression<Func<T, bool>> expression, string errorMessage)

var predicate = new Predicate<T>(expression.Compile());
return Validate(result, predicate, errorMessage);


public static ValidateResult<T> Match<T>(this ValidateResult<T> result, Expression<Func<T, string>> expression, string pattern, string errorMessage)

var getter = expression.Compile();
Predicate<T> predicate = source => Regex.IsMatch(getter(source), pattern);
return Validate(result, predicate, errorMessage);




The idea of the ROP pattern is that the first failure stops any further validation, but without throwing or any other error handling mechanism. You end up in the same place as if everything were OK, and can evaluate the result in one place. If you want to collect all possible failures, you can easily extent ValidateResult<T> with a collection of ValidateResult<T>s and then validate through the chain no matter what each result is.



IMO it's easy to follow, maintain and extent - for instance with the ability to be able to distinguish between degrees of failure. You could for instance implement a Warning<T> : ValdiateResult<T>.




Update



As t3chb0t (kindly I believe) emphasizes in his comment, I missed that he wants to have predefined validation rules. The above pattern can easily accommodate that requirement:



 public class Validator<T>

List<Func<ValidateResult<T>, ValidateResult<T>>> m_rules = new List<Func<ValidateResult<T>, ValidateResult<T>>>();

public ValidateResult<T> Validate(T source)

ValidateResult<T> result = source.Validate();
foreach (var rule in m_rules)

result = rule(result);


return result;


internal void AddRule(Predicate<T> predicate, string errorMessage)

Func<ValidateResult<T>, ValidateResult<T>> rule = result =>

if (result is Success<T> success)

if (!predicate(success.Source))
return new Failure<T>(success.Source, errorMessage);


return result;
;
m_rules.Add(rule);




Extended with validation rules:



 public static class Validation

public static ValidateResult<T> ValidateWith<T>(this T source, Validator<T> validator)

return validator.Validate(source);



public static Validator<T> NotNull<T, TMember>(this Validator<T> validator, Expression<Func<T, TMember>> expression, string errorMessage)

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<Func<T, bool>> expression, string errorMessage)

var predicate = new Predicate<T>(expression.Compile());
validator.AddRule(predicate, errorMessage);
return validator;


public static Validator<T> Match<T>(this Validator<T> validator, Expression<Func<T, string>> expression, string pattern, string errorMessage)

var getter = expression.Compile();
Predicate<T> predicate = source => Regex.IsMatch(getter(source), pattern);
validator.AddRule(predicate, errorMessage);
return validator;




And the same use case:



 Validator<Person> validator = new Validator<Person>();

validator
.NotNull(p => p.LastName, "LastName is Null")
.IsTrue(p => p.FirstName.Length > 3, "FirstName is too short")
.Match(p => p.Address.Street, @"^Sesa(m|n)e Street$", "Street name is invalid");

var result = Tester.ValidateWith(validator);

if (result is Success<Person> success)

Console.WriteLine(success);

else if (result is Failure<Person> failure)

Console.WriteLine(failure);






share|improve this answer











$endgroup$



I like the idea, but I'm in line with dfhwze meaning it's a little too verbose and complicated to follow, especially when unable to debug.



I would prefer a more simple pattern like the one dfhwze suggests:



 var result =
Tester // the person
.Validate()
.NotNull(p => p.LastName, "LastName is Null")
.IsTrue(p => p.FirstName.Length > 3, "FirstName is too short")
.Match(p => p.Address.Street, @"^Sesa(m|n)e Street$", "Street name is invalid");

Console.WriteLine(result);


This can be implemented in a lightweight way like the below, where I use a Railway Orientend Programming-ish pattern:



 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 static class Validation

public static ValidateResult<T> Validate<T>(this T source)

return new Success<T>(source);


private static ValidateResult<T> Validate<T>(this ValidateResult<T> result, Predicate<T> predicate, string errorMessage)

if (result is Success<T> success)

if (!predicate(success.Source))
return new Failure<T>(success.Source, errorMessage);


return result;


public static ValidateResult<T> NotNull<T, TMember>(this ValidateResult<T> result, Expression<Func<T, TMember>> expression, string errorMessage)

var getter = expression.Compile();
Predicate<T> predicate = source => getter(source) != null;
return Validate(result, predicate, errorMessage);


public static ValidateResult<T> IsTrue<T>(this ValidateResult<T> result, Expression<Func<T, bool>> expression, string errorMessage)

var predicate = new Predicate<T>(expression.Compile());
return Validate(result, predicate, errorMessage);


public static ValidateResult<T> Match<T>(this ValidateResult<T> result, Expression<Func<T, string>> expression, string pattern, string errorMessage)

var getter = expression.Compile();
Predicate<T> predicate = source => Regex.IsMatch(getter(source), pattern);
return Validate(result, predicate, errorMessage);




The idea of the ROP pattern is that the first failure stops any further validation, but without throwing or any other error handling mechanism. You end up in the same place as if everything were OK, and can evaluate the result in one place. If you want to collect all possible failures, you can easily extent ValidateResult<T> with a collection of ValidateResult<T>s and then validate through the chain no matter what each result is.



IMO it's easy to follow, maintain and extent - for instance with the ability to be able to distinguish between degrees of failure. You could for instance implement a Warning<T> : ValdiateResult<T>.




Update



As t3chb0t (kindly I believe) emphasizes in his comment, I missed that he wants to have predefined validation rules. The above pattern can easily accommodate that requirement:



 public class Validator<T>

List<Func<ValidateResult<T>, ValidateResult<T>>> m_rules = new List<Func<ValidateResult<T>, ValidateResult<T>>>();

public ValidateResult<T> Validate(T source)

ValidateResult<T> result = source.Validate();
foreach (var rule in m_rules)

result = rule(result);


return result;


internal void AddRule(Predicate<T> predicate, string errorMessage)

Func<ValidateResult<T>, ValidateResult<T>> rule = result =>

if (result is Success<T> success)

if (!predicate(success.Source))
return new Failure<T>(success.Source, errorMessage);


return result;
;
m_rules.Add(rule);




Extended with validation rules:



 public static class Validation

public static ValidateResult<T> ValidateWith<T>(this T source, Validator<T> validator)

return validator.Validate(source);



public static Validator<T> NotNull<T, TMember>(this Validator<T> validator, Expression<Func<T, TMember>> expression, string errorMessage)

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<Func<T, bool>> expression, string errorMessage)

var predicate = new Predicate<T>(expression.Compile());
validator.AddRule(predicate, errorMessage);
return validator;


public static Validator<T> Match<T>(this Validator<T> validator, Expression<Func<T, string>> expression, string pattern, string errorMessage)

var getter = expression.Compile();
Predicate<T> predicate = source => Regex.IsMatch(getter(source), pattern);
validator.AddRule(predicate, errorMessage);
return validator;




And the same use case:



 Validator<Person> validator = new Validator<Person>();

validator
.NotNull(p => p.LastName, "LastName is Null")
.IsTrue(p => p.FirstName.Length > 3, "FirstName is too short")
.Match(p => p.Address.Street, @"^Sesa(m|n)e Street$", "Street name is invalid");

var result = Tester.ValidateWith(validator);

if (result is Success<Person> success)

Console.WriteLine(success);

else if (result is Failure<Person> failure)

Console.WriteLine(failure);







share|improve this answer














share|improve this answer



share|improve this answer








edited 1 hour ago

























answered 4 hours ago









Henrik HansenHenrik Hansen

9,5781 gold badge13 silver badges34 bronze badges




9,5781 gold badge13 silver badges34 bronze badges







  • 1




    $begingroup$
    nice! I've heard about ROP once here... but then forgot about it and spent like 2h today thinking about how to solve failures of preconditions and break the chain.
    $endgroup$
    – t3chb0t
    4 hours ago













  • 1




    $begingroup$
    nice! I've heard about ROP once here... but then forgot about it and spent like 2h today thinking about how to solve failures of preconditions and break the chain.
    $endgroup$
    – t3chb0t
    4 hours ago








1




1




$begingroup$
nice! I've heard about ROP once here... but then forgot about it and spent like 2h today thinking about how to solve failures of preconditions and break the chain.
$endgroup$
– t3chb0t
4 hours ago





$begingroup$
nice! I've heard about ROP once here... but then forgot about it and spent like 2h today thinking about how to solve failures of preconditions and break the chain.
$endgroup$
– t3chb0t
4 hours ago


















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%2f222773%2fsimple-object-validator-with-a-new-api%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

19. јануар Садржај Догађаји Рођења Смрти Празници и дани сећања Види још Референце Мени за навигацијуу

Israel Cuprins Etimologie | Istorie | Geografie | Politică | Demografie | Educație | Economie | Cultură | Note explicative | Note bibliografice | Bibliografie | Legături externe | Meniu de navigaresite web oficialfacebooktweeterGoogle+Instagramcanal YouTubeInstagramtextmodificaremodificarewww.technion.ac.ilnew.huji.ac.ilwww.weizmann.ac.ilwww1.biu.ac.ilenglish.tau.ac.ilwww.haifa.ac.ilin.bgu.ac.ilwww.openu.ac.ilwww.ariel.ac.ilCIA FactbookHarta Israelului"Negotiating Jerusalem," Palestine–Israel JournalThe Schizoid Nature of Modern Hebrew: A Slavic Language in Search of a Semitic Past„Arabic in Israel: an official language and a cultural bridge”„Latest Population Statistics for Israel”„Israel Population”„Tables”„Report for Selected Countries and Subjects”Human Development Report 2016: Human Development for Everyone„Distribution of family income - Gini index”The World FactbookJerusalem Law„Israel”„Israel”„Zionist Leaders: David Ben-Gurion 1886–1973”„The status of Jerusalem”„Analysis: Kadima's big plans”„Israel's Hard-Learned Lessons”„The Legacy of Undefined Borders, Tel Aviv Notes No. 40, 5 iunie 2002”„Israel Journal: A Land Without Borders”„Population”„Israel closes decade with population of 7.5 million”Time Series-DataBank„Selected Statistics on Jerusalem Day 2007 (Hebrew)”Golan belongs to Syria, Druze protestGlobal Survey 2006: Middle East Progress Amid Global Gains in FreedomWHO: Life expectancy in Israel among highest in the worldInternational Monetary Fund, World Economic Outlook Database, April 2011: Nominal GDP list of countries. Data for the year 2010.„Israel's accession to the OECD”Popular Opinion„On the Move”Hosea 12:5„Walking the Bible Timeline”„Palestine: History”„Return to Zion”An invention called 'the Jewish people' – Haaretz – Israel NewsoriginalJewish and Non-Jewish Population of Palestine-Israel (1517–2004)ImmigrationJewishvirtuallibrary.orgChapter One: The Heralders of Zionism„The birth of modern Israel: A scrap of paper that changed history”„League of Nations: The Mandate for Palestine, 24 iulie 1922”The Population of Palestine Prior to 1948originalBackground Paper No. 47 (ST/DPI/SER.A/47)History: Foreign DominationTwo Hundred and Seventh Plenary Meeting„Israel (Labor Zionism)”Population, by Religion and Population GroupThe Suez CrisisAdolf EichmannJustice Ministry Reply to Amnesty International Report„The Interregnum”Israel Ministry of Foreign Affairs – The Palestinian National Covenant- July 1968Research on terrorism: trends, achievements & failuresThe Routledge Atlas of the Arab–Israeli conflict: The Complete History of the Struggle and the Efforts to Resolve It"George Habash, Palestinian Terrorism Tactician, Dies at 82."„1973: Arab states attack Israeli forces”Agranat Commission„Has Israel Annexed East Jerusalem?”original„After 4 Years, Intifada Still Smolders”From the End of the Cold War to 2001originalThe Oslo Accords, 1993Israel-PLO Recognition – Exchange of Letters between PM Rabin and Chairman Arafat – Sept 9- 1993Foundation for Middle East PeaceSources of Population Growth: Total Israeli Population and Settler Population, 1991–2003original„Israel marks Rabin assassination”The Wye River Memorandumoriginal„West Bank barrier route disputed, Israeli missile kills 2”"Permanent Ceasefire to Be Based on Creation Of Buffer Zone Free of Armed Personnel Other than UN, Lebanese Forces"„Hezbollah kills 8 soldiers, kidnaps two in offensive on northern border”„Olmert confirms peace talks with Syria”„Battleground Gaza: Israeli ground forces invade the strip”„IDF begins Gaza troop withdrawal, hours after ending 3-week offensive”„THE LAND: Geography and Climate”„Area of districts, sub-districts, natural regions and lakes”„Israel - Geography”„Makhteshim Country”Israel and the Palestinian Territories„Makhtesh Ramon”„The Living Dead Sea”„Temperatures reach record high in Pakistan”„Climate Extremes In Israel”Israel in figures„Deuteronom”„JNF: 240 million trees planted since 1901”„Vegetation of Israel and Neighboring Countries”Environmental Law in Israel„Executive branch”„Israel's election process explained”„The Electoral System in Israel”„Constitution for Israel”„All 120 incoming Knesset members”„Statul ISRAEL”„The Judiciary: The Court System”„Israel's high court unique in region”„Israel and the International Criminal Court: A Legal Battlefield”„Localities and population, by population group, district, sub-district and natural region”„Israel: Districts, Major Cities, Urban Localities & Metropolitan Areas”„Israel-Egypt Relations: Background & Overview of Peace Treaty”„Solana to Haaretz: New Rules of War Needed for Age of Terror”„Israel's Announcement Regarding Settlements”„United Nations Security Council Resolution 497”„Security Council resolution 478 (1980) on the status of Jerusalem”„Arabs will ask U.N. to seek razing of Israeli wall”„Olmert: Willing to trade land for peace”„Mapping Peace between Syria and Israel”„Egypt: Israel must accept the land-for-peace formula”„Israel: Age structure from 2005 to 2015”„Global, regional, and national disability-adjusted life years (DALYs) for 306 diseases and injuries and healthy life expectancy (HALE) for 188 countries, 1990–2013: quantifying the epidemiological transition”10.1016/S0140-6736(15)61340-X„World Health Statistics 2014”„Life expectancy for Israeli men world's 4th highest”„Family Structure and Well-Being Across Israel's Diverse Population”„Fertility among Jewish and Muslim Women in Israel, by Level of Religiosity, 1979-2009”„Israel leaders in birth rate, but poverty major challenge”„Ethnic Groups”„Israel's population: Over 8.5 million”„Israel - Ethnic groups”„Jews, by country of origin and age”„Minority Communities in Israel: Background & Overview”„Israel”„Language in Israel”„Selected Data from the 2011 Social Survey on Mastery of the Hebrew Language and Usage of Languages”„Religions”„5 facts about Israeli Druze, a unique religious and ethnic group”„Israël”Israel Country Study Guide„Haredi city in Negev – blessing or curse?”„New town Harish harbors hopes of being more than another Pleasantville”„List of localities, in alphabetical order”„Muncitorii români, doriți în Israel”„Prietenia româno-israeliană la nevoie se cunoaște”„The Higher Education System in Israel”„Middle East”„Academic Ranking of World Universities 2016”„Israel”„Israel”„Jewish Nobel Prize Winners”„All Nobel Prizes in Literature”„All Nobel Peace Prizes”„All Prizes in Economic Sciences”„All Nobel Prizes in Chemistry”„List of Fields Medallists”„Sakharov Prize”„Țara care și-a sfidat "destinul" și se bate umăr la umăr cu Silicon Valley”„Apple's R&D center in Israel grew to about 800 employees”„Tim Cook: Apple's Herzliya R&D center second-largest in world”„Lecții de economie de la Israel”„Land use”Israel Investment and Business GuideA Country Study: IsraelCentral Bureau of StatisticsFlorin Diaconu, „Kadima: Flexibilitate și pragmatism, dar nici un compromis în chestiuni vitale", în Revista Institutului Diplomatic Român, anul I, numărul I, semestrul I, 2006, pp. 71-72Florin Diaconu, „Likud: Dreapta israeliană constant opusă retrocedării teritoriilor cureite prin luptă în 1967", în Revista Institutului Diplomatic Român, anul I, numărul I, semestrul I, 2006, pp. 73-74MassadaIsraelul a crescut in 50 de ani cât alte state intr-un mileniuIsrael Government PortalIsraelIsraelIsraelmmmmmXX451232cb118646298(data)4027808-634110000 0004 0372 0767n7900328503691455-bb46-37e3-91d2-cb064a35ffcc1003570400564274ge1294033523775214929302638955X146498911146498911

Кастелфранко ди Сопра Становништво Референце Спољашње везе Мени за навигацију43°37′18″ СГШ; 11°33′32″ ИГД / 43.62156° СГШ; 11.55885° ИГД / 43.62156; 11.5588543°37′18″ СГШ; 11°33′32″ ИГД / 43.62156° СГШ; 11.55885° ИГД / 43.62156; 11.558853179688„The GeoNames geographical database”„Istituto Nazionale di Statistica”проширитиууWorldCat156923403n850174324558639-1cb14643287r(подаци)