The GAC probe only ever looked for the exact folder of the requested version.
For about a hundred assemblies the .NET Framework 4.7.2/4.8 reference assemblies
carry a higher version than the implementation ever installed in the GAC
(System.IO.Compression is 4.2.0.0 against 4.0.0.0 in the GAC, System.Runtime is
4.1.2.0, ...), because out-of-band packages shipped those versions and the ref
assemblies had to keep up. The runtime hides this behind assembly unification;
without an equivalent, every reference to one of them was reported as
unresolvable.
Matching on the major version keeps assemblies apart that share a name but are
different products, e.g. Microsoft.Build.Framework 4.0.0.0 and 15.x.
Assisted-by: Claude:claude-opus-5:Claude Code
A chain of type forwarders is followed by assembly name, and every name resolves relative
to the assembly being decompiled - so a chain that leaves for another framework can be
pulled straight back into the directory it started in. A .NET Standard 2.0 assembly
sitting among .NET Framework 4.6.1 facades lost System.Linq.Enumerable that way: the
chain went netstandard -> System.Core (from the shared framework) -> System.Linq (back to
the input directory) -> netstandard, arriving at an assembly it had already passed
through. Nothing in the closure defines the type, so it stayed unknown and every LINQ
call decompiled as a static call with a delegate cast.
Once the closure is loaded, chains that return to an assembly they already visited are
walked a second time, resolving each hop next to the assembly that forwards it. The
assembly ending the repaired chain is loaded only once it is confirmed to declare the
type; it then wins the deduplication against the assembly of the same name it displaces,
which version order says nothing about. An assembly that neither forwards nor declares
the type ends the walk with nothing loaded, so a failed repair cannot displace anything.
Only chains that are already broken are walked twice. Preferring the forwarder's own
directory as a resolution policy was tried first and rejected: measured against a corpus,
it moved a .NET 8 facade's System.Runtime reference out of a net4x compilation, splitting
type identities so that overrides printed as virtual. The two cases cannot be told apart
where references are resolved, because that layer sees assembly names, not the type whose
chain is or is not terminating.
AssemblyReference now knows the module that declares it, which is what lets a hop be
resolved next to its forwarder, and its metadata reader is that module's.
A chain that cannot be repaired is reported in the reference load log the UI already
shows, once per reference: a facade forwards hundreds of types and they all fail together.
Assisted-by: Claude:claude-opus-5:Claude Code
The events introduced in #2519 timed only the five per-entity DoDecompile
overloads, allocated a Stopwatch and the member's FullName even when no
trace session was attached, and reported whole milliseconds, which rounds
almost every member to zero. Flat one-shot events also gave PerfView and
dotnet-trace no way to show durations or nesting, and the actually
expensive stages (type system initialization, assembly resolution probing,
the IL/AST transform pipelines, whole-project decompilation) were not
instrumented at all.
Start/Stop event pairs let trace viewers derive duration and nesting from
event timestamps, keywords let a session enable only the areas of
interest, and every call site is gated on IsEnabled() so tracing costs a
branch when disabled. Per-transform events are Verbose because of their
volume; unlike the STEP/Stepper mechanism they work in Release builds.
EventSource is in-box for netstandard2.0 and flows over both ETW and
EventPipe, so this stays cross-platform with no new dependency.
Assisted-by: Claude:claude-fable-5:Claude Code
Hosts without a .NET Framework installation (e.g. Linux and macOS) have
no GAC; the only system-wide assembly store there is the shared-framework
directory of the runtime executing the decompiler, and
UniversalAssemblyResolver only consulted it through the version <= 4.0
legacy fallback. This made e.g. the type-forwards of a netstandard facade
(pointing to a versioned System.Runtime) unresolvable, which left
well-known types like Nullable<T> without a definition and among other
things misaligned nullability decoding (Nullable<T> occupies no slot in
the nullable metadata, so it must be recognized).
On Windows nothing the GAC answered changes; the new fallback only adds
resolutions that previously failed outright.
Assisted-by: Claude:claude-fable-5:Claude Code
DecompilerTypeSystem uses this to resolve/load multiple assemblies in parallel.
Unfortunately this doesn't gain us any performance yet in ILSpy because there we have a global assembly-loader-lock :(