Skip to content

feat(binder): configure a host-agnostic default for Bind.PropertiesOf - #183

Merged
Reefact merged 6 commits into
mainfrom
181-host-agnostic-default-options
Jul 18, 2026
Merged

feat(binder): configure a host-agnostic default for Bind.PropertiesOf#183
Reefact merged 6 commits into
mainfrom
181-host-agnostic-default-options

Conversation

@Reefact

@Reefact Reefact commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

Bind.PropertiesOf(request) binds with RequestBinderOptions.Default, which was a fixed built-in — so the only way to bind with custom options (a naming policy, the overridable structural codes from #147) was Bind.WithOptions(...), threading the configured entry point through every call or resolving it from DI. This makes RequestBinderOptions.Default a settable application default, configured once at startup with no DI container needed (a CLI or worker can set it), and frozen on first use so it cannot drift once requests are flowing. Bind gains no configuration surface; a per-call Bind.WithOptions(...) still overrides it.

Type of change

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

Changes

  • Make RequestBinderOptions.Default a settable property (was get-only). Setting it rejects null and throws once the first bind has read it (frozen on first use — the JsonSerializerOptions discipline), so it is configured at composition time and cannot change at runtime. The value is an immutable RequestBinderOptions, so there is no shared-mutable-settings hazard.
  • Bind.PropertiesOf already reads RequestBinderOptions.Default, so Bind is unchanged — no configuration method pollutes the Bind. surface. Bind.WithOptions(o) still overrides per call.
  • Add an internal, scoped, test-only override (OverrideDefaultForTests, backed by an AsyncLocal) so the library's own suite stays parallel-safe without mutating the process default — the same seam pattern as the ambient clock.
  • Add DefaultOptionsTests (7 tests): Bind.PropertiesOf binds with the configured default (naming + codes) without WithOptions; falls back to the built-in outside the scope; a per-call WithOptions wins; the setter rejects null and throws after first use.
  • Document configuring the application-wide default in the RequestBinder guide (EN + FR), and add ADR-0017 (EN + FR).

Testing

  • dotnet build FirstClassErrors.sln — succeeded, 0 warnings / 0 errors.
  • dotnet test FirstClassErrors.sln — all green (998 total, 0 failed); the freeze and AsyncLocal seam do not leak into the other binder tests.
  • Analyzer tests pass (FirstClassErrors.Analyzers.UnitTests) — 85 passed.

Documentation

  • Public API / error documentation updated — XML docs on RequestBinderOptions.Default and the test seam.
  • README / doc/ updated — RequestBinder guide (EN), new "Configuring the default for the whole application" section.
  • French translation updated — the RequestBinder guide (RequestBinder.fr.md) and ADR-0017 (.fr.md), in lockstep.
  • No documentation change required

Architecture decisions

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

ADR-0017 records the decision and is Accepted (per the maintainer). It revisits the process-wide ambient default that ADR-0012 had weighed and rejected as an alternative, adopting it with mitigations (frozen-on-first-use, immutable value, AsyncLocal test seam). ADR-0012 is not superseded — its own decision (a binder's options are fixed at its entry point) is unchanged and stays Accepted.

Related issues

Closes #181

🤖 Generated with Claude Code


Generated by Claude Code

claude added 4 commits July 18, 2026 12:03
Bind.PropertiesOf binds with RequestBinderOptions.Default, which was a
fixed built-in. Make it a settable application default, configured once at
startup (no DI container needed, so a CLI or worker can set it) and frozen
on first use: the first bind reads it, and a later assignment throws, so it
cannot drift once requests are flowing (the JsonSerializerOptions
discipline). Bind gains no configuration surface; a per-call
Bind.WithOptions still overrides it. A scoped, test-only override
(AsyncLocal) keeps the suite parallel-safe.

Refs: #181
Record the decision behind #181: RequestBinderOptions.Default is a settable
application default, configured once at startup and frozen on first use. It
revisits the process-wide ambient default that ADR-0012 had weighed and
rejected as an alternative, adopting it with mitigations; ADR-0012's own
entry-point-fix decision is unchanged, so ADR-0012 is not superseded.
Drafted as Proposed and indexed (EN + FR); the maintainer decides its status.

Refs: #181
The maintainer accepts the decision recorded in ADR-0017: the binder's
options default is a settable application default, configured once at
startup and frozen on first use. Flip its status from Proposed to Accepted
(EN + FR) and update the index.

Refs: #181

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a74f384183

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread FirstClassErrors.RequestBinder/RequestBinderOptions.cs Outdated
A late Default assignment could race the first bind's read: the setter
observed _frozen == false, the getter then set _frozen = true and returned
the old default, and the setter still assigned -- leaving the first binding
on the old options and later bindings on the new, violating the
frozen-on-first-use guarantee. Guard the freeze/check/assign with a lock so
a concurrent set either wins before any read froze the default, or observes
the freeze and throws.

Refs: #181
@Reefact

Reefact commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

@codex no more issues ?

@chatgpt-codex-connector

Copy link
Copy Markdown

No blocking issue found in my follow-up review.

The previously reported concurrency issue appears resolved: RequestBinderOptions.Default now uses the same Gate lock for both the freeze/read path and the setter path, so a setter can either complete before the first read freezes the default, or observe _frozen and throw after the first read. FirstClassErrors.RequestBinder/RequestBinderOptions.csL15-L17 FirstClassErrors.RequestBinder/RequestBinderOptions.csL28-L53

I also checked the related tests around the scoped test override, default fallback, per-call override, null rejection, and frozen-after-first-use behavior; I did not find another actionable issue in that area. FirstClassErrors.RequestBinder.UnitTests/DefaultOptionsTests.csL30-L95

Testing / checks

  • git status --short
  • git diff --stat 12bf086c4aa63e8a19a90146c0a644977187f2b2..HEAD
  • git diff --name-only 12bf086c4aa63e8a19a90146c0a644977187f2b2..HEAD
  • ⚠️ dotnet test FirstClassErrors.RequestBinder.UnitTests/FirstClassErrors.RequestBinder.UnitTests.csproj --no-restore — could not run because dotnet is not installed in this environment.

View task →

@Reefact
Reefact enabled auto-merge July 18, 2026 13:16
@Reefact
Reefact merged commit 531c52d into main Jul 18, 2026
13 checks passed
@Reefact
Reefact deleted the 181-host-agnostic-default-options branch July 18, 2026 13:19
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: a host-agnostic default RequestBinderOptions used by Bind.PropertiesOf

2 participants