Skip to content
← All posts
· Delta1 Labs Glass.NETDecompilation.NETDeep Dive

Decompiling anonymous types: the generated generic class behind new { a, b }

An anonymous type has no name you wrote, but it is a real class in the metadata — a generic one the compiler synthesizes, named something like <>f__AnonymousType0`2, with a readonly property per member, a constructor that takes them all, and overridden Equals, GetHashCode and ToString. The trick that makes it reusable is that the generated class is generic over its property types, so two new { } sites with the same property names in the same order share one backing type instantiated with different type arguments. This post reads the generated class and the newobj call site in IL, shows where the property names actually live (on the generated type’s properties, not on the <Name>j__TPar type parameters), walks the structural Equals and the hash-combine GetHashCode the compiler writes, and explains how Glass.NET recognizes the pattern and folds it back into new { Name = ..., Total = ... } instead of leaving you an unspeakable generic type.

Type a new { } in C# and you create an object with no type you can name. var row = new { Name = customer.Name, Total = customer.Total }; gives you a row you can read .Name and .Total off, pass to LINQ, and compare for equality — but try to write its type and you cannot, because there is no keyword for it. That absence is a source-language illusion, exactly like ?. having no IL opcode. In the compiled assembly the anonymous type is a perfectly ordinary class. The compiler just synthesized it, gave it a name you are not allowed to pronounce, and emitted it alongside your code. Decompiling an anonymous type well means recognizing that synthesized class for what it is and folding it back into the new { } that produced it.

The class the compiler writes

For new { Name = ..., Total = ... } the C# compiler emits a sealed, generic class — one type parameter per property — that looks, decompiled literally, like this:

[CompilerGenerated]
[DebuggerDisplay(@"\{ Name = {Name}, Total = {Total} }", Type = "<Anonymous Type>")]
internal sealed class <>f__AnonymousType0<<Name>j__TPar, <Total>j__TPar>
{
    private readonly <Name>j__TPar <Name>i__Field;
    private readonly <Total>j__TPar <Total>i__Field;

    public <Name>j__TPar Name  => <Name>i__Field;
    public <Total>j__TPar Total => <Total>i__Field;

    public <>f__AnonymousType0(<Name>j__TPar Name, <Total>j__TPar Total)
    {
        <Name>i__Field  = Name;
        <Total>i__Field = Total;
    }
    // Equals, GetHashCode, ToString overrides follow — see below.
}

The backtick-2 in the real metadata name <>f__AnonymousType0\2means two type parameters. Being generic is the whole reason the type is reusable: the compiler closes it over concrete type arguments at each call site, so everynew { }` with the same property names in the same order shares this one generated class.

var a = new { Name = "Acme", Total = 42 };     // <>f__AnonymousType0<string, int>
var b = new { Name = other,  Total = count };  // SAME type, same instantiation
var c = new { Name = "x",    Total = 1L };     // <>f__AnonymousType0<string, long> — same type, different args
var d = new { Total = 1, Name = "x" };         // DIFFERENT type — order of properties differs

Change the names, reorder them, or add a property and the compiler generates a different backing type (<>f__AnonymousType1, and so on). Same names, same order — one shared type, however many literals you wrote.

new { Name=.., Total=.. } #1new { Name=.., Total=.. } #2new { Total=.., Name=.. }<>f__AnonymousType0`2Name, Total — shared backing class<>f__AnonymousType1`2Total, Name — different order, different classsame names + order → one class, closed over different type args

The call site

Creating an anonymous object is nothing exotic in IL — it is a newobj of the constructed generic type, with the property values pushed in declaration order:

// var row = new { Name = c.Name, Total = c.Total };
ldarg.0
callvirt   instance string Customer::get_Name()
ldarg.0
callvirt   instance int32  Customer::get_Total()
newobj     instance void class '<>f__AnonymousType0'<string, int32>::.ctor(!0, !1)
stloc.0

The newobj operand carries the closed generic type and its constructor signature (!0, !1) — the two type parameters, bound here to string and int32. That is the signature of an anonymous type at the point of use: a constructor call that fills every readonly field at once. There is no setter path, because the properties are get-only; an anonymous type is immutable by construction.

Where the names actually live

The easy mistake is to assume the property names hide in the type-parameter names <Name>j__TPar and <Total>j__TPar, and would therefore vanish in a release build. They do not. Those type-parameter names are only hints; the authoritative names are the real Property metadata rows on the generated class — get_Name, get_Total, and the properties that expose them. A decompiler reads the names straight off those rows, which is why new { Name = ..., Total = ... } reconstructs with its members intact even with no PDB. What a PDB would have added is the name of the local you stored the instance in (row), not the shape of the anonymous type, which is fully present in metadata regardless.

The three overrides

Anonymous types are value-equal, and the compiler makes that real with three overrides. Equals is structural: it casts the argument to the same closed generic type and compares each field through EqualityComparer<T>.Default.

public override bool Equals(object value)
{
    var other = value as <>f__AnonymousType0<<Name>j__TPar, <Total>j__TPar>;
    return other != null
        && EqualityComparer<<Name>j__TPar>.Default.Equals(<Name>i__Field,  other.<Name>i__Field)
        && EqualityComparer<<Total>j__TPar>.Default.Equals(<Total>i__Field, other.<Total>i__Field);
}

GetHashCode is a fixed-shape hash combine: it starts from a per-type seed constant the compiler bakes in and folds each field’s hash in with the multiply-by-prime pattern Roslyn uses everywhere.

public override int GetHashCode()
{
    int hash = 339618440;                 // per-type seed baked in by the compiler
    hash = hash * -1521134295 + EqualityComparer<<Name>j__TPar>.Default.GetHashCode(<Name>i__Field);
    hash = hash * -1521134295 + EqualityComparer<<Total>j__TPar>.Default.GetHashCode(<Total>i__Field);
    return hash;
}

ToString builds { Name = ..., Total = ... } with a StringBuilder, which is also what the [DebuggerDisplay] on the class mirrors. None of these three are code you wrote; they are the compiler’s signature for “this is an anonymous type,” and together they are most of what a decompiler keys on.

name prefix <>f__AnonymousType[CompilerGenerated] + [DebuggerDisplay]sealed generic, get-only props over readonly fieldsctor: one arg per prop, declaration orderoverrides Equals / GetHashCode / ToStringfold to projectionnew { Name = .., Total = .. }

How Glass folds it back

No single signal is conclusive — a generic sealed class is ordinary, and [CompilerGenerated] sits on many synthesized types. Glass.NET matches the cluster: the <>f__AnonymousType name prefix, the [CompilerGenerated] and [DebuggerDisplay] pair whose format string literally spells the properties, the get-only properties over readonly fields, the single all-arguments constructor in declaration order, and the three structural overrides in their exact compiler shapes. When they line up, Glass suppresses the synthesized type entirely — you never see <>f__AnonymousType0 in the output — and rewrites every newobj of it as a new { ... } projection, taking the member names from the properties and the values from the constructor arguments at the call site.

// What Glass renders from the newobj above — the synthesized type is gone.
var row = new { Name = c.Name, Total = c.Total };

Two wrinkles are worth naming. When the initializer value already matches the member name — new { c.Name } instead of new { Name = c.Name } — the compiler infers the property name from the expression, and Glass restores the shorter projection initializer form rather than spelling out Name = . And LINQ query syntax leans on anonymous types for let clauses and multi-source joins, where the compiler threads a chain of nested anonymous types called transparent identifiers; Glass has to unwind that nesting to recover the original let rather than surfacing the plumbing. In both cases the principle is the same: the anonymous type is a real class the compiler wrote, fully described in metadata, and the decompiler’s job is to make it unspeakable again — to hand you back the new { } you started with, not the machinery underneath it.

Try Nebula.NET

Harden your .NET code in minutes — start with the free edition.