Skip to content

Fix: batch registration chain to avoid Roslyn CS8078 on large scans#63

Open
waynebrantley wants to merge 1 commit into
Dreamescaper:mainfrom
waynebrantley:fix/generate-service-registrations-cs8078
Open

Fix: batch registration chain to avoid Roslyn CS8078 on large scans#63
waynebrantley wants to merge 1 commit into
Dreamescaper:mainfrom
waynebrantley:fix/generate-service-registrations-cs8078

Conversation

@waynebrantley

Copy link
Copy Markdown

Summary

GenerateRegistrationsSource always emits one fluent chain per generated method:

return services
    .AddTransient<A, B>()
    .AddTransient<C, D>()
    // ...
    .AddTransient<Y, Z>();

That's a single C# expression whose parse tree depth grows linearly with the registration count. Roslyn's operation-tree walkers hit a recursion cap around ~1000 links and the compilation aborts with:

error CS8078: An expression is too long or complex to compile

Any consumer scanning a large assembly (~1000+ matching types) can't compile the generated code at all. Analyzers walking the same tree also stack-overflow, and dotnet format on the generated code crashes with the same trace.

GenerateCustomHandlingSource already emits statement-per-line output, so the ScanForTypes+Handler path doesn't hit this — it's specific to [GenerateServiceRegistrations].

Observed in production: a ~1170-handler CQRS assembly could not compile after switching from Scrutor to [GenerateServiceRegistrations].

Fix

Batch the chain at MaxRegistrationsPerChain = 500 links per statement. Below the threshold: output is byte-for-byte identical to the pre-fix shape (zero snapshot-test churn — all 93 existing tests still green). Above the threshold: split into N services.Add()...; statements, with the final statement carrying the return when the method returns IServiceCollection.

Test

AddServicesTests.LargeAssemblyEmitsBatchedChains — declares 650 handlers, asserts:

  • Exactly 2 chain-statements emitted (500 + 150) rather than one 650-link chain
  • All 650 registrations present in the emit
  • Compiling the generated code as part of the source produces zero CS8078 diagnostics

Verified: fails on main, passes with the fix.

Notes

Threshold value (500) is conservative — actual CS8078 tripping point varies with stack size (~1000+ in typical CI, less on constrained runners). 500 leaves comfortable headroom. Happy to bump or make it configurable if you prefer.

@Dreamescaper

Copy link
Copy Markdown
Owner

I'm thinking if it makes sense to make Roslyn's life easier, and simply make separate statements, like

services.AddTransient<A, B>();
services.AddTransient<C, D>();
...
services.AddTransient<Y, Z>();
return services;

GenerateRegistrationsSource always emitted a single fluent chain:

    return services
        .AddTransient<A, B>()
        .AddTransient<C, D>()
        ...
        .AddTransient<Y, Z>();

That's a single C# expression whose parse tree depth grows linearly with the
registration count. Roslyn's operation-tree walkers hit a stack-based cap
around ~1000 links and abort with CS8078 ("An expression is too long or
complex to compile"). Consumers scanning large assemblies (~1000+ types)
can't compile the generated code at all.

Emit one statement per registration instead:

    services.AddTransient<A, B>();
    services.AddTransient<C, D>();
    ...
    services.AddTransient<Y, Z>();
    return services;

This matches the shape GenerateCustomHandlingSource already uses and
sidesteps CS8078 entirely — each statement is its own top-level expression.
Existing snapshot tests updated to the new shape; new
LargeAssemblyCompilesWithoutCs8078 test declares 1200 handlers and asserts
the generated code compiles clean.
@waynebrantley
waynebrantley force-pushed the fix/generate-service-registrations-cs8078 branch from 87f55f4 to 6e4a988 Compare July 24, 2026 22:02
@waynebrantley

Copy link
Copy Markdown
Author

Great call — updated to statement-per-line. Force-pushed 6e4a988.

  • Same shape you suggested: services.AddTransient<A, B>(); services.AddTransient<C, D>(); ... return services;
  • 45 existing snapshot tests updated via mechanical regex transform + 1 hand-edit for the custom-parameter-name case
  • Renamed the regression test to LargeAssemblyCompilesWithoutCs8078 and bumped it to 1200 handlers so it actually exercises the CS8078 tripping point on typical CI runners
  • All 94 tests green

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.

2 participants