Skip to content

fix(generator): don't intercept RequestDelegate.Invoke (delegate invocation → CS9207) - #17

Merged
ANcpLua merged 1 commit into
mainfrom
fix/requestdelegate-invoke-no-intercept
Jun 30, 2026
Merged

fix(generator): don't intercept RequestDelegate.Invoke (delegate invocation → CS9207)#17
ANcpLua merged 1 commit into
mainfrom
fix/requestdelegate-invoke-no-intercept

Conversation

@ANcpLua

@ANcpLua ANcpLua commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Problem

Hand-written ASP.NET Core middleware invokes the next hop as next(context) / _next(context). The C# semantic model resolves that to RequestDelegate.Invoke, whose MethodKind is DelegateInvoke. The interceptors feature can only intercept ordinary member methods, so emitting an interceptor for a delegate invocation is rejected:

error CS9207: Cannot intercept 'next' because it is not an invocation of an ordinary member method.

The AspNetCoreRequestDelegate matcher had no MethodKind guard (unlike the HttpClient/Db/Azure/Elastic matchers), so it emitted an un-interceptable interceptor for every middleware hop. This package's own demos never hand-write middleware, so CI stayed green — but every real consumer that does breaks. Surfaced in qyl.collector (OtlpApiKeyMiddleware, OtlpCorsMiddleware, EmbeddedDashboardMiddleware), and unmasked once the Build() opt-out (#16) removed the earlier CS9153.

This was pre-existing since v3.1.0cde22004 (the qyl-side bump to 3.1.0) introduced both CS9153 and CS9207; only CS9153 was diagnosed at the time.

Fix

Guard TryGetAspNetCoreRequestDelegateInvocation on MethodKind.Ordinary. RequestDelegate.Invoke is never Ordinary, so the broken interceptor is no longer emitted. Request-pipeline trace coverage already comes from the ASP.NET Core hosting instrumentation, not from wrapping each middleware hop.

Regression test

tools/verify-aspnetcore-middleware-delegate.py (wired into the verify goal suite): a consumer with next(context) middleware must build clean, emit no RequestDelegate interceptor, and still emit a control HttpClient interceptor (proving the generator stayed selective, not silent). Confirmed it fails with CS9207 on the pre-fix generator and passes after.

Verification

  • verify-aspnetcore-middleware-delegate (new)
  • verify-build-interceptor-optout (no regression to feat(generator): opt out of WebApplicationBuilder.Build() interception #16)
  • verify-generator-snapshots (no committed snapshot changed — surgical)
  • verify-contract-invariants (inert matcher keeps all structural counts)
  • ✅ Real qyl.collector Release build against a locally-packed fixed package: 0 warnings, 0 errors (both CS9153 and CS9207 gone)

🤖 Generated with Claude Code

…cation → CS9207)

Hand-written ASP.NET Core middleware invokes the next hop as `next(context)` /
`_next(context)`, which the semantic model resolves to RequestDelegate.Invoke — a
MethodKind.DelegateInvoke. The C# interceptors feature can only intercept ordinary
member methods, so emitting an interceptor for a delegate invocation is rejected with
CS9207 ("Cannot intercept 'next' because it is not an invocation of an ordinary member
method").

The AspNetCoreRequestDelegate matcher had no MethodKind guard, so it emitted an
un-interceptable interceptor for every middleware hop. The package's own demos never
hand-write middleware, so CI stayed green — while real consumers that do (qyl.collector's
OtlpApiKeyMiddleware / OtlpCorsMiddleware / EmbeddedDashboardMiddleware) failed to build.

Guard the matcher on MethodKind.Ordinary, as the HttpClient/Db/Azure/Elastic matchers
already do; RequestDelegate.Invoke is never Ordinary, so the broken interceptor is no
longer emitted. Request-pipeline trace coverage already comes from the ASP.NET Core
hosting instrumentation, not from wrapping each middleware hop.

Adds tools/verify-aspnetcore-middleware-delegate.py — a consumer with `next(context)`
middleware must build clean and emit no RequestDelegate interceptor while still emitting a
control HttpClient interceptor (proving the generator stayed selective, not silent) —
wired into the verify goal suite.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6a87969b-aa0b-42aa-97aa-d64350d50802

📥 Commits

Reviewing files that changed from the base of the PR and between f411947 and d7c6c1b.

📒 Files selected for processing (3)
  • src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.Detection.cs
  • tools/verify-aot-autoinstrumentation-goal.py
  • tools/verify-aspnetcore-middleware-delegate.py
📜 Recent review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: smoke (qyl-linux)
  • GitHub Check: smoke (qyl-macos)
🧰 Additional context used
📓 Path-based instructions (2)
**

⚙️ CodeRabbit configuration file

AGENTS.md

**: # Qyl.OpenTelemetry.AutoInstrumentation agent rules

Mission

This repository is the runtime AOT auto-instrumentation lane for qyl, evolving into a
self-describing observability substrate. The foundation is unchanged: .NET 10
NativeAOT-compatible zero-code instrumentation through managed build assets, source generation,
DiagnosticListener consumption, and module-initializer boot. The direction is the North Star
below.

Keep this repository separate from:

  • semantic-convention package generation (Qyl.OpenTelemetry.SemanticConventions is a referenced
    vocabulary package, not generated here),
  • the old CLR-profiler/OpenTelemetry auto-instrumentation substrate.

North Star — declare and prove the whole stack

Every observability tool today is pull-by-observation: a backend learns what a service emits
by receiving samples over time, and never knows whether it has seen the whole surface. qyl has a
capability none of them have — because instrumentation is source-generated interceptors + a static
contract + a referenced semconv registry + (incrementally) DTO inference, the complete set of
telemetry a binary can ever produce is a compile-time-derivable fact, with provenance.

The substrate goal: every qyl binary ships a complete, machine-readable Telemetry Capability
Graph (TCG)
— the full possible OpenTelemetry surface for that exact binary, each capability
tagged compile-time-owned vs runtime-valued — and proves it by self-hosting (instrumenting its
own pipeline with its own mechanism, zero extra code). Any external entity consumes the TCG to know
the entire stack before a span is sampled. The contract becomes the shared semantic graph; an OTLP
backend is just one consumer.

Three pillars:

  1. Self-host (the proof). qyl instruments qyl with qyl — QylSelfTelemetry /
    SemConvConformanceProcessor are the seed; the binary observing itself is how "declared TCG ==
    runtime reality" is checked.
  2. **Compile-time-complete ...

Files:

  • tools/verify-aot-autoinstrumentation-goal.py
  • src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.Detection.cs
  • tools/verify-aspnetcore-middleware-delegate.py

⚙️ CodeRabbit configuration file

**: Operating principles (solo-dev, agentic SDLC — reviews are advisory, agents act on them):

  1. LAZY: one self-contained, correct review beats ten partial ones. Every finding is
    definitive — concrete evidence with file:line, a concrete fix, no "consider maybe",
    no open or ambiguous questions back to the author. If you cannot decide a point
    from the diff plus repo context, stay silent on it. Never cite a source, API, or
    version you have not verified; an unverifiable claim is a dropped claim.
  2. IMPATIENT: never stall a PR. There are no compatibility obligations here — internal
    and dogfooding code has NO public-API contract; removing shims, breaking signatures,
    and deleting dead paths are normal, desirable changes. Do not flag backward
    compatibility, deprecation ceremony, or migration paths. (SemVer applies only to
    commercially sold libraries — this repo has none.)
  3. EGO: hold the bar of the best reviewer on the market — flag real correctness,
    security, data-loss, and structural problems precisely; produce zero noise.

Files:

  • tools/verify-aot-autoinstrumentation-goal.py
  • src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.Detection.cs
  • tools/verify-aspnetcore-middleware-delegate.py
src/**/*.cs

⚙️ CodeRabbit configuration file

src/**/*.cs: Zero-code instrumentation runtime: this code runs inside EVERY request of host
applications. Top priorities, in order: (1) allocations and boxing on hot paths —
flag closures, LINQ, params arrays, string concat in listener/semantic-tag code;
(2) tag cardinality — any attribute value that is unbounded (raw URLs, user input,
exception messages) explodes at scale; (3) Activity/Meter lifecycle — undisposed
listeners, leaked subscriptions, double-Start/Stop; (4) thread safety of shared
listener state. PublicAPI.Shipped/Unshipped.txt are analyzer-managed: edits must
come from the analyzer flow, and API breaks are fine (internal product, no
compatibility contract).

Files:

  • src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.Detection.cs
🔇 Additional comments (3)
src/Qyl.OpenTelemetry.AutoInstrumentation.SourceGenerators/QylAutoInstrumentationGenerator.Detection.cs (1)

160-168: LGTM!

tools/verify-aspnetcore-middleware-delegate.py (1)

1-129: LGTM!

tools/verify-aot-autoinstrumentation-goal.py (1)

27-27: LGTM!


📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of ASP.NET Core middleware delegate scenarios so builds no longer fail when delegate-style Invoke methods are encountered.
    • Prevented an interceptor from being generated for unsupported middleware delegate patterns, while keeping other instrumentation intact.
  • Tests

    • Added a new verification check for ASP.NET Core middleware delegate coverage to catch this regression in future builds.

Walkthrough

The source generator's TryGetAspNetCoreRequestDelegateInvocation gains a MethodKind.Ordinary guard to suppress interceptor generation for delegate-invocation Invoke shapes (which would cause CS9207). A new Python verification script builds a consumer project exercising that path and asserts the bad interceptor token is absent; the gate script registers it.

Changes

Delegate Invocation Guard and Verification

Layer / File(s) Summary
MethodKind guard in detection
src/.../QylAutoInstrumentationGenerator.Detection.cs
Rejects non-MethodKind.Ordinary Invoke symbols early in TryGetAspNetCoreRequestDelegateInvocation, preventing emission of CS9207-triggering interceptors for delegate invocations.
Verification script and gate registration
tools/verify-aspnetcore-middleware-delegate.py, tools/verify-aot-autoinstrumentation-goal.py
New script builds a consumer with next(context) middleware, reads the generated interceptor file, and asserts RequestDelegate_Invoke is absent and the HttpClient control token is present; registered as "aspnetcore middleware delegate" in the gate.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes


src/.../QylAutoInstrumentationGenerator.Detection.cs:160 — the guard checks symbol.MethodKind != MethodKind.Ordinary but the condition should be confirmed to use the correct Roslyn enum value for a regular method vs. a delegate invocation; MethodKind.DelegateInvoke is the delegate shape, so returning early on anything that is not Ordinary is correct, but verify the else-branch still handles anonymous/local function edges if those ever appear in this detection path.

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately names the generator fix and the CS9207 delegate-invocation issue.
Description check ✅ Passed The description is directly aligned with the middleware delegate interception bug, fix, and regression test.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/requestdelegate-invoke-no-intercept
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/requestdelegate-invoke-no-intercept

Comment @coderabbitai help to get the list of available commands.

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.

1 participant