diff --git a/Directory.Build.props b/Directory.Build.props
index 5ad75623..e17aa47b 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.4.0
+ 2.4.1
13
1570;1571;1572;1573;1574;1587;1591;1701;1702;1711;1735;0618
Jeremy D. Miller;Jaedyn Tonee
diff --git a/build/Build.cs b/build/Build.cs
index 3d23a935..ddb75d10 100644
--- a/build/Build.cs
+++ b/build/Build.cs
@@ -105,6 +105,10 @@ partial class Build : NukeBuild
DotNet("run --framework net9.0 -- describe --environment Testing --applicationName Different --contentRoot /bin", Solution.TestHarnesses.CommandLineRunner.Directory);
DotNet("run --framework net9.0 -- describe --environment=Testing --applicationName=Different --contentRoot=/bin", Solution.TestHarnesses.CommandLineRunner.Directory);
DotNet("run --framework net9.0 -- codegen preview --start", Solution.TestHarnesses.GeneratorTarget.Directory);
+
+ // Validate the `--language fsharp` codegen flag is wired through the CLI and emits F#.
+ // (Compilable/runnable pre-generated F# is proven downstream against real handler chains.)
+ DotNet("run --framework net9.0 -- codegen preview --language fsharp --start", Solution.TestHarnesses.GeneratorTarget.Directory);
});
///
diff --git a/src/CodegenTests/CodegenLanguageTests.cs b/src/CodegenTests/CodegenLanguageTests.cs
new file mode 100644
index 00000000..44d5be98
--- /dev/null
+++ b/src/CodegenTests/CodegenLanguageTests.cs
@@ -0,0 +1,105 @@
+using JasperFx.CodeGeneration;
+using JasperFx.CodeGeneration.Model;
+using Microsoft.Extensions.DependencyInjection;
+using Shouldly;
+
+namespace CodegenTests;
+
+public class CodegenLanguageTests
+{
+ [Fact]
+ public void default_language_is_csharp()
+ {
+ new DynamicCodeBuilder(new ServiceCollection().BuildServiceProvider(), [])
+ .Language.ShouldBe(CodegenLanguage.csharp);
+ }
+
+ [Fact]
+ public void preview_emits_csharp_by_default()
+ {
+ var code = Build(CodegenLanguage.csharp).GenerateAllCode();
+
+ code.ShouldContain("namespace");
+ code.ShouldContain("class GreeterType"); // C# class syntax (F# would be "type GreeterType")
+ }
+
+ [Fact]
+ public void preview_emits_fsharp_when_language_is_fsharp()
+ {
+ var code = Build(CodegenLanguage.fsharp).GenerateAllCode();
+
+ code.ShouldContain("// ");
+ code.ShouldContain("namespace");
+ code.ShouldContain("type GreeterType");
+ code.ShouldNotContain("public class");
+ }
+
+ [Fact]
+ public void write_uses_cs_extension_by_default()
+ {
+ var written = WriteToTempAndCollect(CodegenLanguage.csharp);
+
+ written.ShouldContain(f => f.EndsWith(".cs"));
+ written.ShouldNotContain(f => f.EndsWith(".fs"));
+ }
+
+ [Fact]
+ public void write_uses_fs_extension_and_fsharp_content_when_language_is_fsharp()
+ {
+ var written = WriteToTempAndCollect(CodegenLanguage.fsharp);
+
+ written.ShouldContain(f => f.EndsWith(".fs"));
+ written.ShouldNotContain(f => f.EndsWith(".cs"));
+
+ var fsFile = written.Single(f => f.EndsWith(".fs"));
+ File.ReadAllText(fsFile).ShouldContain("namespace");
+ }
+
+ private static DynamicCodeBuilder Build(CodegenLanguage language, GenerationRules? rules = null)
+ {
+ var collection = new SampleCollection(rules ?? new GenerationRules("Generated"), new SampleFile("Greeter"));
+ return new DynamicCodeBuilder(new ServiceCollection().BuildServiceProvider(), [collection])
+ {
+ Language = language
+ };
+ }
+
+ private static List WriteToTempAndCollect(CodegenLanguage language)
+ {
+ var dir = Path.Combine(Path.GetTempPath(), "jasperfx-codegen-language-" + Guid.NewGuid().ToString("n"));
+ Directory.CreateDirectory(dir);
+ var rules = new GenerationRules("Generated") { GeneratedCodeOutputPath = dir };
+
+ var written = new List();
+ Build(language, rules).WriteGeneratedCode(written.Add);
+ return written;
+ }
+
+ private sealed class SampleFile(string fileName) : ICodeFile
+ {
+ public string FileName { get; } = fileName;
+
+ public void AssembleTypes(GeneratedAssembly assembly) => assembly.AddType(FileName + "Type", typeof(object));
+
+ public Task AttachTypes(GenerationRules rules, System.Reflection.Assembly assembly,
+ IServiceProvider? services, string containingNamespace) => Task.FromResult(false);
+
+ public bool AttachTypesSynchronously(GenerationRules rules, System.Reflection.Assembly assembly,
+ IServiceProvider? services, string containingNamespace) => false;
+
+ public void AssertServiceLocationsAreAllowed(ServiceLocationReport[] reports, IServiceProvider? services) { }
+
+ public bool TryReplaceServiceProvider(out Variable serviceProvider)
+ {
+ serviceProvider = default!;
+ return false;
+ }
+ }
+
+ private sealed class SampleCollection(GenerationRules rules, params ICodeFile[] files) : ICodeFileCollection
+ {
+ public string ChildNamespace => "Sample";
+ public GenerationRules Rules { get; } = rules;
+ public IReadOnlyList BuildFiles() => files;
+ }
+}
diff --git a/src/JasperFx/CodeGeneration/CodegenLanguage.cs b/src/JasperFx/CodeGeneration/CodegenLanguage.cs
new file mode 100644
index 00000000..cf9ccdb8
--- /dev/null
+++ b/src/JasperFx/CodeGeneration/CodegenLanguage.cs
@@ -0,0 +1,11 @@
+namespace JasperFx.CodeGeneration;
+
+///
+/// Target language for generated code. C# is the default and supports both the dynamic (runtime
+/// Roslyn) and pre-generated (static) models. F# targets the pre-generated/static model only.
+///
+public enum CodegenLanguage
+{
+ csharp,
+ fsharp
+}
diff --git a/src/JasperFx/CodeGeneration/Commands/GenerateCodeCommand.cs b/src/JasperFx/CodeGeneration/Commands/GenerateCodeCommand.cs
index d4e8b722..83d5e2aa 100644
--- a/src/JasperFx/CodeGeneration/Commands/GenerateCodeCommand.cs
+++ b/src/JasperFx/CodeGeneration/Commands/GenerateCodeCommand.cs
@@ -42,9 +42,17 @@ public override bool Execute(GenerateCodeInput input)
var builder = new DynamicCodeBuilder(host.Services, collections)
{
- ServiceVariableSource = host.Services.GetService()
+ ServiceVariableSource = host.Services.GetService(),
+ Language = input.LanguageFlag
};
+ if (input.LanguageFlag == CodegenLanguage.fsharp && input.Action == CodeAction.test)
+ {
+ AnsiConsole.MarkupLine(
+ "[red]'codegen test' does not support --language fsharp; the test action uses in-memory C# compilation. Use 'write' or 'preview' for F#.[/]");
+ return false;
+ }
+
switch (input.Action)
{
case CodeAction.preview:
diff --git a/src/JasperFx/CodeGeneration/Commands/GenerateCodeInput.cs b/src/JasperFx/CodeGeneration/Commands/GenerateCodeInput.cs
index c1cab966..dd4463c3 100644
--- a/src/JasperFx/CodeGeneration/Commands/GenerateCodeInput.cs
+++ b/src/JasperFx/CodeGeneration/Commands/GenerateCodeInput.cs
@@ -9,6 +9,9 @@ public class GenerateCodeInput : NetCoreInput
[Description("Optionally limit the preview to only one type of code generation")]
public string? TypeFlag { get; set; }
+ [Description("Target language for the generated code (csharp or fsharp). Default is csharp")]
+ public CodegenLanguage LanguageFlag { get; set; } = CodegenLanguage.csharp;
+
[Description("Start the IHost instead of just building it. Use this when code generation participants are registered during host startup (e.g., ASP.NET Core Startup.Configure)")]
public bool StartFlag { get; set; }
}
\ No newline at end of file
diff --git a/src/JasperFx/CodeGeneration/DynamicCodeBuilder.cs b/src/JasperFx/CodeGeneration/DynamicCodeBuilder.cs
index a4cee1b8..061857e7 100644
--- a/src/JasperFx/CodeGeneration/DynamicCodeBuilder.cs
+++ b/src/JasperFx/CodeGeneration/DynamicCodeBuilder.cs
@@ -30,8 +30,23 @@ public DynamicCodeBuilder(IServiceProvider services, ICodeFileCollection[] colle
public IServiceVariableSource? ServiceVariableSource { get; set; }
+ ///
+ /// Target language for the generated source. F# () emits the
+ /// pre-generated/static code model with the .fs extension; C# is the default.
+ ///
+ public CodegenLanguage Language { get; set; } = CodegenLanguage.csharp;
+
public string[] ChildNamespaces => Collections.Select(x => x.ChildNamespace).ToArray();
+ private string renderCode(GeneratedAssembly generatedAssembly, IServiceVariableSource? services)
+ {
+ return Language == CodegenLanguage.fsharp
+ ? generatedAssembly.GenerateFSharpCode(services)
+ : generatedAssembly.GenerateCode(services);
+ }
+
+ private string fileExtension => Language == CodegenLanguage.fsharp ? ".fs" : ".cs";
+
public IServiceProvider Services { get; }
public ICodeFileCollection[] Collections { get; }
@@ -127,7 +142,7 @@ public void WriteGeneratedCode(Action onFileWritten)
// `services` is shared across every file here, so reset the prior override first.
applyServiceProviderOverride(file, services);
- var code = generatedAssembly.GenerateCode(services);
+ var code = renderCode(generatedAssembly, services);
// #227: enforce ServiceLocationPolicy per-file, matching the
// runtime compile path in DynamicTypeLoader.Initialize. The
@@ -138,7 +153,7 @@ public void WriteGeneratedCode(Action onFileWritten)
// "all generated" result from the CLI.
assertServiceLocationsAllowed(file, services);
- var fileName = Path.Combine(exportDirectory, file.FileName.Replace(" ", "_") + ".cs");
+ var fileName = Path.Combine(exportDirectory, file.FileName.Replace(" ", "_") + fileExtension);
File.WriteAllText(fileName, code);
onFileWritten(fileName);
}
@@ -185,7 +200,7 @@ private string generateCode(ICodeFileCollection collection)
// shared source.
applyServiceProviderOverride(file, services);
- var code = generatedAssembly.GenerateCode(services);
+ var code = renderCode(generatedAssembly, services);
assertServiceLocationsAllowed(file, services);
writer.WriteLine(code);