This library helps you build types that protect their invariants by only allowing valid versions to be constructed.
dotnet add package Chamook.Validationusing 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
