Repository: JasperFx/wolverine (2026-05-29T23:40:32Z)

Issue:
F# code generation audit: teach every Frame subclass to emit F# (phased)
## Context

JasperFx#384 (merged) shipped the F# emitter **infrastructure** at the JasperFx layer:

- `FSharpSourceWriter` / `Frame.GenerateFSharpCode(...)` (virtual, default-throws `NotSupportedException` naming the frame)
- `Variable.FSharpAssignmentUsage` + `Mutable` flag, `Type.FSharpName()`
- Outer-emit machinery: `GeneratedAssembly.GenerateFSharpCode`, `GeneratedType.WriteFSharp`, `GeneratedMethod.WriteFSharpMethod`
- The async-mode matrix (`task { }` vs. direct vs. `Task.CompletedTask`)
- F# emit for a **small core set** of JasperFx-layer frames (`CommentFrame`, `CodeFrame`, `ConstructorFrame`, `MethodCall`, `ReturnFrame`, `CompositeFrame`, `IfBlock`, `IfElseNullGuardFrame`, `IfNullGuardFrame`, `TryFinallyWrapperFrame`)
- A `codegen-fsharp` command + a `CodegenTests.FSharp` integration test that regenerates fixtures and `dotnet build`s them

The PR's own next-step note: *"Generics-heavy frames and the full Wolverine frame set — to be exercised against Wolverine via a local project reference before any nuget."* That is this issue.

`grep -rln ": SyncFrame|: AsyncFrame|: Frame\b" src/` returns **136 candidate Frame files** under `src/`. That's the audit scope.

## Goal

Every `Frame` subclass in the Wolverine codebase either:

1. Overrides `GenerateFSharpCode(...)` to emit valid F# (the common case), or
2. Is explicitly marked **"not applicable in F#"** with a recorded reason (a small minority — anything tied to constructs that have no idiomatic F# mapping).

Net result: a Wolverine application written entirely in F# (handlers, sagas, HTTP endpoints, gRPC services) can use Wolverine's static codegen path with `--language fsharp` (or equivalent) and ship to production.

## Phasing (each phase its own PR, all referencing this issue)

Phases are sequenced so the **smallest viable F# Wolverine app** ships first, then surface-by-surface expansion.

### Phase A — Core handler frames *(milestone 1 of the audit; smallest set that lets an F# `let handle msg = …` work end-to-end)*

Roughly 20–30 frames. Includes: `HandlerContinuationFrame`, `MessageContextFrame`, `ReadEnvelopeHeaderFrame`, the cascade-capture frames driven by `CascadingMessageActionSource`, `SimpleValidationHandlerFrame`, `RequirementResultHandlerFrame`, and the small surrounding plumbing. *(Exact list compiled at start-of-phase; the criterion is "what's needed for an in-process message handler with simple validation and cascading to work end-to-end.")*

**Exit criterion**: the Phase A F# fixture project compiles + its behavioural tests pass under the new CI workflow.

### Phase B — Sagas + persistence

The `Wolverine/Persistence/Sagas/*Frame` set (`LoadSagaOperation`, `SetSagaIdFrame`, `SetSagaIdFromSagaFrame`, `AssertSagaStateExistsFrame`, `CreateNewSagaFrame`), plus `ThrowRequiredDataMissingExceptionFrame`, `ConstructSpecificationFrame`, and the Polecat / Marten data-requirement frames.

### Phase C — Wolverine.Http

Request binding (form / query / header / route value frames), response writing, JSON handling (`JsonHandling.*`), `MaybeEndWithResultFrame`, `WriteProblemDetailsIfNull`, `SimpleValidationHttpFrame`, `RequirementResultHttpFrame`, `MaybeEndHandlerWithProblemDetailsFrame`, `ProblemDetailsContinuationPolicy`, plus the existing attribute-driven F# HTTP discovery surface (composing with #2968).

### Phase D — Wolverine.Grpc

The gRPC-side frame set, including whatever new frames the inline-request/reply work for #2967 introduces (likely none, since that work reuses `HandlerChain.UseForResponse`).

### Phase E — Long tail

Anything not covered above — transport-specific frames, less-trafficked outbox frames, edge cases. Closes out the audit; at this point the "implemented / skipped-with-reason / remaining" tally hits zero remaining.

## Test-solution structure

**One separate solution** — `wolverine_fsharp.slnx` at the repo root, **not** in `wolverine.slnx` — with **one F# project per surface** (per Jeremy: too many projects in the main solution already):

```
wolverine_fsharp.slnx
├── Wolverine.Core.FSharpTests        (Phase A)
├── Wolverine.Persistence.FSharpTests (Phase B — or per-store: .Marten.FSharpTests, .Polecat.FSharpTests, .EfCore.FSharpTests as the surface grows)
├── Wolverine.Http.FSharpTests        (Phase C)
├── Wolverine.Grpc.FSharpTests        (Phase D)
└── (Phase E adds as needed)
```

Each project mirrors the JasperFx#384 fixture pattern: an integration test that (a) regenerates the F# adapter code via the `codegen-fsharp` command, (b) `dotnet build`s the resulting F# fixture, and (c) runs behavioural tests against the generated handlers/endpoints. Phase A's project also serves as the reference shape for later phases.

The existing `src/Http/WolverineWebApiFSharp/` project stays where it is (it's a F# *consumer* of Wolverine.Http, not a codegen fixture); the per-surface fixtures above are new.

## CI workflow

New `.github/workflows/fsharp.yml`:

- **Trigger**: PRs that touch `src/Wolverine/**`, `src/Http/**`, `src/Persistence/**`, or any file matching `**/*Frame.cs`. Also `wolverine_fsharp.slnx` and the per-surface F# projects.
- **Steps**: build `wolverine_fsharp.slnx`, regenerate F# adapters via `codegen-fsharp`, `dotnet test wolverine_fsharp.slnx`.
- **Status**: required-for-merge **on the paths above**; advisory elsewhere (the workflow simply isn't triggered for irrelevant changes). Keeps cycle time honest without leaving regressions undetectable.
- **Retry policy**: mirror JasperFx#384's one-retry for the transient FS0193 internal-compiler crash; a real error is deterministic and still fails.

## "Not applicable in F#" policy

Frames whose semantics have no idiomatic F# emit (anything tied to constructs F# doesn't expose well — pickled examples: `ref` returns, `out` parameters, expression-tree synthesis) get an explicit marker rather than a permanent default-throw:

```csharp
[FSharpEmit(Skip = true, Reason = "Uses out parameter; no F# equivalent")]
internal class SomeFrame : SyncFrame { ... }
```

(Exact attribute name / pattern TBD; could equally be a `protected override bool IsFSharpApplicable => false;` style override. Pick the lower-ceremony option during Phase A.)

This lets the audit track three buckets:

- **Implemented**: emits valid F#, exercised by the fixture for its phase.
- **Intentionally skipped**: marked with reason; never throws, never blocks generation (the code path that would have hit it just isn't reachable from an F# host).
- **Remaining**: defaults to throwing `NotSupportedException` (the JasperFx#384 seam) — these are the open audit items.

Audit dashboard: a one-line CLI under `wolverine-diagnostics` (e.g. `wolverine-diagnostics fsharp-coverage`) that reflects across the loaded Wolverine assemblies and prints the three counts + the "remaining" list. Cheap to write, valuable for tracking progress.

## Scope keepers

- **Stay in Wolverine.** Implement F# emit directly on Wolverine-side frames. If a phase is blocked by a missing F# implementation on a *JasperFx-layer* frame, file the targeted gap as a JasperFx issue and continue with the rest of the phase.
- **Anything that turns out to be general** (F# type-inspection helpers, common emit utilities, …) can be lifted to JasperFx in a later cleanup pass — not a goal of this issue.
- **No carry-forward** from JasperFx#383 / #384's "still deferred" list (constructor property setters, value tuples, ActivityEvents, `Task<T>` from synchronous bodies). Those stay JasperFx-side concerns; address them only if a specific Wolverine phase actually hits them.

## Acceptance criteria

**Per phase**
- [ ] The phase's frame list is enumerated and PR-linked to this issue.
- [ ] Every frame in the phase either emits F# or carries the "not applicable" marker.
- [ ] The phase's F# fixture project compiles + its behavioural tests pass under the new CI workflow.

**Overall**
- [ ] `wolverine_fsharp.slnx` exists with one F# project per surface.
- [ ] `.github/workflows/fsharp.yml` exists and is required-for-merge on PRs that touch the relevant paths.
- [ ] `wolverine-diagnostics fsharp-coverage` reports zero "remaining" frames at audit completion.
- [ ] A small F# sample under `src/Samples/` (or alongside an existing sample) demonstrates an end-to-end F# Wolverine app using the static codegen path.
- [ ] Documentation: a guide page (e.g. `/guide/codegen.html#f-code-generation`) explains how to use `--language fsharp`, the static-codegen-only constraint, and the expected build setup.

## Open questions (to settle in Phase A)

1. **Attribute vs. override** for the "not applicable" marker. Lean lower-ceremony.
2. **CLI flag shape** for selecting F#: `codegen write --language fsharp`? `codegen-fsharp write`? Match whatever JasperFx#384's `codegen-fsharp` command already established.
3. **Per-surface persistence projects** in Phase B: one `Wolverine.Persistence.FSharpTests` covering all stores, or one per store (`.Marten`, `.Polecat`, `.EfCore`, …)? Probably depends on how much store-specific frame surface there actually is once Phase B starts.

## Related

- Sibling: #2968 — Idiomatic F# handler discovery. Discovery and codegen are independent axes and can proceed in parallel; F# discovery shapes (module-level `let handle`, DUs as commands, records-with-`handle`) need to compose with whatever codegen emits.
- Context only: JasperFx#383 (umbrella), JasperFx#384 (emitter infrastructure, already merged). Per scope-keepers above, no items are carried forward from those.
- The HTTP/gRPC inline-request/reply work in #2966 / #2967 doesn't introduce new frames (it reuses `HandlerChain.UseForResponse`), so it shouldn't add to the Phase C/D audit list — but worth double-checking when those land.



Discussion:
## F# code-generation audit — complete ✅

Every Wolverine `Frame` subclass an F# application realistically hits now emits idiomatic F#, an F# app can ship entirely on **pre-generated** code (no runtime Roslyn), and the whole story is documented and run-verified.

### What shipped

**Core audit (frames → F#)**
- **Foundation** — the `fsharp-coverage` diagnostic, the `[FSharpEmit]` skip marker, `FSharpEmitHelpers`, the no-host render harness, and `wolverine_fsharp.slnx` + the `fsharp` CI workflow.
- **Phase A — handlers** — message cast, OTel tagging, the mid-chain abort-guard trio (`HandlerContinuationFrame`, `SimpleValidationHandlerFrame`, `RequirementResultHandlerFrame`).
- **Phase B — sagas** — create/resolve/store/delete + saga-id resolution frames.
- **Phase C — HTTP** — string + typed route/query binding, JSON request/response, terminal `IResult`.

**Store/middleware slices** (each a runnable F# sample + a compile-gate)
- **EF Core** — #2983 (`EnrollDbContextInTransaction`, `CommitEfCoreEnvelopeTransaction`)
- **Marten documents** — #2984 (`OpenMartenSessionFrame`, `CreateDocumentSessionFrame`)
- **Marten event sourcing** — #2985 (`LoadAggregateFrame`/`FetchForWriting`, `TagAggregateOtelFrame`, `MissingAggregateCheckFrame`)
- **FluentValidation + CosmosDB** — #2986 (`TransactionalFrame`; FluentValidation + the `ICosmosDbOp` side effect were already F#)

**Run-verification**
- **Behavioural run-step** — #2987 — boots a real host in `TypeLoadMode.Static` against a pre-generated F# adapter and asserts the handler executes. Upgrades the guarantee from "compiles" to "compiles **and runs**."

**Docs**
- **"Using Wolverine with F#" tutorial** — #2988 (this PR) — handler authoring, what Wolverine emits as F#, running on pre-generated code, and the per-integration examples, all via live mdsnippets from the samples.

### Upstream JasperFx releases enabling the above
- **2.2.4** — named `this` self-identifier for generated F# members (`#393`/`#394`)
- **2.2.5** — `Variable.FSharpUsage` / F#-aware `CastVariable` (`#395`/`#396`)
- **2.2.6** — scoped-DI frames (`ScopedContainerCreation`, `GetServiceFromScopedContainerFrame`) (`#397`/`#398`)
- **2.2.7** — the service-location frame surface: `LazyServiceLocationFrame`, `AppendActivityEventFrame`, `ReturnValueTask`, `NowFetchFrame`, `CreateArrayFrame`, `MemberAccessFrame` (`#399`/`#400`)

### Notable F# specifics captured along the way
- `reraise()` is illegal inside a computation-expression `try/with` → use `ExceptionDispatchInfo.Capture(ex).Throw()` (EF Core outbox).
- Marten convention aggregation (`Create`/`Apply`) is dispatched by the **C#-only** `JasperFx.Events` source generator → F# aggregates override `Evolve` directly.
- F# auto-converts `RuleFor` lambdas to LINQ expression trees, so F# `AbstractValidator`s work as-is.
- Dynamic codegen needs `WolverineFx.RuntimeCompilation` + `UseRuntimeCompilation()` (#2876); pre-generated/static apps don't.
- `// begin-snippet:` markers (not `#region`) work for mdsnippets in `.fs` files — `.fs` is scanned by default.

### Intentionally deferred (optional, non-blocking)
Richer HTTP binding edge cases (property/`AsParameters`, enum/nullable parsed, content-type, form, problem-details), additional transports/stores (RabbitMQ, Azure Service Bus, SignalR, RavenDb, SQL Server), `TemplateFrame` (arbitrary C# template body — left throwing by design), and a couple of narrow Marten paths (`IQuerySession`-alias cast, the batched aggregate load).

Closing via #2988.



Patch:
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
index 6aaab6b39..6f05ee215 100644
--- a/docs/.vitepress/config.mts
+++ b/docs/.vitepress/config.mts
@@ -111,6 +111,7 @@ const config: UserConfig<DefaultTheme.Config> = {
                         {text: 'Modular Monoliths', link: '/tutorials/modular-monolith'},
                         {text: 'Event Sourcing and CQRS with Marten', link: '/tutorials/cqrs-with-marten'},
                         {text: 'Event Sourcing and CQRS with Polecat', link: '/tutorials/cqrs-with-polecat'},
+                        {text: 'Using Wolverine with F#', link: '/tutorials/fsharp'},
                         {text: 'Railway Programming with Wolverine', link: '/tutorials/railway-programming'},
                         {text: 'Interoperability with Non-Wolverine Systems', link: '/tutorials/interop'},
                         {text: 'Leader Election and Agents', link: '/tutorials/leader-election'},
diff --git a/docs/tutorials/fsharp.md b/docs/tutorials/fsharp.md
new file mode 100644
index 000000000..6a49d26a3
--- /dev/null
+++ b/docs/tutorials/fsharp.md
@@ -0,0 +1,276 @@
+# Using Wolverine with F#
+
+Wolverine works with F#, not just C#. You can author message handlers as F# functions/methods, use the
+EF Core, Marten, CosmosDB, and FluentValidation integrations, and — the focus of this tutorial — ship an
+F# application that runs entirely on **pre-generated F# code** with no runtime code generation.
+
+::: tip
+All of the F# in this tutorial comes from runnable samples under
+[`src/Samples`](https://github.com/JasperFx/wolverine/tree/main/src/Samples) (the `Wolverine*FSharpSample`
+projects) and the behavioural test under
+[`src/Testing/Wolverine.Behavioural.FSharp*`](https://github.com/JasperFx/wolverine/tree/main/src/Testing).
+:::
+
+## How Wolverine code generation works with F#
+
+For every message handler, Wolverine generates a small adapter class (a `MessageHandler`) that pulls the
+message off the envelope, resolves dependencies, runs your handler, applies middleware (transactions,
+validation, the outbox, OpenTelemetry tagging, cascading messages…), and saves changes. By default that
+adapter is generated and compiled **at startup** with Roslyn (`TypeLoadMode.Dynamic`). Wolverine can also
+emit that adapter as **F#** and load it from your already-compiled assembly at runtime
+(`TypeLoadMode.Static`) — so an F# app can ship with zero runtime compilation.
+
+There are therefore two ways to run an F# Wolverine app:
+
+1. **Dynamic code generation** — quickest to get going; needs the Roslyn runtime compiler.
+2. **Pre-generated (static) code** — commit the generated F# and load it under `TypeLoadMode.Static`.
+
+## Writing a handler in F#
+
+A handler is just a type with a `Handle`/`Consume` method. Here is an EF Core transactional handler — it
+takes the command and an injected `DbContext`, writes an entity, and returns an event that Wolverine
+cascades as an outgoing message:
+
+<!-- snippet: sample_fsharp_efcore_handler -->
+<a id='snippet-sample_fsharp_efcore_handler'></a>
+```fs
+type CreateItemHandler =
+    [<Transactional>]
+    static member Handle(command: CreateItemCommand, db: ItemsDbContext) : ItemCreated =
+        let item = { Id = Guid.NewGuid(); Name = command.Name }
+        db.Items.Add(item) |> ignore
+        { Id = item.Id }
+```
+<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Samples/WolverineFSharpSample/Handlers.fs#L10-L17' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_fsharp_efcore_handler' title='Start of snippet'>anchor</a></sup>
+<!-- endSnippet -->
+
+Records work well for messages and events, and `[<CLIMutable>]` gives EF Core / Marten / CosmosDB the
+parameterless constructor and settable properties they need for entities and documents.
+
+Bootstrapping looks just like C#, through the `UseWolverine` lambda:
+
+<!-- snippet: sample_fsharp_efcore_bootstrap -->
+<a id='snippet-sample_fsharp_efcore_bootstrap'></a>
+```fs
+let host =
+    Host
+        .CreateDefaultBuilder(args)
+        .UseWolverine(fun opts ->
+            // Register the DbContext with Wolverine's EF Core outbox integration.
+            opts.Services.AddDbContextWithWolverineIntegration<ItemsDbContext>(fun o ->
+                o.UseNpgsql(connectionString) |> ignore)
+            |> ignore
+
+            // Durable message store backing the transactional outbox.
+            opts.PersistMessagesWithPostgresql(connectionString) |> ignore
+
+            opts.UseEntityFrameworkCoreTransactions() |> ignore
+            opts.Policies.AutoApplyTransactions() |> ignore
+            opts.Discovery.IncludeType<CreateItemHandler>() |> ignore
+
+            // Core Wolverine dropped the in-box Roslyn compiler (GH-2876); enable it so this demo
+            // runs via dynamic codegen. (The static F# story is proven by the compile-gate test.)
+            opts.UseRuntimeCompilation() |> ignore)
+        // Provision the Wolverine message-store tables on startup.
+        .UseResourceSetupOnStartup()
+        .Build()
+```
+<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Samples/WolverineFSharpSample/Program.fs#L23-L46' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_fsharp_efcore_bootstrap' title='Start of snippet'>anchor</a></sup>
+<!-- endSnippet -->
+
+::: warning Dynamic code generation needs the runtime compiler
+Core Wolverine no longer ships the Roslyn compiler ([GH-2876](https://github.com/JasperFx/wolverine/issues/2876)).
+To run via **dynamic** code generation, reference the `WolverineFx.RuntimeCompilation` package and call
+`opts.UseRuntimeCompilation()` as shown above. Apps that run on pre-generated code (below) do not need it.
+:::
+
+## What Wolverine generates — as F#
+
+Take this minimal F# handler:
+
+<!-- snippet: sample_fsharp_behavioural_handler -->
+<a id='snippet-sample_fsharp_behavioural_handler'></a>
+```fs
+type BehaviouralPingHandler =
+    static member Handle(ping: BehaviouralPing) = BehaviouralSink.record ping.Value
+```
+<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Testing/Wolverine.Behavioural.FSharpApp/Domain.fs#L25-L28' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_fsharp_behavioural_handler' title='Start of snippet'>anchor</a></sup>
+<!-- endSnippet -->
+
+Wolverine emits the following F# adapter for it. Notice the idiomatic F#: the message downcast with `:?>`,
+the `task { }` computation expression, the OpenTelemetry guard (F# has no `?.`), and the trailing
+`Task.CompletedTask`:
+
+```fsharp
+namespace Internal.Generated.WolverineHandlers
+
+open System
+open System.Threading
+open System.Threading.Tasks
+open Wolverine.Runtime
+open Wolverine.Runtime.Handlers
+
+type BehaviouralPingHandler1244766258() =
+    inherit Wolverine.Runtime.Handlers.MessageHandler()
+    override this.HandleAsync(context: Wolverine.Runtime.MessageContext, cancellation: System.Threading.CancellationToken) : System.Threading.Tasks.Task =
+        // The actual message body
+        let behaviouralPing = context.Envelope.Message :?> WolverineBehaviouralFSharpApp.BehaviouralPing
+
+        if not (isNull System.Diagnostics.Activity.Current) then
+            System.Diagnostics.Activity.Current.SetTag("message.handler", "WolverineBehaviouralFSharpApp.BehaviouralPingHandler") |> ignore
+            System.Diagnostics.Activity.Current.SetTag("handler.type", "WolverineBehaviouralFSharpApp.BehaviouralPingHandler") |> ignore
+
+        // The actual message execution
+        WolverineBehaviouralFSharpApp.BehaviouralPingHandler.Handle(behaviouralPing)
+
+        System.Threading.Tasks.Task.CompletedTask
+```
+
+## Running on pre-generated F# code
+
+To ship an F# app that runs on pre-generated code rather than compiling at startup:
+
+1. Generate the F# adapters and commit them into your application (for example as a `Generated.fs`
+   compiled into the app assembly). The generated type names are deterministic for a given handler graph.
+2. Boot the host in `TypeLoadMode.Static` and point Wolverine's `ApplicationAssembly` at the assembly that
+   contains the pre-generated F# — Wolverine then loads each handler adapter **by name** out of that
+   assembly's exported types, with no Roslyn at runtime:
+
+<!-- snippet: sample_fsharp_static_host -->
+<a id='snippet-sample_fsharp_static_host'></a>
+```cs
+var appAssembly = typeof(BehaviouralPingHandler).Assembly;
+
+using var host = await Host.CreateDefaultBuilder()
+    .UseWolverine(opts =>
+    {
+        // Shared with the generation step so the generated type-name hash matches:
+        // DisableConventionalDiscovery() + IncludeType<BehaviouralPingHandler>().
+        BehaviouralCodegen.Configure(opts);
+
+        // Load the pre-generated F# handler adapter out of the app assembly instead of
+        // compiling at runtime. Setting ApplicationAssembly (which cascades to
+        // CodeGeneration.ApplicationAssembly) pins the assembly Wolverine scans for pre-built
+        // types to the F# app, and TypeLoadMode.Static means no Roslyn at runtime.
+        opts.ApplicationAssembly = appAssembly;
+        opts.CodeGeneration.TypeLoadMode = TypeLoadMode.Static;
+    })
+    .StartAsync();
+
+// The pre-generated F# MessageHandler is loaded by name and executed — no runtime compilation.
+var bus = host.MessageBus();
+await bus.InvokeAsync(new BehaviouralPing(42));
+```
+<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Testing/Wolverine.Behavioural.FSharpTests/BehaviouralRunStep.cs#L33-L55' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_fsharp_static_host' title='Start of snippet'>anchor</a></sup>
+<!-- endSnippet -->
+
+::: tip
+Setting `opts.ApplicationAssembly` cascades to `opts.CodeGeneration.ApplicationAssembly` and pins the
+assembly Wolverine scans for pre-built types. If the committed F# drifts from the configured handler graph
+the generated type name won't match and the host throws `ExpectedTypeMissingException` at startup — a loud,
+useful signal to regenerate.
+:::
+
+## Persistence and middleware in F#
+
+The same handler authoring style works across Wolverine's integrations. Each of these is exercised by a
+runnable sample and a code-generation compile gate.
+
+### Marten documents
+
+A `[<Transactional>]` handler that stores a document through an injected `IDocumentSession`:
+
+<!-- snippet: sample_fsharp_marten_document_handler -->
+<a id='snippet-sample_fsharp_marten_document_handler'></a>
+```fs
+type CreateProductHandler =
+    [<Transactional>]
+    static member Handle(command: CreateProductCommand, session: IDocumentSession) : ProductCreated =
+        let product = { Id = Guid.NewGuid(); Name = command.Name }
+        session.Store<Product>(product)
+        { Id = product.Id }
+```
+<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Samples/WolverineMartenFSharpSample/Handlers.fs#L11-L18' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_fsharp_marten_document_handler' title='Start of snippet'>anchor</a></sup>
+<!-- endSnippet -->
+
+### Marten event sourcing
+
+An `[<AggregateHandler>]` receives the loaded aggregate (via `FetchForWriting`) plus the command and returns
+the event(s) to append:
+
+<!-- snippet: sample_fsharp_aggregate_handler -->
+<a id='snippet-sample_fsharp_aggregate_handler'></a>
+```fs
+type IncrementHandler =
+    [<AggregateHandler>]
+    static member Handle(command: IncrementCounter, counter: Counter) : Incremented =
+        { By = command.By }
+```
+<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Samples/WolverineMartenAggregateFSharpSample/Handlers.fs#L9-L14' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_fsharp_aggregate_handler' title='Start of snippet'>anchor</a></sup>
+<!-- endSnippet -->
+
+::: warning F# aggregates: override `Evolve`
+Marten's convention-based aggregation (`Create`/`Apply` methods) is dispatched by the **C#-only**
+`JasperFx.Events` source generator, which does not run for F# assemblies. For an F# aggregate, override
+`Evolve` directly — an explicit per-event fold — instead of using convention methods:
+:::
+
+<!-- snippet: sample_fsharp_aggregate_projection -->
+<a id='snippet-sample_fsharp_aggregate_projection'></a>
+```fs
+type CounterProjection() =
+    inherit SingleStreamProjection<Counter, Guid>()
+
+    override _.Evolve(snapshot: Counter, _id: Guid, e: IEvent) : Counter =
+        match e.Data with
+        | :? CounterStarted as started -> { Id = started.Id; Count = 0 }
+        | :? Incremented as inc -> { snapshot with Count = snapshot.Count + inc.By }
+        | _ -> snapshot
+```
+<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Samples/WolverineMartenAggregateFSharpSample/Domain.fs#L19-L28' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_fsharp_aggregate_projection' title='Start of snippet'>anchor</a></sup>
+<!-- endSnippet -->
+
+### FluentValidation
+
+Register `opts.UseFluentValidation()` and a validator; the validation middleware runs before the handler.
+F# automatically converts the property-selector lambdas (`fun x -> x.Name`) into the LINQ expression trees
+`RuleFor` expects, so an F# `AbstractValidator` reads naturally:
+
+<!-- snippet: sample_fsharp_fluentvalidation_validator -->
+<a id='snippet-sample_fsharp_fluentvalidation_validator'></a>
+```fs
+type CreateThingValidator() as self =
+    inherit AbstractValidator<CreateThing>()
+
+    do
+        self.RuleFor(fun x -> x.Id).NotEmpty() |> ignore
+        self.RuleFor(fun x -> x.Name).NotEmpty() |> ignore
+```
+<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Samples/WolverineCosmosFSharpSample/Domain.fs#L18-L25' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_fsharp_fluentvalidation_validator' title='Start of snippet'>anchor</a></sup>
+<!-- endSnippet -->
+
+### CosmosDB
+
+A `[<Transactional>]` handler that returns an `ICosmosDbOp` side effect; Wolverine applies it inside the
+CosmosDB outbox transaction:
+
+<!-- snippet: sample_fsharp_cosmos_handler -->
+<a id='snippet-sample_fsharp_cosmos_handler'></a>
+```fs
+type CreateThingHandler =
+    [<Transactional>]
+    static member Handle(command: CreateThing) : ICosmosDbOp =
+        CosmosDbOps.Store<Thing>({ id = command.Id; Name = command.Name })
+```
+<sup><a href='https://github.com/JasperFx/wolverine/blob/main/src/Samples/WolverineCosmosFSharpSample/Handlers.fs#L10-L15' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_fsharp_cosmos_handler' title='Start of snippet'>anchor</a></sup>
+<!-- endSnippet -->
+
+## F# gotchas, summarized
+
+- **Dynamic codegen** needs `WolverineFx.RuntimeCompilation` + `opts.UseRuntimeCompilation()` (GH-2876).
+  Pre-generated/static apps don't.
+- **Entities & documents**: use `[<CLIMutable>]` records so the persistence tooling can construct/populate them.
+- **Marten aggregates**: override `Evolve` rather than using convention `Create`/`Apply` (the source
+  generator is C#-only).
+- **FluentValidation**: F# auto-quotes `RuleFor` lambdas to expression trees — no special handling needed.
+- **Static loading**: set `opts.ApplicationAssembly` to the assembly carrying the committed F# adapters.
diff --git a/src/Samples/WolverineCosmosFSharpSample/Domain.fs b/src/Samples/WolverineCosmosFSharpSample/Domain.fs
index 70597e675..743e0da4a 100644
--- a/src/Samples/WolverineCosmosFSharpSample/Domain.fs
+++ b/src/Samples/WolverineCosmosFSharpSample/Domain.fs
@@ -15,9 +15,11 @@ type ThingCreated = { Id: string }
 /// FluentValidation validator for CreateThing. Wolverine's FluentValidation middleware runs this
 /// before the handler (and short-circuits with a failure result if invalid). F# auto-converts the
 /// property-selector lambdas to the LINQ expression trees RuleFor expects.
+// begin-snippet: sample_fsharp_fluentvalidation_validator
 type CreateThingValidator() as self =
     inherit AbstractValidator<CreateThing>()
 
     do
         self.RuleFor(fun x -> x.Id).NotEmpty() |> ignore
         self.RuleFor(fun x -> x.Name).NotEmpty() |> ignore
+// end-snippet
diff --git a/src/Samples/WolverineCosmosFSharpSample/Handlers.fs b/src/Samples/WolverineCosmosFSharpSample/Handlers.fs
index 8d8318d76..2ca90c143 100644
--- a/src/Samples/WolverineCosmosFSharpSample/Handlers.fs
+++ b/src/Samples/WolverineCosmosFSharpSample/Handlers.fs
@@ -7,7 +7,9 @@ open Wolverine.CosmosDb
 ///  * FluentValidation — the CreateThingValidator runs before this method (validation middleware).
 ///  * CosmosDB persistence — [<Transactional>] enlists the CosmosDB outbox, and the returned
 ///    ICosmosDbOp side effect (CosmosDbOps.Store) is applied within that transaction.
+// begin-snippet: sample_fsharp_cosmos_handler
 type CreateThingHandler =
     [<Transactional>]
     static member Handle(command: CreateThing) : ICosmosDbOp =
         CosmosDbOps.Store<Thing>({ id = command.Id; Name = command.Name })
+// end-snippet
diff --git a/src/Samples/WolverineFSharpSample/Handlers.fs b/src/Samples/WolverineFSharpSample/Handlers.fs
index c58c9adbb..eccfc82c6 100644
--- a/src/Samples/WolverineFSharpSample/Handlers.fs
+++ b/src/Samples/WolverineFSharpSample/Handlers.fs
@@ -7,9 +7,11 @@ open Wolverine.Attributes
 /// [<Transactional>], the command + injected DbContext, add an entity, and return an event that
 /// Wolverine sends as a cascading message. Wolverine's EF Core middleware wraps this in the outbox
 /// transaction (enroll -> SaveChanges -> commit), all of which the F# codegen now emits.
+// begin-snippet: sample_fsharp_efcore_handler
 type CreateItemHandler =
     [<Transactional>]
     static member Handle(command: CreateItemCommand, db: ItemsDbContext) : ItemCreated =
         let item = { Id = Guid.NewGuid(); Name = command.Name }
         db.Items.Add(item) |> ignore
         { Id = item.Id }
+// end-snippet
diff --git a/src/Samples/WolverineFSharpSample/Program.fs b/src/Samples/WolverineFSharpSample/Program.fs
index 176b42988..5baf654c4 100644
--- a/src/Samples/WolverineFSharpSample/Program.fs
+++ b/src/Samples/WolverineFSharpSample/Program.fs
@@ -20,6 +20,7 @@ let connectionString =
 
 [<EntryPoint>]
 let main args =
+    // begin-snippet: sample_fsharp_efcore_bootstrap
     let host =
         Host
             .CreateDefaultBuilder(args)
@@ -42,6 +43,7 @@ let main args =
             // Provision the Wolverine message-store tables on startup.
             .UseResourceSetupOnStartup()
             .Build()
+    // end-snippet
 
     // Create the database + the DbContext's Item table BEFORE starting the host, so Wolverine's
     // message-store resource setup (on Start) finds an existing database to provision into.
diff --git a/src/Samples/WolverineMartenAggregateFSharpSample/Domain.fs b/src/Samples/WolverineMartenAggregateFSharpSample/Domain.fs
index 160119824..30ade3cae 100644
--- a/src/Samples/WolverineMartenAggregateFSharpSample/Domain.fs
+++ b/src/Samples/WolverineMartenAggregateFSharpSample/Domain.fs
@@ -16,6 +16,7 @@ type Counter = { Id: Guid; Count: int }
 /// A single-stream projection that folds the Counter events into the aggregate. It overrides Evolve
 /// directly (an explicit per-event fold) rather than using convention Create/Apply methods: those are
 /// dispatched by the C#-only JasperFx.Events source generator, which does not run for F# assemblies.
+// begin-snippet: sample_fsharp_aggregate_projection
 type CounterProjection() =
     inherit SingleStreamProjection<Counter, Guid>()
 
@@ -24,6 +25,7 @@ type CounterProjection() =
         | :? CounterStarted as started -> { Id = started.Id; Count = 0 }
         | :? Incremented as inc -> { snapshot with Count = snapshot.Count + inc.By }
         | _ -> snapshot
+// end-snippet
 
 /// The command handled by IncrementHandler. CounterId names the aggregate stream to load.
 type IncrementCounter = { CounterId: Guid; By: int }
diff --git a/src/Samples/WolverineMartenAggregateFSharpSample/Handlers.fs b/src/Samples/WolverineMartenAggregateFSharpSample/Handlers.fs
index 787cb9256..d7fc5bb5f 100644
--- a/src/Samples/WolverineMartenAggregateFSharpSample/Handlers.fs
+++ b/src/Samples/WolverineMartenAggregateFSharpSample/Handlers.fs
@@ -6,7 +6,9 @@ open Wolverine.Marten
 /// load the Counter aggregate for the stream named by the command's CounterId (FetchForWriting), pass
 /// it to this method, and append the returned event(s) back to that stream. The returned Incremented
 /// is registered against the loaded stream by the generated adapter.
+// begin-snippet: sample_fsharp_aggregate_handler
 type IncrementHandler =
     [<AggregateHandler>]
     static member Handle(command: IncrementCounter, counter: Counter) : Incremented =
         { By = command.By }
+// end-snippet
diff --git a/src/Samples/WolverineMartenFSharpSample/Handlers.fs b/src/Samples/WolverineMartenFSharpSample/Handlers.fs
index 46b769ead..ef983df85 100644
--- a/src/Samples/WolverineMartenFSharpSample/Handlers.fs
+++ b/src/Samples/WolverineMartenFSharpSample/Handlers.fs
@@ -8,9 +8,11 @@ open Wolverine.Attributes
 /// an injected IDocumentSession: store a document and return an event that Wolverine sends as a
 /// cascading message. Wolverine's Marten middleware opens the outbox-enrolled session and saves
 /// changes around this call — both of which the F# codegen now emits.
+// begin-snippet: sample_fsharp_marten_document_handler
 type CreateProductHandler =
     [<Transactional>]
     static member Handle(command: CreateProductCommand, session: IDocumentSession) : ProductCreated =
         let product = { Id = Guid.NewGuid(); Name = command.Name }
         session.Store<Product>(product)
         { Id = product.Id }
+// end-snippet


Test patch:
diff --git a/src/Testing/Wolverine.Behavioural.FSharpApp/Domain.fs b/src/Testing/Wolverine.Behavioural.FSharpApp/Domain.fs
index 9b73b7bac..7f2c78ef6 100644
--- a/src/Testing/Wolverine.Behavioural.FSharpApp/Domain.fs
+++ b/src/Testing/Wolverine.Behavioural.FSharpApp/Domain.fs
@@ -22,5 +22,7 @@ type BehaviouralPing = { Value: int }
 
 /// A minimal F# handler. The generated F# adapter (Generated.fs) calls this; the behavioural test
 /// boots a host in TypeLoadMode.Static, sends a BehaviouralPing, and asserts the sink recorded it.
+// begin-snippet: sample_fsharp_behavioural_handler
 type BehaviouralPingHandler =
     static member Handle(ping: BehaviouralPing) = BehaviouralSink.record ping.Value
+// end-snippet
diff --git a/src/Testing/Wolverine.Behavioural.FSharpTests/BehaviouralRunStep.cs b/src/Testing/Wolverine.Behavioural.FSharpTests/BehaviouralRunStep.cs
index eac077903..71c7a47cd 100644
--- a/src/Testing/Wolverine.Behavioural.FSharpTests/BehaviouralRunStep.cs
+++ b/src/Testing/Wolverine.Behavioural.FSharpTests/BehaviouralRunStep.cs
@@ -30,26 +30,29 @@ public async Task generated_fsharp_handler_runs_under_static_load()
     {
         BehaviouralSink.reset();
 
+        // begin-snippet: sample_fsharp_static_host
         var appAssembly = typeof(BehaviouralPingHandler).Assembly;
 
         using var host = await Host.CreateDefaultBuilder()
             .UseWolverine(opts =>
             {
+                // Shared with the generation step so the generated type-name hash matches:
+                // DisableConventionalDiscovery() + IncludeType<BehaviouralPingHandler>().
                 BehaviouralCodegen.Configure(opts);
 
                 // Load the pre-generated F# handler adapter out of the app assembly instead of
                 // compiling at runtime. Setting ApplicationAssembly (which cascades to
-                // CodeGeneration.ApplicationAssembly) BEFORE bootstrap pins the assembly Wolverine
-                // scans for pre-built types to the F# app — not this test assembly. If the committed
-                // Generated.fs has drifted from this config, the type name won't match and the host
-                // throws ExpectedTypeMissingException at startup — a loud, useful signal.
+                // CodeGeneration.ApplicationAssembly) pins the assembly Wolverine scans for pre-built
+                // types to the F# app, and TypeLoadMode.Static means no Roslyn at runtime.
                 opts.ApplicationAssembly = appAssembly;
                 opts.CodeGeneration.TypeLoadMode = TypeLoadMode.Static;
             })
             .StartAsync();
 
+        // The pre-generated F# MessageHandler is loaded by name and executed — no runtime compilation.
         var bus = host.MessageBus();
         await bus.InvokeAsync(new BehaviouralPing(42));
+        // end-snippet
 
         using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));
         var received = await BehaviouralSink.received().WaitAsync(timeout.Token);