Validation<T, TError>
Validation<T, TError> is like Result, but instead of short-circuiting on the first error, it accumulates all errors. This makes it ideal for form validation, input checking, and any scenario where you want to report every problem at once.
Creating Validations
var valid = Validation.Valid<string, ValidationError>("Alice");
var invalid = Validation.Invalid<string, ValidationError>(
new ValidationError { Message = "Name is required" });
// Multiple errors
var invalid = Validation.Invalid<string, ValidationError>(new[]
{
new ValidationError { Message = "Too short" },
new ValidationError { Message = "Contains invalid characters" }
});
// Implicit conversion from value
Validation<int, ValidationError> v = 42; // Valid(42)
Validation vs Result
Result<T, TError> |
Validation<T, TError> |
|
|---|---|---|
| Error count | Single error | Multiple errors |
Bind behaviour |
Short-circuits on first error | Short-circuits on first error |
Apply behaviour |
N/A | Accumulates errors |
| Best for | Sequential operations | Parallel validation rules |
Accumulating Errors with Apply
Apply is the key differentiator. It runs all validations and collects every error:
using DarkPeak.Functional.Extensions;
Validation<string, ValidationError> ValidateName(string name) =>
string.IsNullOrWhiteSpace(name)
? Validation.Invalid<string, ValidationError>(
new ValidationError { Message = "Name is required" })
: Validation.Valid<string, ValidationError>(name);
Validation<int, ValidationError> ValidateAge(int age) =>
age is < 0 or > 150
? Validation.Invalid<int, ValidationError>(
new ValidationError { Message = "Age must be 0-150" })
: Validation.Valid<int, ValidationError>(age);
// Combine with Apply — all errors are accumulated
var result = Validation.Valid<Func<string, int, User>, ValidationError>(
(name, age) => new User(name, age))
.Apply(ValidateName(""))
.Apply(ValidateAge(-1));
// Invalid([{ Message = "Name is required" }, { Message = "Age must be 0-150" }])
ZipWith
Combine multiple validations with a projection function:
var result = ValidateName(dto.Name)
.ZipWith(
ValidateAge(dto.Age),
(name, age) => new User(name, age));
// Three-way combine
var result = ValidateName(dto.Name)
.ZipWith(
ValidateAge(dto.Age),
ValidateEmail(dto.Email),
(name, age, email) => new User(name, age, email));
Sequence
Convert a collection of validations into a single validation of a collection:
var validations = new[]
{
ValidateName("Alice"),
ValidateName(""),
ValidateName("Charlie")
};
var result = validations.Sequence();
// Invalid — collects errors from the second item
Interop with Result
using DarkPeak.Functional.Extensions;
// Validation → Result (takes first error)
Result<T, TError> result = validation.ToResult();
// Result → Validation
Validation<T, TError> validation = result.ToValidation();
Standard Operations
Validation supports the same operations as the other monadic types:
// Map
var upper = validation.Map(name => name.ToUpper());
// Bind (short-circuits, does NOT accumulate — use Apply for that)
var result = validation.Bind(name => ValidateLength(name));
// Match
var message = validation.Match(
valid: value => $"OK: {value}",
invalid: errors => string.Join(", ", errors.Select(e => e.Message)));
// Side effects
validation
.Tap(value => Log($"Valid: {value}"))
.TapInvalid(errors => Log($"Errors: {errors.Count}"));
// Extract value
var value = validation.GetValueOrDefault("fallback");
var value = validation.GetValueOrThrow();