diff --git a/.github/workflows/fsharp.yml b/.github/workflows/fsharp.yml
index 64951c9d5..914d80fb5 100644
--- a/.github/workflows/fsharp.yml
+++ b/.github/workflows/fsharp.yml
@@ -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:
@@ -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
diff --git a/Directory.Packages.props b/Directory.Packages.props
index a64e345e1..7ba1cb4c2 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -32,13 +32,13 @@
-
-
-
+
+
+
-
+
diff --git a/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/EnrollDbContextInTransaction.cs b/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/EnrollDbContextInTransaction.cs
index 81cc5d1bb..a8c31b8d3 100644
--- a/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/EnrollDbContextInTransaction.cs
+++ b/src/Persistence/Wolverine.EntityFrameworkCore/Codegen/EnrollDbContextInTransaction.cs
@@ -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,
+ // 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 FindVariables(IMethodVariables chain)
{
_scrapers = chain.FindVariable(typeof(IEnumerable));
@@ -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 FindVariables(IMethodVariables chain)
{
_envelopeTransaction = chain.FindVariable(typeof(EfCoreEnvelopeTransaction));
diff --git a/src/Samples/WolverineFSharpSample/Domain.fs b/src/Samples/WolverineFSharpSample/Domain.fs
new file mode 100644
index 000000000..f43f1f190
--- /dev/null
+++ b/src/Samples/WolverineFSharpSample/Domain.fs
@@ -0,0 +1,27 @@
+namespace WolverineFSharpSample
+
+open System
+open Microsoft.EntityFrameworkCore
+
+/// The EF Core entity persisted by the sample.
+[]
+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) =
+ inherit DbContext(options)
+
+ []
+ val mutable private items: DbSet-
+
+ member this.Items
+ with get () = this.items
+ and set v = this.items <- v
diff --git a/src/Samples/WolverineFSharpSample/Handlers.fs b/src/Samples/WolverineFSharpSample/Handlers.fs
new file mode 100644
index 000000000..c58c9adbb
--- /dev/null
+++ b/src/Samples/WolverineFSharpSample/Handlers.fs
@@ -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:
+/// [], 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 =
+ []
+ static member Handle(command: CreateItemCommand, db: ItemsDbContext) : ItemCreated =
+ let item = { Id = Guid.NewGuid(); Name = command.Name }
+ db.Items.Add(item) |> ignore
+ { Id = item.Id }
diff --git a/src/Samples/WolverineFSharpSample/Program.fs b/src/Samples/WolverineFSharpSample/Program.fs
new file mode 100644
index 000000000..176b42988
--- /dev/null
+++ b/src/Samples/WolverineFSharpSample/Program.fs
@@ -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).
+[]
+let connectionString =
+ "Host=localhost;Port=5433;Database=wolverine_fsharp_sample;Username=postgres;password=postgres"
+
+[]
+let main args =
+ let host =
+ Host
+ .CreateDefaultBuilder(args)
+ .UseWolverine(fun opts ->
+ // Register the DbContext with Wolverine's EF Core outbox integration.
+ opts.Services.AddDbContextWithWolverineIntegration(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() |> 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().Database.EnsureCreated() |> ignore)
+
+ host.Start()
+
+ // Demonstrate the F# EF Core handler end-to-end (dynamic codegen).
+ let bus = host.Services.GetRequiredService()
+ bus.InvokeAsync({ Name = "Sample" }).GetAwaiter().GetResult()
+ printfn "Created an Item through the F# Wolverine + EF Core handler."
+
+ host.StopAsync().GetAwaiter().GetResult()
+ 0
diff --git a/src/Samples/WolverineFSharpSample/README.md b/src/Samples/WolverineFSharpSample/README.md
new file mode 100644
index 000000000..df321da58
--- /dev/null
+++ b/src/Samples/WolverineFSharpSample/README.md
@@ -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 `[]` 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.
diff --git a/src/Samples/WolverineFSharpSample/WolverineFSharpSample.fsproj b/src/Samples/WolverineFSharpSample/WolverineFSharpSample.fsproj
new file mode 100644
index 000000000..470412e0f
--- /dev/null
+++ b/src/Samples/WolverineFSharpSample/WolverineFSharpSample.fsproj
@@ -0,0 +1,48 @@
+
+
+
+
+
+ Exe
+ net9.0
+ false
+
+
+
+ false
+ disable
+ false
+ false
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Testing/Wolverine.EfCore.FSharpFixture/Generated.fs b/src/Testing/Wolverine.EfCore.FSharpFixture/Generated.fs
new file mode 100644
index 000000000..21b485c40
--- /dev/null
+++ b/src/Testing/Wolverine.EfCore.FSharpFixture/Generated.fs
@@ -0,0 +1,56 @@
+//
+
+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) =
+ 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(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()
+ }
+
diff --git a/src/Testing/Wolverine.EfCore.FSharpFixture/Wolverine.EfCore.FSharpFixture.fsproj b/src/Testing/Wolverine.EfCore.FSharpFixture/Wolverine.EfCore.FSharpFixture.fsproj
new file mode 100644
index 000000000..7bd433bc3
--- /dev/null
+++ b/src/Testing/Wolverine.EfCore.FSharpFixture/Wolverine.EfCore.FSharpFixture.fsproj
@@ -0,0 +1,37 @@
+
+
+
+
+
+ net9.0
+ false
+
+
+ false
+ disable
+ false
+ false
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Testing/Wolverine.EfCore.FSharpTests/EfCoreFSharpCodegenSample.cs b/src/Testing/Wolverine.EfCore.FSharpTests/EfCoreFSharpCodegenSample.cs
new file mode 100644
index 000000000..75d1a675a
--- /dev/null
+++ b/src/Testing/Wolverine.EfCore.FSharpTests/EfCoreFSharpCodegenSample.cs
@@ -0,0 +1,76 @@
+using System.Runtime.CompilerServices;
+using JasperFx.CodeGeneration;
+using JasperFx.CodeGeneration.Model;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Diagnostics;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Wolverine;
+using Wolverine.EntityFrameworkCore;
+using Wolverine.Runtime.Handlers;
+using WolverineFSharpSample;
+
+namespace Wolverine.EfCore.FSharpTests;
+
+///
+/// Renders the sample's EF Core handler chain (issue GH-2969) as F#. Builds a minimal in-memory
+/// host that discovers with EF Core transactional middleware
+/// enabled, compiles the handler graph without starting it, and emits the adapter as F# via
+/// — exercising the EF Core transactional
+/// frames (enroll-in-outbox, SaveChanges, commit).
+///
+public static class EfCoreFSharpCodegenSample
+{
+ public static string GenerateCode()
+ {
+ DynamicCodeBuilder.WithinCodegenCommand = true;
+ try
+ {
+ using var host = Host.CreateDefaultBuilder()
+ .UseWolverine(opts =>
+ {
+ opts.Services.AddDbContextWithWolverineIntegration(o =>
+ o.UseInMemoryDatabase("items").ConfigureWarnings(w =>
+ w.Ignore(InMemoryEventId.TransactionIgnoredWarning)));
+
+ opts.UseEntityFrameworkCoreTransactions();
+ opts.Policies.AutoApplyTransactions();
+
+ opts.Discovery.DisableConventionalDiscovery();
+ opts.Discovery.IncludeType();
+ })
+ .Build();
+
+ // Force HandlerGraph.Compile() without starting the host (no Roslyn, no real DB).
+ _ = host.Services.GetServices().ToArray();
+
+ var handlerGraph = host.Services.GetRequiredService();
+ var chain = handlerGraph.ChainFor(typeof(CreateItemCommand))
+ ?? throw new InvalidOperationException("No handler chain was built for CreateItemCommand.");
+
+ var serviceVariableSource = host.Services.GetService();
+ var generatedAssembly = handlerGraph.StartAssembly(handlerGraph.Rules);
+ ((ICodeFile)chain).AssembleTypes(generatedAssembly);
+
+ return generatedAssembly.GenerateFSharpCode(serviceVariableSource);
+ }
+ finally
+ {
+ DynamicCodeBuilder.WithinCodegenCommand = false;
+ }
+ }
+
+ public static string DefaultGeneratedFilePath([CallerFilePath] string thisFile = "")
+ {
+ var testProjectDir = Path.GetDirectoryName(thisFile)!;
+ var srcTestingDir = Path.GetDirectoryName(testProjectDir)!;
+ return Path.Combine(srcTestingDir, "Wolverine.EfCore.FSharpFixture", "Generated.fs");
+ }
+
+ public static string FixtureProjectPath([CallerFilePath] string thisFile = "")
+ {
+ var testProjectDir = Path.GetDirectoryName(thisFile)!;
+ var srcTestingDir = Path.GetDirectoryName(testProjectDir)!;
+ return Path.Combine(srcTestingDir, "Wolverine.EfCore.FSharpFixture", "Wolverine.EfCore.FSharpFixture.fsproj");
+ }
+}
diff --git a/src/Testing/Wolverine.EfCore.FSharpTests/EfCoreFSharpCompileGate.cs b/src/Testing/Wolverine.EfCore.FSharpTests/EfCoreFSharpCompileGate.cs
new file mode 100644
index 000000000..c42e9d717
--- /dev/null
+++ b/src/Testing/Wolverine.EfCore.FSharpTests/EfCoreFSharpCompileGate.cs
@@ -0,0 +1,65 @@
+using System.Diagnostics;
+using Shouldly;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Wolverine.EfCore.FSharpTests;
+
+///
+/// The EF Core acceptance gate (issue GH-2969): regenerates the fixture's Generated.fs from
+/// the sample's EF Core handler chain, then shells dotnet build on the checked-in F# fixture
+/// and asserts a clean build. Mirrors the Core/Http surface gates.
+///
+public class EfCoreFSharpCompileGate
+{
+ private readonly ITestOutputHelper _output;
+
+ public EfCoreFSharpCompileGate(ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
+ [Fact]
+ public void generated_fsharp_compiles_via_dotnet_build()
+ {
+ var code = EfCoreFSharpCodegenSample.GenerateCode();
+ var generatedFile = EfCoreFSharpCodegenSample.DefaultGeneratedFilePath();
+ File.WriteAllText(generatedFile, code);
+
+ File.Exists(generatedFile).ShouldBeTrue();
+ _output.WriteLine(code);
+
+ var fixtureProject = EfCoreFSharpCodegenSample.FixtureProjectPath();
+ var (exitCode, output) = RunDotnet($"build \"{fixtureProject}\" -c Debug --nologo");
+
+ // Retry once on the transient FS0193 internal-compiler crash or a concurrent-build file lock.
+ if (exitCode != 0 && (output.Contains("FS0193") || output.Contains("internal error")
+ || output.Contains("being used by another process")
+ || output.Contains("MSB3883")))
+ {
+ (exitCode, output) = RunDotnet($"build \"{fixtureProject}\" -c Debug --nologo");
+ }
+
+ _output.WriteLine(output);
+ exitCode.ShouldBe(0);
+ }
+
+ private static (int ExitCode, string Output) RunDotnet(string arguments)
+ {
+ var info = new ProcessStartInfo("dotnet", arguments)
+ {
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false
+ };
+ info.Environment["DOTNET_CLI_USE_MSBUILD_SERVER"] = "0";
+ info.Environment["MSBUILDDISABLENODEREUSE"] = "1";
+
+ using var process = Process.Start(info)!;
+ var stdout = process.StandardOutput.ReadToEndAsync();
+ var stderr = process.StandardError.ReadToEndAsync();
+ process.WaitForExit();
+
+ return (process.ExitCode, stdout.GetAwaiter().GetResult() + stderr.GetAwaiter().GetResult());
+ }
+}
diff --git a/src/Testing/Wolverine.EfCore.FSharpTests/Wolverine.EfCore.FSharpTests.csproj b/src/Testing/Wolverine.EfCore.FSharpTests/Wolverine.EfCore.FSharpTests.csproj
new file mode 100644
index 000000000..a992ee36c
--- /dev/null
+++ b/src/Testing/Wolverine.EfCore.FSharpTests/Wolverine.EfCore.FSharpTests.csproj
@@ -0,0 +1,32 @@
+
+
+
+
+
+ net9.0
+ false
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
diff --git a/wolverine_fsharp.slnx b/wolverine_fsharp.slnx
index b3ae2c921..afc4ca0c5 100644
--- a/wolverine_fsharp.slnx
+++ b/wolverine_fsharp.slnx
@@ -16,9 +16,13 @@
+
+
+
+