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 @@ -25,9 +25,12 @@ on:
- 'src/Testing/Wolverine.EfCore.FSharp*/**'
- 'src/Testing/Wolverine.Marten.FSharp*/**'
- 'src/Testing/Wolverine.MartenAggregate.FSharp*/**'
- 'src/Testing/Wolverine.Cosmos.FSharp*/**'
- 'src/Samples/WolverineFSharpSample/**'
- 'src/Samples/WolverineMartenFSharpSample/**'
- 'src/Samples/WolverineMartenAggregateFSharpSample/**'
- 'src/Samples/WolverineCosmosFSharpSample/**'
- 'src/Extensions/Wolverine.FluentValidation/**'
- '.github/workflows/fsharp.yml'
workflow_dispatch:

Expand Down Expand Up @@ -76,3 +79,6 @@ jobs:

- name: Compile-gate (Marten aggregate surface)
run: dotnet test src/Testing/Wolverine.MartenAggregate.FSharpTests/Wolverine.MartenAggregate.FSharpTests.csproj -c "$config" --nologo

- name: Compile-gate (FluentValidation + CosmosDB surface)
run: dotnet test src/Testing/Wolverine.Cosmos.FSharpTests/Wolverine.Cosmos.FSharpTests.csproj -c "$config" --nologo
16 changes: 16 additions & 0 deletions src/Persistence/Wolverine.CosmosDb/Internals/TransactionalFrame.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using JasperFx.CodeGeneration;
using JasperFx.CodeGeneration.Frames;
using JasperFx.CodeGeneration.Model;
using JasperFx.Core.Reflection;
using Microsoft.Azure.Cosmos;
using Wolverine.Configuration;
using Wolverine.Persistence.Sagas;
Expand Down Expand Up @@ -50,4 +51,19 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)

Next?.GenerateCode(method, writer);
}

public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
{
if (_context != null)
{
writer.BlankLine();
writer.WriteComment("Enlist in CosmosDB outbox transaction");
// EnlistInOutbox is synchronous (void); the envelope transaction is a public type so the
// generated F# in a separate assembly can construct it.
writer.Write(
$"{_context.FSharpUsage}.{nameof(MessageContext.EnlistInOutbox)}({typeof(CosmosDbEnvelopeTransaction).FSharpName()}({Container!.FSharpUsage}, {_context.FSharpUsage}))");
}

Next?.GenerateFSharpCode(method, writer);
}
}
23 changes: 23 additions & 0 deletions src/Samples/WolverineCosmosFSharpSample/Domain.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace WolverineCosmosFSharpSample

open FluentValidation

/// The CosmosDB document persisted by the sample. Cosmos identifies documents by a lowercase `id`.
[<CLIMutable>]
type Thing = { id: string; Name: string }

/// The command handled by CreateThingHandler.
type CreateThing = { Id: string; Name: string }

/// The event cascaded out after the document is stored.
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.
type CreateThingValidator() as self =
inherit AbstractValidator<CreateThing>()

do
self.RuleFor(fun x -> x.Id).NotEmpty() |> ignore
self.RuleFor(fun x -> x.Name).NotEmpty() |> ignore
13 changes: 13 additions & 0 deletions src/Samples/WolverineCosmosFSharpSample/Handlers.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace WolverineCosmosFSharpSample

open Wolverine.Attributes
open Wolverine.CosmosDb

/// A message handler written in F# that combines two middleware surfaces:
/// * 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.
type CreateThingHandler =
[<Transactional>]
static member Handle(command: CreateThing) : ICosmosDbOp =
CosmosDbOps.Store<Thing>({ id = command.Id; Name = command.Name })
71 changes: 71 additions & 0 deletions src/Samples/WolverineCosmosFSharpSample/Program.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
module WolverineCosmosFSharpSample.Program

open System
open System.Net.Http
open Microsoft.Extensions.Hosting
open Microsoft.Extensions.DependencyInjection
open Microsoft.Azure.Cosmos
open FluentValidation
open JasperFx.Resources
open Wolverine
open Wolverine.CosmosDb
open Wolverine.FluentValidation
open WolverineCosmosFSharpSample

// The well-known Azure Cosmos DB emulator endpoint + key (the emulator is in docker-compose).
[<Literal>]
let connectionString =
"AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="

[<Literal>]
let databaseName = "wolverine_fsharp_cosmos"

// CosmosClient configured for the local emulator (Gateway mode + accept its self-signed cert).
let private buildClient () =
let options = CosmosClientOptions(ConnectionMode = ConnectionMode.Gateway)
options.HttpClientFactory <-
Func<HttpClient>(fun () ->
new HttpClient(
new HttpClientHandler(
ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator)))
new CosmosClient(connectionString, options)

[<EntryPoint>]
let main args =
// Wolverine's CosmosDB resource setup creates containers but not the database itself; pre-create it.
(use client = buildClient ()
client.CreateDatabaseIfNotExistsAsync(databaseName).GetAwaiter().GetResult() |> ignore)

let host =
Host
.CreateDefaultBuilder(args)
.UseWolverine(fun opts ->
opts.Services.AddSingleton<CosmosClient>(fun _ -> buildClient ()) |> ignore

// FluentValidation middleware + the validator.
opts.Services.AddScoped<IValidator<CreateThing>, CreateThingValidator>() |> ignore
opts.UseFluentValidation() |> ignore

// CosmosDB as the durable message store backing the transactional outbox.
opts.UseCosmosDbPersistence(databaseName) |> ignore

opts.Policies.AutoApplyTransactions() |> ignore
opts.Discovery.IncludeType<CreateThingHandler>() |> 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)
.UseResourceSetupOnStartup()
.Build()

host.Start()

// Demonstrate the F# Wolverine + CosmosDB handler end-to-end (dynamic codegen): FluentValidation
// runs, then the ICosmosDbOp side effect stores the document inside the outbox transaction.
let bus = host.Services.GetRequiredService<IMessageBus>()
bus.InvokeAsync({ Id = Guid.NewGuid().ToString(); Name = "Sample" }).GetAwaiter().GetResult()
printfn "Stored a Thing through the F# Wolverine + CosmosDB handler (with FluentValidation)."

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

A small **F#** Wolverine application demonstrating the F# code-generation approach (issue
[GH-2969](https://github.com/JasperFx/wolverine/issues/2969)). This slice combines **two middleware
surfaces** in one handler:

- **FluentValidation** — `Domain.fs` defines a `CreateThingValidator : AbstractValidator<CreateThing>`;
Wolverine's FluentValidation middleware runs it before the handler (and short-circuits on failure).
- **CosmosDB persistence** — `Handlers.fs` has a `[<Transactional>]` `CreateThingHandler` that returns
an `ICosmosDbOp` (`CosmosDbOps.Store`); the CosmosDB transaction middleware enlists the outbox and
the side effect is applied within it.

`Program.fs` wires `UseWolverine` + `UseFluentValidation()` + `UseCosmosDbPersistence(...)`, backed by
the Azure Cosmos DB emulator.

## Two things this proves

1. **It runs (dynamic codegen).** `dotnet run` boots the app, invokes a `CreateThing`, FluentValidation
validates it, and the `ICosmosDbOp` stores the document inside the CosmosDB outbox transaction. 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.Cosmos.FSharpTests` renders this
handler's real chain to F# via Wolverine's static codegen path and `dotnet build`s the result,
proving the FluentValidation `ExecuteOne` call, the CosmosDB `TransactionalFrame`, the `ISideEffect`
`Execute`, and the outbox flush emit valid, compiling F#. (Only `TransactionalFrame` needed new F#
emit; the rest were already covered by `MethodCall`/existing frames.)

## A note on F# + FluentValidation

`AbstractValidator<T>.RuleFor` takes a LINQ `Expression<Func<T, TProperty>>`. F# auto-converts the
property-selector lambdas (`fun x -> x.Name`) to those expression trees, so the validator reads
naturally.

## Running it

CosmosDB needs the Azure Cosmos DB emulator, 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 (the emulator
takes a minute or two to become ready):

```bash
docker compose up -d cosmosdb
dotnet run --project src/Samples/WolverineCosmosFSharpSample --framework net9.0
```

Expected output: `Stored a Thing through the F# Wolverine + CosmosDB handler (with FluentValidation).`
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<Project Sdk="Microsoft.NET.Sdk">

<!--
A small F# Wolverine sample demonstrating the F# code-generation approach (issue GH-2969).
This slice combines two middleware surfaces: FluentValidation (a validator runs before the
handler) and CosmosDB persistence (the handler returns an ICosmosDbOp side effect applied inside
the CosmosDB outbox transaction). It runs today via dynamic codegen against the Cosmos emulator;
the F# compile-gate (src/Testing/Wolverine.Cosmos.FSharpTests) renders this handler's real chain
to F# and compiles it, proving the FluentValidation + CosmosDB 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>

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

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

<ItemGroup>
<ProjectReference Include="..\..\Persistence\Wolverine.CosmosDb\Wolverine.CosmosDb.csproj" />
<ProjectReference Include="..\..\Extensions\Wolverine.FluentValidation\Wolverine.FluentValidation.csproj" />
<ProjectReference Include="..\..\Wolverine.RuntimeCompilation\Wolverine.RuntimeCompilation.csproj" />
</ItemGroup>

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

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

namespace Internal.Generated.WolverineHandlers

open Microsoft.Azure.Cosmos
open System
open System.Threading
open System.Threading.Tasks
open Wolverine.FluentValidation
open Wolverine.Runtime
open Wolverine.Runtime.Handlers

type CreateThingHandler2004325667(container: Microsoft.Azure.Cosmos.Container, failureActionOfCreateThing: Wolverine.FluentValidation.IFailureAction<WolverineCosmosFSharpSample.CreateThing>) =
inherit Wolverine.Runtime.Handlers.MessageHandler()
let _container = container
let _failureActionOfCreateThing = failureActionOfCreateThing

override this.HandleAsync(context: Wolverine.Runtime.MessageContext, cancellation: System.Threading.CancellationToken) : System.Threading.Tasks.Task =
task {
let createThingValidator = WolverineCosmosFSharpSample.CreateThingValidator()
// The actual message body
let createThing = context.Envelope.Message :?> WolverineCosmosFSharpSample.CreateThing

if not (isNull System.Diagnostics.Activity.Current) then
System.Diagnostics.Activity.Current.SetTag("message.handler", "WolverineCosmosFSharpSample.CreateThingHandler") |> ignore
System.Diagnostics.Activity.Current.SetTag("handler.type", "WolverineCosmosFSharpSample.CreateThingHandler") |> ignore
do! Wolverine.FluentValidation.Internals.FluentValidationExecutor.ExecuteOne<WolverineCosmosFSharpSample.CreateThing>(createThingValidator, _failureActionOfCreateThing, createThing)

// Enlist in CosmosDB outbox transaction
context.EnlistInOutbox(Wolverine.CosmosDb.Internals.CosmosDbEnvelopeTransaction(_container, context))

// The actual message execution
let outgoing1 = WolverineCosmosFSharpSample.CreateThingHandler.Handle(createThing)

if not (isNull outgoing1) then

// Placed by Wolverine's ISideEffect policy
do! outgoing1.Execute(_container)


// 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,36 @@
<Project Sdk="Microsoft.NET.Sdk">

<!--
Checked-in F# fixture for the FluentValidation + CosmosDB F# code-generation surface (issue
GH-2969). Generated.fs is the emitted MessageHandler adapter for the sample's CreateThing handler
chain (FluentValidation ExecuteOne -> enlist CosmosDB outbox -> handler -> apply ICosmosDbOp ->
flush). The compile-gate regenerates it and `dotnet build`s this project to prove those frames
emit compiling F#. Mirrors the Core/Http/EfCore/Marten 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>
<ProjectReference Include="..\..\Samples\WolverineCosmosFSharpSample\WolverineCosmosFSharpSample.fsproj" />
</ItemGroup>

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

</Project>
Loading