MOFAKH.COM
← Back to profile
Miscellaneous

Anonymous types in C#: the shape-on-the-spot object

Aug 31, 202614 min readWritten

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:

csharp
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.

What an anonymous type is

A normal object needs a named class:

csharp
var employee = new Employee { Id = 10, Name = "John" };   // Employee is the type

An anonymous object skips the name. The new keyword is followed directly by the property list, and the compiler invents a type to hold it:

csharp
var employee = new
{
    Id = 10,
    Name = "John"
};
 
Console.WriteLine(employee.Id);    // 10
Console.WriteLine(employee.Name);  // John

Behind the scenes the compiler generates a class roughly like this — a real type, just one without a name a programmer can write:

csharp
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.

Select projects each item into a new shape
Shift
EmployeeId, Start, End
Select
anonymous
Id, Hours

var is required, but the type is real

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 var on 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. var here 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.

Reading and computing properties

The properties are accessed like any object's, and their values do not have to be simple copies — they can be computed inline:

csharp
var summary = new
{
    Total = 10 + 20,
    Average = (10 + 20) / 2.0,
    IsValid = 10 > 5
};
 
// Total = 30, Average = 15, IsValid = true

That 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.

Property names: explicit, inferred, or renamed

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:

csharp
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, FullName

This 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.

Anonymous types are immutable

The generated properties are read-only. Once an anonymous object is created, its properties cannot be reassigned:

csharp
var original = new { Id = 1, Name = "John" };
// original.Id = 2;                          // compile error: property is read-only

To get a modified version, make a new object. Since C# 10, the with expression does this concisely — a copy with selected properties changed:

csharp
var changed = original with { Id = 2 };      // new object: Id = 2, Name = "John"

The original is untouched; changed is a separate instance.

Two anonymous objects can share one type

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:

csharp
var a = new { Id = 1, Name = "x" };
var b = new { Id = 2, Name = "y" };
a = b;   // legal: a and b have the identical anonymous type

This 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:

csharp
foreach (var row in perEmployee)
    Console.WriteLine($"Employee {row.Id}: {row.Hours} hours");

Value equality, and a readable ToString

Anonymous types come with value-based equality and a helpful string form generated for free:

csharp
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 ToString

The 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.

Where anonymous types earn their keep

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:

csharp
var listing = employees.Select(e => new { e.Id, e.Name });   // salary etc. dropped

Grouping with aggregation. The opening example — group, then build one summary object per group:

csharp
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:

csharp
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:

csharp
var uniquePairs = people
    .Select(p => new { p.City, p.Country })
    .Distinct();      // two rows are duplicates only if both City and Country match

Shaping join results. Combine fields from both sides of a join into a single flat object:

csharp
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/GetHashCode written by hand. An anonymous initializer gives that value equality for free, so it is the natural composite key for GroupBy and Distinct — and the place anonymous types are least replaceable.

The one thing they cannot do: leave the method

Because the type has no name, there is nothing to write as a method's return type:

csharp
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:

csharp
public record EmployeeDto(int Id, string Name);
 
public EmployeeDto GetEmployee() => new(10, "John");

Choosing between the alternatives

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:

ApproachHas a nameCompile-time safeCan leave the methodBest for
Anonymous typenoyesnotemporary shapes inside a LINQ query
Tuple (ValueTuple)no (elements can be named)yesyesreturning a couple of values from a method
recordyesyesyesa real data type reused in several places
class / DTOyesyesyespublic API shapes and entities
Dictionary<string, object>nonoyestruly 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.

What this covered

  • An anonymous type is a nameless, compiler-generated class created by new with no type name; var holds it because there is no name to write.
  • It is fully typedvar is not dynamic; every property is compile-time checked.
  • Properties are read-only, names can be explicit or inferred, and a with expression makes modified copies.
  • Anonymous objects with the same shape share one type, and have value equality plus a readable ToString.
  • The core use cases are LINQ projections, grouping with aggregation, composite GroupBy/Distinct keys, and join results — with composite keys being the standout.
  • They cannot be returned or named, so a record, tuple, or DTO takes over the moment the shape must leave the method.