Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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'},
Expand Down
276 changes: 276 additions & 0 deletions docs/tutorials/fsharp.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions src/Samples/WolverineCosmosFSharpSample/Domain.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions src/Samples/WolverineCosmosFSharpSample/Handlers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions src/Samples/WolverineFSharpSample/Handlers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions src/Samples/WolverineFSharpSample/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ let connectionString =

[<EntryPoint>]
let main args =
// begin-snippet: sample_fsharp_efcore_bootstrap
let host =
Host
.CreateDefaultBuilder(args)
Expand All @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions src/Samples/WolverineMartenAggregateFSharpSample/Domain.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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>()

Expand All @@ -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 }
2 changes: 2 additions & 0 deletions src/Samples/WolverineMartenAggregateFSharpSample/Handlers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions src/Samples/WolverineMartenFSharpSample/Handlers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions src/Testing/Wolverine.Behavioural.FSharpApp/Domain.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading