Composer<TState, TOut>
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.
Mental model
- 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.
API at a glance
// 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 */));
Threading & immutability
- 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.
Minimal examples
1) Basic composition
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)
2) Left-to-right transform order
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 == 20
3) Multiple validators (first failure wins)
static 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);
4) Reuse a composer
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)
Patterns & tips
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.
Error handling
BuildthrowsInvalidOperationExceptionwith the first validation message that is notnull/empty.- If there are no validators,
Buildalways succeeds.
Performance notes
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.
Reference (public API)
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);
}
See also
- ChainBuilder – collect items, project to a product.
- BranchBuilder – collect predicate/handler pairs + optional default.
- Strategy / TryStrategy / AsyncStrategy – consumers of these creational patterns.