TypedStateBuilder.Generator
1.1.0
See the version list below for details.
dotnet add package TypedStateBuilder.Generator --version 1.1.0
NuGet\Install-Package TypedStateBuilder.Generator -Version 1.1.0
<PackageReference Include="TypedStateBuilder.Generator" Version="1.1.0" />
<PackageVersion Include="TypedStateBuilder.Generator" Version="1.1.0" />
<PackageReference Include="TypedStateBuilder.Generator" />
paket add TypedStateBuilder.Generator --version 1.1.0
#r "nuget: TypedStateBuilder.Generator, 1.1.0"
#:package TypedStateBuilder.Generator@1.1.0
#addin nuget:?package=TypedStateBuilder.Generator&version=1.1.0
#tool nuget:?package=TypedStateBuilder.Generator&version=1.1.0
TypedStateBuilder
A Roslyn incremental source generator that produces compile-time safe builders using the type-state pattern.
It takes a builder template and generates the boilerplate needed for a strongly-typed, guided construction API where correctness is enforced by the compiler - not by runtime checks or conventions.
Table of Contents
- Why this exists
- What this solves
- Comparison
- Example
- What gets generated
- How it works
- Attributes API
- Defining steps
- Step overloads
- Optional values and defaults
- Validation
- Build methods
- Constructors
- Dependency Injection
- Performance characteristics
- Constraints and limitations
- Diagnostics overview
- Why use it
- Summary
Why this exists
Traditional builders in C# rely on:
- runtime validation
- defensive programming
- developer discipline
This often leads to:
- missing required values
- duplicated or conflicting assignments
- scattered validation logic
- bugs discovered only at runtime
What this solves
TypedStateBuilder shifts structural correctness to compile time, while keeping flexibility:
Build()is only available when all required values are set- required steps can be executed in any order
- each step can only be called once (in the typed API)
- optional values can be defaulted automatically
- validation is centralized and always executed
- one logical step can expose multiple input shapes via overloads
Result: invalid builder usage becomes unrepresentable code, without sacrificing fluent API flexibility.
Note: value correctness is still enforced at runtime via validation.
Comparison
| Feature | Simple Builder | Interface Step Builder | TypedStateBuilder |
|---|---|---|---|
| Compile-time safety | ❌ | ✅ | ✅ |
| Ensures required steps | ❌ | ✅ | ✅ |
| Prevents duplicate steps | ❌ | ✅ | ✅ |
| Flexible ordering | ✅ | ❌ | ✅ |
| Boilerplate required | Low | High | Low |
| Runtime overhead | Low | Medium | Low |
| Default values | Manual | Manual | Built-in |
| Validation | Manual | Manual | Built-in |
| Step overloads | Manual | Manual | Built-in |
| IDE experience | High | Medium | High |
Example
Define a builder template:
[TypedStateBuilder]
public class UserBuilder
{
private readonly IEmailService _emailService;
public UserBuilder(IEmailService emailService)
{
_emailService = emailService;
}
[StepForValue]
[ValidateValue(nameof(ValidateEmail))]
private string _email;
[StepForValue]
[StepOverload(nameof(FullNameToName))]
private string _name;
[StepForValue(nameof(DefaultAge))]
private int _age;
private int DefaultAge() => 18;
private string FullNameToName(string firstName, string lastName)
=> $"{firstName} {lastName}";
private async Task ValidateEmail(string email)
{
if (!await _emailService.IsValidAsync(email))
throw new InvalidOperationException("Invalid email");
}
[Build]
public User Build()
=> new User(_name, _email, _age);
}
Usage:
var user = TypedStateBuilders
.CreateUserBuilder(emailService)
.SetName("Alice", "Walker") // overload-generated step
.SetEmail("alice@example.com")
.Build(); // age defaults to 18
The direct step method still exists too:
var user = TypedStateBuilders
.CreateUserBuilder(emailService)
.SetName("Alice Walker")
.SetEmail("alice@example.com")
.Build();
Invalid usage is caught at compile time:
var invalid = TypedStateBuilders
.CreateUserBuilder(emailService)
.SetName("Alice")
.Build(); // ❌ compile-time error (email not set)
Key takeaway
You get:
- compile-time guarantees (required steps must be set)
- flexible ordering (no enforced sequence)
- no duplicated steps (cannot call the same logical step twice)
- multiple input forms for a step via
StepOverload - support for dependency injection (usable in validation, build logic, default providers, or overload methods)
What gets generated
For each [TypedStateBuilder] class, the generator produces:
- Typed wrapper (
TypedMyBuilder<...>) - Fluent step extension methods
- Fluent step overload extension methods
- Strongly-typed build methods
- Factory methods (
CreateMyBuilder) - Internal accessor layer (via
UnsafeAccessor)
You define a builder template, and the generator produces the repetitive type-state boilerplate.
How it works
Each step is encoded as a type-state:
ValueUnset → ValueSet
The wrapper carries one state per step as generic parameters.
Example progression:
TypedBuilder<ValueUnset, ValueUnset, ValueUnset>
→ SetName → TypedBuilder<ValueSet, ValueUnset, ValueUnset>
→ SetEmail → TypedBuilder<ValueSet, ValueSet, ValueUnset>
Build() becomes available only when all required states are ValueSet.
Step overloads reuse the same state transition. For example, both of these set the same logical step:
builder.SetName("Alice Walker");
builder.SetName("Alice", "Walker");
Both transition the same step from ValueUnset to ValueSet.
Attributes API
| Attribute | Target | Purpose | Parameters | Rules |
|---|---|---|---|---|
TypedStateBuilder |
Class | Enables generation | None | Must be non-nested, non-partial, no inheritance, public/internal |
StepForValue |
Field | Defines a step | Optional: nameof(provider) |
Field must be mutable instance field |
StepOverload |
Field | Adds step input overloads | nameof(method) |
Field must already be a step |
Build |
Method | Defines build entry | None | Must be instance method |
ValidateValue |
Field | Adds validation | nameof(validator) |
Must match validator signature |
Defining steps
[StepForValue]
private string _name;
Rules
- must be a field
- must be instance
- must not be
static - must not be
readonly
Each step generates:
builder.SetName(value)
Important behavior
- each step can be called only once in the typed API
- enforced by the type system, not runtime checks
- underlying fields remain mutable, but repeated calls are not expressible through generated step methods
Step overloads
StepOverload lets one step expose additional generated extension methods.
[StepForValue]
[StepOverload(nameof(CreateName))]
private string _name;
private string CreateName(string first, string last)
=> $"{first} {last}";
This generates an additional overload of the step method:
builder.SetName(string value)
builder.SetName(string first, string last)
Behavior
When an overload-generated step method is called:
- the referenced builder method is invoked
- its return value becomes the field value
- the original step is applied internally
- the step state changes from
ValueUnsettoValueSet
So this:
builder.SetName("Alice", "Walker")
behaves like:
var value = CreateName("Alice", "Walker");
builder.SetName(value);
Rules
A StepOverload target method:
- must use
nameof(...) - must be declared on the same builder class
- must be a method
- must be non-generic
- must return the same type as the target field
- may be instance or static
- may have any parameter list supported by the generator parameter model
Multiple overloads
You can add multiple overload methods to the same step:
[StepForValue]
[StepOverload(nameof(CreateFromFullName))]
[StepOverload(nameof(CreateFromParts))]
private string _name;
Important restrictions
Generated step overloads must not collide by signature.
Examples of invalid configurations:
- two overload methods that would both generate
SetName(string value) - a direct step plus an overload method that would generate the same parameter signature
- multiple parameterless overload methods for the same step
This is invalid:
[StepForValue]
[StepOverload(nameof(CreateDefaultA))]
[StepOverload(nameof(CreateDefaultB))]
private string _name;
private string CreateDefaultA() => "A";
private string CreateDefaultB() => "B";
because both would generate:
builder.SetName()
Only one parameterless StepOverload is allowed per step.
Optional values and defaults
[StepForValue(nameof(DefaultAge))]
private int _age;
Behavior
- default values are assigned during
Create... - the step becomes optional
- optional steps can be skipped before calling
Build()
Important details
- the step is still
ValueUnsetin the type-state system - you can still call the step explicitly afterward
- optionality affects build availability, not initial state
- default providers run eagerly during builder creation
Rules
A default provider:
- must use
nameof(...) - must be declared on the builder
- must be parameterless
- must be non-generic
- must return the exact field type
Validation
[ValidateValue(nameof(ValidateName))]
private string _name;
Rules
- must use
nameof(...) - must be declared on the builder
- must accept exactly one parameter of the field type
- must be non-generic
- must return
voidorTask
Behavior
- runs automatically before build
- runs for all steps
- exceptions are aggregated
throw new AggregateException(...)
Execution details
- async validators are executed synchronously
- internally,
GetAwaiter().GetResult()is used - this introduces blocking behavior
Build methods
[Build]
public User Build() => ...
Features
- multiple build methods supported
- parameters preserved
- generic methods supported
- return type preserved
Available only when required steps are satisfied.
Rules
A build method:
- must be marked with
[Build] - must be an instance method
- may be generic
- may have parameters and optional parameter defaults
Constructors
Constructors are exposed via factory methods:
TypedStateBuilders.CreateUserBuilder(...)
Features
- multiple constructors supported
- parameters preserved (
ref,out,in, defaults) - constructor signatures are surfaced as generated
Create...methods
Dependency Injection
Constructor parameters flow directly into the builder:
store them in non-step fields
use them in:
- build logic
- validation
- default providers
- step overload methods
Performance characteristics
- incremental generator (fast IDE experience)
- no reflection
- minimal runtime overhead
- wrapper allocation per step transition
- direct access via
UnsafeAccessor
Notes
- validation may allocate when exceptions occur
- async validation introduces blocking due to
GetAwaiter().GetResult() - step overloads add no extra runtime abstraction beyond the generated forwarding call
Constraints and limitations
Builder shape
- must be a class
- must be non-nested
- must be non-partial
- must not use inheritance
- must be public or internal
Steps
- fields only (no properties)
- must be mutable
- names must not collide after normalization
Step overloads
- only valid on fields that are already steps
- overload target methods must be non-generic
- overload target methods must return the exact field type
- generated overload signatures must not collide
- only one parameterless overload is allowed per step
Validators
- only
voidorTasksupported - executed synchronously
Behavior constraints
- steps are single-assignment in the generated typed API
- optional steps are not marked as set automatically
- validation runs before build
- wrappers share the same underlying mutable builder instance
Diagnostics overview
The generator reports diagnostics when builder definitions violate its rules.
Current diagnostic IDs:
| ID | Meaning |
|---|---|
TSB001 |
Invalid builder shape |
TSB002 |
Static step field not supported |
TSB003 |
Readonly step field not supported |
TSB005 |
Invalid build method |
TSB006 |
Invalid step default provider syntax |
TSB007 |
Invalid step default provider member |
TSB008 |
Duplicate generated step method |
TSB009 |
Builder has no steps |
TSB010 |
Builder has no build methods |
TSB011 |
Invalid validator syntax |
TSB012 |
Invalid validator member |
TSB013 |
Invalid step overload syntax |
TSB014 |
Invalid step overload member |
TSB015 |
Duplicate generated step overload method |
TSB016 |
Multiple parameterless step overloads are not supported |
Why use it
Strong compile-time guarantees
- cannot call
Build()prematurely - cannot forget required steps
- cannot call steps multiple times
Flexible API design
- steps in any order
- no rigid step chaining
- supports generics and multiple build paths
- supports multiple generated input forms for the same logical step
Built-in capabilities
- optional values with defaults
- centralized validation
- exception aggregation
- overload-based value construction for steps
Clean encapsulation
- works with private members
- no need to expose internals
Reduced boilerplate
- no step interfaces
- no manual validation wiring
- no manual forwarding overload methods in the fluent API
- no runtime guards for structural correctness
Summary
TypedStateBuilder generates a compile-time verified builder API that combines:
- flexibility (steps in any order)
- safety (required steps enforced by the compiler)
- simplicity (no manual type-state boilerplate)
It supports:
- required and optional steps
- validation
- multiple build methods
- constructor-based creation
- step overload generation
All while letting you define builders in plain, idiomatic C#.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.CodeAnalysis.CSharp (>= 4.3.0)
- Microsoft.CodeAnalysis.CSharp.Workspaces (>= 4.3.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.