RSCG – TypedStateBuilder.Generator
| name | TypedStateBuilder.Generator |
| nuget | https://www.nuget.org/packages/TypedStateBuilder.Generator/ |
| link | https://github.com/Georgiy-Petrov/TypedStateBuilder.Generator |
| author | Georgiy Petrov |
Generate strongly typed state builders for C# applications, enforced at compile time
**Summary of TypedStateBuilder.Generator**
**Purpose:** Generates compile-time safe step-by-step builders using the *type-state pattern* — each required property must be set in order before Build() is available, enforced at compile time.
**NuGet:** https://www.nuget.org/packages/TypedStateBuilder.Generator/
**GitHub:** https://github.com/Georgiy-Petrov/TypedStateBuilder.Generator
**Author:** Georgiy Petrov
**How to use:**
1. Decorate a builder class with [TypedStateBuilder], mark each required step with [StepForValue], and optionally add [ValidateValue] for validation:
“`csharp
[TypedStateBuilder]
public class PersonBuilder
{
[StepForValue]
[ValidateValue(nameof(ValidateName))]
private string lastName = string.Empty;
[StepForValue]
[ValidateValue(nameof(ValidateName))]
private string firstName = string.Empty;
public void ValidateName(string name)
{
if (string.IsNullOrWhiteSpace(name) || name.Length <= 1)
throw new ArgumentException(“Name must be at least 2 characters long.”);
}
[Build]
public Person Build() => new Person(firstName, lastName);
}
“`
2. Use the generated fluent builder — steps are enforced in order at compile time:
“`csharp
var p = TypedStateBuilders
.CreatePersonBuilder()
.SetFirstName(“Andrei”)
.SetLastName(“Ignat”)
.Build();
Console.WriteLine(p.FullName()); // “Andrei Ignat”
“`
This is how you can use TypedStateBuilder.Generator .
The code that you start with is
<project sdk="Microsoft.NET.Sdk">
<propertygroup>
<outputtype>Exe</outputtype>
<targetframework>net10.0</targetframework>
<nullable>enable</nullable>
</propertygroup>
<propertygroup>
<emitcompilergeneratedfiles>true</emitcompilergeneratedfiles>
<compilergeneratedfilesoutputpath>$(BaseIntermediateOutputPath)\GX</compilergeneratedfilesoutputpath>
</propertygroup>
<itemgroup>
<packagereference outputitemtype="Analyzer" privateassets="all" version="1.2.0" include="TypedStateBuilder.Generator">
</packagereference>
</itemgroup>
The code that you will use is
using Builder;
using TypedStateBuilder;
Console.WriteLine("create person builder");
var p = TypedStateBuilders
.CreatePersonBuilder()
.SetFirstName("Andrei")
.SetLastName("Ignat")
.Build()
;
;
Console.WriteLine(p.FullName());
using TypedStateBuilder;
namespace Builder;
[TypedStateBuilder]
public class PersonBuilder
{
[StepForValue]
[ValidateValue(nameof(ValidateName))]
private string lastName = string.Empty;
[StepForValue]
[ValidateValue(nameof(ValidateName))]
private string firstName = string.Empty;
public void ValidateName(string name)
{
if (string.IsNullOrWhiteSpace(name) || name.Length <= 1)
{
throw new ArgumentException("Name must be at least 2 characters long.", nameof(name));
}
}
[Build]
public Person Build()
=> new Person(firstName, lastName);
}
public record Person(string firstName, string lastName)
{
public string FullName() => $"{firstName} {lastName}";
}
The code that is generated is
// <auto-generated>
#nullable enable
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace Builder
{
file static class PersonBuilder_Accessors
{
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
internal static extern global::Builder.PersonBuilder Create();
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "lastName")]
internal static extern ref string SetLastNameField(global::Builder.PersonBuilder builder);
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "firstName")]
internal static extern ref string SetFirstNameField(global::Builder.PersonBuilder builder);
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ValidateName")]
internal static extern void Validate_SetLastName_ValidateName_0(global::Builder.PersonBuilder owner, string value);
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ValidateName")]
internal static extern void Validate_SetFirstName_ValidateName_0(global::Builder.PersonBuilder owner, string value);
[UnsafeAccessor(UnsafeAccessorKind.Method)]
internal static extern global::Builder.Person Build(global::Builder.PersonBuilder builder);
}
public sealed class TypedPersonBuilder<tlastnamestate , tfirstnamestate="">
where TLastNameState : global::TypedStateBuilder.IValueState
where TFirstNameState : global::TypedStateBuilder.IValueState
{
private global::Builder.PersonBuilder Inner { get; }
internal TypedPersonBuilder(global::Builder.PersonBuilder inner)
{
Inner = inner;
}
internal static TypedPersonBuilder<global::typedstatebuilder.valueset , tfirstnamestate=""> SetLastNameCore<tfirstnamestate>(TypedPersonBuilder<global::typedstatebuilder.valueunset , tfirstnamestate=""> builder, string value)
where TFirstNameState : global::TypedStateBuilder.IValueState
{
PersonBuilder_Accessors.SetLastNameField(builder.Inner) = value;
return new TypedPersonBuilder<global::typedstatebuilder.valueset , tfirstnamestate="">(builder.Inner);
}
internal static TypedPersonBuilder<tlastnamestate , global::typedstatebuilder.valueset=""> SetFirstNameCore<tlastnamestate>(TypedPersonBuilder<tlastnamestate , global::typedstatebuilder.valueunset=""> builder, string value)
where TLastNameState : global::TypedStateBuilder.IValueState
{
PersonBuilder_Accessors.SetFirstNameField(builder.Inner) = value;
return new TypedPersonBuilder<tlastnamestate , global::typedstatebuilder.valueset="">(builder.Inner);
}
internal static global::Builder.Person BuildCore(TypedPersonBuilder<global::typedstatebuilder.valueset , global::typedstatebuilder.valueset=""> builder)
{
List<exception>? exceptions = null;
try
{
PersonBuilder_Accessors.Validate_SetLastName_ValidateName_0(builder.Inner, PersonBuilder_Accessors.SetLastNameField(builder.Inner));
}
catch (Exception ex)
{
(exceptions ??= new List<exception>()).Add(ex);
}
try
{
PersonBuilder_Accessors.Validate_SetFirstName_ValidateName_0(builder.Inner, PersonBuilder_Accessors.SetFirstNameField(builder.Inner));
}
catch (Exception ex)
{
(exceptions ??= new List<exception>()).Add(ex);
}
if (exceptions is not null)
{
throw new AggregateException(exceptions);
}
return PersonBuilder_Accessors.Build(builder.Inner);
}
}
public static partial class TypedPersonBuilderExtensions
{
/// <summary>
/// Sets the lastName.
/// </summary>
/// <param name="builder">The builder.
/// <param name="value">The value for <c>lastName</c>.
/// <returns>
/// The updated builder.
/// </returns>
public static TypedPersonBuilder<global::typedstatebuilder.valueset , tfirstnamestate=""> SetLastName<tfirstnamestate>(this TypedPersonBuilder<global::typedstatebuilder.valueunset , tfirstnamestate=""> builder, string value)
where TFirstNameState : global::TypedStateBuilder.IValueState
=> TypedPersonBuilder<global::typedstatebuilder.valueunset , tfirstnamestate="">.SetLastNameCore<tfirstnamestate>(builder, value);
/// <summary>
/// Sets the firstName.
/// </summary>
/// <param name="builder">The builder.
/// <param name="value">The value for <c>firstName</c>.
/// <returns>
/// The updated builder.
/// </returns>
public static TypedPersonBuilder<tlastnamestate , global::typedstatebuilder.valueset=""> SetFirstName<tlastnamestate>(this TypedPersonBuilder<tlastnamestate , global::typedstatebuilder.valueunset=""> builder, string value)
where TLastNameState : global::TypedStateBuilder.IValueState
=> TypedPersonBuilder<tlastnamestate , global::typedstatebuilder.valueunset="">.SetFirstNameCore<tlastnamestate>(builder, value);
/// <summary>
/// Builds the result.
/// </summary>
/// <param name="builder">The builder.
/// <returns>
/// The built result.
/// </returns>
/// <exception cref="System.AggregateException">Thrown if validation fails.</exception>
public static global::Builder.Person Build(this TypedPersonBuilder<global::typedstatebuilder.valueset , global::typedstatebuilder.valueset=""> builder)
=> TypedPersonBuilder<global::typedstatebuilder.valueset , global::typedstatebuilder.valueset="">.BuildCore(builder);
}
}
namespace TypedStateBuilder
{
public static partial class TypedStateBuilders
{
/// <summary>
/// Creates a new <see cref="Builder.TypedPersonBuilder">.
/// </see></summary>
/// <returns>
/// A new builder instance.
/// </returns>
public static global::Builder.TypedPersonBuilder<global::typedstatebuilder.valueunset , global::typedstatebuilder.valueunset=""> CreatePersonBuilder()
{
var inner = global::Builder.PersonBuilder_Accessors.Create();
return new global::Builder.TypedPersonBuilder<global::typedstatebuilder.valueunset , global::typedstatebuilder.valueunset="">(inner);
}
}
}
// <auto-generated>
#nullable enable
using System;
namespace TypedStateBuilder;
/// <summary>
/// Represents a builder step state.
/// </summary>
public interface IValueState { }
/// <summary>
/// Indicates that a builder step has been set.
/// </summary>
public sealed class ValueSet : IValueState { }
/// <summary>
/// Indicates that a builder step has not been set.
/// </summary>
public sealed class ValueUnset : IValueState { }
/// <summary>
/// Marks a method as a build method.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public sealed class BuildAttribute : Attribute
{
/// <summary>
/// Marks a method as a build method.
/// </summary>
public BuildAttribute()
{
}
/// <summary>
/// Marks a method as a build method for a specific branch.
/// </summary>
/// <param name="targetBranch">The branch path.
public BuildAttribute(string targetBranch)
{
}
}
/// <summary>
/// Marks a field as a builder step.
/// </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
public class StepForValueAttribute : Attribute
{
/// <summary>
/// Marks a required builder step.
/// </summary>
public StepForValueAttribute()
{
}
/// <summary>
/// Marks an optional builder step with a default value provider.
/// </summary>
/// <param name="providerMemberName">The provider member name.
public StepForValueAttribute(string providerMemberName)
{
}
}
/// <summary>
/// Assigns a step to a branch.
/// </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
public sealed class StepBranchAttribute : Attribute
{
/// <summary>
/// Assigns a step to a branch.
/// </summary>
/// <param name="branchPath">The branch path.
public StepBranchAttribute(string branchPath)
{
}
}
/// <summary>
/// Adds another way to set a builder step.
/// </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true, Inherited = false)]
public sealed class StepOverloadAttribute : Attribute
{
/// <summary>
/// Adds another way to set a builder step.
/// </summary>
/// <param name="overloadMemberName">The overload member name.
public StepOverloadAttribute(string overloadMemberName)
{
}
}
/// <summary>
/// Adds validation for a builder step.
/// </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true, Inherited = false)]
public sealed class ValidateValueAttribute : Attribute
{
/// <summary>
/// Adds validation for a builder step.
/// </summary>
/// <param name="validatorMemberName">The validator member name.
public ValidateValueAttribute(string validatorMemberName)
{
}
}
/// <summary>
/// Enables typed builder generation for a builder class.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class TypedStateBuilderAttribute : Attribute
{
}
Code and pdf at
https://ignatandrei.github.io/RSCG_Examples/v2/docs/TypedStateBuilder.Generator