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;