diff --git a/FirstClassErrors.RequestBinder.UnitTests/DefaultOptionsTests.cs b/FirstClassErrors.RequestBinder.UnitTests/DefaultOptionsTests.cs
new file mode 100644
index 00000000..cdf0d666
--- /dev/null
+++ b/FirstClassErrors.RequestBinder.UnitTests/DefaultOptionsTests.cs
@@ -0,0 +1,106 @@
+#region Usings declarations
+
+using NFluent;
+
+#endregion
+
+namespace FirstClassErrors.RequestBinder.UnitTests;
+
+///
+/// The application-wide : it is what
+/// binds with, configurable once at startup and frozen on first use. These tests inject it through the scoped,
+/// parallel-safe test seam (OverrideDefaultForTests) so they never mutate the process default — the binder
+/// suite keeps seeing the built-in default.
+///
+public sealed class DefaultOptionsTests {
+
+ private static BookingRequest MissingEmail() {
+ return new BookingRequest(null, "R", null, null, null, null, null);
+ }
+
+ private static Error BindMissingEmail(RequestBinderEnvelopeStage start) {
+ var bind = start.FailWith(BookingEnvelopeError.CommandInvalid);
+ bind.SimpleProperty(r => r.GuestEmail).AsRequired(EmailAddress.Parse);
+
+ return bind.New(_ => "x").Error!.InnerErrors.Single();
+ }
+
+ // ── Bind.PropertiesOf binds with the configured default ───────────────────────────────────────────────
+
+ [Fact(DisplayName = "Bind.PropertiesOf binds with the configured default options — naming and structural codes — without WithOptions.")]
+ public void BindPropertiesOfUsesTheConfiguredDefault() {
+ var configured = new RequestBinderOptions(new SnakeCaseNameProvider(),
+ ErrorCode.Create("ACME_ARGUMENT_REQUIRED"),
+ ErrorCode.Create("ACME_ARGUMENT_INVALID"));
+
+ using (RequestBinderOptions.OverrideDefaultForTests(configured)) {
+ Error error = BindMissingEmail(Bind.PropertiesOf(MissingEmail()));
+
+ Check.That(error.Code.ToString()).IsEqualTo("ACME_ARGUMENT_REQUIRED");
+ Check.That(BindingAssertions.ArgumentPathOf(error)).IsEqualTo("guest_email");
+ }
+ }
+
+ [Fact(DisplayName = "Outside the configured scope, Bind.PropertiesOf falls back to the built-in default.")]
+ public void OutsideScopeFallsBackToBuiltIn() {
+ using (RequestBinderOptions.OverrideDefaultForTests(new RequestBinderOptions(new SnakeCaseNameProvider()))) { }
+
+ Error error = BindMissingEmail(Bind.PropertiesOf(MissingEmail()));
+
+ Check.That(error.Code.ToString()).IsEqualTo("REQUEST_ARGUMENT_REQUIRED");
+ Check.That(BindingAssertions.ArgumentPathOf(error)).IsEqualTo("GuestEmail");
+ }
+
+ [Fact(DisplayName = "A per-call Bind.WithOptions overrides the configured default.")]
+ public void WithOptionsWinsOverTheConfiguredDefault() {
+ var appDefault = new RequestBinderOptions(new SnakeCaseNameProvider(),
+ ErrorCode.Create("DEFAULT_REQUIRED"),
+ ErrorCode.Create("DEFAULT_INVALID"));
+ var perCall = new RequestBinderOptions(new SnakeCaseNameProvider(),
+ ErrorCode.Create("PERCALL_REQUIRED"),
+ ErrorCode.Create("PERCALL_INVALID"));
+
+ using (RequestBinderOptions.OverrideDefaultForTests(appDefault)) {
+ Error error = BindMissingEmail(Bind.WithOptions(perCall).PropertiesOf(MissingEmail()));
+
+ Check.That(error.Code.ToString()).IsEqualTo("PERCALL_REQUIRED");
+ }
+ }
+
+ // ── The default resolves to the built-in when unconfigured ────────────────────────────────────────────
+
+ [Fact(DisplayName = "Unconfigured, RequestBinderOptions.Default is the built-in default (default structural codes).")]
+ public void DefaultIsBuiltInWhenUnconfigured() {
+ Check.That(RequestBinderOptions.Default.ArgumentRequiredCode == RequestBindingError.DefaultArgumentRequiredCode).IsTrue();
+ Check.That(RequestBinderOptions.Default.ArgumentInvalidCode == RequestBindingError.DefaultArgumentInvalidCode).IsTrue();
+ }
+
+ // ── Setter contract: null-rejecting and frozen-after-first-use ────────────────────────────────────────
+
+ [Fact(DisplayName = "Setting RequestBinderOptions.Default to null throws ArgumentNullException.")]
+ public void SettingNullThrows() {
+ Check.ThatCode(() => RequestBinderOptions.Default = null!).Throws();
+ }
+
+ [Fact(DisplayName = "Configuring RequestBinderOptions.Default after the first bind has read it throws — the default is frozen.")]
+ public void ConfiguringAfterFirstUseThrows() {
+ _ = RequestBinderOptions.Default; // reading it (as the first bind does) freezes it; idempotent
+
+ Check.ThatCode(() => RequestBinderOptions.Default = new RequestBinderOptions(new SnakeCaseNameProvider()))
+ .Throws();
+ }
+
+ [Fact(DisplayName = "The test seam rejects a null options.")]
+ public void OverrideForTestsRejectsNull() {
+ Check.ThatCode(() => RequestBinderOptions.OverrideDefaultForTests(null!)).Throws();
+ }
+
+ private sealed class SnakeCaseNameProvider : IArgumentNameProvider {
+
+ public string GetArgumentNameFrom(System.Reflection.PropertyInfo property) {
+ return string.Concat(property.Name.Select((c, i) => i > 0 && char.IsUpper(c) ? "_" + char.ToLowerInvariant(c) : char.ToLowerInvariant(c).ToString()));
+ }
+
+ }
+
+}
diff --git a/FirstClassErrors.RequestBinder/RequestBinderOptions.cs b/FirstClassErrors.RequestBinder/RequestBinderOptions.cs
index a983a17f..08e1b7cd 100644
--- a/FirstClassErrors.RequestBinder/RequestBinderOptions.cs
+++ b/FirstClassErrors.RequestBinder/RequestBinderOptions.cs
@@ -1,16 +1,76 @@
namespace FirstClassErrors.RequestBinder;
///
-/// The binding options of a . Options are fixed once, before binding
-/// begins — through — and inherited by nested binders; they are never global
-/// mutable state and can never change while a binder is binding.
+/// The binding options of a . A binder's options are fixed once, before
+/// binding begins — through or the application-wide — and
+/// inherited by nested binders; they never change while a binder is binding. The process-wide
+/// may be configured once at application startup and is frozen on first use.
///
public sealed class RequestBinderOptions {
#region Statics members declarations
- /// The default options: argument names are the C# property names, structural codes are the defaults.
- public static RequestBinderOptions Default { get; } = new(new DefaultArgumentNameProvider());
+ private static readonly RequestBinderOptions BuiltIn = new(new DefaultArgumentNameProvider());
+ private static readonly AsyncLocal TestOverride = new();
+ private static readonly object Gate = new();
+ private static RequestBinderOptions _default = BuiltIn;
+ private static bool _frozen;
+
+ ///
+ /// The application-wide default options that binds with. Assign it
+ /// once at application startup — before any binding — to configure the binder host-wide without threading
+ /// options through every call; a per-call still overrides it. The first bind
+ /// reads it and thereby freezes it, so the default cannot drift once binding has begun. Defaults to the
+ /// built-in options (C# property names and the default structural codes).
+ ///
+ /// Thrown when set to null.
+ /// Thrown when set after the first bind has already read it.
+ public static RequestBinderOptions Default {
+ get {
+ RequestBinderOptions? overridden = TestOverride.Value;
+ if (overridden is not null) { return overridden; }
+
+ // Freeze and read under the gate: a concurrent set() either wins entirely (before any read froze the
+ // default) or observes the freeze and throws — never a torn "first bind on the old default, later binds
+ // on the new one". The gate is uncontended once the default is configured at startup.
+ lock (Gate) {
+ _frozen = true;
+
+ return _default;
+ }
+ }
+ set {
+ if (value is null) { throw new ArgumentNullException(nameof(value)); }
+
+ lock (Gate) {
+ if (_frozen) {
+ throw new InvalidOperationException(
+ "RequestBinderOptions.Default was already read by a binding; configure it once at application startup, before the first bind.");
+ }
+
+ _default = value;
+ }
+ }
+ }
+
+ ///
+ /// Test-only seam: overrides for the current execution context until the returned scope
+ /// is disposed. Backed by an , so it flows with the test's context and never leaks
+ /// across tests running in parallel — the same pattern as the ambient clock. It does not touch the production
+ /// default and never freezes it. Internal, for the library's own tests; a consumer-facing seam belongs in a
+ /// dedicated testing package.
+ ///
+ /// The options to bind with while the scope is active.
+ /// A scope that restores the previous override when disposed.
+ /// Thrown when is null.
+ internal static IDisposable OverrideDefaultForTests(RequestBinderOptions options) {
+ if (options is null) { throw new ArgumentNullException(nameof(options)); }
+
+ RequestBinderOptions? previous = TestOverride.Value;
+ TestOverride.Value = options;
+
+ return new TestOverrideScope(previous);
+ }
#endregion
@@ -47,4 +107,26 @@ public RequestBinderOptions(IArgumentNameProvider argumentNameProvider, ErrorCod
/// The code the binder raises when an argument is present but fails to convert (defaults to REQUEST_ARGUMENT_INVALID).
public ErrorCode ArgumentInvalidCode { get; }
+ #region Nested types
+
+ private sealed class TestOverrideScope : IDisposable {
+
+ private readonly RequestBinderOptions? _previous;
+ private bool _disposed;
+
+ internal TestOverrideScope(RequestBinderOptions? previous) {
+ _previous = previous;
+ }
+
+ public void Dispose() {
+ if (_disposed) { return; }
+
+ _disposed = true;
+ TestOverride.Value = _previous;
+ }
+
+ }
+
+ #endregion
+
}
diff --git a/doc/handwritten/for-maintainers/adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.fr.md b/doc/handwritten/for-maintainers/adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.fr.md
new file mode 100644
index 00000000..6ffd2b5f
--- /dev/null
+++ b/doc/handwritten/for-maintainers/adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.fr.md
@@ -0,0 +1,135 @@
+# ADR-0017 | Fournir un défaut d'options configurable à l'échelle de l'application
+
+🌍 🇬🇧 [English](0017-provide-a-configurable-application-wide-default-for-the-binder-options.md) · 🇫🇷 Français (ce fichier)
+
+**Statut :** Accepté
+**Date :** 2026-07-18
+**Décideurs :** Reefact
+
+## Contexte
+
+* `Bind.PropertiesOf(request)` lie avec `RequestBinderOptions.Default`. Lier avec des
+ options personnalisées exige sinon `Bind.WithOptions(options).PropertiesOf(...)` — faire
+ transiter le point d'entrée configuré par chaque appel, ou le résoudre depuis un
+ conteneur DI.
+* Un hôte sans conteneur DI — une CLI, un worker, un petit outil — n'a aucun moyen
+ host-agnostic de poser un défaut applicatif que le simple `Bind.PropertiesOf` ramasse.
+* L'ADR-0012 a fixé les options d'un binder à son point d'entrée (aucun changement une
+ fois la liaison commencée) et, parmi ses alternatives rejetées, a écarté « un défaut
+ ambiant à l'échelle du processus, configuré une fois (un `Configure` statique) » au
+ motif qu'il introduirait un état global mutable, fuirait entre les tests exécutés en
+ parallèle, et pourrait être configuré au mauvais moment.
+* `RequestBinderOptions` est une valeur immuable : une instance partagée ne porte aucun
+ état de settings mutable, donc le hasard classique — muter un objet de settings partagé
+ pendant qu'il est utilisé — ne s'y applique pas.
+* La convention .NET est partagée : `JsonConvert.DefaultSettings` de `Newtonsoft.Json` est
+ un global librement re-posable ; `JsonSerializerOptions` de `System.Text.Json` devient
+ immuable à la première utilisation (gelé) et se configure par-instance ou via DI.
+* La bibliothèque n'expose aucun autre état ambiant mutable : les points d'extension de
+ l'horloge, de l'identifiant d'instance et des valeurs arbitraires sont des défauts
+ immuables avec un override `AsyncLocal`, scoped, réservé aux tests (ADR-0006).
+* La bibliothèque est en pré-version, non publiée sur NuGet et sans consommateur externe.
+
+## Décision
+
+`RequestBinderOptions.Default` — les options avec lesquelles `Bind.PropertiesOf` lie —
+est un défaut applicatif posable, configuré une fois au démarrage de l'application et gelé
+à la première liaison qui le lit.
+
+## Justification
+
+* Un défaut processus posable est le seul moyen host-agnostic pour que le simple
+ `Bind.PropertiesOf` ramasse une politique applicative sans conteneur DI, ce dont une CLI
+ ou un worker a besoin ; le point d'entrée favorable à la DI (`Bind.WithOptions`) reste
+ disponible là où un conteneur existe.
+* Le hasard contre lequel l'ADR-0012 se prémunissait — un défaut ambiant qui dérive à
+ l'exécution — est supprimé par le gel à la première utilisation : dès que la première
+ liaison le lit, une réaffectation lève, donc c'est un choix au moment de la composition
+ qui ne peut pas changer une fois les requêtes en vol. C'est la discipline de
+ `System.Text.Json` (immuable une fois utilisé), pas celle, librement mutable, de
+ `JsonConvert.DefaultSettings`.
+* Parce que `RequestBinderOptions` est immuable, un défaut partagé ne porte aucun état de
+ settings mutable, donc le piège classique des settings globaux ne s'applique pas ; le
+ seul état global est vers quelles options immuables pointe le défaut, fixé une fois.
+* Garder la configuration sur `RequestBinderOptions.Default` plutôt que sur une méthode de
+ `Bind` laisse le point d'entrée de liaison sans surface de configuration : un
+ développeur qui lie des requêtes ne voit que des verbes de liaison.
+* La préoccupation d'isolation des tests parallèles soulevée par l'ADR-0012 se limite aux
+ tests de la bibliothèque elle-même, et est satisfaite par un override scoped réservé aux
+ tests (un `AsyncLocal`) — le même patron que l'horloge (ADR-0006) — qui ne touche ni ne
+ gèle jamais le défaut de production.
+* Le statut de pré-version signifie que la surface est arrêtée maintenant, quand il n'y a
+ aucun consommateur à migrer.
+
+## Alternatives considérées
+
+### Garder les options au seul point d'entrée (le statu quo de l'ADR-0012)
+
+Considérée parce qu'elle est déjà livrée et n'a aucun état global.
+
+Rejetée parce qu'elle n'offre aucun défaut applicatif host-agnostic : chaque point d'appel
+doit faire transiter le point d'entrée configuré, ou un conteneur DI doit le fournir —
+indisponible pour une CLI ou un worker qui veut configurer le binder une fois.
+
+### Un défaut global librement re-posable (le modèle JsonConvert.DefaultSettings)
+
+Considérée parce que c'est le global posable le plus simple et une convention répandue.
+
+Rejetée parce qu'un défaut réaffectable pendant que les requêtes sont en vol peut dériver,
+réintroduisant le hasard de configuration à l'exécution contre lequel l'ADR-0012
+prévenait. Le gel à la première utilisation garde l'ergonomie tout en supprimant la
+dérive.
+
+### L'injection de dépendances seule (le modèle System.Text.Json / ASP.NET)
+
+Considérée parce que c'est l'idiome moderne là où un conteneur existe, et pleinement
+sûre vis-à-vis des tests.
+
+Rejetée comme unique mécanisme parce qu'elle n'est pas host-agnostic : une CLI, un worker,
+ou tout hôte sans conteneur DI ne peut pas s'en servir pour que le simple
+`Bind.PropertiesOf` ramasse un défaut applicatif. Le point d'entrée injecté reste
+disponible ; cette décision ajoute le chemin sans conteneur.
+
+## Conséquences
+
+### Positives
+
+* N'importe quel hôte — avec ou sans DI — configure la politique de nommage et les codes
+ structurels du binder une fois au démarrage, et le simple `Bind.PropertiesOf` les
+ utilise.
+* Le gel à la première utilisation empêche la dérive à l'exécution ; le seul global mutable
+ est une référence d'options immuables posée une fois.
+* La surface de `Bind` reste sans configuration ; un `Bind.WithOptions` par appel surcharge
+ quand même le défaut.
+
+### Négatives
+
+* La bibliothèque gagne un état global de processus (le défaut posable) — le premier de la
+ bibliothèque, accepté délibérément pour l'ergonomie host-agnostic.
+* Les tests de la bibliothèque ont besoin d'un seam d'override scoped réservé aux tests pour
+ rester parallèle-safe ; le défaut de production n'est pas directement posable dans une
+ suite parallèle.
+
+### Risques
+
+* Un consommateur qui lit `RequestBinderOptions.Default` avant de le configurer le gèle et
+ ne peut alors plus le configurer ; atténué par le diagnostic du setter qui lève
+ (« configurez au démarrage, avant la première liaison ») et par la documentation.
+
+## Actions de suivi
+
+* Faire apparaître le seam d'override de test aux consommateurs via un paquet de test dédié
+ si une demande apparaît (il est actuellement interne, pour les tests de la bibliothèque).
+
+## Références
+
+* ADR-0012 — fixer les options du binder avant le début de la liaison ; cette décision
+ revisite le défaut ambiant à l'échelle du processus que l'ADR-0012 avait pesé puis
+ rejeté comme alternative, en l'adoptant avec des garde-fous. La décision propre à
+ l'ADR-0012 — les options d'un binder sont fixées à son point d'entrée — est inchangée,
+ donc l'ADR-0012 n'est pas supersédé.
+* ADR-0006 — fournir les valeurs de test arbitraires depuis une source unique réamorçable ;
+ le patron de seam de test `AsyncLocal` que cette décision réutilise pour ses tests.
+* Issue #181 — la demande que cette décision résout.
+* `JsonConvert.DefaultSettings` (Newtonsoft.Json) et `JsonSerializerOptions`
+ (System.Text.Json) — les deux conventions pesées.
diff --git a/doc/handwritten/for-maintainers/adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.md b/doc/handwritten/for-maintainers/adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.md
new file mode 100644
index 00000000..571fc433
--- /dev/null
+++ b/doc/handwritten/for-maintainers/adr/0017-provide-a-configurable-application-wide-default-for-the-binder-options.md
@@ -0,0 +1,130 @@
+# ADR-0017 | Provide a configurable application-wide default for the binder options
+
+🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0017-provide-a-configurable-application-wide-default-for-the-binder-options.fr.md)
+
+**Status:** Accepted
+**Date:** 2026-07-18
+**Decision Makers:** Reefact
+
+## Context
+
+* `Bind.PropertiesOf(request)` binds with `RequestBinderOptions.Default`. Binding with
+ custom options otherwise requires `Bind.WithOptions(options).PropertiesOf(...)` —
+ threading the configured entry point through each call, or resolving it from a DI
+ container.
+* A host without a DI container — a CLI, a worker, a small tool — has no host-agnostic
+ way to set an application-wide default that the bare `Bind.PropertiesOf` picks up.
+* ADR-0012 fixed a binder's options at its entry point (no change once binding has
+ begun) and, among its rejected alternatives, rejected "a process-wide ambient default
+ configured once (a static `Configure`)" on the grounds that it would introduce global
+ mutable state, leak across tests running in parallel, and could be configured at the
+ wrong time.
+* `RequestBinderOptions` is an immutable value: a shared instance carries no mutable
+ settings state, so the classic hazard — mutating a shared settings object while it is
+ in use — does not apply to it.
+* The .NET convention is split: `Newtonsoft.Json`'s `JsonConvert.DefaultSettings` is a
+ freely re-settable global; `System.Text.Json`'s `JsonSerializerOptions` becomes
+ immutable on first use (frozen) and is configured per-instance or through DI.
+* The library exposes no other ambient mutable state: the clock, instance-id and
+ arbitrary-value seams are immutable defaults with an `AsyncLocal`, scoped, test-only
+ override (ADR-0006).
+* The library is pre-release, unpublished on NuGet with no external consumers.
+
+## Decision
+
+`RequestBinderOptions.Default` — the options `Bind.PropertiesOf` binds with — is a
+settable application default, configured once at application startup and frozen on the
+first bind that reads it.
+
+## Rationale
+
+* A settable process default is the only host-agnostic way for the bare
+ `Bind.PropertiesOf` to pick up an application-wide policy without a DI container, which
+ a CLI or worker needs; the DI-friendly entry point (`Bind.WithOptions`) stays available
+ where a container exists.
+* The hazard ADR-0012 guarded against — an ambient default that drifts at runtime — is
+ removed by freezing on first use: once the first bind reads it, reassignment throws, so
+ it is a composition-time choice that cannot change while requests are flowing. This is
+ the `System.Text.Json` discipline (immutable-once-used), not the freely-mutable
+ `JsonConvert.DefaultSettings` one.
+* Because `RequestBinderOptions` is immutable, a shared default carries no mutable
+ settings state, so the classic global-settings footgun does not apply; the only global
+ state is which immutable options the default points at, fixed once.
+* Keeping the configuration on `RequestBinderOptions.Default` rather than on a method of
+ `Bind` leaves the binding entry point free of configuration surface: a developer binding
+ requests sees only binding verbs.
+* The parallel-test-isolation concern ADR-0012 raised is confined to the library's own
+ tests, and is met by a scoped, test-only override (an `AsyncLocal`) — the same seam
+ pattern the clock uses (ADR-0006) — which never touches or freezes the production
+ default.
+* The pre-release status means the surface is settled now, when there are no consumers to
+ migrate.
+
+## Alternatives Considered
+
+### Keep options entry-point-only (the status quo of ADR-0012)
+
+Considered because it is already shipped and has no global state at all.
+
+Rejected because it offers no host-agnostic application-wide default: every call site must
+thread the configured entry point, or a DI container must supply it — unavailable to a CLI
+or worker that wants to configure the binder once.
+
+### A freely re-settable global default (the JsonConvert.DefaultSettings model)
+
+Considered because it is the simplest settable global and a widely-used convention.
+
+Rejected because a default that can be reassigned while requests are flowing can drift,
+reintroducing the runtime-configuration hazard ADR-0012 warned about. Freezing on first
+use keeps the ergonomic while removing the drift.
+
+### Dependency injection only (the System.Text.Json / ASP.NET model)
+
+Considered because it is the modern idiom where a container exists, and is fully
+test-safe.
+
+Rejected as the sole mechanism because it is not host-agnostic: a CLI, a worker, or any
+host without a DI container cannot use it to make the bare `Bind.PropertiesOf` pick up an
+application default. The injected entry point stays available; this decision adds the
+container-free path.
+
+## Consequences
+
+### Positive
+
+* Any host — with or without DI — configures the binder's naming policy and structural
+ codes once at startup, and the bare `Bind.PropertiesOf` uses them.
+* Freezing on first use prevents runtime drift; the only mutable global is an
+ immutable-options reference set once.
+* `Bind`'s surface stays free of configuration; a per-call `Bind.WithOptions` still
+ overrides the default.
+
+### Negative
+
+* The library gains one piece of process-global state (the settable default) — the first
+ such state in the library, accepted deliberately for the host-agnostic ergonomic.
+* The library's own tests need a scoped, test-only override seam to stay parallel-safe;
+ the production default is not directly settable within a parallel suite.
+
+### Risks
+
+* A consumer that reads `RequestBinderOptions.Default` before configuring it freezes it
+ and can then no longer configure it; mitigated by the throwing setter's diagnostic
+ ("configure at startup, before the first bind") and by documentation.
+
+## Follow-up Actions
+
+* Surface the test-override seam to consumers through a dedicated testing package if
+ consumer demand appears (it is currently internal, for the library's own tests).
+
+## References
+
+* ADR-0012 — fix the binder options before binding begins; this decision revisits the
+ process-wide ambient default that ADR-0012 weighed and rejected as an alternative,
+ adopting it with mitigations. ADR-0012's own decision — a binder's options are fixed at
+ its entry point — is unchanged, so ADR-0012 is not superseded.
+* ADR-0006 — supply arbitrary test values from a single seedable source; the `AsyncLocal`
+ test-seam pattern this decision reuses for its own tests.
+* Issue #181 — the request this decision resolves.
+* `JsonConvert.DefaultSettings` (Newtonsoft.Json) and `JsonSerializerOptions`
+ (System.Text.Json) — the two conventions weighed.
diff --git a/doc/handwritten/for-maintainers/adr/README.md b/doc/handwritten/for-maintainers/adr/README.md
index 6ce8c625..48f9c20f 100644
--- a/doc/handwritten/for-maintainers/adr/README.md
+++ b/doc/handwritten/for-maintainers/adr/README.md
@@ -190,3 +190,4 @@ Optional supporting material:
| [ADR-0014](0014-bind-a-required-list-by-presence-not-cardinality.md) | Bind a required list by presence, not cardinality | Accepted |
| [ADR-0015](0015-cap-any-combine-at-arity-eight.md) | Cap Any.Combine at arity eight | Proposed |
| [ADR-0016](0016-make-the-binders-structural-error-codes-configurable.md) | Make the binder's structural error codes configurable | Proposed |
+| [ADR-0017](0017-provide-a-configurable-application-wide-default-for-the-binder-options.md) | Provide a configurable application-wide default for the binder options | Accepted |
diff --git a/doc/handwritten/for-users/RequestBinder.en.md b/doc/handwritten/for-users/RequestBinder.en.md
index 7d4844f9..824b0913 100644
--- a/doc/handwritten/for-users/RequestBinder.en.md
+++ b/doc/handwritten/for-users/RequestBinder.en.md
@@ -390,13 +390,13 @@ var bind = Bind.WithOptions(new RequestBinderOptions(new SnakeCaseNames()))
```
Options are chosen **once**, on `Bind.WithOptions`, before the binder even exists —
-so a binder's naming policy can never change mid-binding. They are per-binder, never
-global mutable state, and **nested binders inherit** the options in effect when they
-are created — so `Stay.check_in` is renamed consistently, top to bottom. The entry
-point `Bind.WithOptions(...)` returns carries no per-request state, so you can build
-it once (for example at application startup) and reuse it for every request. The
-library deliberately ships only the default (C# property names): which serializer
-names the wire keys is the host's knowledge, not the library's.
+so a binder's naming policy can never change mid-binding. They are fixed for the life
+of a binding, and **nested binders inherit** the options in effect when they are
+created — so `Stay.check_in` is renamed consistently, top to bottom. The entry point
+`Bind.WithOptions(...)` returns carries no per-request state, so you can build it once
+(for example at application startup) and reuse it for every request. The library
+deliberately ships only the default (C# property names): which serializer names the
+wire keys is the host's knowledge, not the library's.
## Structural error codes
@@ -429,6 +429,29 @@ or, when you keep the defaults, the ones the binder exposes.
if (error.Code == RequestBindingError.DefaultArgumentRequiredCode) { return 422; }
```
+## Configuring the default for the whole application
+
+`Bind.PropertiesOf(request)` binds with `RequestBinderOptions.Default`. That default is
+configurable **once, at application startup** — so a whole host (ASP.NET, a CLI, a
+worker) shares one naming policy and one set of structural codes without threading
+options through every call, and without a DI container:
+
+```csharp
+// Program.cs, before the first bind:
+RequestBinderOptions.Default = new RequestBinderOptions(
+ new SnakeCaseNames(),
+ argumentRequiredCode: ErrorCode.Create("ACME_ARGUMENT_REQUIRED"),
+ argumentInvalidCode: ErrorCode.Create("ACME_ARGUMENT_INVALID"));
+
+// anywhere after that — no options threaded through:
+var bind = Bind.PropertiesOf(request).FailWith(PlaceBookingError.Invalid);
+```
+
+The default is **frozen on first use**: the first bind reads it, and any later
+assignment throws — so it is set at composition time and can never drift once requests
+are flowing (the same discipline as `JsonSerializerOptions`). A per-call
+`Bind.WithOptions(...)` still overrides it for a single binding.
+
## The bug channel: what throws vs what is collected
The binder draws a hard line between a **client error** (recorded, surfaced once
diff --git a/doc/handwritten/for-users/RequestBinder.fr.md b/doc/handwritten/for-users/RequestBinder.fr.md
index b6d7072f..95d8468e 100644
--- a/doc/handwritten/for-users/RequestBinder.fr.md
+++ b/doc/handwritten/for-users/RequestBinder.fr.md
@@ -403,7 +403,7 @@ var bind = Bind.WithOptions(new RequestBinderOptions(new SnakeCaseNames()))
Les options sont choisies **une seule fois**, sur `Bind.WithOptions`, avant même que
le binder n’existe — la politique de nommage d’un binder ne peut donc jamais changer
-en cours de liaison. Elles sont par binder, jamais un état global mutable, et **les
+en cours de liaison. Elles sont fixées pour toute la durée d’une liaison, et **les
binders imbriqués héritent** des options en vigueur à leur création — donc
`Stay.check_in` est renommé de manière cohérente, de haut en bas. Le point d’entrée
renvoyé par `Bind.WithOptions(...)` ne porte aucun état par requête : vous pouvez le
@@ -444,6 +444,30 @@ expose.
if (error.Code == RequestBindingError.DefaultArgumentRequiredCode) { return 422; }
```
+## Configurer le défaut pour toute l’application
+
+`Bind.PropertiesOf(request)` lie avec `RequestBinderOptions.Default`. Ce défaut est
+configurable **une seule fois, au démarrage de l’application** — un hôte entier
+(ASP.NET, une CLI, un worker) partage ainsi une politique de nommage et un jeu de codes
+structurels sans faire transiter d’options par chaque appel, et sans conteneur DI :
+
+```csharp
+// Program.cs, avant la première liaison :
+RequestBinderOptions.Default = new RequestBinderOptions(
+ new SnakeCaseNames(),
+ argumentRequiredCode: ErrorCode.Create("ACME_ARGUMENT_REQUIRED"),
+ argumentInvalidCode: ErrorCode.Create("ACME_ARGUMENT_INVALID"));
+
+// n’importe où ensuite — aucune option à faire transiter :
+var bind = Bind.PropertiesOf(request).FailWith(PlaceBookingError.Invalid);
+```
+
+Le défaut est **gelé à la première utilisation** : la première liaison le lit, et toute
+affectation ultérieure lève — il est donc défini au moment de la composition et ne peut
+jamais dériver une fois les requêtes en vol (la même discipline que
+`JsonSerializerOptions`). Un `Bind.WithOptions(...)` par appel le surcharge quand même
+pour une liaison donnée.
+
## Le canal des bugs : ce qui lève vs ce qui est collecté
Le binder trace une frontière nette entre une **erreur client** (enregistrée,