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

Expand Down Expand Up @@ -60,3 +63,6 @@ jobs:

- name: Compile-gate + fsharp-coverage (Http surface)
run: dotnet test src/Testing/Wolverine.Http.FSharpTests/Wolverine.Http.FSharpTests.csproj -c "$config" --nologo

- name: Compile-gate (EF Core surface)
run: dotnet test src/Testing/Wolverine.EfCore.FSharpTests/Wolverine.EfCore.FSharpTests.csproj -c "$config" --nologo
8 changes: 4 additions & 4 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@
<PackageVersion Include="Grpc.StatusProto" Version="2.76.0" />
<PackageVersion Include="Grpc.Tools" Version="2.76.0" />
<PackageVersion Include="HtmlTags" Version="9.0.0" />
<PackageVersion Include="JasperFx" Version="2.2.5" />
<PackageVersion Include="JasperFx.Events" Version="2.2.5" />
<PackageVersion Include="JasperFx.Events.SourceGenerator" Version="2.2.5" />
<PackageVersion Include="JasperFx" Version="2.2.7" />
<PackageVersion Include="JasperFx.Events" Version="2.2.7" />
<PackageVersion Include="JasperFx.Events.SourceGenerator" Version="2.2.7" />
<!-- RuntimeCompiler is on its own 5.x line (the Roslyn compiler package) — not the 2.1.x
family; it stays at 5.0.0. -->
<PackageVersion Include="JasperFx.RuntimeCompiler" Version="5.0.0" />
<PackageVersion Include="JasperFx.SourceGenerator" Version="2.2.5" />
<PackageVersion Include="JasperFx.SourceGenerator" Version="2.2.7" />
<PackageVersion Include="Marten" Version="9.2.0" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="6.1.3" />
<PackageVersion Include="Polecat" Version="4.2.1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,47 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
writer.FinishBlock();
}

public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
{
// This middleware only ever runs inside an async handler/endpoint, so the body is a `task { }`
// computation expression: awaits are `do!` (or `let! _ =` when a result must be discarded),
// and `.ConfigureAwait(false)` is dropped (the CE controls scheduling).
writer.Write("");
writer.WriteComment(
"Enroll the DbContext & IMessagingContext in the outgoing Wolverine outbox transaction");
writer.Write($"{_envelopeTransaction.FSharpAssignmentUsage} = {typeof(EfCoreEnvelopeTransaction).FSharpName()}({_dbContext.FSharpUsage}, {_context!.FSharpUsage}, {_scrapers.FSharpUsage})");
writer.Write($"do! {_context.FSharpUsage}.{nameof(MessageContext.EnlistInOutboxAsync)}({_envelopeTransaction.FSharpUsage})");

writer.WriteComment("Start the actual database transaction if one does not already exist");
// F# has no `== null`; use `isNull`. BeginTransactionAsync returns Task<IDbContextTransaction>,
// so bind-and-discard with `let! _ =` (then `()` makes the then-branch unit, as `if/then` requires).
writer.Write($"BLOCK:if isNull {_dbContext.FSharpUsage}.Database.CurrentTransaction then");
writer.Write($"let! _ = {_dbContext.FSharpUsage}.Database.BeginTransactionAsync({_cancellation.FSharpUsage})");
writer.Write("()");
writer.FinishBlock();

writer.Write("BLOCK:try");

// EF Core can only do eager idempotent checks
if (_idempotencyStyle == IdempotencyStyle.Eager || _idempotencyStyle == IdempotencyStyle.Optimistic)
{
writer.Write($"do! {_context.FSharpUsage}.{nameof(MessageContext.AssertEagerIdempotencyAsync)}({_cancellation.FSharpUsage})");
}

// See the C# overload for why the commit/flush lives in Next (the CommitEfCoreEnvelopeTransaction
// postprocessor) rather than here. GH-2917.
Next?.GenerateFSharpCode(method, writer);

writer.FinishBlock();
// F# exception handler: roll back, then rethrow. `reraise()` is illegal inside a computation
// expression's try/with (FS0413), so use ExceptionDispatchInfo to preserve the original stack
// trace — the exact semantics of C# `throw;`.
writer.Write("BLOCK:with ex ->");
writer.Write($"do! {_envelopeTransaction.FSharpUsage}.RollbackAsync()");
writer.Write($"{typeof(System.Runtime.ExceptionServices.ExceptionDispatchInfo).FSharpName()}.Capture(ex).Throw()");
writer.FinishBlock();
}

public override IEnumerable<Variable> FindVariables(IMethodVariables chain)
{
_scrapers = chain.FindVariable(typeof(IEnumerable<IDomainEventScraper>));
Expand Down Expand Up @@ -99,6 +140,14 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
Next?.GenerateCode(method, writer);
}

public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
{
writer.WriteComment(
"Commit the EF Core transaction and flush outgoing messages before writing the response (GH-2917)");
writer.Write($"do! {_envelopeTransaction.FSharpUsage}.CommitAsync({_cancellation.FSharpUsage})");
Next?.GenerateFSharpCode(method, writer);
}

public override IEnumerable<Variable> FindVariables(IMethodVariables chain)
{
_envelopeTransaction = chain.FindVariable(typeof(EfCoreEnvelopeTransaction));
Expand Down
27 changes: 27 additions & 0 deletions src/Samples/WolverineFSharpSample/Domain.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace WolverineFSharpSample

open System
open Microsoft.EntityFrameworkCore

/// The EF Core entity persisted by the sample.
[<CLIMutable>]
type Item = { Id: Guid; Name: string }

/// The command handled by CreateItemHandler.
type CreateItemCommand = { Name: string }

/// The event cascaded out after the item is created.
type ItemCreated = { Id: Guid }

/// The sample's EF Core DbContext. Wolverine's EF Core integration enrolls this in its
/// transactional outbox; the generated handler adapter constructs/commits an EfCoreEnvelopeTransaction
/// around it.
type ItemsDbContext(options: DbContextOptions<ItemsDbContext>) =
inherit DbContext(options)

[<DefaultValue>]
val mutable private items: DbSet<Item>

member this.Items
with get () = this.items
and set v = this.items <- v
15 changes: 15 additions & 0 deletions src/Samples/WolverineFSharpSample/Handlers.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace WolverineFSharpSample

open System
open Wolverine.Attributes

/// An EF Core transactional message handler written in F#. Mirrors the C# EFCoreSample handler:
/// [<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.
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 }
59 changes: 59 additions & 0 deletions src/Samples/WolverineFSharpSample/Program.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
module WolverineFSharpSample.Program

open Microsoft.EntityFrameworkCore
open Microsoft.Extensions.Hosting
open Microsoft.Extensions.DependencyInjection
open JasperFx.Resources
open Wolverine
open Wolverine.EntityFrameworkCore
open Wolverine.Postgresql
open WolverineFSharpSample

// The same Postgres instance the rest of the Wolverine test suite uses (docker-compose, :5433).
// Wolverine's EF Core outbox requires a durable message store, so the sample is not infra-free; the
// *static* F# story (no DB, no host) is what the compile-gate test proves.
// A dedicated database so EF Core's EnsureCreated() provisions the Item table on a fresh DB (it
// no-ops against the shared `postgres` DB, which already has tables).
[<Literal>]
let connectionString =
"Host=localhost;Port=5433;Database=wolverine_fsharp_sample;Username=postgres;password=postgres"

[<EntryPoint>]
let main args =
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()

// 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.
(use scope = host.Services.CreateScope()
scope.ServiceProvider.GetRequiredService<ItemsDbContext>().Database.EnsureCreated() |> ignore)

host.Start()

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

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

A small **F#** Wolverine application demonstrating the F# code-generation approach (issue
[GH-2969](https://github.com/JasperFx/wolverine/issues/2969)). This is the first slice of the F#
sample and exercises **EF Core**:

- `Domain.fs` — an `Item` entity, an `ItemsDbContext`, a `CreateItemCommand`, and an `ItemCreated` event.
- `Handlers.fs` — `CreateItemHandler.Handle`, a `[<Transactional>]` handler that takes the command +
the `ItemsDbContext`, adds an `Item`, and returns an `ItemCreated` (cascaded through the outbox).
- `Program.fs` — a generic host wiring `UseWolverine` + `AddDbContextWithWolverineIntegration` +
`UseEntityFrameworkCoreTransactions` + `AutoApplyTransactions`, backed by a Postgres message store.

## Two things this proves

1. **It runs (dynamic codegen).** `dotnet run` boots the app, invokes a `CreateItemCommand`, and the
F# handler writes through EF Core 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.EfCore.FSharpTests` renders this
handler's real chain to F# via Wolverine's static codegen path and `dotnet build`s the result,
proving the EF Core transactional frames (scoped-DI resolution, `EnrollDbContextInTransaction`,
`SaveChangesAsync`, `CommitEfCoreEnvelopeTransaction`) emit valid, compiling F#.

## Running it

Wolverine's EF Core outbox needs a durable message store, 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/WolverineFSharpSample --framework net9.0
```

Expected output: `Created an Item through the F# Wolverine + EF Core handler.`

The sample uses a dedicated `wolverine_fsharp_sample` database (EF Core `EnsureCreated()` provisions
the `Item` table; `UseResourceSetupOnStartup()` provisions the Wolverine message-store tables).

## Later slices

Per #2969, the sample grows to mix Marten (document + event-sourced aggregate) and richer HTTP, each
driving the remaining store-specific frames' F# emit toward complete coverage.
48 changes: 48 additions & 0 deletions src/Samples/WolverineFSharpSample/WolverineFSharpSample.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<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>
<ProjectReference Include="..\..\Persistence\Wolverine.EntityFrameworkCore\Wolverine.EntityFrameworkCore.csproj" />
<!-- Wolverine's EF Core outbox needs a durable message store; Postgres backs both the DbContext
and the Wolverine message store (the same instance the rest of the suite uses on :5433). -->
<ProjectReference Include="..\..\Persistence\Wolverine.Postgresql\Wolverine.Postgresql.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" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
</ItemGroup>

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

namespace Internal.Generated.WolverineHandlers

open Microsoft.Extensions.DependencyInjection
open System
open System.Collections.Generic
open System.Threading
open System.Threading.Tasks
open Wolverine.Runtime
open Wolverine.Runtime.Handlers

type CreateItemCommandHandler670389475(serviceScopeFactory: Microsoft.Extensions.DependencyInjection.IServiceScopeFactory, domainEventScraperIEnumerable: System.Collections.Generic.IEnumerable<Wolverine.EntityFrameworkCore.IDomainEventScraper>) =
inherit Wolverine.Runtime.Handlers.MessageHandler()
let _serviceScopeFactory = serviceScopeFactory
let _domainEventScraperIEnumerable = domainEventScraperIEnumerable

override this.HandleAsync(context: Wolverine.Runtime.MessageContext, cancellation: System.Threading.CancellationToken) : System.Threading.Tasks.Task =
task {
use serviceScope = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.CreateAsyncScope(_serviceScopeFactory)
// This service has been marked as requiring service location independent of Wolverine's ability to use constructor injection of everything else
let itemsDbContext = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService<WolverineFSharpSample.ItemsDbContext>(serviceScope.ServiceProvider)
// The actual message body
let createItemCommand = context.Envelope.Message :?> WolverineFSharpSample.CreateItemCommand


// Enroll the DbContext & IMessagingContext in the outgoing Wolverine outbox transaction
let efCoreEnvelopeTransaction = Wolverine.EntityFrameworkCore.Internals.EfCoreEnvelopeTransaction(itemsDbContext, context, _domainEventScraperIEnumerable)
do! context.EnlistInOutboxAsync(efCoreEnvelopeTransaction)
// Start the actual database transaction if one does not already exist
if isNull itemsDbContext.Database.CurrentTransaction then
let! _ = itemsDbContext.Database.BeginTransactionAsync(cancellation)
()
try
if not (isNull System.Diagnostics.Activity.Current) then
System.Diagnostics.Activity.Current.SetTag("message.handler", "WolverineFSharpSample.CreateItemHandler") |> ignore
System.Diagnostics.Activity.Current.SetTag("handler.type", "WolverineFSharpSample.CreateItemHandler") |> ignore

// The actual message execution
let outgoing1 = WolverineFSharpSample.CreateItemHandler.Handle(createItemCommand, itemsDbContext)


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


// Added by EF Core Transaction Middleware
let! result_of_SaveChangesAsync = itemsDbContext.SaveChangesAsync(cancellation)

// Commit the EF Core transaction and flush outgoing messages before writing the response (GH-2917)
do! efCoreEnvelopeTransaction.CommitAsync(cancellation)
with ex ->
do! efCoreEnvelopeTransaction.RollbackAsync()
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(ex).Throw()
}

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 EF Core F# code-generation surface (issue GH-2969). Generated.fs
is the emitted MessageHandler adapter for the sample's EF Core handler chain (enroll-in-outbox ->
SaveChanges -> commit). The compile-gate regenerates it and `dotnet build`s this project to prove
the EF Core transactional frames emit compiling F#. Mirrors the Core/Http 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 + the public
Wolverine.EntityFrameworkCore EfCoreEnvelopeTransaction. -->
<ProjectReference Include="..\..\Samples\WolverineFSharpSample\WolverineFSharpSample.fsproj" />
</ItemGroup>

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

</Project>
Loading
Loading