Skip to content

fix(binder): lock binder options before any property is bound - #174

Merged
Reefact merged 3 commits into
mainfrom
145-lock-binder-options
Jul 18, 2026
Merged

fix(binder): lock binder options before any property is bound#174
Reefact merged 3 commits into
mainfrom
145-lock-binder-options

Conversation

@Reefact

@Reefact Reefact commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

RequestBinder.WithOptions could be called mid-binding, swapping the IArgumentNameProvider after some properties were already bound — so one failure envelope could report argument paths under two naming policies (GuestEmail beside guest_email), silently inconsistent for a client mapping paths back to the keys it sent. Option configuration moves to the entry point (Bind.WithOptions(options).PropertiesOf(request)), fixed once before the binder exists; the instance WithOptions is removed, so the mid-binding swap is now impossible to write rather than merely discouraged.

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Refactoring
  • Analyzer / diagnostic change
  • Tests
  • Documentation

Changes

  • Add Bind.WithOptions(options) returning a new ConfiguredBind entry point whose PropertiesOf(request) starts binding with those options. Bind.PropertiesOf(request) keeps the default options.
  • Remove the instance RequestBinder.WithOptions; Options is now immutable. Because the mid-binding setter is gone, an inconsistent envelope cannot be written.
  • Thread the options through RequestBinderEnvelopeStage so FailWith builds the binder with the configured options; nested binders keep inheriting them.
  • ConfiguredBind carries no per-request state, so it can be built once (e.g. at application startup) and reused for every request. Options no longer depend on TRequest.
  • Update the three existing option-configuring tests to the new entry point and the null guard (Bind.WithOptions(null!)), and add a test pinning the reuse-across-requests property (binder suite: 76 tests).
  • Update the RequestBinder guide (EN + FR): the new Bind.WithOptions(...).PropertiesOf(...) example and the "configure once, reuse per request" note.
  • Add ADR-0012 recording the decision (EN + FR) and index it.

Testing

  • dotnet build FirstClassErrors.sln — succeeded, 0 warnings / 0 errors.
  • dotnet test FirstClassErrors.sln — all green (Core 423, GenDoc 155, Dummies 128, Analyzers 85, RequestBinder 76, Cli 64, Property 21), 952 total, 0 failed.
  • Analyzer tests pass (FirstClassErrors.Analyzers.UnitTests) — 85 passed (run as part of the full-solution suite above).

Documentation

  • Public API / error documentation updated — XML docs on Bind.WithOptions, ConfiguredBind, and the immutable Options.
  • README / doc/ updated — RequestBinder guide (EN).
  • French translation updated — the RequestBinder guide (doc/handwritten/for-users/RequestBinder.fr.md) and ADR-0012 (.fr.md) updated in lockstep.
  • No documentation change required

Architecture decisions

  • No architectural decision in this pull request
  • New decision recorded — ADR drafted as Proposed: ADR-0012
  • Supersedes an existing ADR — successor proposed, status not flipped: ADR-____
  • ⚠️ Conflicts with an existing ADR — flagged for the maintainer: ADR-____

ADR-0012 records the public-API-contract decision (options fixed at the entry point) and, per the maintainer's direction, is set to Accepted in this PR. It keeps the library's no-global-mutable-state stance (ADR-0006) and explicitly rejects a static Configure, leaving the application-level configuration ergonomic to the future ASP.NET Core / DI integration.

Related issues

Closes #145

🤖 Generated with Claude Code


Generated by Claude Code

claude added 3 commits July 18, 2026 07:37
RequestBinder.WithOptions could be called mid-binding, swapping the
IArgumentNameProvider after some properties were already bound, so a single
failure envelope could mix argument paths under two naming policies
(GuestEmail beside guest_email) — silently inconsistent for a client
mapping paths back to the keys it sent.

Move option configuration to the entry point: Bind.WithOptions(options)
.PropertiesOf(request) fixes the options once, before the binder exists,
and the instance WithOptions is removed. The mid-binding swap is now
impossible to write rather than merely discouraged, and Options is
immutable. The configured entry point (ConfiguredBind) carries no
per-request state, so it can be built once and reused per request; options
no longer depend on the request type.

Refs: #145
Record the public-API-contract decision behind #145: the binder's options
are fixed once at the entry point (Bind.WithOptions(options).PropertiesOf)
before binding begins, keeping the library's no-global-mutable-state
stance and rejecting a static Configure. Drafted as Proposed and indexed
(EN + FR); the maintainer decides its status.

Refs: #145
The maintainer accepts the decision recorded in ADR-0012: the binder
options are fixed once at the entry point before binding begins. Flip its
status from Proposed to Accepted (EN + FR) and update the index.

Refs: #145
@Reefact
Reefact merged commit 6b2ea36 into main Jul 18, 2026
13 checks passed
@Reefact
Reefact deleted the 145-lock-binder-options branch July 18, 2026 08:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Binder: WithOptions is accepted after binding starts, allowing mixed naming within one envelope

2 participants