Skip to content

feat(binder)!: build a command from attached input sources - #196

Merged
Reefact merged 1 commit into
mainfrom
148-request-binder-sources
Jul 19, 2026
Merged

feat(binder)!: build a command from attached input sources#196
Reefact merged 1 commit into
mainfrom
148-request-binder-sources

Conversation

@Reefact

@Reefact Reefact commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

Restructure the request binder so it builds a command or query from independently attached input sources rather than from a single DTO, and bind out-of-DTO arguments (route/query/header/…) as peers of the DTO's properties — the feature requested in #148. The binder becomes source-agnostic and untyped: it is entered from its failure envelope (Bind.Request(envelope)), and the command type is named only at the New/Create terminal.

Type of change

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

Changes

  • Source-agnostic entry: Bind.Request(envelope) / Bind.WithOptions(...).Request(envelope) return a non-generic RequestBinder; a DTO is attached with bind.PropertiesOf(dto) (→ PropertySource<TDto>) and out-of-DTO values with bind.Argument(name) / bind.ArgumentList(name).
  • Out-of-DTO arguments: Argument(name).From(source, value), with FromRoute/FromQuery/FromHeader/FromBody/FromForm sugar (scalar and list), bound with the same converter surface as DTO properties.
  • Provenance capture: an argument's source is recorded in the failure context under the new public RequestArgumentSource key; the argument-path key is public too (RequestBindingError.ArgumentPathKey / ArgumentSourceKey). A DTO property carries no source (its provenance is implicit).
  • Terminals New/Create now carry the command type parameter, inferred from the assembler; a single RequestBinder type serves top-level and nested binding; nested-binding functions take (RequestBinder, TDto) — no explicit type argument at the call site.
  • Converter types drop their TRequest type parameter; value-type argument overloads (where T : struct) mirror the value-type property overloads.
  • Docs: request-binder guide rewritten (EN + FR) with a new out-of-DTO argument section; README.nuget.md updated to the new entry.

Testing

  • dotnet build FirstClassErrors.sln — 0 warnings, 0 errors
  • dotnet test FirstClassErrors.sln — 1070 passed, 0 failed
  • Analyzer tests pass (FirstClassErrors.Analyzers.UnitTests) — ran as part of the full-solution test

Documentation

  • Public API / error documentation updated
  • README / doc/ updated (request-binder guide EN/FR, README.nuget.md)
  • French translation updated — the request-binder guide RequestBinder.fr.md was kept in sync; the top-level README.fr.md references no changed behavior and needed no edit
  • No documentation change required

Architecture decisions

  • No architectural decision in this pull request
  • New decision recorded — ADR drafted as Proposed: ADR-0021 (bind out-of-DTO arguments as peers through a source-agnostic untyped entry)
  • Supersedes an existing ADR — successor proposed, status not flipped: ADR-____
  • ⚠️ Conflicts with an existing ADR — flagged for the maintainer: ADR-____

ADR-0021 records the entry-point change; the decisions of ADR-0007, ADR-0012 and ADR-0017 are unaffected — their illustrative Bind.PropertiesOf surface is now historical, left unedited per the ADR discipline (an accepted ADR is not edited in place).

Related issues

Closes #148


🤖 Generated with Claude Code

https://claude.ai/code/session_01J12ghrzM1xiAKw4LETJDqr

Restructure the request binder so it builds a command or query from
independently attached sources rather than from a single DTO. The binder
is source-agnostic and untyped: it is entered from its failure envelope —
Bind.Request(envelope) — and a DTO's properties (PropertiesOf(dto)) and
individually named, source-tagged out-of-DTO arguments
(Argument(name).From(source, value) / ArgumentList, with
FromRoute/FromQuery/... sugar) are attached as peers, sharing one
collect-all envelope and one set of argument paths. The command type is
named only at the New<TCommand>/Create<TCommand> terminal, inferred from
the assembler; a single RequestBinder type serves top-level and nested
binding, and a nested complex binding receives that RequestBinder plus
the sub-DTO, so its result type is inferred with no explicit type
argument.

An argument's provenance is captured in the failure context under the new
public RequestArgumentSource key (the argument-path key is public too), so
an adapter can project a structured per-field response; a DTO property
carries no source (its provenance is implicit).

The request-binder guide (EN/FR) and the package README are rewritten to
the new model, and ADR-0021 records the decision.

BREAKING CHANGE: the entry point changes from
Bind.PropertiesOf(dto).FailWith(env) to Bind.Request(env) +
bind.PropertiesOf(dto); the property selectors move to PropertySource;
the terminals New/Create carry the command type parameter; the converter
types drop their TRequest type parameter; nested-binding functions take a
non-generic RequestBinder and the sub-DTO. Pre-release, no external
consumers.

Refs: #148

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J12ghrzM1xiAKw4LETJDqr
@Reefact
Reefact force-pushed the 148-request-binder-sources branch from 369fd9e to 2068a4f Compare July 19, 2026 19:28
@Reefact
Reefact merged commit de98218 into main Jul 19, 2026
14 checks passed
@Reefact
Reefact deleted the 148-request-binder-sources branch July 19, 2026 19:35
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: no out-of-DTO argument binding, though the README promises "body + route"

2 participants