diff --git a/docs/guide/codegen.md b/docs/guide/codegen.md index 82181e1eb..ca1e7ddd2 100644 --- a/docs/guide/codegen.md +++ b/docs/guide/codegen.md @@ -203,7 +203,7 @@ As of Wolverine 5.0, you now have the ability to better control the usage of the code generation to potentially avoid unwanted usage: - + ```cs var builder = Host.CreateApplicationBuilder(); builder.UseWolverine(opts => @@ -225,7 +225,7 @@ builder.UseWolverine(opts => opts.ServiceLocationPolicy = ServiceLocationPolicy.NotAllowed; }); ``` -snippet source | anchor +snippet source | anchor ::: note @@ -393,3 +393,62 @@ Which will use: 1. `TypeLoadMode.Dynamic` when the .NET environment is "Development" and dynamically generate types on the first usage 2. `TypeLoadMode.Static` for other .NET environments for optimized cold start times + +## Customizing the Generated Code Output Path + +By default, Wolverine writes generated code to `Internal/Generated` under your project's content root. +For Console applications or non-standard project structures, you may need to customize this path. + +### Using CritterStackDefaults + +You can configure the output path globally for all Critter Stack tools: + + + +```cs +var builder = Host.CreateApplicationBuilder(); +builder.Services.CritterStackDefaults(opts => +{ + // Set a custom output path for generated code + opts.GeneratedCodeOutputPath = "/path/to/your/project/Internal/Generated"; +}); +``` +snippet source | anchor + + +### Auto-Resolving Project Root for Console Apps + +Console applications often have `ContentRootPath` pointing to the `bin` folder, which causes +generated code to be written to the wrong location. Enable automatic project root resolution: + + + +```cs +var builder = Host.CreateApplicationBuilder(); +builder.Services.CritterStackDefaults(opts => +{ + // Automatically find the project root by looking for .csproj/.sln files + // Useful for Console apps where ContentRootPath defaults to bin folder + opts.AutoResolveProjectRoot = true; +}); +``` +snippet source | anchor + + +### Direct Wolverine Configuration + +You can also configure the path directly on Wolverine: + + + +```cs +var builder = Host.CreateApplicationBuilder(); +builder.UseWolverine(opts => +{ + opts.CodeGeneration.GeneratedCodeOutputPath = "/path/to/output"; +}); +``` +snippet source | anchor + + +Note that explicit Wolverine configuration takes precedence over `CritterStackDefaults`. diff --git a/src/Samples/DocumentationSamples/CodegenUsage.cs b/src/Samples/DocumentationSamples/CodegenUsage.cs index 3471d1fbb..406702622 100644 --- a/src/Samples/DocumentationSamples/CodegenUsage.cs +++ b/src/Samples/DocumentationSamples/CodegenUsage.cs @@ -83,4 +83,46 @@ public async Task use_optimized_workflow() #endregion } + + public async Task configure_generated_code_output_path() + { + #region sample_configure_generated_code_output_path + + var builder = Host.CreateApplicationBuilder(); + builder.Services.CritterStackDefaults(opts => + { + // Set a custom output path for generated code + opts.GeneratedCodeOutputPath = "/path/to/your/project/Internal/Generated"; + }); + + #endregion + } + + public async Task auto_resolve_project_root() + { + #region sample_auto_resolve_project_root + + var builder = Host.CreateApplicationBuilder(); + builder.Services.CritterStackDefaults(opts => + { + // Automatically find the project root by looking for .csproj/.sln files + // Useful for Console apps where ContentRootPath defaults to bin folder + opts.AutoResolveProjectRoot = true; + }); + + #endregion + } + + public async Task direct_wolverine_output_path() + { + #region sample_direct_wolverine_output_path + + var builder = Host.CreateApplicationBuilder(); + builder.UseWolverine(opts => + { + opts.CodeGeneration.GeneratedCodeOutputPath = "/path/to/output"; + }); + + #endregion + } } \ No newline at end of file diff --git a/src/Testing/CoreTests/Configuration/generated_code_output_path_configuration.cs b/src/Testing/CoreTests/Configuration/generated_code_output_path_configuration.cs new file mode 100644 index 000000000..c350f64a4 --- /dev/null +++ b/src/Testing/CoreTests/Configuration/generated_code_output_path_configuration.cs @@ -0,0 +1,99 @@ +using JasperFx; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace CoreTests.Configuration; + +public class generated_code_output_path_configuration +{ + [Fact] + public void wolverine_options_should_use_jasperfx_generated_code_output_path() + { + var jasperfx = new JasperFxOptions + { + GeneratedCodeOutputPath = "/custom/path/Generated", + }; + + var wolverineOptions = new WolverineOptions(); + wolverineOptions.ReadJasperFxOptions(jasperfx); + + wolverineOptions.CodeGeneration.GeneratedCodeOutputPath + .ShouldBe("/custom/path/Generated"); + } + + [Fact] + public void wolverine_options_should_not_override_explicit_generated_code_output_path() + { + var jasperfx = new JasperFxOptions + { + GeneratedCodeOutputPath = "/jasperfx/path", + }; + + var wolverineOptions = new WolverineOptions(); + wolverineOptions.CodeGeneration.GeneratedCodeOutputPath = "/explicit/path"; + wolverineOptions.ReadJasperFxOptions(jasperfx); + + // Should keep explicit setting + wolverineOptions.CodeGeneration.GeneratedCodeOutputPath + .ShouldBe("/explicit/path"); + } + + [Fact] + public async Task critter_stack_defaults_generated_code_output_path_flows_to_wolverine() + { + using var host = await Host.CreateDefaultBuilder() + .ConfigureServices(services => + { + services.CritterStackDefaults(opts => + { + opts.GeneratedCodeOutputPath = "/test/output/path"; + }); + }) + .UseWolverine() + .StartAsync(); + + var wolverineOptions = host.Services.GetRequiredService(); + wolverineOptions.CodeGeneration.GeneratedCodeOutputPath + .ShouldBe("/test/output/path"); + } + + [Fact] + public async Task explicit_wolverine_path_takes_precedence_over_critter_stack_defaults() + { + using var host = await Host.CreateDefaultBuilder() + .ConfigureServices(services => + { + services.CritterStackDefaults(opts => + { + opts.GeneratedCodeOutputPath = "/jasperfx/path"; + }); + }) + .UseWolverine(opts => + { + opts.CodeGeneration.GeneratedCodeOutputPath = "/explicit/wolverine/path"; + }) + .StartAsync(); + + var wolverineOptions = host.Services.GetRequiredService(); + wolverineOptions.CodeGeneration.GeneratedCodeOutputPath + .ShouldBe("/explicit/wolverine/path"); + } + + [Fact] + public void wolverine_options_should_not_copy_null_generated_code_output_path() + { + var jasperfx = new JasperFxOptions + { + // GeneratedCodeOutputPath is null by default + }; + + var wolverineOptions = new WolverineOptions(); + var defaultPath = wolverineOptions.CodeGeneration.GeneratedCodeOutputPath; + wolverineOptions.ReadJasperFxOptions(jasperfx); + + // Should keep the default path when JasperFxOptions has null + wolverineOptions.CodeGeneration.GeneratedCodeOutputPath + .ShouldBe(defaultPath); + } +} diff --git a/src/Wolverine/HostBuilderExtensions.cs b/src/Wolverine/HostBuilderExtensions.cs index eb548d72d..9f3127fb4 100644 --- a/src/Wolverine/HostBuilderExtensions.cs +++ b/src/Wolverine/HostBuilderExtensions.cs @@ -125,22 +125,43 @@ internal static IServiceCollection AddWolverine(this IServiceCollection services var environment = s.GetService(); var directory = environment?.ContentRootPath ?? AppContext.BaseDirectory; -#if DEBUG - if (directory.EndsWith("Debug", StringComparison.OrdinalIgnoreCase)) - { - directory = directory.ParentDirectory()!.ParentDirectory(); - } - else if (directory.ParentDirectory()!.EndsWith("Debug", StringComparison.OrdinalIgnoreCase)) + // Don't correct for the path if it's already been set (from JasperFxOptions or user) + if (options.CodeGeneration.GeneratedCodeOutputPath == "Internal/Generated") { - directory = directory.ParentDirectory()!.ParentDirectory()!.ParentDirectory(); - } +#if DEBUG + // In DEBUG builds, try to resolve project root like JasperFx does during codegen + if (jasperfx.AutoResolveProjectRoot) + { + var resolvedRoot = JasperFxOptions.ResolveProjectRoot(directory); + if (resolvedRoot != null) + { + directory = resolvedRoot; + } + } + else + { + // Legacy behavior for backward compatibility when AutoResolveProjectRoot is false + if (directory.EndsWith("Debug", StringComparison.OrdinalIgnoreCase)) + { + directory = directory.ParentDirectory()!.ParentDirectory(); + } + else if (directory.ParentDirectory()!.EndsWith("Debug", StringComparison.OrdinalIgnoreCase)) + { + directory = directory.ParentDirectory()!.ParentDirectory()!.ParentDirectory(); + } + } #endif - // Don't correct for the path if it's already been set - if (options.CodeGeneration.GeneratedCodeOutputPath == "Internal/Generated") - { - options.CodeGeneration.GeneratedCodeOutputPath = - directory!.AppendPath("Internal", "Generated"); + // Use JasperFxOptions path if set, otherwise use the resolved directory + if (jasperfx.GeneratedCodeOutputPath != null) + { + options.CodeGeneration.GeneratedCodeOutputPath = jasperfx.GeneratedCodeOutputPath; + } + else + { + options.CodeGeneration.GeneratedCodeOutputPath = + directory!.AppendPath("Internal", "Generated"); + } } return options; diff --git a/src/Wolverine/WolverineOptions.cs b/src/Wolverine/WolverineOptions.cs index 1a5d3037a..a1c95a3ca 100644 --- a/src/Wolverine/WolverineOptions.cs +++ b/src/Wolverine/WolverineOptions.cs @@ -447,6 +447,12 @@ internal void ReadJasperFxOptions(JasperFxOptions jasperfx) { _autoBuildMessageStorageOnStartup = jasperfx.ActiveProfile.ResourceAutoCreate; } + + // Propagate GeneratedCodeOutputPath from JasperFxOptions if not explicitly set + if (CodeGeneration.GeneratedCodeOutputPath == "Internal/Generated" && jasperfx.GeneratedCodeOutputPath != null) + { + CodeGeneration.GeneratedCodeOutputPath = jasperfx.GeneratedCodeOutputPath; + } } public void RegisterMessageType(Type messageType, string messageAlias)