Skip to main content

Fluent Builder

A fluent builder accumulates configuration from the test and builds a XceptoState when the chain is complete. It is registered as a future step in its constructor — the step is placed in the state machine immediately, and the builder fills in its configuration before the machine runs.

Extend AbstractStateBuilderIdentity

public sealed class EntityFlowBuilder
: AbstractStateBuilderIdentity<EntityFlowBuilder>
{
private string? _entityId;
private DomainClient? _client;
private string? _displayName;
private EntityStatus? _expectedStatus;

public EntityFlowBuilder(IStateMachineBuilder builder) : base(builder) {}

protected override string DefaultName
=> $"Entity {_entityId ?? "?"} reaches {_expectedStatus}";
protected override bool DefaultRetry => true;

public EntityFlowBuilder WithEntityId(string id)
{
_entityId = id;
return this;
}

public EntityFlowBuilder WithClient(DomainClient client)
{
_client = client;
return this;
}

public EntityFlowBuilder WithDisplayName(string name)
{
_displayName = name;
return this;
}

public EntityFlowBuilder ShouldHaveStatus(EntityStatus status)
{
_expectedStatus = status;
return this;
}

protected override XceptoState Build()
=> new EntityStatusState(Name, _client!, _entityId!,
_displayName!, _expectedStatus!.Value);
}

HTTP-based builders

For custom HTTP adapters, extend AbstractHttpStateBuilderIdentity<T> to inherit the full shared fluent API (assertions, query arguments, retry control, URL building):

public sealed class MyHttpBuilder
: AbstractRestStateBuilderIdentity<MyHttpBuilder>
{
private string? _tenantId;

public MyHttpBuilder(IStateMachineBuilder builder) : base(builder) {}

protected override string AdapterPrefix => "MyApi";

public MyHttpBuilder WithTenantId(string tenantId)
{
_tenantId = tenantId;
return this;
}

protected override XceptoState BuildRestState()
=> new MyHttpState(Name, UrlProducer, ClientProducer,
Verb, ResolveRetry(), Assertions, _tenantId);
}

Design rules

Every fluent method should add real value. Avoid no-argument methods that only make the chain read nicely. Each method should contribute data, select a target, choose an action, or declare an expectation.

Builders must not hide case data. Input values, entity identifiers, expected outcomes — these must come from the test through explicit fluent calls. A builder may define reusable domain operations and sensible defaults, but never hardcode test-case-specific values.

One state class per evaluation shape, not per test case. If two tests need the same kind of check with different parameters, parameterize the state through its constructor. See Custom State → State reuse.

Test usage

With the builder in place, test code reads as domain behavior:

await XceptoTest.Given(new UserScenario(), builder =>
{
var users = new DomainAdapterBuilder(builder)
.WithClient(scenario.Client)
.Build();

users.Entity("entity-42")
.WithDisplayName("Operator")
.ShouldHaveStatus(EntityStatus.Active);
});