If we cast back our thoughts back to the start of this series, one of the limitations of using string to return each of our errors was that every message has to be an error. What do we do if we want to support another kind of message?

To illustrate, let’s extend our system with a warning - a non-fatal message.

Before we look at the code, where would a warning be used? Here are two examples.

In a parts inventory system, someone will have the ability to change the unique part number used for a product - we can’t have a system where correcting an error is impossible. But, such a change has an extremely wide impact - it shouldn’t happen casually. It can be useful to show a warning when this happens as a hedge against unintended consequences: All users will need to use the new part number, effective start of business tomorrow.

Or you might have a promotional system where each marketing campaign has start and end dates. Sometimes it’s necessary** to modify the dates of a campaign that’s already underway - but it’s not something to do lightly. A warning to remind the user that the campaign is live can be a useful safety net to prevent unexpected changes (as when they think they’re changing the *next campaign, not the current one).

With the semantic type approach we’ve developed so far, we can add support for warnings by introducing a new descendant of ValidationResult:

public sealed class WarningResult : ValidationResult
{
    public string Message { get; }

    public WarningResult(string message)
    {
        Message = message 
            ?? throw new ArgumentNullException(nameof(message));
    }
}

Our static Validation class gets extended with add new factory methods:

public static class Validation
{
    public static ValidationResult Warning(string message)
        => new WarningResult(message);

    public static ValidationResult WarningWhen(
        bool isWarning, string message)
        => isWarning ? new WarningResult(message) : _success;
}

The base ValidationResult class needs to be modified to allow access to the warnings. One approach would be to parallel the approach taken for errors by adding members:

public abstract class ValidationResult
{
    // Existing
    public abstract IEnumerable<ErrorResult> Errors();
    public abstract bool HasErrors { get; }

    // New
    public abstract IEnumerable<WarningResult> Warnings();
    public abstract bool HasWarnings { get; }
}

An alternative would be to rename Errors to Results and rely on consumers checking the type of each item:

public abstract class ValidationResult
{
    // Renamed
    public abstract IEnumerable<ValidationResult> Results();
}

However, back in the earlier post on type recovery we talked about the desirability of avoiding such boilerplate.

For this series, I’ll go with the former approach, adding Warnings and HasWarnings. The implementaitons for these are very simple for the most part; here’s how it works for AggregateResult, the most complex case:

public sealed class AggregateResult : ValidationResult
{
    public override IEnumerable<WarningResult> Warnings()
    {
        return Results.OfType<WarningResult>();
    }

    public override bool HasWarnings => Warnings().Any();
}

The last piece of the puzzle is to implement the Add() method so that our + operator works properly. We’ll follow the existing pattern - a success value disppears, leaving a warning intact; otherwise we don’t want to drop any results.

For WarningResult, the implementation looks like this:

public sealed class WarningResult : ValidationResult
{
    protected override ValidationResult Add(ValidationResult right)
    {
        switch (right)
        {
            case SuccessResult _:
                return this;

            case WarningResult w:
                return new AggregateResult(
                    ImmutableHashSet<ValidationResult>.Empty
                        .Add(this)
                        .Add(w));

            case ErrorResult e:
                return new AggregateResult(
                    ImmutableHashSet<ValidationResult>.Empty
                        .Add(this)
                        .Add(e));

            case AggregateResult a:
                return new AggregateResult(
                    a.Results.Add(this));

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

Don’t forget that the other implementations of Plus() need to be extended to handle a WarningResult as well. They’re pretty straightforward, so I’m leaving those as an exercise for the reader.

Prior post in this series:
Validation recap
Next post in this series:
Validation Metadata

Comments

blog comments powered by Disqus