Skip to content

Fix: generator crash when external constructor parameter names differ only by case - #30

Merged
carl-andersson-at-westermo merged 2 commits into
mainfrom
perf
Aug 24, 2026
Merged

carl-andersson-at-westermo merged 2 commits into
mainfrom
perf

Conversation

@carl-andersson-at-westermo

@carl-andersson-at-westermo carl-andersson-at-westermo commented Aug 24, 2026 •

Copy link
Copy Markdown
Collaborator

Update: confirmed root cause found and fixed

I initially suspected a missing DevelopmentDependency packaging setting (still included below as a valid best-practice fix), but I got direct access to the downstream consumer's repository and was able to reproduce the exact crash locally and confirm the true root cause end-to-end.

Confirmed root cause

Roslyn's SourceProductionContext.AddSource compares hint names with StringComparer.OrdinalIgnoreCase (verified directly against dotnet/roslyn's GeneratorDriver.cs and its own test suite: Assert.Equal(expectedName, source.HintName, StringComparer.OrdinalIgnoreCase)), but GenerateCode's declarations/arrayDeclarations dictionaries used the default (ordinal, case-sensitive) comparer for their keys.

Those dictionary keys double as both the generated hint name and a valid C# identifier. In the downstream consumer's real codebase, two completely unrelated classes each take an external IContainer-typed constructor parameter:

  • SessionFactory(..., IContainer lifeTimeScope)
  • Engine(IContainer lifetimeScope)

Both are entirely valid, independent C# — case-sensitive identifiers are allowed to differ only by case. Both survived as separate entries in the case-sensitive dictionary, and each independently tried to become its own generated source file via context.AddSource. Roslyn's case-insensitive uniqueness check rejected the second one, throwing ArgumentException("...must be unique within a generator") and aborting the entire generator run for that compilation with zero output — producing the exact reported CS8785 warning and the cascading CS0234 (missing Generated namespace).

This is a long-standing latent bug — the affected dictionaries have used the default comparer since the project's very first commit — not something introduced by the recently-merged perf work. It was simply never previously triggered by two same-name-different-case identifiers landing in the same compilation.

Fix

Since a partial class's members can be split across any number of source files (or consolidated into one) with zero effect on the compiled result, declarations are now grouped by a case-insensitive hint name, and any colliding entries are merged into a single generated file instead of one AddSource call per dictionary key. This preserves every declaration — both differently-cased members are still generated and remain independently usable — rather than silently dropping one via a case-insensitive dictionary overwrite, or crashing the whole generator.

Validation

  1. Full test suite: 91/91 passing, including two new regression tests (identical external-parameter names across injections, and names differing only by case).
  2. Reproduced the exact crash live against the actual downstream consumer's real WeConfig.Application project — confirmed SessionFactory/Engine with lifeTimeScope/lifetimeScope are both reachable from the same compilation.
  3. Patched the locally-cached analyzer DLL with this fix and rebuilt WeConfig.Application end-to-end: build now succeeds with 0 errors (previously failed with the reported CS8785/CS0234).

Also included: analyzer packaging best practice

FactoryGenerator.csproj never set <DevelopmentDependency>true</DevelopmentDependency>, the standard/documented setting for Roslyn analyzer/source-generator NuGet packages, which prevents the analyzer from flowing to a referencing project's own downstream consumers regardless of that project's own PrivateAssets setting. This wasn't the cause of this specific crash, but it's an unambiguously correct, low-risk improvement kept in this PR.

FactoryGenerator.csproj never set DevelopmentDependency=true, the
standard/documented setting for Roslyn analyzer/source-generator NuGet
packages. Without it, a project that only transitively depends on
FactoryGenerator (e.g. via a ProjectReference to another library that
itself references it directly) also gets the FactoryGenerator analyzer
registered on top of that project's own direct reference. Roslyn does not
deduplicate separate <Analyzer> items even when they load the same
generator, so both instances independently try to AddSource the same
generated-source hint names (e.g. FactoryGenerator.Declarations/{member}.g.cs
for an IContainer-typed constructor parameter). The second AddSource call
throws ArgumentException('...must be unique within a generator'), aborting
the whole generator run for that compilation with zero output - which is
what produces a cascading 'CS0234: ...Generated does not exist' in
consuming code, since the generator failed to emit anything.

DevelopmentDependency=true stops the analyzer from flowing to a
referencing project's own downstream consumers, regardless of whether
that project's own PackageReference sets PrivateAssets. Verified via
dotnet pack that the resulting nuspec now correctly emits
<developmentDependency>true</developmentDependency>. This setting has
never been present in this file's history (confirmed via git log), so
this is a pre-existing packaging gap, not something introduced by the
recently-merged perf work.

Also adds a regression test covering a related, previously-untested (but
safe) scenario: two separate injections both taking an IContainer-typed
external constructor parameter with the identical name - confirms that
case is correctly deduplicated within a single generator run and was not
itself the source of the crash.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
CONFIRMED root cause, reproduced end-to-end against the real downstream
consumer's repository: Roslyn's SourceProductionContext.AddSource compares
hint names with StringComparer.OrdinalIgnoreCase (verified against
dotnet/roslyn's GeneratorDriver.cs and its own test suite), but
GenerateCode's declarations/�rrayDeclarations dictionaries used the
default (ordinal, case-SENSITIVE) comparer for their keys. Those keys
double as generated hint names AND valid C# identifiers - e.g. two
unrelated injections each taking an external IContainer-typed constructor
parameter named 'lifeTimeScope' and 'lifetimeScope' are both completely
valid on their own (C# identifiers are case-sensitive), so both survived
as separate dictionary entries, but then both independently tried to
become their own generated source file. Roslyn's case-insensitive
uniqueness check rejected the second one, throwing
ArgumentException('...must be unique within a generator') and aborting
the entire generator run for that compilation with zero output - the
exact reported regression (CS8785 warning, cascading CS0234 for the
missing Generated namespace).

This is a long-standing latent bug (the affected dictionaries have used
the default comparer since the project's initial commit), not something
introduced by the recently-merged perf work - it was just never
previously triggered by two same-name-different-case identifiers landing
in the same compilation.

Fix: since a partial class's members can be split across any number of
source files (or consolidated into one) with zero effect on the compiled
result, group declarations by a case-insensitive hint name and merge any
colliding entries into a single generated file instead of emitting one
AddSource call per dictionary key. This preserves every declaration (both
differently-cased members still get generated and remain independently
usable) rather than silently dropping one via a case-insensitive
dictionary overwrite, or crashing the whole generator.

Validated three ways:
1. Full test suite: 91/91 passing, including two new regression tests
   (identical external-parameter names, and names differing only by
   case).
2. Reproduced the exact crash live against the actual downstream
   consumer's real WeConfig.Application project (confirmed two classes,
   SessionFactory and Engine, with external IContainer parameters
   'lifeTimeScope'/'lifetimeScope' reachable from the same compilation).
3. Patched the locally-cached FactoryGenerator analyzer DLL with this fix
   and rebuilt WeConfig.Application end-to-end: build now succeeds with
   0 errors (previously failed with the reported CS8785/CS0234).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@carl-andersson-at-westermo carl-andersson-at-westermo changed the title Fix: analyzer package missing DevelopmentDependency (transitive double-registration) Fix: generator crash when external constructor parameter names differ only by case Aug 24, 2026
@carl-andersson-at-westermo
carl-andersson-at-westermo merged commit 213e5a7 into main Aug 24, 2026
4 checks passed
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.

1 participant