VulCode
Blog

ILSpy: the decompiler no longer leaves a delegate cache field behind

For _ = (Action<int>)M; ILSpy produced output with a <>O.<0>__M field that does not compile. That spot now becomes a plain delegate construction.

DualFroz - VulCode CEODualFroz - VulCode CEO1 min read
ILSpy: the decompiler no longer leaves a delegate cache field behind - DualFroz - VulCode CEO
Title in the repository
Remove the delegate cache left by a discarded method-group conversion
Merged
19 Sep 2026
Written up here
6 Oct 2026

The note was written 17 days after the change was merged.

ILSpy
Technologies:C#.NETIL

What was broken

The C# compiler stores a delegate created from a method group in a static cache field, so it does not have to build it again on every call. ILSpy, the open source .NET decompiler, recognises that pattern and turns it back into readable code. The transform, however, assumed the cache field would be read exactly once.

With _ = (Action<int>)M; the delegate is created and immediately discarded, so the field is never read. The pattern did not match, and the decompiled output kept a field named <>O.<0>__M, a name that cannot be used in C#. That code does not compile (issue #3965).

What we changed

When the cache field has no reads at all, the cache check is replaced by the delegate construction it guarded. The output now contains new Action<int>(M);, which compiles and does the same thing.

How we tested it

We added an Issue3965 test case with nine scenarios: a single discarded conversion, a conversion before and after a use of the same method, several conversions in a row, two to the same method, and conversions inside a condition and a loop. Each has its expected output written out, and the test compares the decompiled result against it.

The result

The change was merged on 19 September 2026. ILSpy also accepted our second change (#4134), which recognises the type test in a Visual Basic exception filter.

The same case before and after the change

On the example of the note: what came out before the fix and what comes out now.

The C# code we decompile
_ = (Action<int>)M;

A delegate made from a method group and discarded at once, so its cache field is never read.

Before the change
<>O.<0>__M

The output kept the cache field, with a name C# cannot use. The code did not compile (issue #3965).

After the change
new Action<int>(M);

A plain delegate creation: it compiles and does the same thing.

What it taught us

A decompiler has to recognise the patterns a compiler emits, or it leaves optimisation debris in the code. Each fix meant reading exactly what the C# and VB compilers emit.

From the change in pull request #4133. The Issue3965 test cases hold nine scenarios, each with its expected output.

Questions about change #4133

Where the change is, how we tested it and who merged it.