Skip to content

Validation

The Validation type is like Result, but can hold multiple errors.

csharp
// `ValueOf` is a simple value wrapper type that overrides equality checks,  hashcode, and provides
// implicit conversions to the underlying type.
public sealed class Age : ValueOf<int>
{
    private Age(int value)
        : base(value)
    {
    }

    public static Validation<Age, string> Parse(int age)
    {
        var errors = new List<string>();
        if (age < 18)
        {
            errors.Add("You must be at least 18 years of age");
        }

        if (age % 2 != 0)
        {
            errors.Add("Your age must be an even number - sorry, I don't make the rules");
        }

        return errors.Count == 0
            ? Valid(new Age(age))
            : Invalid(errors.AsEnumerable());
    }
}

public sealed class Name : ValueOf<string>
{
    private Name(string value)
        : base(value)
    {
    }

    public static Validation<Name, string> Parse(string name) =>
        // `NonNullOrWhiteSpace` returns an `Option<string>` which
        // we can turn into a `Validation` like so:
        StringParser.NonNullOrWhiteSpace(name)
            .ValidOr("Name must not be empty")
            .Map(name => new Name(name));
}

[Lambda] // generates a function we can use for lifting
public partial record Person(Name Name, Age Age);

We have defined 2 value objects with Parse methods that allow us to parse a primitive value into a rich type. We have also defined a Person type which is a composition of the 2 previous types.

How do we construct a Person but also collect all validation errors together without a bunch of boilerplate?

We can use the applicative property of Validation to do this.

csharp
Validation<Person, string> personValidation =
    // λ is a function generated by the `[Lambda]` attribute which
    // lets us pass `Person`'s constructor as a regular function.
    // Use your editor's autocomplete for this.
    Valid(Person.λ)
        // Next, we can apply each parameter.
        .Apply(Name.Parse("Bob"))
        .Apply(Age.Parse(26));

// If all the validations are valid, then we'll get a constructed `Person`.
// Otherwise, all the errors will be collected.
if (personValidation.TryGet(out var person, out var errors))
{
    // ...
}

Released under the MIT License.