From 880fbfee7afd21e6d131749157b84d910ddf5205 Mon Sep 17 00:00:00 2001 From: Christoph Mett Date: Sat, 22 Aug 2026 19:38:33 +0200 Subject: [PATCH] Keep externally set ServiceProvider when ConfigureGlobalOptions triggers a rebuild ConfigureGlobalOptions sets isRequireCallBuildAndSetServiceProvider, which made the generated BuildAndSetServiceProvider build an empty ServiceCollection and replace a ServiceProvider that was already set externally (e.g. by the hosting integration's ToConsoleAppBuilder), silently breaking DI. The generated BuildAndSetServiceProvider now returns early when ConsoleApp.ServiceProvider is already set and nothing DI-related was configured on the builder itself. --- src/ConsoleAppFramework/Emitter.cs | 11 ++++++ .../CSharpGeneratorRunner.cs | 14 +++++--- .../ConsoleAppFramework.GeneratorTests.csproj | 1 + .../DITest.cs | 34 ++++++++++++++++++- 4 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/ConsoleAppFramework/Emitter.cs b/src/ConsoleAppFramework/Emitter.cs index 7481bf2..b75b3c2 100644 --- a/src/ConsoleAppFramework/Emitter.cs +++ b/src/ConsoleAppFramework/Emitter.cs @@ -974,6 +974,17 @@ public void EmitConfigure(SourceBuilder sb, DllReference dllReference) } sb.AppendLine("isRequireCallBuildAndSetServiceProvider = false;"); + // keep an already set ServiceProvider(e.g. hosting integration's ToConsoleAppBuilder) when nothing was configured on the builder itself + var nothingConfigured = "configureServices == null && createServiceProvider == null && postConfigureServices == null"; + if (dllReference.HasLogging) + { + nothingConfigured += " && configureLogging == null"; + } + using (sb.BeginBlock($"if (ConsoleApp.ServiceProvider != null && {nothingConfigured})")) + { + sb.AppendLine("return;"); + } + if (dllReference.HasConfiguration) { sb.AppendLine("var config = configuration;"); diff --git a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs index 68f5432..b740159 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/CSharpGeneratorRunner.cs @@ -31,7 +31,7 @@ public CSharpGeneratorRunner() baseCompilation = compilation; } - public (Compilation, ImmutableArray) RunGenerator([StringSyntax("C#-test")] string source, string[]? preprocessorSymbols = null, AnalyzerConfigOptionsProvider? options = null) + public (Compilation, ImmutableArray) RunGenerator([StringSyntax("C#-test")] string source, string[]? preprocessorSymbols = null, AnalyzerConfigOptionsProvider? options = null, MetadataReference[]? additionalReferences = null) { if (preprocessorSymbols == null) { @@ -76,14 +76,18 @@ public static class Environment } """; var compilation = baseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(source, parseOptions), CSharpSyntaxTree.ParseText(captureStaticCode, parseOptions)); + if (additionalReferences != null) + { + compilation = compilation.AddReferences(additionalReferences); + } driver.RunGeneratorsAndUpdateCompilation(compilation, out var newCompilation, out var diagnostics); return (newCompilation, diagnostics); } - public (Compilation Compilation, ImmutableArray Diagnostics, string Stdout, int ExitCode) CompileAndExecute(string source, string[] args, string[]? preprocessorSymbols = null, AnalyzerConfigOptionsProvider? options = null) + public (Compilation Compilation, ImmutableArray Diagnostics, string Stdout, int ExitCode) CompileAndExecute(string source, string[] args, string[]? preprocessorSymbols = null, AnalyzerConfigOptionsProvider? options = null, MetadataReference[]? additionalReferences = null) { - var (compilation, diagnostics) = RunGenerator(source, preprocessorSymbols, options); + var (compilation, diagnostics) = RunGenerator(source, preprocessorSymbols, options, additionalReferences); using var ms = new MemoryStream(); var emitResult = compilation.Emit(ms); @@ -207,11 +211,11 @@ public async Task Verify(int id, [StringSyntax("C#-test")] string code, string d // Execute and check stdout result - public async Task Execute([StringSyntax("C#-test")] string code, string args, string expected, [CallerArgumentExpression("code")] string? codeExpr = null) + public async Task Execute([StringSyntax("C#-test")] string code, string args, string expected, MetadataReference[]? additionalReferences = null, [CallerArgumentExpression("code")] string? codeExpr = null) { Console.WriteLine(codeExpr!); - var (compilation, diagnostics, stdout, exitCode) = CSharpGeneratorRunner.CompileAndExecute(code, args == "" ? [] : args.Split(' ')); + var (compilation, diagnostics, stdout, exitCode) = CSharpGeneratorRunner.CompileAndExecute(code, args == "" ? [] : args.Split(' '), additionalReferences: additionalReferences); foreach (var item in diagnostics) { Console.WriteLine(item.ToString()); diff --git a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj index 9f145b9..3c32903 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj +++ b/tests/ConsoleAppFramework.GeneratorTests/ConsoleAppFramework.GeneratorTests.csproj @@ -12,6 +12,7 @@ + diff --git a/tests/ConsoleAppFramework.GeneratorTests/DITest.cs b/tests/ConsoleAppFramework.GeneratorTests/DITest.cs index fffe842..cb48afb 100644 --- a/tests/ConsoleAppFramework.GeneratorTests/DITest.cs +++ b/tests/ConsoleAppFramework.GeneratorTests/DITest.cs @@ -1,4 +1,6 @@ -namespace ConsoleAppFramework.GeneratorTests; +using Microsoft.CodeAnalysis; + +namespace ConsoleAppFramework.GeneratorTests; [ClassDataSource] public class DITest(VerifyHelper verifier) @@ -102,4 +104,34 @@ partial void BuildAndSetServiceProvider(ConsoleAppContext context) } """, "cmd test", "Test"); } + + // https://github.com/Cysharp/ConsoleAppFramework/issues/251 + [Test] + public async Task ConfigureGlobalOptionsKeepsExternallySetServiceProvider() + { + MetadataReference[] dependencyInjectionReferences = + [ + MetadataReference.CreateFromFile(typeof(Microsoft.Extensions.DependencyInjection.IServiceCollection).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Microsoft.Extensions.DependencyInjection.ServiceProvider).Assembly.Location), + ]; + + await verifier.Execute(""" +#nullable enable +using Microsoft.Extensions.DependencyInjection; + +var services = new ServiceCollection(); +services.AddSingleton(new MyService("foo")); +ConsoleApp.ServiceProvider = services.BuildServiceProvider(); + +var app = ConsoleApp.Create(); +app.ConfigureGlobalOptions((ref ConsoleApp.GlobalOptionsBuilder builder) => builder.AddGlobalOption("--verbose", "")); +app.Add("", ([FromServices] MyService service, int x) => Console.Write(service.Name + ":" + x)); +app.Run(args); + +public class MyService(string name) +{ + public string Name => name; +} +""", args: "--x 10 --verbose", expected: "foo:10", additionalReferences: dependencyInjectionReferences); + } }