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
5 changes: 5 additions & 0 deletions .github/workflows/fsharp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ on:
- 'src/Testing/Wolverine.Core.FSharp*/**'
- 'src/Testing/Wolverine.Http.FSharp*/**'
- 'src/Testing/Wolverine.EfCore.FSharp*/**'
- 'src/Testing/Wolverine.Marten.FSharp*/**'
- 'src/Samples/WolverineFSharpSample/**'
- 'src/Samples/WolverineMartenFSharpSample/**'
- '.github/workflows/fsharp.yml'
workflow_dispatch:

Expand Down Expand Up @@ -66,3 +68,6 @@ jobs:

- name: Compile-gate (EF Core surface)
run: dotnet test src/Testing/Wolverine.EfCore.FSharpTests/Wolverine.EfCore.FSharpTests.csproj -c "$config" --nologo

- name: Compile-gate (Marten document surface)
run: dotnet test src/Testing/Wolverine.Marten.FSharpTests/Wolverine.Marten.FSharpTests.csproj -c "$config" --nologo
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,27 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)

Next?.GenerateCode(method, writer);
}

public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
{
if (_createsSession)
{
writer.BlankLine();
writer.WriteComment("Open a new document session registered with the Wolverine");
writer.WriteComment("message context to support the outbox functionality");
// `using var` -> F# `use`: the session is disposed at the end of the task { } block.
writer.Write(
$"use {Session!.Usage} = {_factory!.FSharpUsage}.{nameof(OutboxedSessionFactory.OpenSession)}({_context!.FSharpUsage})");
}

if (_chain.Idempotency != IdempotencyStyle.None)
{
writer.BlankLine();
writer.WriteComment("This message handler is configured for Eager idempotency checks");
writer.Write(
$"do! {_context!.FSharpUsage}.{nameof(MessageContext.AssertEagerIdempotencyAsync)}({_cancellation!.FSharpUsage})");
}

Next?.GenerateFSharpCode(method, writer);
}
}
32 changes: 32 additions & 0 deletions src/Persistence/Wolverine.Marten/Codegen/OpenMartenSessionFrame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,38 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
Next?.GenerateCode(method, writer);
}

public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
{
if (_justCast)
{
Next?.GenerateFSharpCode(method, writer);
return;
}

var methodName = ReturnVariable.VariableType == typeof(IQuerySession)
? nameof(OutboxedSessionFactory.QuerySession)
: nameof(OutboxedSessionFactory.OpenSession);

// `await using var` -> F# `use`: inside the task { } computation expression `use` disposes an
// IAsyncDisposable (the session) via DisposeAsync at the end of the block.
if (_context == null)
{
writer.Write($"use {ReturnVariable.Usage} = {_martenFactory!.FSharpUsage}.{methodName}()");
}
else if (_tenantId == null)
{
writer.WriteComment("Building the Marten session");
writer.Write($"use {ReturnVariable.Usage} = {_factory!.FSharpUsage}.{methodName}({_context!.FSharpUsage})");
}
else
{
writer.WriteComment("Building the Marten session using the detected tenant id");
writer.Write($"use {ReturnVariable.Usage} = {_factory!.FSharpUsage}.{methodName}({_context!.FSharpUsage}, {_tenantId.FSharpUsage})");
}

Next?.GenerateFSharpCode(method, writer);
}

public override IEnumerable<Variable> FindVariables(IMethodVariables chain)
{
if (_sessionType == typeof(IQuerySession))
Expand Down
14 changes: 14 additions & 0 deletions src/Samples/WolverineMartenFSharpSample/Domain.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace WolverineMartenFSharpSample

open System

/// The Marten document persisted by the sample. Marten identifies documents by an `Id` member;
/// [<CLIMutable>] gives the record the parameterless constructor + settable properties Marten needs.
[<CLIMutable>]
type Product = { Id: Guid; Name: string }

/// The command handled by CreateProductHandler.
type CreateProductCommand = { Name: string }

/// The event cascaded out after the product is stored.
type ProductCreated = { Id: Guid }
16 changes: 16 additions & 0 deletions src/Samples/WolverineMartenFSharpSample/Handlers.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace WolverineMartenFSharpSample

open System
open Marten
open Wolverine.Attributes

/// A Marten document transactional message handler written in F#. [<Transactional>] + the command +
/// 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.
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 }
52 changes: 52 additions & 0 deletions src/Samples/WolverineMartenFSharpSample/Program.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
module WolverineMartenFSharpSample.Program

open Microsoft.Extensions.Hosting
open Microsoft.Extensions.DependencyInjection
open Marten
open JasperFx.Resources
open Wolverine
open Wolverine.Marten
open WolverineMartenFSharpSample

// The same Postgres instance the rest of the Wolverine test suite uses (docker-compose, :5433).
// Marten creates its own schema objects on the fly, and IntegrateWithWolverine provisions the
// Wolverine message-store tables, so a dedicated schema keeps the sample's tables tidy in the
// shared `postgres` database (no separate database to create).
[<Literal>]
let connectionString =
"Host=localhost;Port=5433;Database=postgres;Username=postgres;password=postgres"

[<EntryPoint>]
let main args =
let host =
Host
.CreateDefaultBuilder(args)
.UseWolverine(fun opts ->
// Marten as both the document store and (via IntegrateWithWolverine) the durable
// message store backing Wolverine's transactional outbox.
opts.Services
.AddMarten(fun (m: StoreOptions) ->
m.Connection(connectionString)
m.DatabaseSchemaName <- "wolverine_fsharp_marten")
.IntegrateWithWolverine()
|> ignore

opts.Policies.AutoApplyTransactions() |> ignore
opts.Discovery.IncludeType<CreateProductHandler>() |> 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 Marten + Wolverine message-store schema on startup.
.UseResourceSetupOnStartup()
.Build()

host.Start()

// Demonstrate the F# Marten document handler end-to-end (dynamic codegen).
let bus = host.Services.GetRequiredService<IMessageBus>()
bus.InvokeAsync({ Name = "Sample" }).GetAwaiter().GetResult()
printfn "Created a Product through the F# Wolverine + Marten handler."

host.StopAsync().GetAwaiter().GetResult()
0
44 changes: 44 additions & 0 deletions src/Samples/WolverineMartenFSharpSample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# WolverineMartenFSharpSample

A small **F#** Wolverine application demonstrating the F# code-generation approach (issue
[GH-2969](https://github.com/JasperFx/wolverine/issues/2969)). This slice exercises **Marten document
persistence** (the EF Core slice lives in `WolverineFSharpSample`):

- `Domain.fs` — a `Product` document, a `CreateProductCommand`, and a `ProductCreated` event.
- `Handlers.fs` — `CreateProductHandler.Handle`, a `[<Transactional>]` handler that takes the command +
an injected `IDocumentSession`, stores a `Product`, and returns a `ProductCreated` (cascaded).
- `Program.fs` — `UseWolverine` + `AddMarten(...).IntegrateWithWolverine()`, so Marten is both the
document store and the durable message store backing the transactional outbox.

## Two things this proves

1. **It runs (dynamic codegen).** `dotnet run` boots the app, invokes a `CreateProductCommand`, and the
F# handler stores a document through Marten inside Wolverine's transactional outbox. Core Wolverine
no longer ships the Roslyn compiler ([GH-2876](https://github.com/JasperFx/wolverine/issues/2876)),
so the sample references `Wolverine.RuntimeCompilation` and calls `opts.UseRuntimeCompilation()`.

2. **It static-codegens to F# (compile-gate).** `src/Testing/Wolverine.Marten.FSharpTests` renders this
handler's real chain to F# via Wolverine's static codegen path and `dotnet build`s the result,
proving the Marten document frames (`OpenMartenSessionFrame`, `CreateDocumentSessionFrame`,
`SaveChangesAsync`, outbox flush) emit valid, compiling F#.

## Running it

Marten needs Postgres, so the runnable sample is **not** infra-free (the *static* F# story in the
compile-gate is). Start the repo's docker-compose infrastructure first:

```bash
docker compose up -d # Postgres on :5433
dotnet run --project src/Samples/WolverineMartenFSharpSample --framework net9.0
```

Expected output: `Created a Product through the F# Wolverine + Marten handler.`

The sample uses a dedicated `wolverine_fsharp_marten` schema in the shared `postgres` database; Marten
provisions its document tables and `IntegrateWithWolverine` + `UseResourceSetupOnStartup()` provision
the Wolverine message-store tables.

## Later slices

Per #2969, the next slice is the **Marten event-sourced aggregate** (`MartenOps.StartStream`,
`[Aggregate]` loading) — the heaviest frame set — followed by richer HTTP and the behavioural run-step.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">

<!--
A small F# Wolverine sample demonstrating the F# code-generation approach (issue GH-2969).
This slice exercises Marten document persistence: a [<Transactional>] message handler that stores
a document through an IDocumentSession and cascades an event. It runs today via dynamic codegen;
the F# compile-gate (src/Testing/Wolverine.Marten.FSharpTests) renders this handler's real chain
to F# and compiles it, proving the Marten document frames emit valid F#.
-->

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net9.0</TargetFrameworks>
<IsPackable>false</IsPackable>

<!-- F# project: clear the C#-oriented props inherited from Directory.Build.props. -->
<LangVersion></LangVersion>
<ImplicitUsings>false</ImplicitUsings>
<Nullable>disable</Nullable>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>

<!-- Pin FSharp.Core to the centrally-managed version; see WolverineWebApiFSharp.fsproj. -->
<DisableImplicitFSharpCoreReference>true</DisableImplicitFSharpCoreReference>
</PropertyGroup>

<ItemGroup>
<Compile Include="Domain.fs" />
<Compile Include="Handlers.fs" />
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<!-- Marten as both the document store and the durable Wolverine message store
(IntegrateWithWolverine), backing the transactional outbox. -->
<ProjectReference Include="..\..\Persistence\Wolverine.Marten\Wolverine.Marten.csproj" />
<!-- Core Wolverine no longer ships the runtime (Roslyn) compiler (GH-2876); the runnable sample
uses dynamic codegen, so reference the runtime-compilation module + UseRuntimeCompilation(). -->
<ProjectReference Include="..\..\Wolverine.RuntimeCompilation\Wolverine.RuntimeCompilation.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="FSharp.Core" />
</ItemGroup>

</Project>
43 changes: 43 additions & 0 deletions src/Testing/Wolverine.Marten.FSharpFixture/Generated.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// <auto-generated/>

namespace Internal.Generated.WolverineHandlers

open System
open System.Threading
open System.Threading.Tasks
open Wolverine.Marten.Publishing
open Wolverine.Runtime
open Wolverine.Runtime.Handlers

type CreateProductCommandHandler1629670092(outboxedSessionFactory: Wolverine.Marten.Publishing.OutboxedSessionFactory) =
inherit Wolverine.Runtime.Handlers.MessageHandler()
let _outboxedSessionFactory = outboxedSessionFactory

override this.HandleAsync(context: Wolverine.Runtime.MessageContext, cancellation: System.Threading.CancellationToken) : System.Threading.Tasks.Task =
task {
// Building the Marten session
use documentSession = _outboxedSessionFactory.OpenSession(context)
// The actual message body
let createProductCommand = context.Envelope.Message :?> WolverineMartenFSharpSample.CreateProductCommand

if not (isNull System.Diagnostics.Activity.Current) then
System.Diagnostics.Activity.Current.SetTag("message.handler", "WolverineMartenFSharpSample.CreateProductHandler") |> ignore
System.Diagnostics.Activity.Current.SetTag("handler.type", "WolverineMartenFSharpSample.CreateProductHandler") |> ignore

// The actual message execution
let outgoing1 = WolverineMartenFSharpSample.CreateProductHandler.Handle(createProductCommand, documentSession)


// Outgoing, cascaded message
do! context.EnqueueCascadingAsync(outgoing1)


// Save all pending changes to this Marten session
do! documentSession.SaveChangesAsync(cancellation)


// Have to flush outgoing messages just in case Marten did nothing because of https://github.com/JasperFx/wolverine/issues/536
do! context.FlushOutgoingMessagesAsync()

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">

<!--
Checked-in F# fixture for the Marten document F# code-generation surface (issue GH-2969).
Generated.fs is the emitted MessageHandler adapter for the sample's Marten document handler chain
(open outbox-enrolled session -> Store -> SaveChanges). The compile-gate regenerates it and
`dotnet build`s this project to prove the Marten document frames emit compiling F#. Mirrors the
Core/Http/EfCore fixtures.
-->

<PropertyGroup>
<TargetFrameworks>net9.0</TargetFrameworks>
<IsPackable>false</IsPackable>

<LangVersion></LangVersion>
<ImplicitUsings>false</ImplicitUsings>
<Nullable>disable</Nullable>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>

<DisableImplicitFSharpCoreReference>true</DisableImplicitFSharpCoreReference>
</PropertyGroup>

<ItemGroup>
<Compile Include="Generated.fs" />
</ItemGroup>

<ItemGroup>
<!-- The generated adapter references the sample's handler/domain types + Marten's IDocumentSession. -->
<ProjectReference Include="..\..\Samples\WolverineMartenFSharpSample\WolverineMartenFSharpSample.fsproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="FSharp.Core" />
</ItemGroup>

</Project>
Loading
Loading