Writing new with no class name creates an anonymous type — a compiler-generated, strongly-typed, read-only object built on the spot. It is the workhorse of LINQ projections and grouping. This article covers what it is, how it behaves, every place it earns its keep, and the one thing it cannot do.
This misc-topic note is about the small piece of syntax that appears constantly in LINQ and confuses almost everyone the first time: a new expression with no type name after it, followed by an object initializer. It creates an anonymous type. Understanding it makes a huge amount of everyday C# suddenly readable.
The example that usually raises the question is a grouping query like this one, which totals hours worked per employee:
var perEmployee = shifts
.GroupBy(s => s.EmployeeId)
.Select(g => new
{
Id = g.Key,
Hours = g.Sum(s => (s.End - s.Start).TotalHours)
});The new with no class name is the thing to explain. It is not a special LINQ feature — it is a general C# tool that LINQ happens to lean on heavily.
A normal object needs a named class:
var employee = new Employee { Id = 10, Name = "John" }; // Employee is the typeAn anonymous object skips the name. The new keyword is followed directly by the property list, and the compiler invents a type to hold it:
var employee = new
{
Id = 10,
Name = "John"
};
Console.WriteLine(employee.Id); // 10
Console.WriteLine(employee.Name); // JohnBehind the scenes the compiler generates a class roughly like this — a real type, just one without a name a programmer can write:
class SomeGeneratedType // name is compiler-controlled and unspeakable
{
public int Id { get; }
public string Name { get; }
}In LINQ, this is called a projection: taking each item in a sequence and reshaping it into a new object that holds exactly the fields wanted.
Because the type has no name, there is nothing to write on the left side of the assignment except var. That leads to a common misunderstanding, so it is worth stating plainly:
var is not dynamic. Writing
varon an anonymous object does not make it loosely typed. The compiler knows every property and its exact type, so misusing one — calling a string method on an integer property, for instance — is a compile error, not a runtime surprise.varhere means only "compiler, infer the exact type," and that type just happens to be one that cannot be spelled by hand.
So employee.Id is known to be an int and employee.Name a string, with full IntelliSense and compile-time checking, exactly as a named class would give.
The properties are accessed like any object's, and their values do not have to be simple copies — they can be computed inline:
var summary = new
{
Total = 10 + 20,
Average = (10 + 20) / 2.0,
IsValid = 10 > 5
};
// Total = 30, Average = 15, IsValid = trueThat is exactly what the shifts query does: Hours is not read from anywhere, it is calculated with g.Sum(...) at the moment the object is built.
There are three ways the property names get set. They can be given explicitly, inferred from a variable's name, or inferred from a member being accessed — the last two are called projection initializers:
int id = 10;
string name = "John";
var a = new { id, name }; // names taken from the variables: id, name
var b = new { employee.Id, employee.Name }; // names taken from the members: Id, Name
var c = new { EmployeeId = id, FullName = name };// explicit names: EmployeeId, FullNameThis is why LINQ projections are often very terse — a new listing just x.Name and x.Price — the member names carry over automatically, and only fields that need renaming or computing get an explicit Name = assignment.
The generated properties are read-only. Once an anonymous object is created, its properties cannot be reassigned:
var original = new { Id = 1, Name = "John" };
// original.Id = 2; // compile error: property is read-onlyTo get a modified version, make a new object. Since C# 10, the with expression does this concisely — a copy with selected properties changed:
var changed = original with { Id = 2 }; // new object: Id = 2, Name = "John"The original is untouched; changed is a separate instance.
A subtle but important rule: within the same assembly, two anonymous initializers with the same property names, of the same types, in the same order produce the same generated type. They are not two look-alike types — they are one:
var a = new { Id = 1, Name = "x" };
var b = new { Id = 2, Name = "y" };
a = b; // legal: a and b have the identical anonymous typeThis is exactly why a LINQ Select that builds anonymous objects returns a clean IEnumerable<T> of a single type rather than a jumble — every iteration produces the same shape, so the compiler gives them all the same type. That is what makes foreach over the shifts result work:
foreach (var row in perEmployee)
Console.WriteLine($"Employee {row.Id}: {row.Hours} hours");Anonymous types come with value-based equality and a helpful string form generated for free:
var a = new { Id = 1, Name = "John" };
var b = new { Id = 1, Name = "John" };
Console.WriteLine(a.Equals(b)); // True — equal because every property value matches
Console.WriteLine(a == b); // False — == is still reference comparison for a class
Console.WriteLine(a); // { Id = 1, Name = John } — generated ToStringThe distinction matters: Equals (and GetHashCode) compare by value across all properties, but == is not overloaded, so it stays reference comparison. That value-based Equals/GetHashCode is the quiet feature that powers the best use cases below.
The whole point is temporary, in-method data shapes — most often inside a LINQ query. The main use cases:
Projecting a subset of fields. Pull only what is needed from a larger object, dropping the rest:
var listing = employees.Select(e => new { e.Id, e.Name }); // salary etc. droppedGrouping with aggregation. The opening example — group, then build one summary object per group:
var perEmployee = shifts
.GroupBy(s => s.EmployeeId)
.Select(g => new { Id = g.Key, Hours = g.Sum(s => (s.End - s.Start).TotalHours) });Composite keys for GroupBy. This is the use case where anonymous types are genuinely hard to replace. Grouping by more than one field needs a key that is equal when all its fields are equal — which is exactly anonymous-type value equality:
var byRegionYear = orders
.GroupBy(o => new { o.Region, o.Year }) // one key per Region+Year combination
.Select(g => new
{
g.Key.Region,
g.Key.Year,
Total = g.Sum(o => o.Amount)
});Distinct across multiple fields. The same value equality makes de-duplicating on a combination of fields a one-liner:
var uniquePairs = people
.Select(p => new { p.City, p.Country })
.Distinct(); // two rows are duplicates only if both City and Country matchShaping join results. Combine fields from both sides of a join into a single flat object:
var staffing = employees.Join(
departments,
e => e.DeptId,
d => d.Id,
(e, d) => new { e.Name, Department = d.Name });The one to remember: composite keys. Grouping or de-duplicating by several fields is awkward without anonymous types, because a plain class would need
Equals/GetHashCodewritten by hand. An anonymous initializer gives that value equality for free, so it is the natural composite key forGroupByandDistinct— and the place anonymous types are least replaceable.
Because the type has no name, there is nothing to write as a method's return type:
public ??? GetEmployee() // no name exists to put here
{
return new { Id = 10, Name = "John" };
}Anonymous types are confined to the method that creates them (they can be passed around as object, but then all the type information is lost). The moment a shape needs to be returned, stored in a field, or used in a public API, it needs a real named type instead:
public record EmployeeDto(int Id, string Name);
public EmployeeDto GetEmployee() => new(10, "John");Anonymous types are one of several ways to bundle a few values. The right choice depends on whether the shape needs a name and whether it crosses method boundaries:
| Approach | Has a name | Compile-time safe | Can leave the method | Best for |
|---|---|---|---|---|
| Anonymous type | no | yes | no | temporary shapes inside a LINQ query |
Tuple (ValueTuple) | no (elements can be named) | yes | yes | returning a couple of values from a method |
record | yes | yes | yes | a real data type reused in several places |
| class / DTO | yes | yes | yes | public API shapes and entities |
Dictionary<string, object> | no | no | yes | truly dynamic keys unknown at compile time |
The rule of thumb: anonymous type for a throwaway shape inside one query; a record or DTO the moment that shape needs a name. Tuples fill the gap when a method needs to return a small, unnamed bundle — the one thing anonymous types cannot do.
new with no type name; var holds it because there is no name to write.var is not dynamic; every property is compile-time checked.with expression makes modified copies.ToString.GroupBy/Distinct keys, and join results — with composite keys being the standout.record, tuple, or DTO takes over the moment the shape must leave the method.