Skip to main content
Commonmark migration
Source Link

###Simplified the API

Simplified the API

###Changes

Changes

###Simplified the API

###Changes

Simplified the API

Changes

spelling
Source Link
dfhwze
  • 14.2k
  • 3
  • 40
  • 101

You were right, the API was too verbose so I drasticllydrastically simplified it to. It now presents itself like that:

I was not able to incorporate the ROP here this time, but the many other ideas where very helpful. Here's the summary:

There are only two main extensions Accept that enforces a rule and Reject that negates it (internally), so there is no need for other extensions prefixed with Not.

The last extension, or rather a modifier is called Hard. This one yields a different rule, one that when failed aborts the validation. This also means that I now have two rules which are descendantesdescendants of the abstract class ValidationRule<T, TContext>:

Generating messages internally still has to be improved, but as far as the main API is concerned, I'm happy with it.

You were right, the API was too verbose so I drasticlly simplified it to. It now presents itself like that:

I was not able to incorporate the ROP here this time but the many other ideas where very helpful. Here's the summary:

There are only two main extensions Accept that enforces a rule and Reject that negates it (internally) so there is no need for other extensions prefixed with Not.

The last extension, or rather a modifier is called Hard. This one yields a different rule, one that when failed aborts the validation. This also means that I now have two rules which are descendantes of the abstract class ValidationRule<T, TContext>:

Generating messages internally still has to be improved but as far as the main API is concerned, I'm happy with it.

You were right, the API was too verbose so I drastically simplified it. It now presents itself like that:

I was not able to incorporate the ROP here this time, but the many other ideas where very helpful. Here's the summary:

There are only two main extensions Accept that enforces a rule and Reject that negates it (internally), so there is no need for other extensions prefixed with Not.

The last extension, or rather a modifier is called Hard. This one yields a different rule, one that when failed aborts the validation. This also means that I now have two rules which are descendants of the abstract class ValidationRule<T, TContext>:

Generating messages internally still has to be improved, but as far as the main API is concerned, I'm happy with it.

Source Link
t3chb0t
  • 44.7k
  • 9
  • 84
  • 191

(self-answer)


###Simplified the API

You were right, the API was too verbose so I drasticlly simplified it to. It now presents itself like that:

    [Fact]
    public void Simplified()
    {
        var rules =
            ValidationRuleCollection
                .For<Person>()
                .Reject(b => b.Null(x => x).Hard())
                .Reject(b => b.NullOrEmpty(x => x.FirstName))
                .Accept(b => b.Pattern(x => x.FirstName, "^cookie", RegexOptions.IgnoreCase))
                .Accept(b => b.When(x => x.FirstName.Length > 3));

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

        Assert.Equal(0, results.OfType<Information>().Count());
        Assert.Equal(1, results.OfType<Error>().Count());

        
        Assert.ThrowsAny<DynamicException>(() => default(Person).ValidateWith(rules).ThrowIfValidationFailed());
    }

I was not able to incorporate the ROP here this time but the many other ideas where very helpful. Here's the summary:

###Changes

There are only two main extensions Accept that enforces a rule and Reject that negates it (internally) so there is no need for other extensions prefixed with Not.

I also liked @Flater's idea from the other question by @Henrik Hansen where it was suggested picking something more general than IsTrue/IsFalse etc. I decided here to use When. All other extensions have only one overload now. Consequently I could rename Match to Pattern for RegEx.

The last extension, or rather a modifier is called Hard. This one yields a different rule, one that when failed aborts the validation. This also means that I now have two rules which are descendantes of the abstract class ValidationRule<T, TContext>:

public class Hard<T, TContext> : ValidationRule<T, TContext>
{
    public Hard
    (
        [NotNull] Expression<ValidationPredicate<T, TContext>> predicate,
        [NotNull] Expression<MessageCallback<T, TContext>> message
    ) : base(predicate, message) { }

    protected override IValidationResult CreateResult(bool success, string expression, string message)
    {
        return
            success
                ? (IValidationResult)new Information(expression, message)
                : (IValidationResult)new Error(expression, message);
    }
}

public class Soft<T, TContext> : ValidationRule<T, TContext>
{
    public Soft
    (
        [NotNull] Expression<ValidationPredicate<T, TContext>> predicate,
        [NotNull] Expression<MessageCallback<T, TContext>> message
    ) : base(predicate, message) { }

    protected override IValidationResult CreateResult(bool success, string expression, string message)
    {
        return
            success
                ? (IValidationResult)new Information(expression, message)
                : (IValidationResult)new Warning(expression, message);
    }
}

When evaluated they return one of three possible results: Information, Warning, Error.

public class Information : ValidationResult
{
    public Information([NotNull] string expression, [NotNull] string message)
        : base(expression, message) { }
}

public class Warning : ValidationResult
{
    public Warning([NotNull] string expression, [NotNull] string message)
        : base(expression, message) { }
}

public class Error : ValidationResult
{
    public Error([NotNull] string expression, [NotNull] string message)
        : base(expression, message) { }
}

An internal API uses it to break the validation:

    public static ValidationResultCollection<T> ValidateWith<T, TContext>(this T obj, IImmutableList<IValidationRule<T, TContext>> rules)
    {
        return obj.ValidateWith(rules, default);
    }

    private static IEnumerable<IValidationResult> Evaluate<T, TContext>(this IImmutableList<IValidationRule<T, TContext>> rules, T obj, TContext context)
    {
        var result = default(IValidationResult);
        foreach (var rule in rules)
        {
            yield return result = rule.Evaluate(obj, context);
            if (result is Error) yield break;
        }
    }

There is also a new ValidationResultCollection that replaces the tuple I used previously:

public class ValidationResultCollection<T> : IEnumerable<IValidationResult>
{
    private readonly IImmutableList<IValidationResult> _results;

    public ValidationResultCollection(T value, IImmutableList<IValidationResult> results)
    {
        Value = value;
        _results = results;
    }

    public T Value { get; }

    public IEnumerator<IValidationResult> GetEnumerator() => _results.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    public static implicit operator T(ValidationResultCollection<T> results) => results.Value;
}

I use it to chain extensions for throwing exception:

default(Person)
    .ValidateWith(rules) // <-- ValidationResultCollection
    .ThrowIfValidationFailed(); // autocast to T or throw

Generating messages internally still has to be improved but as far as the main API is concerned, I'm happy with it.

End-users can add thier own extension to b. It is a ValidationRuleBuilder<T, TContext> that lets them modify the validation expression: (like I use it for

    public ValidationRuleBuilder<T, TContext> Predicate(Func<LambdaExpression, LambdaExpression> expression)
    {
        _predicate = expression(_predicate);
        return this;
    }
    

I use this too, e.g. for Reject, that Negates the expression:

    public static IImmutableList<IValidationRule<T, object>> Reject<T>
    (
        this IImmutableList<IValidationRule<T, object>> rules,
        Func<ValidationRuleBuilder<T, object>, ValidationRuleBuilder<T, object>> builder
    )
    {
        return rules.Add(builder(ValidationRule<T, object>.Ensure).Negate().Build());
    }