Skip to content

Resolve embedded assemblies by name and version, not by simple name - #74

Merged
ByronMayne merged 1 commit into
ByronMayne:masterfrom
JesseKlaasse:fix/resolve-embedded-by-version
Oct 1, 2026
Merged

ByronMayne merged 1 commit into
ByronMayne:masterfrom
JesseKlaasse:fix/resolve-embedded-by-version

Conversation

@JesseKlaasse

Copy link
Copy Markdown
Contributor

Problem

The hoist's resolver keys loaded assemblies on their simple name (AssemblyNameComparer drops everything after the first comma). A single process — a compiler server (VBCSCompiler), an IDE — hosts the analyzers of every project it builds, so the first assembly loaded under a name shadows every later one. Two ways this breaks a generator:

  1. An older build without embedded assemblies loads first. AddAssembly sees the name already registered and returns before it looks at the newer build's SGF.Assembly:: resources, so nothing is unpacked. The embedded SourceGenerator.Foundations.Contracts never loads:

    warning CS8784: Generator 'GenerateDtosGeneratorHoist' failed to initialize. It will not contribute to the output and compilation errors may occur as a result. Exception was of type 'FileNotFoundException' with message 'Could not load file or assembly 'SourceGenerator.Foundations.Contracts, Version=2.0.16.0, Culture=neutral, PublicKeyToken=null'. The system cannot find the file specified.'
    

    Hit in the wild with Facet: 6.6.9+ is built on SGF, 6.6.8 and older are not. A dev machine that builds one repo on Facet 6.6.8 and another on 6.6.13 gets this warning in the second one until the compiler server restarts. Order-dependent: 6.6.13 first, then 6.6.8, is clean. CI never sees it (fresh process per build).

  2. Two generators embed different versions of the same dependency. The second gets the first one's copy: TypeLoadException / MissingMethodException. That is TypeLoadException across generators: embedded SourceGenerator.Foundations.Contracts resolves to an older loaded version (simple-name collision) #73.

Fix

  • AssemblyNameComparer compares name and version, so a second build or a second version gets its own entry, and AddAssembly unpacks its resources.
  • ResolveMissingAssembly: with no exact match, return the newest loaded version that is at least the requested one — the runtime binds a reference to a newer version the same way. Never an older one: it can lack what the caller was built against. A request without a version takes the newest loaded.

The rest of ResolveMissingAssembly is unchanged. Side note, not touched here: s_assembliesWithResources is never added to, so the resource-scan loop after the lookup never runs; resolution relies entirely on AddAssembly having unpacked everything up front.

Tests

AssemblyResolverTests in the Sandbox test project (it can see the generated ConsoleAppSourceGeneratorHoist). Each test emits small assemblies with Roslyn, some carrying SGF.Assembly:: resources, and loads each into its own AssemblyLoadContext, the way Roslyn loads analyzers:

Test On master With fix
Embedded_Assembly_Resolves_When_An_Assembly_With_The_Same_Name_Was_Loaded_First (case 1) ❌ FileNotFoundException ✅
Embedded_Assembly_Resolves_In_The_Requested_Version (case 2, #73) ❌ gets 1.0.0.0 ✅
Request_For_An_Older_Version_Resolves_To_The_Newer_One_Loaded ✅ (by accident: name-only) ✅
Embedded_Assembly_Resolves_For_A_Request_Without_A_Version ✅ (by accident: name-only) ✅
Request_For_A_Newer_Version_Does_Not_Resolve_To_An_Older_One ❌ gets 1.0.0.0 ✅

The two tests that pass on master guard the fallback; with the fallback disabled they fail.

Also checked end to end: packed this branch as 2.0.17-local.1, built Facet against it, and ran the failing order in one compiler server (Facet 6.6.8 project, then the locally built Facet). Stock 6.6.13: CS8784. Facet on this branch: 0 warnings; stock 6.6.13 afterwards in that same server was clean too, because the fixed resolver was attached first.

Locally (.NET SDK 10.0.300): Sandbox tests 8/8, SourceGenerator.Foundations.Tests 3/3 (net6.0, rolled forward to 8.0 because no 6.0 runtime is installed), solution build 0 warnings.

Relates to #73.

🤖 Generated with Claude Code

The hoist's resolver keyed loaded assemblies on their simple name. One process
(a compiler server, an IDE) hosts several generators and several builds of one,
so the first assembly loaded under a name shadowed every later one:

- an older build of a generator without embedded assemblies took the name, and
  the newer build never unpacked its own: FileNotFoundException, CS8784;
- two generators embedding different versions of a dependency (Contracts
  2.0.14 and 2.0.16) got whichever loaded first: TypeLoadException, ByronMayne#73.

The comparer now matches name and version. A request without an exact match
gets the newest loaded version that is at least the requested one, as the
runtime binds a reference to a newer version; never an older one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ByronMayne

Copy link
Copy Markdown
Owner

Hey Jesse,

I appreciate the effort to track down the issue and contribute a fix back to the project. Due to the nature of this project it can be rather difficult to debug due to all the different environments that generators can run in. I am just going to take a quick look but at it's face value it looks good to me.

@ByronMayne ByronMayne added the bug Something isn't working label Oct 1, 2026
@ByronMayne
ByronMayne merged commit 19e0805 into ByronMayne:master Oct 1, 2026
4 checks passed
@ByronMayne

Copy link
Copy Markdown
Owner

I created a PR to address the other issues you called out in your review #76

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants