Skip to content

F# codegen: Marten event-sourced aggregate slice — runnable sample + compile-gate (GH-2969)#2985

Merged
jeremydmiller merged 1 commit into
mainfrom
feat-2969-fsharp-marten-aggregate
May 29, 2026
Merged

F# codegen: Marten event-sourced aggregate slice — runnable sample + compile-gate (GH-2969)#2985
jeremydmiller merged 1 commit into
mainfrom
feat-2969-fsharp-marten-aggregate

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Part of the F# code-generation audit (#2969), following the EF Core (#2983) and Marten document (#2984) slices. This is the third store-specific slice: Marten event sourcing — the heaviest frame set — with its own runnable F# sample + compile-gate.

Marten aggregate frame F# emit

All in Wolverine.Marten/Codegenno JasperFx release needed (all Wolverine-side):

  • LoadAggregateFramelet! stream = session.Events.FetchForWriting<Agg>(id, ct) (covers the natural-key, exclusive-writing, and versioned overloads; stream.AlwaysEnforceConsistency <- true).
  • TagAggregateOtelFrame → guarded Activity.Current.SetTag(...) |> ignore (no ?. in F#).
  • MissingAggregateCheckFrameif isNull stream.Aggregate then raise(UnknownAggregateException(typeof<Agg>, id)).

RegisterEvents (AppendOne) and SaveChangesAsync are MethodCall/already-F# frames.

Generated F# (from the gate):

task {
    use documentSession = _outboxedSessionFactory.OpenSession(context)
    let incrementCounter = context.Envelope.Message :?> WolverineMartenAggregateFSharpSample.IncrementCounter
    let counter_Id = incrementCounter.CounterId
    // Loading Marten aggregate as part of the aggregate handler workflow
    let! eventStreamOfCounter = documentSession.Events.FetchForWriting<...Counter>(counter_Id, cancellation)
    if not (isNull System.Diagnostics.Activity.Current) then System.Diagnostics.Activity.Current.SetTag("wolverine.stream.id", counter_Id.ToString()) |> ignore
    let outgoing1 = ...IncrementHandler.Handle(incrementCounter, eventStreamOfCounter.Aggregate)
    eventStreamOfCounter.AppendOne(outgoing1)
    do! documentSession.SaveChangesAsync(cancellation)
    do! context.FlushOutgoingMessagesAsync()
}

Runnable sample — src/Samples/WolverineMartenAggregateFSharpSample

F# Counter aggregate + CounterStarted/Incremented events + an [<AggregateHandler>] IncrementHandler. Seeds a stream, then increments it (load via FetchForWriting, append the returned event). Postgres-backed; dynamic codegen (Wolverine.RuntimeCompilation + opts.UseRuntimeCompilation(), #2876). Verified end-to-end: prints Incremented the Counter through the F# Wolverine + Marten aggregate handler.

A key F# + Marten event-sourcing constraint

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. So CounterProjection overrides Evolve directly (an explicit per-event fold) — the supported escape hatch for F# self-aggregating types. Documented in the sample README.

Compile-gate — src/Testing/Wolverine.MartenAggregate.FSharp{Tests,Fixture}

Renders the sample's real IncrementCounter chain to F# via the no-host HandlerGraphAssembleTypesGenerateFSharpCode path, then dotnet builds the fixture. The gate driver does not register the projection (codegen only emits the FetchForWriting<Counter> call). Mirrors the prior gates.

Wire-up

  • Sample + gate added to wolverine_fsharp.slnx.
  • fsharp.yml runs the aggregate gate as its own sequential step.

Verification

  • Marten aggregate compile-gate green.
  • dotnet build wolverine.slnx -c Release clean (per CLAUDE.md the slim build isn't sufficient).
  • Sample runs end-to-end on Postgres.

Next up (separate work): an F# sample exercising FluentValidation middleware + CosmosDb persistence.

🤖 Generated with Claude Code

Third store-specific slice of the F# code-generation audit (the heaviest frame
set): a runnable F# Wolverine + Marten event-sourcing app and a compile-gate
proving its [<AggregateHandler>] chain emits valid F#.

Marten aggregate frame F# emit (all Wolverine.Marten/Codegen, no JasperFx release):
- LoadAggregateFrame       -> `let! stream = session.Events.FetchForWriting<Agg>(id, ct)`
  (covers natural-key, exclusive, and versioned overloads; AlwaysEnforceConsistency <- true).
- TagAggregateOtelFrame    -> guarded `Activity.Current.SetTag(...) |> ignore` (no `?.` in F#).
- MissingAggregateCheckFrame -> `if isNull stream.Aggregate then raise(UnknownAggregateException(typeof<Agg>, id))`.
RegisterEvents (AppendOne) + SaveChanges are MethodCall/already-F# frames.

Runnable sample (src/Samples/WolverineMartenAggregateFSharpSample):
- F# Counter aggregate + CounterStarted/Incremented events + an [<AggregateHandler>]
  IncrementHandler. Seeds a stream, then increments it (load via FetchForWriting,
  append the returned event). Postgres-backed, dynamic codegen (Wolverine.RuntimeCompilation
  + UseRuntimeCompilation, GH-2876). Verified end-to-end:
  "Incremented the Counter through the F# Wolverine + Marten aggregate handler."
- KEY F# constraint: Marten's convention Create/Apply aggregation is dispatched by the
  C#-only JasperFx.Events source generator, which does not run for F#. CounterProjection
  therefore overrides Evolve directly (an explicit per-event fold) — the supported escape
  hatch for F# self-aggregating types.

Compile-gate (src/Testing/Wolverine.MartenAggregate.FSharp{Tests,Fixture}):
- Renders the sample's real IncrementCounter chain to F# via the no-host
  HandlerGraph/AssembleTypes/GenerateFSharpCode path and dotnet-builds the fixture. The
  gate driver does not register the projection (codegen only emits FetchForWriting<Counter>).

Wire-up: sample + gate added to wolverine_fsharp.slnx; fsharp.yml runs the aggregate gate
as its own sequential step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jeremydmiller jeremydmiller merged commit f7f737e into main May 29, 2026
24 checks passed
This was referenced Jun 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant