Skip to content

F# codegen: HTTP route/query binding — string + typed (GH-2969)#2981

Merged
jeremydmiller merged 3 commits into
mainfrom
feat-2969-fsharp-http-typed-binding
May 29, 2026
Merged

F# codegen: HTTP route/query binding — string + typed (GH-2969)#2981
jeremydmiller merged 3 commits into
mainfrom
feat-2969-fsharp-http-typed-binding

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Extends ReadHttpFrame F# emit to typed (parsed) HTTP binding for method arguments, on top of the string binding.

⚠️ Includes the route/query string binding from #2980, which merged into the stacked #2979 branch and never reached main (the stacked-merge gotcha — base was the #2979 branch, not main). This PR re-lands that commit plus the new typed binding, both against main.

Typed binding

  • writeParsedValueFSharp: F# auto-tuples the C# out-parameter TryParse into match Type.TryParse(raw, InvariantCulture) with | true, v -> … | _ -> ….
    • Route value: null or parse-failure 404s + aborts; success binds the variable and renders the rest of the chain in the success arm (no F# early return).
    • Query/form/header: binds the parsed value, or Unchecked.defaultof<T> on failure, then continues.
  • Nullable/optional and enum parsed binding still throw NotSupported (deferred).

Generated F#

// GET /fsharp/things/{id}/items/{count}  (string + int route values)
let count_rawValue = (httpContext.GetRouteValue("count") :?> string)
if isNull count_rawValue then
    httpContext.Response.StatusCode <- 404
    ()
else
    match System.Int32.TryParse(count_rawValue, System.Globalization.CultureInfo.InvariantCulture) with
    | true, count -> ... thingEndpoints.GetItems(id, count) ...
    | _ -> httpContext.Response.StatusCode <- 404; ()

// GET /fsharp/paged?page=  (int query)
let page = match System.Int32.TryParse(page_rawValue, ...) with | true, v -> v | _ -> Unchecked.defaultof<int>

Verification

  • Fixture renders + compiles GET /things/{id} (string route), /search (string query), /things/{id}/items/{count} (string + int route), /paged (int query), plus the existing string/JSON endpoints.
  • Both F# gates green; full wolverine.slnx Release regression — 0 warnings, 0 errors.

Part of #2969.

🤖 Generated with Claude Code

jeremydmiller and others added 3 commits May 29, 2026 15:03
Phase C long tail: teach ReadHttpFrame to emit F# for string binding to a
method argument — route values and query string.

- ReadHttpFrame.GenerateFSharpCode covers the string path:
  - route value -> `let id = (httpContext.GetRouteValue("id") :?> string)`, with
    the required-value 404 guard rendered as `if isNull id then (set 404; ())
    else <rest of chain>` (F# has no early return, so Next renders in the else).
  - query string -> `let q = httpContext.Request.Query.["q"].ToString()`.
  - header/form string sources via the static ReadSingleHeaderValue / Form lookup.
- Parsed (non-string, out-var TryParse) binding and property/AsParameters binding
  throw a clear NotSupportedException for now (deferred).
- Fixture: GET /things/{id} (route) and GET /search (query) render + compile.

fsharp-coverage with Wolverine.Http loaded: 29 implemented / 2 skipped /
50 remaining (ReadHttpFrame now implemented).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends ReadHttpFrame's F# emit to parsed (non-string) binding for a method
argument, building on the string binding.

- writeParsedValueFSharp: F# auto-tuples the C# out-parameter TryParse into a
  `match Type.TryParse(raw, InvariantCulture) with | true, v -> ... | _ -> ...`.
  - Route value: null or parse-failure 404s + aborts; success binds the variable
    and renders the rest of the chain in the success arm (no F# early return).
  - Query/form/header: binds the parsed value or Unchecked.defaultof<T> on
    failure, then continues.
- Nullable/optional and enum parsed binding still throw NotSupported (deferred).
- Fixture: GET /things/{id}/items/{count} (string + int route values) and
  GET /paged?page= (int query) render + compile.

NOTE: this branch also carries the route/query *string* binding commit (PR #2980)
that merged into the stacked #2979 branch and never reached main; re-landing it
here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The fsharp workflow ran `dotnet test wolverine_fsharp.slnx`, which executes the
Core and Http gate test assemblies in parallel. Each gate shells a nested
`dotnet build` of its F# fixture, and both fixtures transitively build Debug
Wolverine.dll — concurrent nested builds raced on
obj/Debug/net9.0/refint/Wolverine.dll (MSB3883 / IOException "being used by
another process"), failing whichever gate lost the race.

- Run the two surface gates as separate, sequential `dotnet test` steps so their
  nested fixture builds never overlap.
- Defense-in-depth: extend each gate's one-time retry to also cover the file-lock
  signature ("being used by another process" / MSB3883), alongside the existing
  FS0193 retry.

Verified: both gates pass when run sequentially.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jeremydmiller
jeremydmiller merged commit 3bd824e into main May 29, 2026
24 checks passed
This was referenced Jun 1, 2026
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