diff --git a/src/Testing/Wolverine.Core.FSharpContracts/Contracts.cs b/src/Testing/Wolverine.Core.FSharpContracts/Contracts.cs
index b50b8f18e..e54b82b1f 100644
--- a/src/Testing/Wolverine.Core.FSharpContracts/Contracts.cs
+++ b/src/Testing/Wolverine.Core.FSharpContracts/Contracts.cs
@@ -1,3 +1,4 @@
+using Wolverine.Attributes;
using Wolverine.Runtime;
namespace Wolverine.Core.FSharpContracts;
@@ -17,3 +18,81 @@ public interface IFoundationProbe
{
void Run(IWolverineRuntime runtime);
}
+
+// -----------------------------------------------------------------------------
+// Phase A handler surface (issue GH-2969): the smallest in-process handler that
+// exercises message extraction, simple validation (abort), and a cascading
+// message. The driver discovers NameHandler, renders its real Wolverine handler
+// chain to F#, and the fixture compiles the generated adapter against these
+// public types. (Authoring handlers in F# is the separate concern of #2968;
+// this audit only proves the codegen frames emit F#.)
+// -----------------------------------------------------------------------------
+
+/// The command handled by . The [Audit] member also
+/// exercises AuditToActivityFrame.
+public record CreateName([property: Audit] string Name);
+
+/// The event cascaded back out by .
+public record NameCreated(string Name);
+
+///
+/// A minimal async in-process handler with simple validation + a cascading return. Produces, in
+/// chain order: message extraction, OTel tags, a Validate continuation (abort-if-invalid),
+/// the handler call, and a cascaded . Exercises the abort guard inside a
+/// task { } body.
+///
+public class NameHandler
+{
+ public IEnumerable Validate(CreateName command)
+ {
+ return string.IsNullOrWhiteSpace(command.Name)
+ ? new[] { "Name is required" }
+ : Array.Empty();
+ }
+
+ public NameCreated Handle(CreateName command)
+ {
+ return new NameCreated(command.Name);
+ }
+}
+
+/// The command handled by .
+public record CheckThing(string Value);
+
+///
+/// A synchronous handler whose Before returns a , exercising
+/// RequirementResultHandlerFrame and the non-task { } abort path (the method returns
+/// Task and the abort branch yields Task.CompletedTask).
+///
+public class CheckThingHandler
+{
+ public RequirementResult Before(CheckThing command)
+ {
+ return string.IsNullOrEmpty(command.Value)
+ ? new RequirementResult(HandlerContinuation.Stop, new[] { "Value is required" })
+ : RequirementResult.AllGood();
+ }
+
+ public void Handle(CheckThing command)
+ {
+ }
+}
+
+/// The command handled by .
+public record Gate(bool Ok);
+
+///
+/// A synchronous handler whose Before returns a ,
+/// exercising HandlerContinuationFrame.
+///
+public class GateHandler
+{
+ public HandlerContinuation Before(Gate command)
+ {
+ return command.Ok ? HandlerContinuation.Continue : HandlerContinuation.Stop;
+ }
+
+ public void Handle(Gate command)
+ {
+ }
+}
diff --git a/src/Testing/Wolverine.Core.FSharpFixture/Generated.fs b/src/Testing/Wolverine.Core.FSharpFixture/Generated.fs
index 1df7551fd..26e4157d3 100644
--- a/src/Testing/Wolverine.Core.FSharpFixture/Generated.fs
+++ b/src/Testing/Wolverine.Core.FSharpFixture/Generated.fs
@@ -1,15 +1,89 @@
//
-namespace Wolverine.Core.FSharpFixture.Generated
+namespace Internal.Generated.WolverineHandlers
+open Microsoft.Extensions.Logging
open System
-open Wolverine.Core.FSharpContracts
+open System.Threading
+open System.Threading.Tasks
open Wolverine.Runtime
+open Wolverine.Runtime.Handlers
-type GeneratedFoundationProbe() =
- interface Wolverine.Core.FSharpContracts.IFoundationProbe with
- member _.Run(runtime: Wolverine.Runtime.IWolverineRuntime) : unit =
- // Generated by Wolverine F# code generation (GH-2969 foundation probe)
- let messageContext = Wolverine.Runtime.MessageContext(runtime)
+type CheckThingHandler649476295(loggerForMessage: Microsoft.Extensions.Logging.ILogger) =
+ inherit Wolverine.Runtime.Handlers.MessageHandler()
+ let _loggerForMessage = loggerForMessage
+
+ override _.HandleAsync(context: Wolverine.Runtime.MessageContext, cancellation: System.Threading.CancellationToken) : System.Threading.Tasks.Task =
+ // The actual message body
+ let checkThing = context.Envelope.Message :?> Wolverine.Core.FSharpContracts.CheckThing
+
+ Wolverine.Runtime.WolverineTracing.ApplyExecutionDiagnosticTags(System.Diagnostics.Activity.Current, context.Envelope)
+ if not (isNull System.Diagnostics.Activity.Current) then
+ System.Diagnostics.Activity.Current.SetTag("message.handler", "Wolverine.Core.FSharpContracts.CheckThingHandler") |> ignore
+ System.Diagnostics.Activity.Current.SetTag("handler.type", "Wolverine.Core.FSharpContracts.CheckThingHandler") |> ignore
+ let checkThingHandler = Wolverine.Core.FSharpContracts.CheckThingHandler()
+ let requirementResult1 = checkThingHandler.Before(checkThing)
+ // Check RequirementResult and abort if Branch == Stop
+ if Wolverine.Middleware.RequirementResultContinuationPolicy.ShouldStop((_loggerForMessage :> Microsoft.Extensions.Logging.ILogger), requirementResult1) then
+ ()
+ else
+
+ // The actual message execution
+ checkThingHandler.Handle(checkThing)
+
+ System.Threading.Tasks.Task.CompletedTask
+
+type CreateNameHandler1923366998(loggerForMessage: Microsoft.Extensions.Logging.ILogger) =
+ inherit Wolverine.Runtime.Handlers.MessageHandler()
+ let _loggerForMessage = loggerForMessage
+
+ override _.HandleAsync(context: Wolverine.Runtime.MessageContext, cancellation: System.Threading.CancellationToken) : System.Threading.Tasks.Task =
+ task {
+ // The actual message body
+ let createName = context.Envelope.Message :?> Wolverine.Core.FSharpContracts.CreateName
+
+ Wolverine.Runtime.WolverineTracing.ApplyExecutionDiagnosticTags(System.Diagnostics.Activity.Current, context.Envelope)
+ // Application-specific Open Telemetry auditing
+ if not (isNull System.Diagnostics.Activity.Current) then
+ System.Diagnostics.Activity.Current.SetTag("name", createName.Name) |> ignore
+ if not (isNull System.Diagnostics.Activity.Current) then
+ System.Diagnostics.Activity.Current.SetTag("message.handler", "Wolverine.Core.FSharpContracts.NameHandler") |> ignore
+ System.Diagnostics.Activity.Current.SetTag("handler.type", "Wolverine.Core.FSharpContracts.NameHandler") |> ignore
+ let nameHandler = Wolverine.Core.FSharpContracts.NameHandler()
+ let stringValueIEnumerable1 = nameHandler.Validate(createName)
+ // Check for any simple validation messages and abort if any exist
+ if Wolverine.Middleware.SimpleValidationContinuationPolicy.LogValidationMessages((_loggerForMessage :> Microsoft.Extensions.Logging.ILogger), stringValueIEnumerable1) then
+ ()
+ else
+
+ // The actual message execution
+ let outgoing1 = nameHandler.Handle(createName)
+
+
+ // Outgoing, cascaded message
+ do! context.EnqueueCascadingAsync(outgoing1)
+
+ }
+
+type GateHandler1696712162() =
+ inherit Wolverine.Runtime.Handlers.MessageHandler()
+ override _.HandleAsync(context: Wolverine.Runtime.MessageContext, cancellation: System.Threading.CancellationToken) : System.Threading.Tasks.Task =
+ // The actual message body
+ let gate = context.Envelope.Message :?> Wolverine.Core.FSharpContracts.Gate
+
+ Wolverine.Runtime.WolverineTracing.ApplyExecutionDiagnosticTags(System.Diagnostics.Activity.Current, context.Envelope)
+ if not (isNull System.Diagnostics.Activity.Current) then
+ System.Diagnostics.Activity.Current.SetTag("message.handler", "Wolverine.Core.FSharpContracts.GateHandler") |> ignore
+ System.Diagnostics.Activity.Current.SetTag("handler.type", "Wolverine.Core.FSharpContracts.GateHandler") |> ignore
+ let gateHandler = Wolverine.Core.FSharpContracts.GateHandler()
+ let result_of_Before1 = gateHandler.Before(gate)
+ // Evaluate whether or not the execution should stop based on the HandlerContinuation value
+ if result_of_Before1 = Wolverine.HandlerContinuation.Stop then
()
+ else
+
+ // The actual message execution
+ gateHandler.Handle(gate)
+
+ System.Threading.Tasks.Task.CompletedTask
diff --git a/src/Testing/Wolverine.Core.FSharpTests/FSharpCodegenSample.cs b/src/Testing/Wolverine.Core.FSharpTests/FSharpCodegenSample.cs
index 3aa17e3f8..9494e8888 100644
--- a/src/Testing/Wolverine.Core.FSharpTests/FSharpCodegenSample.cs
+++ b/src/Testing/Wolverine.Core.FSharpTests/FSharpCodegenSample.cs
@@ -1,50 +1,78 @@
using System.Runtime.CompilerServices;
using JasperFx.CodeGeneration;
-using JasperFx.CodeGeneration.Frames;
+using JasperFx.CodeGeneration.Model;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Wolverine;
using Wolverine.Core.FSharpContracts;
using Wolverine.Runtime.Handlers;
namespace Wolverine.Core.FSharpTests;
///
-/// Builds the "milestone 0" for Wolverine's F# code generation
-/// (issue GH-2969): a single generated type that implements and
-/// exercises the smallest real Wolverine frame — — alongside
-/// the JasperFx-provided and .
+/// Renders the Phase A handler chain (issue GH-2969) as F#. Builds a minimal in-memory Wolverine
+/// host that discovers , compiles its real handler chain (message
+/// extraction → simple validation abort → handler call → cascaded message), and emits the adapter
+/// as F# via — the same path
+/// WolverineDiagnosticsCommand.GenerateSingleFileCode uses for C#, swapping
+/// GenerateCode for GenerateFSharpCode.
///
-///
-/// This is the foundation harness: it proves the regenerate → compile pipeline end-to-end with a
-/// deterministic, fully-controlled assembly. Phase A replaces this hand-built assembly with a real
-/// HandlerGraph rendering (handlerGraph.StartAssembly(rules) +
-/// file.AssembleTypes(assembly) + assembly.GenerateFSharpCode()) and richer handlers.
-///
public static class FSharpCodegenSample
{
- public const string GeneratedNamespace = "Wolverine.Core.FSharpFixture.Generated";
-
- public static GeneratedAssembly BuildSampleAssembly()
+ /// Builds the real handler chain for and renders it as F# source.
+ public static string GenerateCode()
{
- var assembly = new GeneratedAssembly(new GenerationRules(GeneratedNamespace));
+ // Apply lightweight codegen mode so the host stands up without transports / persistence and so
+ // resolving the code-file collections compiles the handler graph without starting it.
+ DynamicCodeBuilder.WithinCodegenCommand = true;
+ try
+ {
+ using var host = Host.CreateDefaultBuilder()
+ .UseWolverine(opts =>
+ {
+ opts.Discovery.DisableConventionalDiscovery();
+ opts.Discovery.IncludeType();
+ opts.Discovery.IncludeType();
+ opts.Discovery.IncludeType();
- var type = assembly.AddType("GeneratedFoundationProbe", typeof(IFoundationProbe));
- var method = type.MethodFor(nameof(IFoundationProbe.Run));
+ // Inserts ApplyExecutionDiagnosticTagsFrame at the head of every chain.
+ opts.Tracking.HandlerExecutionDiagnosticsEnabled = true;
+ })
+ .Build();
- // A leading comment (CommentFrame emits identical F# / C#).
- method.Frames.Add(new CommentFrame("Generated by Wolverine F# code generation (GH-2969 foundation probe)"));
+ // Force HandlerGraph.Compile() to run *without starting the host* (no Roslyn, no
+ // transport/persistence connections) — exactly as the describe-handlers command does.
+ _ = host.Services.GetServices().ToArray();
- // The headline Wolverine frame under test: `let messageContext = MessageContext(runtime)`.
- method.Frames.Add(new MessageContextFrame());
+ var handlerGraph = host.Services.GetRequiredService();
+ var serviceVariableSource = host.Services.GetService();
+ var generatedAssembly = handlerGraph.StartAssembly(handlerGraph.Rules);
- // A trailing unit expression so the F# member body is complete: `()`.
- method.Frames.Add(new ReturnFrame());
+ // Render every handler chain defined in the contracts assembly into one Generated.fs so the
+ // compile gate exercises the whole Phase A frame set (validation, requirement-result, the
+ // HandlerContinuation gate, OTel/audit tags, cascading) in one build.
+ var contractsAssembly = typeof(CreateName).Assembly;
+ var chains = handlerGraph.AllChains()
+ .Where(c => c.MessageType.Assembly == contractsAssembly)
+ .OrderBy(c => c.MessageType.Name)
+ .ToArray();
- return assembly;
- }
+ if (chains.Length == 0)
+ {
+ throw new InvalidOperationException("No handler chains were built for the contracts assembly.");
+ }
- /// Builds the sample and renders it as F# source.
- public static string GenerateCode()
- {
- return BuildSampleAssembly().GenerateFSharpCode();
+ foreach (var chain in chains)
+ {
+ ((ICodeFile)chain).AssembleTypes(generatedAssembly);
+ }
+
+ return generatedAssembly.GenerateFSharpCode(serviceVariableSource);
+ }
+ finally
+ {
+ DynamicCodeBuilder.WithinCodegenCommand = false;
+ }
}
///
diff --git a/src/Wolverine/Configuration/FSharpEmitHelpers.cs b/src/Wolverine/Configuration/FSharpEmitHelpers.cs
new file mode 100644
index 000000000..310ec17cc
--- /dev/null
+++ b/src/Wolverine/Configuration/FSharpEmitHelpers.cs
@@ -0,0 +1,60 @@
+using JasperFx.CodeGeneration;
+using JasperFx.CodeGeneration.Frames;
+using JasperFx.CodeGeneration.Model;
+using JasperFx.Core.Reflection;
+
+namespace Wolverine.Configuration;
+
+///
+/// Small shared helpers for emitting F# from Wolverine frames (issue GH-2969). These cover gaps in
+/// the JasperFx-layer model where a value's C# rendering is not valid F#, plus the recurring
+/// "abort-or-continue" continuation shape.
+///
+internal static class FSharpEmitHelpers
+{
+ ///
+ /// Emits the F# form of a C# "abort the handler if is
+ /// true, otherwise run the rest of the chain" guard. F# has no early return, so the
+ /// remainder of the chain () is rendered inside the else branch.
+ /// The abort branch is a no-op (): the method's Task result comes from the enclosing
+ /// task { } body or the machinery-appended trailing Task.CompletedTask, so both
+ /// branches are unit and the guard sits cleanly in statement position. Mirrors the C#
+ /// if (cond) return; <next>.
+ ///
+ public static void WriteAbortGuard(ISourceWriter writer, GeneratedMethod method, string conditionExpression,
+ Frame? next)
+ {
+ writer.Write($"BLOCK:if {conditionExpression} then");
+ writer.Write("()");
+ writer.FinishBlock();
+
+ writer.Write("BLOCK:else");
+ if (next != null)
+ {
+ next.GenerateFSharpCode(method, writer);
+ }
+ else
+ {
+ writer.Write("()");
+ }
+
+ writer.FinishBlock();
+ }
+
+ ///
+ /// The F# rendering of a variable's usage. F# has no C-style cast, but
+ /// bakes a C# ((Type)x) cast into its
+ /// (e.g. the injected ILogger<TMessage> handed to validation frames as ILogger).
+ /// Rewrite that as an F# upcast (x :> Type); everything else uses its usage verbatim.
+ ///
+ ///
+ /// The proper fix is an F#-aware usage on JasperFx's CastVariable itself; tracked as an
+ /// upstream JasperFx gap. Until then this keeps the audit moving without leaving Wolverine.
+ ///
+ public static string FSharpUsage(Variable variable)
+ {
+ return variable is CastVariable cast
+ ? $"({cast.Inner.Usage} :> {cast.VariableType.FSharpName()})"
+ : variable.Usage;
+ }
+}
diff --git a/src/Wolverine/Logging/AuditToActivityFrame.cs b/src/Wolverine/Logging/AuditToActivityFrame.cs
index 4a02c1f20..3a0b56a27 100644
--- a/src/Wolverine/Logging/AuditToActivityFrame.cs
+++ b/src/Wolverine/Logging/AuditToActivityFrame.cs
@@ -36,4 +36,27 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
Next?.GenerateCode(method, writer);
}
+
+ public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
+ {
+ writer.WriteComment("Application-specific Open Telemetry auditing");
+
+ // F# has no null-conditional operator, and SetTag returns the Activity (discarded), so guard
+ // Activity.Current once and pipe each tagging call to `ignore`. Skip the guard entirely when
+ // there are no audited members so the `if` body is never empty.
+ if (_members.Count > 0)
+ {
+ var current = $"{typeof(Activity).FSharpName()}.{nameof(Activity.Current)}";
+ writer.Write($"BLOCK:if not (isNull {current}) then");
+ foreach (var member in _members)
+ {
+ writer.Write(
+ $"{current}.{nameof(Activity.SetTag)}(\"{member.OpenTelemetryName}\", {FSharpEmitHelpers.FSharpUsage(_input!)}.{member.Member.Name}) |> ignore");
+ }
+
+ writer.FinishBlock();
+ }
+
+ Next?.GenerateFSharpCode(method, writer);
+ }
}
\ No newline at end of file
diff --git a/src/Wolverine/Logging/TagHandlerFrame.cs b/src/Wolverine/Logging/TagHandlerFrame.cs
index 8578929d0..0902c2177 100644
--- a/src/Wolverine/Logging/TagHandlerFrame.cs
+++ b/src/Wolverine/Logging/TagHandlerFrame.cs
@@ -45,4 +45,24 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
Next?.GenerateCode(method, writer);
}
+
+ public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
+ {
+ if (_chain.HandlerCalls().Length == 1)
+ {
+ // F# has no null-conditional operator, and SetTag returns the Activity (which must be
+ // discarded), so guard Activity.Current explicitly and pipe each call to `ignore`.
+ var current = $"{typeof(Activity).FSharpName()}.{nameof(Activity.Current)}";
+ var handlerTypeName = _chain.HandlerCalls()[0].HandlerType.FullNameInCode();
+
+ writer.Write($"BLOCK:if not (isNull {current}) then");
+ writer.Write(
+ $"{current}.{nameof(Activity.SetTag)}(\"{WolverineTracing.MessageHandler}\", \"{handlerTypeName}\") |> ignore");
+ writer.Write(
+ $"{current}.{nameof(Activity.SetTag)}(\"{WolverineTracing.HandlerType}\", \"{handlerTypeName}\") |> ignore");
+ writer.FinishBlock();
+ }
+
+ Next?.GenerateFSharpCode(method, writer);
+ }
}
\ No newline at end of file
diff --git a/src/Wolverine/Middleware/HandlerContinuationFrame.cs b/src/Wolverine/Middleware/HandlerContinuationFrame.cs
index 2aa0a995c..f3c639a1e 100644
--- a/src/Wolverine/Middleware/HandlerContinuationFrame.cs
+++ b/src/Wolverine/Middleware/HandlerContinuationFrame.cs
@@ -2,6 +2,7 @@
using JasperFx.CodeGeneration.Frames;
using JasperFx.CodeGeneration.Model;
using JasperFx.Core.Reflection;
+using Wolverine.Configuration;
namespace Wolverine.Middleware;
@@ -36,4 +37,13 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
Next?.GenerateCode(method, writer);
}
+
+ public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
+ {
+ // F# has no early `return`; render the remainder of the chain inside the `else` branch.
+ writer.WriteComment("Evaluate whether or not the execution should stop based on the HandlerContinuation value");
+ var condition =
+ $"{_variable.Usage} = {typeof(HandlerContinuation).FSharpName()}.{nameof(HandlerContinuation.Stop)}";
+ FSharpEmitHelpers.WriteAbortGuard(writer, method, condition, Next);
+ }
}
\ No newline at end of file
diff --git a/src/Wolverine/Middleware/RequirementResultContinuationPolicy.cs b/src/Wolverine/Middleware/RequirementResultContinuationPolicy.cs
index a0ca27517..6b17078e5 100644
--- a/src/Wolverine/Middleware/RequirementResultContinuationPolicy.cs
+++ b/src/Wolverine/Middleware/RequirementResultContinuationPolicy.cs
@@ -114,4 +114,13 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
Next?.GenerateCode(method, writer);
}
+
+ public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
+ {
+ // F# has no early `return`; render the remainder of the chain inside the `else` branch.
+ writer.WriteComment("Check RequirementResult and abort if Branch == Stop");
+ var condition =
+ $"{typeof(RequirementResultContinuationPolicy).FSharpName()}.{nameof(RequirementResultContinuationPolicy.ShouldStop)}({FSharpEmitHelpers.FSharpUsage(_logger!)}, {_variable.Usage})";
+ FSharpEmitHelpers.WriteAbortGuard(writer, method, condition, Next);
+ }
}
diff --git a/src/Wolverine/Middleware/SimpleValidationContinuationPolicy.cs b/src/Wolverine/Middleware/SimpleValidationContinuationPolicy.cs
index 322654037..5b759218a 100644
--- a/src/Wolverine/Middleware/SimpleValidationContinuationPolicy.cs
+++ b/src/Wolverine/Middleware/SimpleValidationContinuationPolicy.cs
@@ -133,4 +133,14 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
Next?.GenerateCode(method, writer);
}
+
+ public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
+ {
+ // F# has no early `return`; WriteAbortGuard renders the remainder of the chain inside the
+ // `else` branch so the abort path simply yields the method's result.
+ writer.WriteComment("Check for any simple validation messages and abort if any exist");
+ var condition =
+ $"{typeof(SimpleValidationContinuationPolicy).FSharpName()}.{nameof(SimpleValidationContinuationPolicy.LogValidationMessages)}({FSharpEmitHelpers.FSharpUsage(_logger!)}, {_variable.Usage})";
+ FSharpEmitHelpers.WriteAbortGuard(writer, method, condition, Next);
+ }
}
diff --git a/src/Wolverine/Middleware/TryCatchFinallyFrame.cs b/src/Wolverine/Middleware/TryCatchFinallyFrame.cs
index 2e354eaae..d35abd156 100644
--- a/src/Wolverine/Middleware/TryCatchFinallyFrame.cs
+++ b/src/Wolverine/Middleware/TryCatchFinallyFrame.cs
@@ -5,6 +5,7 @@
using JasperFx.Core.Reflection;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
+using Wolverine.Configuration;
namespace Wolverine.Middleware;
@@ -13,6 +14,10 @@ namespace Wolverine.Middleware;
/// Catch blocks are ordered by exception type specificity (most derived first).
/// Finally blocks execute cleanup code regardless of exceptions.
///
+[FSharpEmit(Skip = true,
+ Reason = "Imperatively rewires child frames' Next pointers and renders multiple inheritance-ordered " +
+ "catch blocks; porting to F#'s try/with-as-an-expression model is deferred past Phase A. " +
+ "Unreachable in a minimal in-process F# handler chain.")]
public class TryCatchFinallyFrame : Frame
{
private readonly List _catchBlocks = [];
diff --git a/src/Wolverine/Runtime/ApplyExecutionDiagnosticTagsFrame.cs b/src/Wolverine/Runtime/ApplyExecutionDiagnosticTagsFrame.cs
index 50dd4e257..184de7f69 100644
--- a/src/Wolverine/Runtime/ApplyExecutionDiagnosticTagsFrame.cs
+++ b/src/Wolverine/Runtime/ApplyExecutionDiagnosticTagsFrame.cs
@@ -43,4 +43,14 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
Next?.GenerateCode(method, writer);
}
+
+ public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
+ {
+ // A void static call; the helper short-circuits on a null activity so no guard is needed.
+ writer.Write(
+ $"{typeof(WolverineTracing).FSharpName()}.{nameof(WolverineTracing.ApplyExecutionDiagnosticTags)}(" +
+ $"{typeof(Activity).FSharpName()}.{nameof(Activity.Current)}, {_envelope!.Usage})");
+
+ Next?.GenerateFSharpCode(method, writer);
+ }
}
diff --git a/src/Wolverine/Runtime/Handlers/MessageFrame.cs b/src/Wolverine/Runtime/Handlers/MessageFrame.cs
index bf14cc6d8..4fb9d8991 100644
--- a/src/Wolverine/Runtime/Handlers/MessageFrame.cs
+++ b/src/Wolverine/Runtime/Handlers/MessageFrame.cs
@@ -24,4 +24,15 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
writer.BlankLine();
Next?.GenerateCode(method, writer);
}
+
+ public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
+ {
+ // F#: `envelope.Message` is `obj`, so the cast to the concrete message type is a dynamic
+ // downcast (`:?>`): `let message = envelope.Message :?> SomeMessage`.
+ writer.WriteComment("The actual message body");
+ writer.Write(
+ $"{_message.FSharpAssignmentUsage} = {_envelope.Usage}.{nameof(Envelope.Message)} :?> {_message.VariableType.FSharpName()}");
+ writer.BlankLine();
+ Next?.GenerateFSharpCode(method, writer);
+ }
}
\ No newline at end of file
diff --git a/src/Wolverine/Runtime/Handlers/ReadEnvelopeHeaderFrame.cs b/src/Wolverine/Runtime/Handlers/ReadEnvelopeHeaderFrame.cs
index 388066520..c3295a0de 100644
--- a/src/Wolverine/Runtime/Handlers/ReadEnvelopeHeaderFrame.cs
+++ b/src/Wolverine/Runtime/Handlers/ReadEnvelopeHeaderFrame.cs
@@ -2,6 +2,7 @@
using JasperFx.CodeGeneration.Frames;
using JasperFx.CodeGeneration.Model;
using JasperFx.Core.Reflection;
+using Wolverine.Configuration;
namespace Wolverine.Runtime.Handlers;
@@ -9,6 +10,10 @@ namespace Wolverine.Runtime.Handlers;
/// Code generation frame that reads a header value from the message Envelope.
/// Supports string and typed values via TryParse.
///
+[FSharpEmit(Skip = true,
+ Reason = "Emits out-var TryGetHeader/TryParse and a reassigned `default` local — none of which map " +
+ "cleanly to F#. A tuple-return rework is deferred past Phase A; unreachable in a minimal " +
+ "in-process F# handler chain (only injected when binding a parameter from an envelope header).")]
internal class ReadEnvelopeHeaderFrame : SyncFrame
{
private readonly string _headerKey;