Repository navigation
Idiomatic F# handler discovery: module-level functions, discriminated unions, and records #3007
jeremydmiller
started this conversation in
Ideas
Replies: 1 comment
|
Moving this to a discussion just to cheat and get to inbox 0 |
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Context
Today's convention-based handler discovery (
Wolverine/Configuration/HandlerDiscovery.cs) looks for instance methods namedHandle/Consume/Orchestrate/Start/ etc. on classes. That maps cleanly to C# idioms (a class + aHandle(...)method), but it's awkward for F# users, who prefer module-level functions, discriminated unions for commands, and records colocated with their handlers.Wolverine.Http already handles F# reasonably via attribute-driven routing —
[<WolverinePost("/foo")>] let myHandler() = ...insrc/Http/WolverineWebApiFSharp/Library.fsworks today. The gap is on the non-HTTP convention-based discovery side: messaging / mediator handlers in idiomatic F#.Goal
Treat three idiomatic F# shapes as first-class for handler discovery, alongside today's class-with-
Handle-method shape.The three shapes
1. Module-level handler functions
The most common F# idiom:
A F#
modulecompiles to a[CompilerGenerated] sealed abstractclass (i.e. astaticclass) withstaticmethods.HandlerDiscovery._validMethodsalready includes the right method names — the work is teaching the discovery layer to find them asstaticmethods onstaticclasses (today the method-match path is instance-method-oriented).Curried dependency parameters should bind via the existing IoC-resolution path (each curried parameter is just another argument in the compiled signature).
2. Discriminated unions as command messages
F# DUs compile to a base class + nested case classes. Two routing models worth considering, settled in the spike:
OrderCommand, handler does internalmatch. Lowest-friction, but every case shares a single handler chain and routing rule.(a) very likely works already if Wolverine can resolve the DU root as a concrete message type. (b) is the interesting one and the real ask.
3. Records as commands with colocated handlers
F# folks often colocate the command and its handler on the same type via
static member. Discovery should findstatic member handleon the record type so this organization style works without an external module.Out of scope (for this issue)
The bigger F# story — teaching every Wolverine
Framesubclass to emit F# adapter code — is intentionally deferred to a separate follow-up issue. #384 (already merged) shipped the F# emitter infrastructure at the JasperFx layer; the Wolverine-side frame audit + per-surface F# test projects + dedicated F# CI workflow will be planned in that follow-up. This issue is only about discovery.Existing F# footprint to consider
src/Http/WolverineWebApiFSharp/— proves attribute-driven F# HTTP discovery works. Worth reading for shape-of-the-thing reference; the messaging convention-discovery work in this issue should compose with it cleanly (an F# project mixing[<WolverinePost>]HTTP endpoints with module-level message handlers should Just Work).Open design questions (settle during the spike)
task/Task/Asyncreturns. F# handlers commonly returntask { ... }(Task/Task<'T>) but Wolverine should also accept idiomaticAsync<'T>returns and bridge to the task-based execution path. Worth deciding whetherAsync<'T>is supported in v1 or deferred.unitreturn ≡ C#void. Confirm the return-type inspection treatsMicrosoft.FSharp.Core.Unitas "no cascaded message," same asvoid/Task.let handle msg dep1 dep2 = ...compiles to a method with N parameters; tupledlet handle (msg, dep1, dep2) = ...compiles to a single tuple parameter. The first composes with Wolverine's argument injection; the second probably doesn't and should be explicitly rejected with a clear error.HandlerDiscoveryscans assemblies for concrete message types + their handlers. Module-class scanning may need a separate include rule (the_messageQuery.Excludes.IsStatic()line excludes static classes as messages; the handler-method side likely needs the inverse hint for methods).Acceptance criteria
let handle (msg: Foo) (deps...) = ...in an F# project is discovered as a handler forFoowithout any extra attributes.static member handle (msg: Foo) (deps...) = ...on an F# record type is discovered the same way.match).let handle (msg, dep) = ...either works or fails with a clear "use curried form" error.task { ... }returningTask/Task<'T>from an F# handler works for both fire-and-forget andInvokeAsync<T>.Microsoft.FSharp.Core.Unitreturn treated asvoid(no cascade).src/Http/WolverineWebApiFSharp/(or a new sibling) exercises each shape end-to-end through an in-process host./guide/handlers/(or similar) showing each F# shape with a working snippet.Scope keepers
Related
Framesubclass to emit F# code, phased, with per-surface F# test projects in a separate solution and a dedicated CI workflow).All reactions