Last Updated: May 17, 2026
An anonymous type is a small, readonly class the compiler generates on the spot when you write new { ... } without a type name. It's useful when you need to bundle a few values together for a short stretch of code, especially when shaping the result of a query, and you don't want to write a whole class just to hold them. This chapter covers the syntax, what the compiler builds behind the scenes, where anonymous types fit, and where they fall short.
The simplest way to see an anonymous type is to make one:
No class declaration anywhere. No Product type defined in the file. Just new { ... } with named properties, and a variable to hold the result.
What actually happens at compile time is that the C# compiler invents a class for you. It picks an unspeakable name (something like <>f__AnonymousType0), gives it two readonly properties (Name of type string and Price of type decimal), generates a constructor that takes both, and overrides Equals, GetHashCode, and ToString based on those properties. From that point on, the variable product is just an instance of that hidden class.
Three things follow from this:
var is the only way to declare a variable that holds one.A var product = new { ... } declaration is the canonical shape. Forget any one of the three pieces (the var, the new, the { ... }), and you're writing something different.
Anonymous types pick up a shortcut for the common case where the property you want to expose comes straight from another variable or member. Instead of writing new { Name = product.Name }, you can write new { product.Name }, and the compiler infers the property name from the source.
The anonymous type has two properties named customerName and cartTotal, exactly matching the variable names. The compiler does this for any expression that ends in an identifier: local variables, parameters, fields, properties, and method-call-free member accesses like product.Price or order.Total.
This makes projections much shorter when you're forwarding existing data:
The listing variable has exactly two properties, both inferred. The full product object has four, but the projection only keeps the two you care about. This pattern is the workhorse of LINQ projections, which the LINQ section covers in depth.
Property name inference only works when the expression is itself an identifier or a member access. If you compute a value, you have to name it explicitly:
Property names within a single anonymous type must be distinct, just like fields in a regular class. You can't have two Price properties.
It helps to see the equivalent class the compiler builds. If you write:
The compiler generates something roughly equivalent to:
You never see this class in your source code, but it has all the standard plumbing. The ToString override is especially convenient when debugging:
No format string, no custom code. The compiler-generated ToString walks the properties and prints them in a readable form.
The generated class is internal sealed, meaning it lives only inside the assembly that contains the code that created it, and nothing can inherit from it. You can't add methods to it, you can't extend it with extra properties later, and you can't reference it by name from another file.
Here's a diagram of what's happening behind the scenes:
The single literal turns into a full class plus an object of that class. You write four lines and get a fully equipped reference type, complete with value-based equality.
Because the compiler overrides Equals and GetHashCode, two anonymous objects compare equal when every property has the same value, not when the two references point to the same heap object. This is structural equality, the same flavor records use.
a and b are two separate heap objects (ReferenceEquals confirms it), but the generated Equals walks both properties, finds them equal, and returns true. a and c differ in Price, so they're not equal.
A second rule follows from how the compiler picks anonymous types: two anonymous objects share the same generated class only when their property names, types, and order match. If you flip the order, you get a different generated type, and Equals returns false because the types don't match.
Property order is part of the type identity. If you want a and b to compare equal, write the properties in the same order in both places.
Cost: Each call site like new { Name = ..., Price = ... } produces a heap allocation. Inside a tight loop that runs millions of times, those allocations add up. For hot paths consider a record or a named struct.
Anonymous types earn their keep when you need to reshape data temporarily, without bothering to declare a class. The textbook example is a LINQ projection that picks specific columns out of a richer source:
The full Product class has four properties, but the listing only needs two. Declaring a separate ProductListing class with just Name and Price would work, but it'd be a class that exists for one method, used in one loop, and named because the language insists. The anonymous projection drops that ceremony.
The same idea shows up when joining or grouping. Suppose you want a quick summary of orders by customer:
The shape { Customer, Total, Count } only exists between the Select and the foreach. There is no reason to give that shape a name in the type system. Anonymous types let the projection stay focused on what it produces, not on how to declare the holder.
Anonymous types are deliberately small. The compiler builds just enough to support the basic projection use case, and not more. The limits matter because they decide when an anonymous type is the right tool and when you need to reach for something else.
You can't write the type name. The generated class has an unspeakable identifier, so you can't declare a field, parameter, or return type using it. This is what forces var for the variable.
They're hard to return from a method. Since you can't name the type, a method that returns an anonymous object has to declare its return type as object, dynamic, or a generic parameter. None of those preserve the property names at the call site, which defeats the point.
You'd have to use reflection or dynamic to read the properties, both of which give up compile-time checking. If a method needs to return a shaped object, declare a real type for it (or a record).
You can't add methods. The generated class has only properties, plus the standard equality and ToString overrides. There's no way to add GetDiscountedPrice() to an anonymous type. If your shape needs behavior, it should be a class or a record.
You can't inherit from them. The generated class is sealed. You can't subclass it, you can't have it implement an interface you define, and you can't add it to a heterogeneous list via a shared base type. Anonymous types are leaf types, not building blocks for hierarchies.
You can't use them across assemblies cleanly. Even though the generated class is internal, two assemblies can each produce anonymous types with the same shape, and they'll have different runtime types. Equality between them fails. Anonymous types are local to one compilation unit by design.
The pattern that captures all of this: use anonymous types where they're created, throw them away in the same method, and don't try to give them a longer life.
C# has three close-by ways to bundle a few values together: anonymous types, value tuples ((decimal Total, int Count)), and records. They overlap, but the trade-offs are real and worth knowing.
| Feature | Anonymous type | ValueTuple | Record |
|---|---|---|---|
| Declaration | new { Name, Price } | (Name, Price) | record Listing(string Name, decimal Price); |
| Type category | Reference (class) | Value (struct) | Reference (default) or value (record struct) |
| Property names | Required, part of type | Optional, not part of type | Required, part of type |
| Mutable? | No | Yes (fields) | Init-only by default |
| Equality | Value, by all properties | Value, by all fields | Value, by all properties |
| Can return from method | Only as object | Yes, fully named | Yes, fully named |
| Can be used across files | No | Yes | Yes |
| Inheritance | No (sealed) | No | Yes (with other records) |
| Add methods | No | No (helper extensions only) | Yes |
| Heap allocation | Yes (every new) | No (stack, unless boxed) | Yes (unless record struct) |
Read the table column by column. Anonymous types are the lightest in syntax for a one-off shape, but the most limited in lifespan. Tuples are perfect for returning two or three loosely related values from a method without dragging in a class. Records are the right tool when the shape has a name worth declaring and lives across multiple methods or files.
Here are the same three values expressed all three ways:
The output is the same, but the code shape is different. The anonymous version says "I need a holder, here is one." The tuple version says "I want two values without naming the type." The record version says "this shape has a name and a place in my codebase."
A simple decision rule:
Select produce something useful, use an anonymous type.A small program that pulls the chapter's ideas together. The cart has products with several properties; we project a couple of shaped views for display and reporting, never declaring an extra named class.
Two anonymous types appear. The first, inside Select, has four properties: three forwarded from Product (using property name inference) and one computed (Subtotal, which has to be named explicitly because it's an expression). The second, summary, is a single object that holds three aggregates. Neither shape is worth a named class. They each exist for half a screen of code, then disappear.
If the cart-row shape grew into something the rest of the app needed (say a CartRowDto returned from a service), the right move would be to promote it to a record. That's the natural progression: anonymous in one method, record when it crosses boundaries.
internal sealed class with read-only properties, structural Equals/GetHashCode, and a readable ToString. You create one with new { Property = value, ... }.var because the generated class name is unspeakable.new { product.Name, product.Price } and have the compiler use the right-hand identifier as the property name. Computed values still need an explicit name.Equals when they share the generated type (same property names, types, and order) and every property value matches. == falls back to reference equality.Select that shapes a few values for local consumption. They make projections short without forcing you to declare a one-off class.object), can't add methods, can't inherit, and can't be shared across assemblies cleanly. As soon as a shape outgrows one method, promote it to a record or a class.