diff --git a/Directory.Build.props b/Directory.Build.props
index 5fa02c8..86d4457 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -7,7 +7,7 @@
JasperFx.Events, JasperFx.Events.SourceGenerator) set
$(JasperFxVersion) so they always release together. Bump this one
value to release the whole set. -->
- 2.2.6
+ 2.2.7
13
1570;1571;1572;1573;1574;1587;1591;1701;1702;1711;1735;0618
Jeremy D. Miller;Jaedyn Tonee
diff --git a/src/CodegenTests.FSharp/FSharpCodegenSample.cs b/src/CodegenTests.FSharp/FSharpCodegenSample.cs
index 6c107dd..730962f 100644
--- a/src/CodegenTests.FSharp/FSharpCodegenSample.cs
+++ b/src/CodegenTests.FSharp/FSharpCodegenSample.cs
@@ -62,10 +62,125 @@ public static GeneratedAssembly BuildSampleAssembly()
AddCalculatorType(assembly);
AddCastType(assembly);
AddScopedConsumerType(assembly);
+ AddActivityEmitterType(assembly);
+ AddNowHandlerType(assembly);
+ AddValueTaskHandlerType(assembly);
+ AddMemberAccessType(assembly);
+ AddArrayHandlerType(assembly);
+ AddLazyHandlerType(assembly);
return assembly;
}
+ ///
+ /// Appends an activity event — exercises . F# has no
+ /// null-conditional operator, so the body emits an explicit if not (isNull Activity.Current)
+ /// guard and pipes the chained AddEvent result to ignore.
+ ///
+ private static void AddActivityEmitterType(GeneratedAssembly assembly)
+ {
+ var type = assembly.AddType("GeneratedActivityEmitter", typeof(IActivityEmitter));
+ var method = type.MethodFor(nameof(IActivityEmitter.Emit));
+ method.Frames.Add(new AppendActivityEventFrame("sample.activity.event"));
+ }
+
+ ///
+ /// Resolves the current time through and passes it to a
+ /// service — exercises (let now = System.DateTime.UtcNow).
+ ///
+ private static void AddNowHandlerType(GeneratedAssembly assembly)
+ {
+ var type = assembly.AddType("GeneratedNowHandler", typeof(INowHandler));
+ var method = type.MethodFor(nameof(INowHandler.Stamp));
+ method.Sources.Add(new NowTimeVariableSource());
+
+ var service = new InjectedField(typeof(ClockService), "clockService");
+ var call = new MethodCall(typeof(ClockService), nameof(ClockService.Stamp))
+ {
+ Target = service,
+ ReturnAction = ReturnAction.Return
+ };
+ method.Frames.Add(call);
+ }
+
+ ///
+ /// A synchronous body behind a signature — exercises
+ /// : the trailing F# expression is ValueTask<string>(result).
+ ///
+ private static void AddValueTaskHandlerType(GeneratedAssembly assembly)
+ {
+ var type = assembly.AddType("GeneratedValueTaskHandler", typeof(IValueTaskHandler));
+ var method = type.MethodFor(nameof(IValueTaskHandler.HandleAsync));
+
+ var service = new InjectedField(typeof(ControlFlowService), "controlFlowService");
+ var call = new MethodCall(typeof(ControlFlowService), nameof(ControlFlowService.Fallback))
+ {
+ Target = service
+ };
+ method.Frames.Add(call);
+ method.Frames.Add(new ReturnValueTask(typeof(string)));
+ }
+
+ ///
+ /// Reads a member off a constructed object — exercises
+ /// (let value = mutableBox.Value).
+ ///
+ private static void AddMemberAccessType(GeneratedAssembly assembly)
+ {
+ var type = assembly.AddType("GeneratedMemberAccessHandler", typeof(IMemberAccessHandler));
+ var method = type.MethodFor(nameof(IMemberAccessHandler.Read));
+
+ var ctor = new ConstructorFrame(typeof(MutableBox), typeof(MutableBox).GetConstructors().Single());
+ method.Frames.Add(ctor);
+
+ var member = typeof(MutableBox).GetProperty(nameof(MutableBox.Value))!;
+ var access = new MemberAccessFrame(typeof(MutableBox), member, "value");
+ method.Frames.Add(access);
+
+ method.Frames.Add(new ReturnFrame(access.Variable));
+ }
+
+ ///
+ /// Builds an array literal from two injected elements — exercises
+ /// (let things = [| thingA; thingB |]).
+ ///
+ private static void AddArrayHandlerType(GeneratedAssembly assembly)
+ {
+ var type = assembly.AddType("GeneratedArrayHandler", typeof(IArrayHandler));
+ var method = type.MethodFor(nameof(IArrayHandler.Build));
+
+ var thingA = new InjectedField(typeof(Thing), "thingA");
+ var thingB = new InjectedField(typeof(Thing), "thingB");
+ var array = new CreateArrayFrame(typeof(Thing[]), typeof(Thing), new Variable[] { thingA, thingB });
+ method.Frames.Add(array);
+
+ method.Frames.Add(new ReturnFrame(array.Variable));
+ }
+
+ ///
+ /// Resolves a service flagged for service location off an injected
+ /// — exercises
+ /// (let controlFlowService = …GetRequiredService<ControlFlowService>(provider)).
+ ///
+ private static void AddLazyHandlerType(GeneratedAssembly assembly)
+ {
+ var type = assembly.AddType("GeneratedLazyHandler", typeof(ILazyHandler));
+ var method = type.MethodFor(nameof(ILazyHandler.Handle));
+
+ // The injected IServiceProvider is the scope the LazyServiceLocationFrame resolves against;
+ // register it as a constructor field so FindVariable(IServiceProvider) resolves.
+ type.AllInjectedFields.Add(new InjectedField(typeof(IServiceProvider), "provider"));
+ var lazy = new LazyServiceLocationFrame(typeof(ControlFlowService));
+ method.Frames.Add(lazy);
+
+ var call = new MethodCall(typeof(ControlFlowService), nameof(ControlFlowService.Fallback))
+ {
+ Target = lazy.Variable,
+ ReturnAction = ReturnAction.Return
+ };
+ method.Frames.Add(call);
+ }
+
///
/// A handler that resolves a service-located and awaits it.
/// Exercises the scoped-DI frames: use serviceScope = …CreateAsyncScope(…) +
diff --git a/src/CodegenTests.FSharpFixture/Generated.fs b/src/CodegenTests.FSharpFixture/Generated.fs
index ae8bb1e..94d9f08 100644
--- a/src/CodegenTests.FSharpFixture/Generated.fs
+++ b/src/CodegenTests.FSharpFixture/Generated.fs
@@ -125,3 +125,49 @@ type GeneratedScopedConsumer(serviceScopeFactory: Microsoft.Extensions.Dependenc
do! scopedThing.DoAsync()
}
+type GeneratedActivityEmitter() =
+ interface FSharpCodegenTarget.IActivityEmitter with
+ member this.Emit() : unit =
+ if not (isNull System.Diagnostics.Activity.Current) then System.Diagnostics.Activity.Current.AddEvent(System.Diagnostics.ActivityEvent("sample.activity.event")) |> ignore
+
+type GeneratedNowHandler(clockService: FSharpCodegenTarget.ClockService) =
+ let _clockService = clockService
+
+ interface FSharpCodegenTarget.INowHandler with
+ member this.Stamp() : string =
+ let now = System.DateTime.UtcNow
+ _clockService.Stamp(now)
+
+type GeneratedValueTaskHandler(controlFlowService: FSharpCodegenTarget.ControlFlowService) =
+ let _controlFlowService = controlFlowService
+
+ interface FSharpCodegenTarget.IValueTaskHandler with
+ member this.HandleAsync() : System.Threading.Tasks.ValueTask =
+ let result_of_Fallback = _controlFlowService.Fallback()
+ System.Threading.Tasks.ValueTask(result_of_Fallback)
+
+type GeneratedMemberAccessHandler() =
+ interface FSharpCodegenTarget.IMemberAccessHandler with
+ member this.Read() : int =
+ let mutableBox = FSharpCodegenTarget.MutableBox()
+ let value = mutableBox.Value
+ value
+
+type GeneratedArrayHandler(thingA: FSharpCodegenTarget.Thing, thingB: FSharpCodegenTarget.Thing) =
+ let _thingA = thingA
+ let _thingB = thingB
+
+ interface FSharpCodegenTarget.IArrayHandler with
+ member this.Build() : FSharpCodegenTarget.Thing[] =
+ let thingArray = [| _thingA; _thingB |]
+ thingArray
+
+type GeneratedLazyHandler(provider: System.IServiceProvider) =
+ let _provider = provider
+
+ interface FSharpCodegenTarget.ILazyHandler with
+ member this.Handle() : string =
+ // This service has been marked as requiring service location independent of Wolverine's ability to use constructor injection of everything else
+ let controlFlowService = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(_provider)
+ controlFlowService.Fallback()
+
diff --git a/src/FSharpCodegenTarget/Contracts.cs b/src/FSharpCodegenTarget/Contracts.cs
index 45fd7f3..eba932f 100644
--- a/src/FSharpCodegenTarget/Contracts.cs
+++ b/src/FSharpCodegenTarget/Contracts.cs
@@ -102,6 +102,71 @@ public interface IScopedConsumerHandler
Task Handle();
}
+// --- Contracts for the remaining JasperFx F# frame surface (jasperfx#399) ---
+
+///
+/// A sync void handler used to exercise AppendActivityEventFrame: the generated body emits a
+/// guarded Activity.Current.AddEvent(...) (F# has no ?., so an explicit isNull guard).
+///
+public interface IActivityEmitter
+{
+ void Emit();
+}
+
+///
+/// A handler that stamps the current time. The injected resolves through
+/// NowTimeVariableSource → NowFetchFrame (let now = System.DateTime.UtcNow).
+///
+public interface INowHandler
+{
+ string Stamp();
+}
+
+public class ClockService
+{
+ public string Stamp(DateTime now)
+ {
+ return now.ToString("O");
+ }
+}
+
+///
+/// A handler whose signature returns with a synchronous body — exercises
+/// ReturnValueTask: the trailing F# expression constructs a ValueTask<string>(result).
+///
+public interface IValueTaskHandler
+{
+ ValueTask HandleAsync();
+}
+
+///
+/// A handler that reads a member off a constructed object — exercises MemberAccessFrame
+/// (let value = mutableBox.Value).
+///
+public interface IMemberAccessHandler
+{
+ int Read();
+}
+
+///
+/// A handler that builds an array of injected elements — exercises CreateArrayFrame's F# array
+/// literal (let things = [| thingA; thingB |]).
+///
+public interface IArrayHandler
+{
+ Thing[] Build();
+}
+
+///
+/// A handler that resolves a service flagged AlwaysUseServiceLocationFor off an injected
+/// — exercises LazyServiceLocationFrame
+/// (let controlFlowService = ServiceProviderServiceExtensions.GetRequiredService<…>(provider)).
+///
+public interface ILazyHandler
+{
+ string Handle();
+}
+
///
/// A base class with a public instance method () and an abstract method to
/// override (). The generated subclass overrides Compute and calls the
diff --git a/src/JasperFx/CodeGeneration/Frames/AppendActivityEventFrame.cs b/src/JasperFx/CodeGeneration/Frames/AppendActivityEventFrame.cs
index 8f06813..defba48 100644
--- a/src/JasperFx/CodeGeneration/Frames/AppendActivityEventFrame.cs
+++ b/src/JasperFx/CodeGeneration/Frames/AppendActivityEventFrame.cs
@@ -38,5 +38,15 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
Next?.GenerateCode(method, writer);
}
+ public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
+ {
+ // F# has no null-conditional operator; guard Activity.Current explicitly. AddEvent returns the
+ // Activity for chaining, so pipe the result to ignore.
+ writer.Write(
+ $"if not (isNull {typeof(Activity).FSharpName()}.{nameof(Activity.Current)}) then {typeof(Activity).FSharpName()}.{nameof(Activity.Current)}.AddEvent({typeof(ActivityEvent).FSharpName()}(\"{_eventName}\")) |> ignore");
+
+ Next?.GenerateFSharpCode(method, writer);
+ }
+
public override IEnumerable FindVariables(IMethodVariables chain) => [];
}
diff --git a/src/JasperFx/CodeGeneration/Frames/ReturnValueTaskFrame.cs b/src/JasperFx/CodeGeneration/Frames/ReturnValueTaskFrame.cs
index 9f1808b..d420f53 100644
--- a/src/JasperFx/CodeGeneration/Frames/ReturnValueTaskFrame.cs
+++ b/src/JasperFx/CodeGeneration/Frames/ReturnValueTaskFrame.cs
@@ -27,4 +27,12 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
writer.WriteLine(
$"return new {typeof(ValueTask).FullNameInCode()}<{_variableType.FullNameInCode()}>({_returnValue.Usage});");
}
+
+ public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
+ {
+ // Terminal frame: in F# the trailing expression IS the return value (no `return` keyword).
+ // ValueTask is a struct, so it is constructed without `new`.
+ writer.WriteLine(
+ $"{typeof(ValueTask).FSharpName()}<{_variableType.FSharpName()}>({_returnValue.FSharpUsage})");
+ }
}
\ No newline at end of file
diff --git a/src/JasperFx/CodeGeneration/Model/NowTimeVariableSource.cs b/src/JasperFx/CodeGeneration/Model/NowTimeVariableSource.cs
index c27386b..3464622 100644
--- a/src/JasperFx/CodeGeneration/Model/NowTimeVariableSource.cs
+++ b/src/JasperFx/CodeGeneration/Model/NowTimeVariableSource.cs
@@ -1,4 +1,5 @@
using JasperFx.CodeGeneration.Frames;
+using JasperFx.Core.Reflection;
namespace JasperFx.CodeGeneration.Model;
@@ -47,6 +48,12 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
writer.WriteLine($"var {Variable.Usage} = {Variable.VariableType.FullName}.{nameof(DateTime.UtcNow)};");
Next?.GenerateCode(method, writer);
}
+
+ public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
+ {
+ writer.WriteLine($"{Variable.FSharpAssignmentUsage} = {Variable.VariableType.FSharpName()}.{nameof(DateTime.UtcNow)}");
+ Next?.GenerateFSharpCode(method, writer);
+ }
}
#endregion
\ No newline at end of file
diff --git a/src/JasperFx/CodeGeneration/Services/ArrayPlan.cs b/src/JasperFx/CodeGeneration/Services/ArrayPlan.cs
index 4cf2a9b..7a69997 100644
--- a/src/JasperFx/CodeGeneration/Services/ArrayPlan.cs
+++ b/src/JasperFx/CodeGeneration/Services/ArrayPlan.cs
@@ -131,4 +131,23 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
writer.WriteLine($"{_serviceType.FullNameInCode()} {Variable.Usage} = new {_elementType.FullNameInCode()}[]{{{_elements.Select(x => x.Usage).Join(", ")}}};");
Next?.GenerateCode(method, writer);
}
+
+ public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
+ {
+ // Mirror the C# fail-fast null guard for a missing keyed singleton mirror (jasperfx#381).
+ foreach (var element in _elements)
+ {
+ if (element is InjectedSingleton { Descriptor: { IsKeyedService: true } descriptor }
+ && EnumerableSingletons.IsMirrorKey(descriptor.ServiceKey))
+ {
+ var message = EnumerableSingletons.MissingMirrorMessage(_elementType, descriptor.ServiceKey);
+ writer.WriteLine(
+ $"if isNull {element.FSharpUsage} then raise({typeof(InvalidOperationException).FSharpName()}({CodeFormatter.Write(message)}))");
+ }
+ }
+
+ // F# array literal: [| e1; e2 |]. The element type is inferred, so no explicit annotation.
+ writer.WriteLine($"{Variable.FSharpAssignmentUsage} = [| {_elements.Select(x => x.FSharpUsage).Join("; ")} |]");
+ Next?.GenerateFSharpCode(method, writer);
+ }
}
\ No newline at end of file
diff --git a/src/JasperFx/CodeGeneration/Services/LazyServiceLocationFrame.cs b/src/JasperFx/CodeGeneration/Services/LazyServiceLocationFrame.cs
index 7b24280..b217de4 100644
--- a/src/JasperFx/CodeGeneration/Services/LazyServiceLocationFrame.cs
+++ b/src/JasperFx/CodeGeneration/Services/LazyServiceLocationFrame.cs
@@ -53,4 +53,12 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
$"var {Variable.Usage} = {typeof(ServiceProviderServiceExtensions).FullNameInCode()}.{nameof(ServiceProviderServiceExtensions.GetRequiredService)}<{Variable.VariableType.FullNameInCode()}>({_scoped.Usage});");
Next?.GenerateCode(method, writer);
}
+
+ public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
+ {
+ writer.WriteComment("This service has been marked as requiring service location independent of Wolverine's ability to use constructor injection of everything else");
+ writer.Write(
+ $"{Variable.FSharpAssignmentUsage} = {typeof(ServiceProviderServiceExtensions).FSharpName()}.{nameof(ServiceProviderServiceExtensions.GetRequiredService)}<{Variable.VariableType.FSharpName()}>({_scoped.FSharpUsage})");
+ Next?.GenerateFSharpCode(method, writer);
+ }
}
\ No newline at end of file
diff --git a/src/JasperFx/CodeGeneration/Services/MemberAccessFrame.cs b/src/JasperFx/CodeGeneration/Services/MemberAccessFrame.cs
index 46a9bcf..416da9d 100644
--- a/src/JasperFx/CodeGeneration/Services/MemberAccessFrame.cs
+++ b/src/JasperFx/CodeGeneration/Services/MemberAccessFrame.cs
@@ -29,6 +29,12 @@ public override void GenerateCode(GeneratedMethod method, ISourceWriter writer)
Next?.GenerateCode(method, writer);
}
+ public override void GenerateFSharpCode(GeneratedMethod method, ISourceWriter writer)
+ {
+ writer.Write($"{Variable.FSharpAssignmentUsage} = {_parent.FSharpUsage}.{_member.Name}");
+ Next?.GenerateFSharpCode(method, writer);
+ }
+
public override IEnumerable FindVariables(IMethodVariables chain)
{
_parent = chain.FindVariable(_targetType);