Skip to content

Make GraphQLRequestHandler extensible via virtual members - #602

Merged
xperiandri merged 2 commits into
devfrom
refactor/graphql-request-handler-extensibility
Sep 15, 2026
Merged

xperiandri merged 2 commits into
devfrom
refactor/graphql-request-handler-extensibility

Conversation

@xperiandri

Copy link
Copy Markdown
Collaborator

Turn the introspection/operation/dispatch steps of GraphQLRequestHandler from private let-bound closures into abstract/default members, so a derived handler can override a single step (e.g. per-request introspection, or custom operation execution) without reimplementing the whole class:

  • ExecuteIntrospectionQuery and ExecuteOperation are now virtual, each resolving options.CurrentValue.SchemaExecutor itself instead of receiving it as a parameter (dropping the now-pointless generic <'Root> from ExecuteOperation, which used to shadow the class's own 'Root without being constrained by it).
  • CheckOperationType becomes a public member (was a private local function), with a <remarks> note that it consumes the request body, so calling it more than once on the same request fails to bind on the second call.
  • HandleAsync is now virtual too, and dispatches to the other members through handler instead of closing over local functions.

Also move the class-level <summary> from the primary constructor's doc comment to a /// line on the type itself, and give the constructor its own "Initializes a new instance..." comment - the constructor comment documented the class, not the constructor.

Copilot AI 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.

🔵 Needs a closer look

Add coverage confirming HandleAsync dispatches through the overridable members.

Pull request overview

This PR makes GraphQLRequestHandler extensible through overridable request-processing members.

Changes:

  • Adds virtual introspection, operation execution, and handling steps.
  • Exposes CheckOperationType and documents request-body consumption.
  • Updates type and constructor XML documentation.
File summaries
File Summary Review note
src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLRequestHandler.fs Adds virtual handler extension points and updated documentation. Moderate: add tests verifying derived-handler overrides are reached.
Review details

Suppressed comments (1)

src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLRequestHandler.fs:261

  • The new virtual extension points and the handler.* dispatch are not covered by tests; the existing ASP.NET Core tests only exercise serialization/websocket messages. Please add an xUnit test with a derived handler overriding the execution/dispatch members and assert that HandleAsync reaches those overrides, otherwise this central extensibility contract can regress back to closed-over implementations unnoticed.
            match! handler.CheckOperationType () with
            | IntrospectionQuery optionalAstDocument -> return! handler.ExecuteIntrospectionQuery optionalAstDocument
            | OperationQuery content -> return! handler.ExecuteOperation (content)
  • Files reviewed: 1/1 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

xperiandri added a commit that referenced this pull request Sep 15, 2026
The Copilot review on PR #602 asked for coverage that HandleAsync
reaches ExecuteIntrospectionQuery/ExecuteOperation/HandleAsync through
virtual dispatch rather than closed-over let functions, so the
extensibility contract can't silently regress. No test in the repo
built an HttpContext or exercised the request handler.

Add tests/FSharp.Data.GraphQL.Tests/AspNetCore/RequestHandlerExtensibilityTests.fs:
- RecordingHandler overrides ExecuteIntrospectionQuery/ExecuteOperation,
  records each call, then defers to the base implementation.
- SentinelHandler overrides only HandleAsync, to check HandleAsync
  itself is reached through a GraphQLRequestHandler-typed reference.
- createHandler wires a handler through the real AddGraphQL DI
  registration (ServiceCollection + AddGraphQL + CreateScope), against
  a DefaultHttpContext with a given method/body, using the existing
  AspNetCore.TestSchema. It resolves the handler as the base
  GraphQLRequestHandler<Root> type before calling HandleAsync, so a
  regression to non-virtual dispatch cannot pass unnoticed.
- Four tests cover: GET (introspection, no body), POST introspection
  query, POST operation query, and the HandleAsync override.

Verified the tests actually guard the contract: temporarily bypassed
the virtual ExecuteIntrospectionQuery call in HandleAsync (inlining
its body directly), which failed the two introspection tests with
"Expected: 1, Actual: 0" while leaving the operation and sentinel
tests green, then reverted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
xperiandri added a commit that referenced this pull request Sep 15, 2026
The Copilot review on PR #602 asked for coverage that HandleAsync
reaches ExecuteIntrospectionQuery/ExecuteOperation/HandleAsync through
virtual dispatch rather than closed-over let functions, so the
extensibility contract can't silently regress. No test in the repo
built an HttpContext or exercised the request handler.

Add tests/FSharp.Data.GraphQL.Tests/AspNetCore/RequestHandlerExtensibilityTests.fs:
- RecordingHandler overrides ExecuteIntrospectionQuery/ExecuteOperation,
  records each call, then defers to the base implementation.
- SentinelHandler overrides only HandleAsync, to check HandleAsync
  itself is reached through a GraphQLRequestHandler-typed reference.
- createHandler wires a handler through the real AddGraphQL DI
  registration (ServiceCollection + AddGraphQL + CreateScope), against
  a DefaultHttpContext with a given method/body, using the existing
  AspNetCore.TestSchema. It resolves the handler as the base
  GraphQLRequestHandler<Root> type before calling HandleAsync, so a
  regression to non-virtual dispatch cannot pass unnoticed.
- Four tests cover: GET (introspection, no body), POST introspection
  query, POST operation query, and the HandleAsync override.

Verified the tests actually guard the contract: temporarily bypassed
the virtual ExecuteIntrospectionQuery call in HandleAsync (inlining
its body directly), which failed the two introspection tests with
"Expected: 1, Actual: 0" while leaving the operation and sentinel
tests green, then reverted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the refactor/graphql-request-handler-extensibility branch from 4f7ccf1 to 9c02c9c Compare September 15, 2026 17:29
@xperiandri
xperiandri force-pushed the refactor/option-to-voption branch 2 times, most recently from 8217c3a to 3f5686a Compare September 15, 2026 17:51
xperiandri added a commit that referenced this pull request Sep 15, 2026
The Copilot review on PR #602 asked for coverage that HandleAsync
reaches ExecuteIntrospectionQuery/ExecuteOperation/HandleAsync through
virtual dispatch rather than closed-over let functions, so the
extensibility contract can't silently regress. No test in the repo
built an HttpContext or exercised the request handler.

Add tests/FSharp.Data.GraphQL.Tests/AspNetCore/RequestHandlerExtensibilityTests.fs:
- RecordingHandler overrides ExecuteIntrospectionQuery/ExecuteOperation,
  records each call, then defers to the base implementation.
- SentinelHandler overrides only HandleAsync, to check HandleAsync
  itself is reached through a GraphQLRequestHandler-typed reference.
- createHandler wires a handler through the real AddGraphQL DI
  registration (ServiceCollection + AddGraphQL + CreateScope), against
  a DefaultHttpContext with a given method/body, using the existing
  AspNetCore.TestSchema. It resolves the handler as the base
  GraphQLRequestHandler<Root> type before calling HandleAsync, so a
  regression to non-virtual dispatch cannot pass unnoticed.
- Four tests cover: GET (introspection, no body), POST introspection
  query, POST operation query, and the HandleAsync override.

Verified the tests actually guard the contract: temporarily bypassed
the virtual ExecuteIntrospectionQuery call in HandleAsync (inlining
its body directly), which failed the two introspection tests with
"Expected: 1, Actual: 0" while leaving the operation and sentinel
tests green, then reverted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the refactor/graphql-request-handler-extensibility branch from 9c02c9c to 6c9faa5 Compare September 15, 2026 17:51
Base automatically changed from refactor/option-to-voption to dev September 15, 2026 18:01
@xperiandri
xperiandri requested a lite review from Copilot September 15, 2026 18:01
xperiandri and others added 2 commits September 15, 2026 20:01
Turn the introspection/operation/dispatch steps of
GraphQLRequestHandler from private `let`-bound closures into
`abstract`/`default` members, so a derived handler can override a
single step (e.g. per-request introspection, or custom operation
execution) without reimplementing the whole class:

- `ExecuteIntrospectionQuery` and `ExecuteOperation` are now virtual,
  each resolving `options.CurrentValue.SchemaExecutor` itself instead
  of receiving it as a parameter (dropping the now-pointless generic
  `<'Root>` from `ExecuteOperation`, which used to shadow the class's
  own `'Root` without being constrained by it).
- `CheckOperationType` becomes a public member (was a private local
  function), with a `<remarks>` note that it consumes the request
  body, so calling it more than once on the same request fails to
  bind on the second call.
- `HandleAsync` is now virtual too, and dispatches to the other
  members through `handler` instead of closing over local functions.

Also move the class-level `<summary>` from the primary constructor's
doc comment to a `///` line on the type itself, and give the
constructor its own "Initializes a new instance..." comment - the
constructor comment documented the class, not the constructor.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Copilot review on PR #602 asked for coverage that HandleAsync
reaches ExecuteIntrospectionQuery/ExecuteOperation/HandleAsync through
virtual dispatch rather than closed-over let functions, so the
extensibility contract can't silently regress. No test in the repo
built an HttpContext or exercised the request handler.

Add tests/FSharp.Data.GraphQL.Tests/AspNetCore/RequestHandlerExtensibilityTests.fs:
- RecordingHandler overrides ExecuteIntrospectionQuery/ExecuteOperation,
  records each call, then defers to the base implementation.
- SentinelHandler overrides only HandleAsync, to check HandleAsync
  itself is reached through a GraphQLRequestHandler-typed reference.
- createHandler wires a handler through the real AddGraphQL DI
  registration (ServiceCollection + AddGraphQL + CreateScope), against
  a DefaultHttpContext with a given method/body, using the existing
  AspNetCore.TestSchema. It resolves the handler as the base
  GraphQLRequestHandler<Root> type before calling HandleAsync, so a
  regression to non-virtual dispatch cannot pass unnoticed.
- Four tests cover: GET (introspection, no body), POST introspection
  query, POST operation query, and the HandleAsync override.

Verified the tests actually guard the contract: temporarily bypassed
the virtual ExecuteIntrospectionQuery call in HandleAsync (inlining
its body directly), which failed the two introspection tests with
"Expected: 1, Actual: 0" while leaving the operation and sentinel
tests green, then reverted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@xperiandri
xperiandri force-pushed the refactor/graphql-request-handler-extensibility branch from 6c9faa5 to b5cc7d1 Compare September 15, 2026 18:01

Copilot AI 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.

🟢 Approval recommended

No unresolved blocking issues were identified.

Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@github-actions

Copy link
Copy Markdown

Test Results

    9 files      9 suites   11m 10s ⏱️
  710 tests   705 ✅  5 💤 0 ❌
2 130 runs  2 115 ✅ 15 💤 0 ❌

Results for commit b5cc7d1.

@xperiandri
xperiandri merged commit 5d608ce into dev Sep 15, 2026
5 checks passed
@xperiandri
xperiandri deleted the refactor/graphql-request-handler-extensibility branch September 15, 2026 18:13
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.

3 participants