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))
{
// ...
}