C# 13 added a small, sharp tool to the overload-resolution rules: [OverloadResolutionPriority]. You put it on one member of an overload group and give it an integer; among the candidates the compiler already considers applicable, the one with the highest priority wins, overriding the usual "betterness" tie-breakers. The .NET 9 BCL uses it to make span-based overloads quietly take precedence over their older array- and string-based siblings, so that existing call sites bind to the faster path on a simple recompile — without a source change and without an ambiguity error.
It is also a genuinely interesting decompilation problem, and for the opposite reason to most of the constructs we have written about. The field keyword is invisible in metadata and visible only in the accessor IL. [OverloadResolutionPriority] is the reverse: it is perfectly visible on the definition and completely invisible at the call site. The attribute is a compile-time directive whose entire job is to steer a decision, and once that decision is made it leaves no fingerprint on the code that benefited from it. This post shows the metadata it writes, the call-site IL it does not write, and how Glass.NET reads each side for what it actually is.
The overload group and the attribute
Here is the shape the BCL adopted dozens of times in .NET 9: an older overload that takes an array, and a newer, faster one that takes a ReadOnlySpan<T>. Both are applicable when you pass an array, because there is an implicit array-to-span conversion. Historically the array overload won (it is an exact match), so a recompile would never move to the span path. The attribute flips that:
using System; using System.Runtime.CompilerServices; public static class Sink { [OverloadResolutionPriority(1)] public static void Add(ReadOnlySpan<int> items) { /* fast path */ } public static void Add(int[] items) { /* legacy path */ } } OverloadResolutionPriorityAttribute lives in System.Runtime.CompilerServices and is declared, in essence, like this:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] public sealed class OverloadResolutionPriorityAttribute : Attribute { public OverloadResolutionPriorityAttribute(int priority) => Priority = priority; public int Priority { get; } } The priority is a plain int; the default (everything unmarked) is 0, and negatives de-prioritize a member. The rule only ever applies within candidates declared in the same type, and only after the compiler has reduced the set to those that are applicable — it is a final tie-breaker, not a way to call something that otherwise would not bind.
What the attribute writes into metadata
Applied to Add(ReadOnlySpan<int>), the attribute compiles to a single custom-attribute record hanging off that method:
.method public hidebysig static void Add(valuetype [System.Runtime]System.ReadOnlySpan`1<int32> items) cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute::.ctor(int32) = ( 01 00 01 00 00 00 00 00 ) // ... } The blob decodes exactly the way any single-int32-argument attribute does. The leading 01 00 is the fixed prolog. The next four bytes 01 00 00 00 are the int32 argument in little-endian — the value 1. The trailing 00 00 says there are zero named arguments. There is nothing construct-specific here: it is the same CustomAttribute (0x0C) table row that [Obsolete] or [MethodImpl] would produce, differing only in its Type column (the attribute's .ctor) and its Value blob.
That is the whole metadata footprint. Glass reads the row, resolves the Type token to OverloadResolutionPriorityAttribute, decodes the blob against the constructor signature, and prints the attribute back verbatim above the method. Because the attribute is ordinary, this round-trips cleanly — what you decompile is what the author wrote, priority value and all. You can confirm the raw bytes yourself in Glass's metadata pane, which shows the CustomAttribute row with its parent token and the literal blob, right next to the rendered C#.
The call site, where the attribute leaves no trace
Now the interesting half. Here is code that calls into the group with an array argument:
Sink.Add(new[] { 1, 2, 3 }); Because of the priority, the compiler binds this to the span overload and inserts the implicit array-to-span conversion. The emitted IL:
ldc.i4.3 newarr [System.Runtime]System.Int32 dup ldc.i4.0 ldc.i4.1 stelem.i4 // … stelem.i4 for elements 1 and 2 … call valuetype [System.Runtime]System.ReadOnlySpan`1<int32> [System.Runtime]System.ReadOnlySpan`1<int32>::op_Implicit(!0[]) call void Sink::Add(valuetype [System.Runtime]System.ReadOnlySpan`1<int32>) Compare it with the IL you would have gotten before the attribute existed, when the array overload won:
ldc.i4.3 newarr [System.Runtime]System.Int32 dup ldc.i4.0 ldc.i4.1 stelem.i4 // … stelem.i4 for elements 1 and 2 … call void Sink::Add(int32[]) The two differ only in which Sink::Add is named, plus the op_Implicit call the span path needs. Neither IL stream contains any reference to OverloadResolutionPriority. The attribute did its work entirely at compile time: it tilted the resolution, the resolution produced a bound call, and the bound call is all the IL remembers. Reading the caller, a decompiler cannot say "an overload-resolution priority was consulted here" — there is nothing to read. It can only report the method that was actually invoked.
Why "faithful to the IL" is the right behavior
It would be tempting to imagine a decompiler that, seeing a call to the span overload, "helpfully" simplified the op_Implicit away and showed Sink.Add(new[] { 1, 2, 3 }) as if nothing special happened. Glass does exactly that at the source level — it renders the natural C# call, because that is what the author wrote — but it does so by decompiling the bound call it found, not by re-running overload resolution. That distinction matters.
If Glass re-resolved the overload from scratch while rendering the caller, it would need to know the priority to reproduce the original binding. Attributes are preserved in reference assemblies, so the information is usually there — but a decompiler that depends on re-resolution is one bad input (a stripped attribute, a mismatched reference set) away from silently rebinding the call to the array overload and producing source that compiles to different IL. By decompiling the instruction stream the compiler actually emitted, Glass sidesteps the whole question: the chosen target and its conversion are facts in the IL, and Glass reports facts. The priority attribute is rendered where it lives — on the definition — and the call site is shown as the concrete call it is.
Reading it in practice
Point Glass at the assembly and two things fall out cleanly. On the type Sink, the span overload carries [OverloadResolutionPriority(1)] exactly as written, and the metadata pane shows the backing CustomAttribute row if you want to see the blob. On any caller, you see the ordinary, bound call — to the span overload, with the array-to-span conversion inline — and no mention of priority, because none was emitted. From the CLI the same split appears in the exported source:
glass decompile Sink.dll --type Sink --member Add glass decompile App.dll --type Program --member Main The first prints the attribute on the definition; the second prints the resolved call with no attribute anywhere near it. That asymmetry is not a gap in the tool — it is an honest picture of what [OverloadResolutionPriority] is: a compile-time instruction that documents an intent on the definition and leaves the rest of the program none the wiser. Reading it back is less about reconstruction and more about knowing which half of the binary to look at, and trusting the IL for the other half.

