Skip to content

Optimize the binder's happy path and settle the v1 selector API - #201

Merged
Reefact merged 6 commits into
mainfrom
claude/issue-151-discussion-a8o321
Jul 19, 2026
Merged

Optimize the binder's happy path and settle the v1 selector API#201
Reefact merged 6 commits into
mainfrom
claude/issue-151-discussion-a8o321

Conversation

@Reefact

@Reefact Reefact commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

Re-measures issue #151 on the current code, removes every library-internal happy-path cost it found (cached compiled getters, argument paths deferred to failure recording, lazy error lists), and settles the issue's v1 gate with ADR-0023: the call-site expression tree — ~70–75 % of the per-property cost — stays, as an accepted API decision rather than an optimization target.

Type of change

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

Changes

  • PropertyGetters<TDto,TValue>: DTO properties are read through compiled getters cached per (DTO type, property) — removing the per-bind reflection invoke, the nullable value-type box, and the Nullable.GetUnderlyingType check on every re-bind. The mis-declaration guard moves into the compile step and still throws on every call; a nullable-selector coercion unboxes to the underlying type first, preserving the CLR's boxed-enum/underlying compatibility the reflection path allowed; environments without IL emission fall back to the interpreter, then to the original reflection read.
  • Deferred argument paths: converter stages carry the selected PropertyInfo (or the already-resolved out-of-DTO name) in one union field resolved through ArgumentPaths only when a path is first needed — an all-valid bind of scalar and list properties builds no path string and never consults the IArgumentNameProvider; a bound complex property still resolves its one prefix segment, exactly what the eager code paid.
  • Deferred list-element paths (Tags[2]) and complex-list element prefixes (Guests[2]) through IElementPathSource; result lists pre-sized from ICollection<T>.Count.
  • Per-binding error List<> created on first failure, including nested and per-element bindings.
  • New tests pin the contracts: getter reuse and isolation, concurrent first binds, the guard thrown on every call, the raw-exception bug channel, enum/underlying coercion selectors, provider call counts (0 on all-valid scalar/list binds, 1 per bound complex property, 1 per failing argument), and byte-identical prefixed/indexed error paths.
  • FirstClassErrors.RequestBinder.Benchmarks (in the solution, never packed, not a test project): BenchmarkDotNet harness with a hand-written floor, hoisted-selector and expression-vs-delegate micro scenarios, and a README carrying the measured tables and their reading.
  • Sonar: the benchmark project is excluded from coverage only (still analyzed).
  • ADR-0023 (EN + FR, indexed) records the v1 selector decision.

Measured (byte-exact, back-to-back probes; details in the benchmark README): lists of 10 −35 % allocated (−63 % time), nullable value-type property −104 B (−39 % time), full realistic booking −1 264 B (−16 % time); failure path unchanged (−96 B). Two intended bug-channel deltas, both pinned by tests: a throwing DTO getter surfaces its own exception instead of TargetInvocationException, and the name provider is consulted at record time, never more than the historical once-per-property bound.

Testing

  • dotnet build FirstClassErrors.sln
  • dotnet test FirstClassErrors.sln
  • Analyzer tests pass (FirstClassErrors.Analyzers.UnitTests)

1 085 tests pass on net10.0 (Release), zero warnings with the CI ratchet flags simulated locally. The net472 floor leg of FirstClassErrors.RequestBinder.PropertyTests builds locally (-p:EnableNet472Floor=true) but executes only on the Windows CI leg — not run in this Linux session. Benchmarks were run locally before and after; results are in FirstClassErrors.RequestBinder.Benchmarks/README.md.

Documentation

  • Public API / error documentation updated
  • README / doc/ updated
  • French translation (doc/handwritten/for-users/README.fr.md) updated if user-facing behavior changed
  • No documentation change required

Benchmark README (measurement tables and cost decomposition) and ADR-0023 (EN + FR). No user-facing behavior change, so the user README and its French translation are untouched.

Architecture decisions

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

ADR-0023 was drafted as Proposed and flipped to Accepted in this branch on the maintainer's explicit instruction (@Reefact selected the decision in the working session); the acceptance is its own commit so it can be reverted independently if that recording is not wanted.

Related issues

Closes #151

🤖 Generated with Claude Code

https://claude.ai/code/session_01Edeoi56UaLxHqwg4q2HLo8


Generated by Claude Code

@Reefact

Reefact commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

@codex Please perform an in-depth review of the code introduced or modified by this pull request, which implements a new feature.

The objective is to determine whether the feature is correctly designed, correctly implemented, sufficiently tested, and consistently integrated into the existing project.

Review scope

Start by reading:

  1. the pull request description;
  2. any referenced issues, specifications, or ADRs;
  3. the relevant functional and technical documentation;
  4. the repository conventions, including CLAUDE.md, AGENTS.md, contribution guides, and applicable architectural rules;
  5. the complete pull request diff;
  6. any existing code required to understand how the feature integrates with the rest of the system.

Do not review the diff in isolation. Verify that the changes are consistent with the abstractions, behaviors, contracts, and conventions already established in the project.

Review areas

1. Requirements and expected behavior

Verify that the implementation actually satisfies the described requirements.

Look specifically for:

  • missing or only partially implemented requirements;
  • behaviors introduced without being specified;
  • ambiguities resolved in a questionable way;
  • unhandled edge cases;
  • discrepancies between the expected behavior, documentation, tests, and implementation.

Do not treat a behavior as incorrect merely because a different design would also have been possible. Report an issue only when there is a demonstrable contradiction, omission, defect, or concrete risk.

2. Design and architecture

Evaluate:

  • consistency with the existing architecture;
  • separation of responsibilities;
  • newly introduced dependencies;
  • coupling and cohesion;
  • the quality and stability of public abstractions;
  • enforcement of domain invariants;
  • error handling;
  • consistency with existing patterns;
  • meaningful duplication;
  • premature abstractions;
  • classes or components carrying too many responsibilities.

Also verify that the feature does not introduce a second competing way to achieve an existing operation without an explicit and justified reason.

3. Public API and developer experience

For every public API, fluent API, extension method, overload, or generic abstraction added or modified, review:

  • naming clarity;
  • consistency with existing APIs;
  • generic type inference;
  • generic constraints;
  • nullability;
  • overload resolution and ambiguity risks;
  • compilation failures for otherwise legitimate usages;
  • IntelliSense discoverability;
  • error-message quality;
  • source and binary compatibility;
  • surprising or difficult-to-predict behavior.

Mentally validate several realistic usage scenarios, including edge cases and invalid usages.

4. Code correctness

Look for concrete defects, including:

  • logical errors;
  • incorrect conditional branches;
  • incorrectly propagated values;
  • incomplete null handling;
  • unexpected exceptions;
  • type conversion, variance, or generic inference issues;
  • incorrect behavior for empty collections;
  • execution-order problems;
  • uncontrolled mutable state;
  • unintended non-deterministic behavior;
  • invalid assumptions about inputs;
  • concurrency or thread-safety issues, where relevant;
  • significant performance or allocation problems.

Do not report micro-optimizations without a realistic impact.

5. Tests

Assess whether the tests genuinely demonstrate the intended behavior of the feature.

Verify coverage of:

  • nominal scenarios;
  • expected failures;
  • edge cases;
  • likely regressions;
  • null values and empty collections;
  • relevant generic variants;
  • deterministic behavior;
  • interactions between new components and existing code;
  • backward-compatibility guarantees.

Identify tests that may pass without actually proving the announced behavior, as well as tests that are excessively coupled to implementation details.

Do not mechanically request a test for every line or method. Recommend tests only when they protect an important behavior or an identified risk.

6. Documentation

Verify that:

  • the documentation describes the behavior actually implemented;
  • examples are conceptually compilable and use valid APIs;
  • signatures and names are up to date;
  • French and English documentation remain consistent when both are affected;
  • important limitations and design choices are documented;
  • no misleading, obsolete, or technically incorrect statement has been introduced.

Do not criticize writing style unless it creates a real technical ambiguity or communicates incorrect behavior.

7. Regressions and integration

Analyze the impact on existing code, including:

  • source or binary compatibility breaks;
  • unintended changes to existing behavior;
  • semantic changes to an existing API;
  • impact on downstream consumers;
  • interactions with analyzers, generators, renderers, publishers, or other affected modules;
  • newly introduced compiler warnings;
  • nullability regressions;
  • performance or memory consequences;
  • inconsistencies between packages or modules.

Do not perform a general audit of the entire repository. Inspect existing code only when it is necessary to evaluate the changes introduced by this pull request.

Expected output

Start with a concise summary containing:

  • the overall assessment;

  • the confidence level in the implementation;

  • the main identified risks, if any;

  • an explicit conclusion using one of the following:

    • ready to merge;
    • ready after minor corrections;
    • significant corrections required.

Then report only genuinely actionable findings, classified using these severity levels:

  • Blocking: bug, contract violation, security issue, data loss, confirmed regression, or substantially incorrect functionality;
  • Important: a real issue that should be corrected before merge but does not make the entire feature unusable;
  • Useful improvement: a justified improvement that is not required before merge.

For each finding, include:

  1. the affected file and lines;
  2. the current behavior;
  3. the concrete problem;
  4. a scenario demonstrating or explaining the risk;
  5. the recommended correction;
  6. the test that should protect the correction, when relevant.

Group findings that share the same root cause instead of reporting multiple variations of the same issue.

After the findings, add a separate section for open questions. Do not present a question as a defect unless the answer establishes that an actual issue exists.

Finish with:

  • aspects that were particularly well implemented;
  • requirements that were correctly implemented and covered;
  • residual risks that cannot be confirmed through static code review alone.

Review rules

  • Do not invent problems merely to produce a longer review.
  • Do not recommend a complete rewrite when a local correction is sufficient.
  • Do not criticize stylistic choices already accepted by the repository.
  • Do not request refactoring without a concrete benefit for this feature.
  • Do not present personal preferences as project requirements.
  • Do not report hypothetical risks without explaining precisely how they could occur.
  • Verify every finding before reporting it.
  • Prefer a small number of strong, actionable findings over a long list of minor remarks.
  • Do not modify any files. This task is strictly a code review and analysis of the pull request.
  • Use the highest available reasoning depth.
  • Do not conclude before reviewing the complete diff and the main execution paths affected by the feature.

claude added 6 commits July 19, 2026 22:22
Re-measured issue #151 on the current code: ~70-75% of the ~700 B
per-property happy-path cost is the call-site expression tree, which no
internal change can remove; the removable remainder was uncached
reflection reads and path strings built for arguments that never fail.

Remove that remainder, at constant observable behavior:

* Read DTO properties through compiled getters cached per
  (DTO type, property) in PropertyGetters<TDto,TValue> — deleting the
  per-bind reflection invoke, the nullable value-type box, and the
  Nullable.GetUnderlyingType check on every re-bind. The mis-declaration
  guard moves into the compile step and still throws on every call;
  environments without IL emission fall back to the interpreter, then to
  the original reflection read.
* Defer argument-path building to failure recording: converter stages
  carry the selected PropertyInfo (or the already-resolved out-of-DTO
  name) in a single union field resolved through ArgumentPaths, so an
  all-valid bind builds no path string and never consults the
  IArgumentNameProvider for scalar and list properties. A bound complex
  property still resolves its one prefix segment, exactly what the eager
  code paid.
* Defer list-element paths ("Tags[2]") and complex-list element prefixes
  ("Guests[2]") through IElementPathSource — built only for the elements
  that actually fail — and pre-size the result list from
  ICollection<T>.Count.
* Create the per-binding error list on first failure instead of eagerly,
  including for nested and per-element bindings.

Byte-exact before/after on the benchmark scenarios: lists of 10 go from
2504 B to 1624 B (-35%), a nullable-int property from 920 B to 816 B,
the full booking request from 13256 B to 11992 B; list binds are ~2.7x
faster and nullable value-type binds ~1.6x. Error paths, codes,
messages, recording order, and the eager selector guards are unchanged;
the one intended bug-channel delta is that a throwing DTO getter now
surfaces its own exception instead of TargetInvocationException.

Refs: #151
Pin what the perf change promised: one compiled getter reused per (DTO
type, property) with no cross-type collision and safe concurrent first
binds; the non-nullable value-type guard thrown on every call, never
cached away; a throwing DTO getter surfacing its own exception (the
binder's bug channel, no reflection wrapper); zero name-provider calls
on an all-valid bind of scalar and list properties, one per bound
complex property, one per failing argument otherwise; and byte-identical
prefixed, indexed error paths on nested and list failures.

Refs: #151
Issue #151 asked for a re-measurement before any remedy: add the
BenchmarkDotNet project that produced those numbers, so the next perf
question starts from a runnable harness instead of a stale figure. It
measures the happy path against a hand-written floor, isolates the
call-site expression-tree cost (hoisted-selector and micro scenarios),
covers lists, nesting, out-of-DTO arguments and the failure path, and
its README carries the measured tables and their reading — including
the decomposition behind ADR-0023.

The project builds with the solution (never packed, never run by CI's
test step) so it cannot bit-rot; BenchmarkDotNet is pinned centrally.

Refs: #151
The benchmark project is a measurement harness: never shipped, never
unit-tested. Excluding it from coverage only — it still gets the
SonarAnalyzer pass — keeps the new-code coverage gate about the library.

Refs: #151
Issue #151 gated its remedy on a decision to take before the v1 API
freeze. The re-measurement shows ~70-75% of the per-property cost is
the call-site expression tree — out of the library's reach — while the
internal remainder is now removed. Draft the decision that closes the
gate as Proposed: keep expression-tree selectors as the sole v1
selector API, accept the per-call expression cost (hoisted selectors
remain the measured, non-breaking mitigation), and defer any
delegate-based fast path to a post-v1, additive decision.

English and French versions, indexed; status left to the maintainer.

Refs: #151
The maintainer validated the proposed decision: the v1 binder keeps
expression-tree selectors as its sole selector API, the per-call
expression cost is accepted (hoisted selectors remain the measured,
non-breaking mitigation for hot call sites), and any delegate-based
fast path stays a post-v1, additive option. Recorded on the
maintainer's explicit instruction.

Refs: #151
@Reefact
Reefact force-pushed the claude/issue-151-discussion-a8o321 branch from f385328 to b4f9342 Compare July 19, 2026 22:23
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: f38532820e

ℹ️ 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".

@Reefact
Reefact merged commit c9bc4ba into main Jul 19, 2026
15 checks passed
@Reefact
Reefact deleted the claude/issue-151-discussion-a8o321 branch July 19, 2026 22:29
Reefact pushed a commit that referenced this pull request Jul 19, 2026
PR #201 merged and claimed ADR-0023 (keep expression-tree selectors
for the v1 binder API) while this branch was still open with its own
ADR-0023 (the one-time editorial-refactoring exception). Renumbers the
latter to ADR-0024 and updates every cross-reference so no two records
share a number.
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: per-property binding uses uncached reflection and allocates a path string on the happy path

2 participants