A tiny, explicit functional builder: you accumulate immutable state (usually a small struct) via pure transformations, optionally add validations, then project the final state into your output type.
- Seed → Transform → Validate → Project.
Withcomposes functions left-to-right (i.e.,b(a(seed))).Requirechains validators; the first non-null message throws.- Nothing happens until
Build— that’s when transforms and validators run.
// Create with a seed factory (prefer static to avoid captures)
var c = Composer<State, Dto>.New(static () => default);
// Add transforms (pure functions State -> State)
c.With(static s => /* change s */);
// Add validators (State -> string?); return null when OK
c.Require(static s => /* message-or-null */);
// Finish: transform final State into your output type
Dto dto = c.Build(static s => new Dto(/* from s */));- The composer instance is mutable until
Build. You can keep callingWith/RequireandBuildrepeatedly. - The state you produce should be treated as immutable; prefer small
record structs for perf.
public readonly record struct PersonState(string? Name, int Age);
public sealed record PersonDto(string Name, int Age);
var dto = Composer<PersonState, PersonDto>
.New(static () => default) // (Name=null, Age=0)
.With(static s => s with { Name = "Ada" })
.With(static s => s with { Age = 30 })
.Require(static s => string.IsNullOrWhiteSpace(s.Name) ? "Name is required." : null)
.Build(static s => new PersonDto(s.Name!, s.Age));
// -> PersonDto("Ada", 30)static PersonState A(PersonState s) => s with { Age = 10 };
static PersonState B(PersonState s) => s with { Age = 20 };
var dto = Composer<PersonState, PersonDto>
.New(static () => default)
.With(A) // sets Age to 10
.With(B) // then overrides to 20
.Require(static _ => null)
.Build(static s => new PersonDto(s.Name ?? "?", s.Age));
// Age == 20static string? NameRequired(PersonState s)
=> string.IsNullOrWhiteSpace(s.Name) ? "Name is required." : null;
static string? AgeInRange(PersonState s)
=> s.Age is < 0 or > 130 ? $"Age must be within [0..130] but was {s.Age}." : null;
var ex = Assert.Throws<InvalidOperationException>(() =>
Composer<PersonState, PersonDto>.New(static () => new(null, -5))
.Require(NameRequired) // fails first -> throws this message
.Require(AgeInRange)
.Build(static s => new PersonDto(s.Name!, s.Age)));
Assert.Equal("Name is required.", ex.Message);var comp = Composer<PersonState, PersonDto>
.New(static () => default)
.With(static s => s with { Name = "Ada" })
.Require(static _ => null);
var dto1 = comp.Build(static s => new PersonDto(s.Name!, s.Age)); // ("Ada", 0)
var dto2 = comp.With(static s => s with { Age = 30 })
.Build(static s => new PersonDto(s.Name!, s.Age)); // ("Ada", 30)-
Prefer method pointers over capturing lambdas for AOT/JIT friendliness:
static PersonState SetName(PersonState s, string n) => s with { Name = n }; c.With(static s => SetName(s, "Ada"));
-
Validation as composition: chain small rules with
Require; returnnullon success. -
One projection, many outputs: You can build multiple outputs by calling
Buildwith different projectors. -
No side effects in transforms: keep
Withpure (deterministic, no I/O) for easy reasoning and testing.
BuildthrowsInvalidOperationExceptionwith the first validation message that is notnull/empty.- If there are no validators,
Buildalways succeeds.
Withcomposes delegates; composition cost is O(#With) executed once perBuild.- Use small, shallow
record structstate to minimize copying. - Prefer
staticlambdas / method groups to avoid allocations from captures.
public sealed class Composer<TState, TOut>
{
public static Composer<TState, TOut> New(Func<TState> seed);
public Composer<TState, TOut> With(Func<TState, TState> transform);
public Composer<TState, TOut> Require(Func<TState, string?> validate);
public TOut Build(Func<TState, TOut> project);
}- ChainBuilder – collect items, project to a product.
- BranchBuilder – collect predicate/handler pairs + optional default.
- Strategy / TryStrategy / AsyncStrategy – consumers of these creational patterns.