Skip to content

Keep externally set ServiceProvider when ConfigureGlobalOptions triggers a rebuild - #252

Open
cytoph wants to merge 1 commit into
Cysharp:masterfrom
cytoph:fix/global-options-hosting-di
Open

Keep externally set ServiceProvider when ConfigureGlobalOptions triggers a rebuild#252
cytoph wants to merge 1 commit into
Cysharp:masterfrom
cytoph:fix/global-options-hosting-di

Conversation

@cytoph

@cytoph cytoph commented Aug 22, 2026

Copy link
Copy Markdown

Fixes #251

Problem

When combining the hosting integration with global options, DI breaks silently:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddSingleton<Greeter>();
ConsoleApp.ConsoleAppBuilder app = builder.ToConsoleAppBuilder();
app.ConfigureGlobalOptions((ref b) => b.AddGlobalOption<bool>("--verbose"));
app.Add<HelloCommand>();
await app.RunAsync(args);

ConfigureGlobalOptions sets isRequireCallBuildAndSetServiceProvider = true, so the generated BuildAndSetServiceProvider runs during RunAsync, builds an empty ServiceCollection (nothing was configured on the builder itself - the services live in the host), and replaces the ServiceProvider that ToConsoleAppBuilder had already set. Constructor dependencies then resolve to null and blow up with NullReferenceException on first use.

Fix

The generated BuildAndSetServiceProvider now returns early when ConsoleApp.ServiceProvider is already set and nothing DI-related was configured on the builder itself (configureServices, createServiceProvider, postConfigureServices, and configureLogging where applicable, all null). The flag-set in ConfigureGlobalOptions is left untouched, because the flag also gates global-options parsing in the generated Run path for context-less commands.

Generated code (with DependencyInjection referenced) now begins:

partial void BuildAndSetServiceProvider(ConsoleAppContext context)
{
    if (!isRequireCallBuildAndSetServiceProvider)
    {
        return;
    }
    isRequireCallBuildAndSetServiceProvider = false;

    if (ConsoleApp.ServiceProvider != null && configureServices == null && createServiceProvider == null && postConfigureServices == null)
    {
        return;
    }
    ...

Behavior when a provider was configured on the builder (or none was set externally) is unchanged.

Test

DITest.ConfigureGlobalOptionsKeepsExternallySetServiceProvider reproduces the issue with a pre-set ServiceProvider plus ConfigureGlobalOptions and asserts that the service still resolves and the global option is still parsed. It fails without the fix and passes with it.

Since the DI emit path (DllReference.HasDependencyInjection) had no coverage in GeneratorTests, the runner gained an optional additionalReferences parameter and the test project references Microsoft.Extensions.DependencyInjection 8.0.1 (8.x on purpose: its net8.0 lib matches the runner's .NET 8 reference assemblies; 9.x triggers CS1705).

🤖 Generated with Claude Code

…ers 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ConfigureGlobalOptions silently replaces the ServiceProvider set by ToConsoleAppBuilder with an empty one

1 participant