diff --git a/src/Http/Wolverine.Http/CodeGen/IReadHttpFrame.cs b/src/Http/Wolverine.Http/CodeGen/IReadHttpFrame.cs
index 1a68c302e..bebd2791e 100644
--- a/src/Http/Wolverine.Http/CodeGen/IReadHttpFrame.cs
+++ b/src/Http/Wolverine.Http/CodeGen/IReadHttpFrame.cs
@@ -3,6 +3,7 @@
using JasperFx.CodeGeneration.Model;
using JasperFx.Core.Reflection;
using Microsoft.AspNetCore.Http;
+using Wolverine.Configuration;
namespace Wolverine.Http.CodeGen;
@@ -133,7 +134,7 @@ private void writeStringValueFSharp(GeneratedMethod method, ISourceWriter writer
// A missing required route value 404s and aborts. F# has no early return, so the rest of
// the chain renders inside the else branch.
writer.Write($"BLOCK:if isNull {Variable.Usage} then");
- writeStatusAbort(writer);
+ writeStatusAbort(method, writer);
writer.FinishBlock();
writer.Write("BLOCK:else");
WriteNextOrUnit(method, writer);
@@ -167,7 +168,7 @@ private void writeParsedValueFSharp(GeneratedMethod method, ISourceWriter writer
// Required route value: null or parse-failure 404s + aborts; success binds the variable and
// renders the rest of the chain in the success match arm (no F# early return).
writer.Write($"BLOCK:if isNull {raw} then");
- writeStatusAbort(writer);
+ writeStatusAbort(method, writer);
writer.FinishBlock();
writer.Write("BLOCK:else");
writer.Write($"BLOCK:match {fsharpTryParse(raw)} with");
@@ -175,7 +176,7 @@ private void writeParsedValueFSharp(GeneratedMethod method, ISourceWriter writer
WriteNextOrUnit(method, writer);
writer.FinishBlock();
writer.Write("BLOCK:| _ ->");
- writeStatusAbort(writer);
+ writeStatusAbort(method, writer);
writer.FinishBlock();
writer.FinishBlock(); // match
writer.FinishBlock(); // else
@@ -197,14 +198,14 @@ private void WriteNextOrUnit(GeneratedMethod method, ISourceWriter writer)
}
else
{
- writer.Write("()");
+ writer.Write(FSharpEmitHelpers.AbortExpression(method));
}
}
- private static void writeStatusAbort(ISourceWriter writer)
+ private static void writeStatusAbort(GeneratedMethod method, ISourceWriter writer)
{
writer.Write($"httpContext.Response.{nameof(HttpResponse.StatusCode)} <- 404");
- writer.Write("()");
+ writer.Write(FSharpEmitHelpers.AbortExpression(method));
}
// The F# tuple form of the C# out-parameter TryParse (F# auto-tuples the out arg).
diff --git a/src/Http/Wolverine.Http/CodeGen/ResultContinuationPolicy.cs b/src/Http/Wolverine.Http/CodeGen/ResultContinuationPolicy.cs
index d5ac0d270..4995843d8 100644
--- a/src/Http/Wolverine.Http/CodeGen/ResultContinuationPolicy.cs
+++ b/src/Http/Wolverine.Http/CodeGen/ResultContinuationPolicy.cs
@@ -73,4 +73,9 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
Next?.GenerateCode(method, writer);
}
+
+ // NOTE: F# emit for MaybeEndWithResultFrame is deferred. The frame is added by ResultContinuationPolicy
+ // during a full endpoint compile (MapWolverineEndpoints); the no-host ChainFor harness used by the F#
+ // fixture doesn't apply continuation policies, so it can't exercise this frame yet. It will be done
+ // with the behavioural (host-based) harness (GH-2969).
}
\ No newline at end of file
diff --git a/src/Testing/Wolverine.Http.FSharpContracts/Endpoints.cs b/src/Testing/Wolverine.Http.FSharpContracts/Endpoints.cs
index 6f80209e5..12157eac5 100644
--- a/src/Testing/Wolverine.Http.FSharpContracts/Endpoints.cs
+++ b/src/Testing/Wolverine.Http.FSharpContracts/Endpoints.cs
@@ -1,3 +1,5 @@
+using Microsoft.AspNetCore.Http;
+
namespace Wolverine.Http.FSharpContracts;
/// The JSON body bound by .
@@ -52,4 +54,13 @@ public string Paged(int page)
{
return $"page {page}";
}
+
+ // IResult return: a terminal IResult endpoint. The handler's IResult is executed directly as the
+ // returned Task (ReturnFromLastNode); combined with a route value it exercises the AsyncMode-aware
+ // abort (a missing route value yields Task.CompletedTask, not unit).
+ [WolverineGet("/fsharp/result/{id}")]
+ public IResult GetResult(string id)
+ {
+ return string.IsNullOrEmpty(id) ? Results.NotFound() : Results.Ok($"thing {id}");
+ }
}
diff --git a/src/Testing/Wolverine.Http.FSharpFixture/Generated.fs b/src/Testing/Wolverine.Http.FSharpFixture/Generated.fs
index 4b35de242..cc4394e2e 100644
--- a/src/Testing/Wolverine.Http.FSharpFixture/Generated.fs
+++ b/src/Testing/Wolverine.Http.FSharpFixture/Generated.fs
@@ -122,3 +122,20 @@ type GET_fsharp_paged(wolverineHttpOptions: Wolverine.Http.WolverineHttpOptions)
do! Wolverine.Http.HttpHandler.WriteString(httpContext, result_of_Paged)
}
+type GET_fsharp_result_id(wolverineHttpOptions: Wolverine.Http.WolverineHttpOptions) =
+ inherit Wolverine.Http.HttpHandler(wolverineHttpOptions)
+ let _wolverineHttpOptions = wolverineHttpOptions
+
+ override this.Handle(httpContext: Microsoft.AspNetCore.Http.HttpContext) : System.Threading.Tasks.Task =
+ let id = (httpContext.GetRouteValue("id") :?> string)
+ if isNull id then
+ httpContext.Response.StatusCode <- 404
+ System.Threading.Tasks.Task.CompletedTask
+ else
+ let thingEndpoints = Wolverine.Http.FSharpContracts.ThingEndpoints()
+
+ // The actual HTTP request handler execution
+ let result = thingEndpoints.GetResult(id)
+
+ result.ExecuteAsync(httpContext)
+
diff --git a/src/Testing/Wolverine.Http.FSharpTests/HttpFSharpCodegenSample.cs b/src/Testing/Wolverine.Http.FSharpTests/HttpFSharpCodegenSample.cs
index c468a3fd5..06ba51b74 100644
--- a/src/Testing/Wolverine.Http.FSharpTests/HttpFSharpCodegenSample.cs
+++ b/src/Testing/Wolverine.Http.FSharpTests/HttpFSharpCodegenSample.cs
@@ -37,7 +37,8 @@ public static string GenerateCode()
HttpChain.ChainFor(x => x.GetById(null!), httpGraph),
HttpChain.ChainFor(x => x.Search(null!), httpGraph),
HttpChain.ChainFor(x => x.GetItems(null!, 0), httpGraph), // typed int route value
- HttpChain.ChainFor(x => x.Paged(0), httpGraph) // typed int query value
+ HttpChain.ChainFor(x => x.Paged(0), httpGraph), // typed int query value
+ HttpChain.ChainFor(x => x.GetResult(null!), httpGraph) // terminal IResult + route value
};
var generatedAssembly = httpGraph.StartAssembly(httpGraph.Rules);
diff --git a/src/Wolverine/Configuration/FSharpEmitHelpers.cs b/src/Wolverine/Configuration/FSharpEmitHelpers.cs
index 791c7d702..cf4b9e3a9 100644
--- a/src/Wolverine/Configuration/FSharpEmitHelpers.cs
+++ b/src/Wolverine/Configuration/FSharpEmitHelpers.cs
@@ -1,5 +1,6 @@
using JasperFx.CodeGeneration;
using JasperFx.CodeGeneration.Frames;
+using JasperFx.CodeGeneration.Model;
namespace Wolverine.Configuration;
@@ -22,8 +23,10 @@ internal static class FSharpEmitHelpers
public static void WriteAbortGuard(ISourceWriter writer, GeneratedMethod method, string conditionExpression,
Frame? next)
{
+ var abort = AbortExpression(method);
+
writer.Write($"BLOCK:if {conditionExpression} then");
- writer.Write("()");
+ writer.Write(abort);
writer.FinishBlock();
writer.Write("BLOCK:else");
@@ -33,9 +36,23 @@ public static void WriteAbortGuard(ISourceWriter writer, GeneratedMethod method,
}
else
{
- writer.Write("()");
+ writer.Write(abort);
}
writer.FinishBlock();
}
+
+ ///
+ /// The expression a branch yields when it does NOT continue the chain (abort / no-op). In a
+ /// task { } body (AsyncMode.AsyncTask) or a synchronous-Task method (AsyncMode.None, where
+ /// the machinery appends a trailing Task.CompletedTask) that's just (). But when the
+ /// method body IS a bare trailing Task expression (AsyncMode.ReturnFromLastNode) every branch must
+ /// itself be a Task, so the no-op branch yields Task.CompletedTask.
+ ///
+ public static string AbortExpression(GeneratedMethod method)
+ {
+ return method.AsyncMode == AsyncMode.ReturnFromLastNode
+ ? "System.Threading.Tasks.Task.CompletedTask"
+ : "()";
+ }
}