This library helps you build types that protect their invariants by only allowing valid versions to be constructed.
dotnet add package Chamook.Validation
using Chamook.Validation;
The Validated
type represents the result of a validation operation. Only one of the values
(either Valid or Error) can be present.
A new Validated
object is created using one of the static methods corresponding to states:
var valid = Validated<CoolObject, string>.Valid(myCoolObject);
var invalid = Validated<CoolObject, string>.Error("Not cool enough");
The recommended way to extract a value is by using the Match
method which requires a
function to handle both possiblities:
valid.Match(
ifValid: v => Console.WriteLine(v.ToString()),
ifError: e => Console.WriteLine(e));
If the handlers return a value, they must both return the same type of value.
var outcome =
valid.Match(
ifValid: v => v.ToString(),
ifError: e => e);
A Constrained Type is a handy way to wrap an existing type with a simple validation rule.
Validation rules are implemented as a class that implements the IConstraint<T>
interface.
public interface IConstraint<T>
{
bool IsValid(T candidate);
}
The single method IsValid
defines validation rules for the type to be tested T
.
Note that ConstrainedType
requires an IConstraint
to have a parameterless constructor.
With a constraint defined, a class can inherit from ConstrainedType
, fill in the type
parameters, and implement 2 methods to be complete. See the example below of NonEmptyString
:
///<summary>
///A string that can't be empty or whitespace
///</summary>
public sealed class NonEmptyString: ConstrainedType<NonEmptyString.Constraint, string>
{
public sealed class Constraint : IConstraint<string>
{
public Constraint() {}
public bool IsValid(string candidate) => !string.IsNullOrWhiteSpace(candidate);
}
private NonEmptyString(string valid) : base(valid) {}
public new static Validated<NonEmptyString, TError> Validate<TError>(
string? candidate,
Func<TError> errorCreator) =>
DoValidate(candidate ?? "", errorCreator).Map(x => new NonEmptyString(x));
}
The constructor for NonEmptyString
just calls the base constructor with a valid value.
The static Validate
method passes through to the DoValidate
method on the base class
and maps the type of the result to NonEmptyString
.
Note that NonEmptyString
also handles null
input values by coalescing to an empty
string before validating (which will of course result in an error) - but handling null
input is not strictly necessary.
Validate
also requires that a simple function to create an appropriate error response is
provided - this is to allow for better error messages in practice.
Validation results can be combined together using the And
method. This allows for many
individual validation steps to be performed when attempting to build a larger class. Either
all errors are collected or the Map
method can be used to build the final result type.
See the Sample
below:
public sealed record Sample(
NonEmptyString One,
NonEmptyString Two,
NonEmptyString Three)
{
public static Validated<Sample, string> Validate(
string? one,
string? two,
string? three) =>
NonEmptyString.Validate(one, () => "One is required")
.And(NonEmptyString.Validate(two, () => "Two is required"))
.And(NonEmptyString.Validate(three, () => "Three is required"))
.Map((x1, x2, x3) => new Sample(x1, x2, x3))
.MapError(x => string.Join(", ", x));
}
💡 If you like to build rules on their own maybe FluentValidation is more your speed
💡 If you’re a big fan of annotations, you could try MiniValidation
This is licensed under the Apache License 2.0