We’ve created the basics of our validation library, but we haven’t yet addressed the problem of aggregation. How do we make it easy for our consumers to combine multiple validation results together into one. Ideally, we want this to be so simple that they don’t have to think about it at all.

Looking back at the start of this series, we can see a good example of what we don’t want to have - a lot of boilerplate code that makes it harder to see what’s actually being checked.

public IEnumerable<string> Validate()
{
    var result = new List<string>();

    result.AddRange(ValidateFullName());
    result.AddRange(ValidateFamilyName());
    result.AddRange(ValidateKnownAs());
    result.AddRange(ValidateSortKey());

    return result;
}

Rewriting this to use AggregateResult would be straightforward:

public ValidationResult Validate()
{
    var content = ImmutableHashSet<ValidationResult>.Empty
        .Add(ValidateFullName())
        .Add(ValidateFamilyName())
        .Add(ValidateKnownAs())
        .Add(ValidateSortKey());

    return new AggregateResult(content);
}

But this is hardly satisfactory - the level of boilerplate totally dominates the method.

We could add a second constructor to AggregateResult that uses params, allowing us to write:

public ValidationResult Validate()
{
    return new AggregateResult(
        ValidateFullName(),
        ValidateFamilyName(),
        ValidateKnownAs(),
        ValidateSortKey());
}

This isn’t terrible, but it’s not boilerplate free either - the use of AggregateResult is painfully obvious and it draws the readers attention away from the actual function of the method.

What if we could instead write things this way?

public ValidationResult Validate()
{
    return ValidateFullName()
           + ValidateFamilyName()
           + ValidateKnownAs()
           + ValidateSortKey();
}

To make this work, we need to implement the plus (+) operator. While operator overloading has been possible in C# since before the first release, it’s not a common approach.

By the way, this is why ValidationResult was declared as an abstract class in the first post - operator overloading in C# doesn’t work with interfaces.

On ValidationResult, we declare the operator itself:

public static ValidationResult operator +(
    ValidationResult left, ValidationResult right)
{
    if (left == null)
    {
        throw new ArgumentNullException(nameof(left));
    }

    return left.Add(right
        ?? throw new ArgumentNullException(nameof(right)));
}

After doing some essential parameter checks, the instance methd Add() is invoked to do all the hard work. While I could have called the method Plus(), that didn’t seem like it would flow well - so I used a more idomatic name.

Note that I’ve made a choice here that passing null to add should result in an exception - because I think that no validation result at all is very different to successful validation. That said, you could write the method differently to treat the two as the same - a similar approach to the way some frameworks treat a null string as equal to an empty string.

The declaration of Add is straightforward:

protected abstract ValidationResult Add(ValidationResult right);

The implementation for SuccessResult is easy: Adding success to anything doesn’t change it:

public sealed class SuccessResult : ValidationResult
{
    protected override ValidationResult Add(ValidationResult right)
            => right;
}

For an ErrorResult, we need to ensure we always conserve the message, because we don’t want to lose track of any errors that we’ve found. Pay attention to the comments that break down how it works.

public sealed class ErrorResult : ValidationResult
{
    protected override ValidationResult Add(ValidationResult right)
    {
        // We're using the new pattern matching syntax 
        // of C# to test for specific types.
        switch (right)
        {
            // If we're adding a `SuccessResult`, we 
            // just return the error we already have:
            case SuccessResult _:
                return this;

            // If we have another `ErrorResult`, we create an 
            // `AggregateResult` that contains both errors:
            case ErrorResult e:
                return new AggregateResult(
                    ImmutableHashSet<ValidationResult>.Empty
                        .Add(this)
                        .Add(e));

            // If we already have an `AggregateResult`, create
            // a new one that also includes this error
            case AggregateResult a:
                return new AggregateResult(
                    a.Results.Add(this));

            // And if something goes awry, throw an exception
            default:
                throw new InvalidOperationException(
                    "Unexpected subclass of ValidationResult found");
        }
    }
}

Lastly, for an AggregateResult, the logic is pretty similar:

public sealed class AggregateResult : ValidationResult
{
    protected override ValidationResult Add(ValidationResult right)
    {
        switch (right)
        {
            // If we're adding a `SuccessResult`, we 
            // just return the aggregate we already have:
            case SuccessResult _:
                return this;

            // If we have an `ErrorResult`, we create an 
            // `AggregateResult` that contains all the errors:
            case ErrorResult e:
                return new AggregateResult(
                    Results.Add(e));

            // If we have another `AggregateResult`, we create 
            // an `AggregateResult` that contains all the errors:
            case AggregateResult a:
                return new AggregateResult(
                    a.Results.Union(Results));

            default:
                throw new InvalidOperationException(
                    "Unexpected subclass of ValidationResult found");
        }
    }
}

It’s important to observe that we don’t nest AggregateResults - if we’re adding two of them together, we create a new AggregateResult that combines all of the individual results together.

Prior post in this series:
Recovery of validation types
Next post in this series:
Short-circuiting validation

Comments

blog comments powered by Disqus