📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What are anonymous types in C#? Explain what the compiler generates, how equality and immutability work, why == behaves differently from Equals, when two anonymous types share a type, their use in LINQ projections and EF Core, and their limits compared with tuples and records.

Anonymous types: compiler-generated internal classes, value equality via Equals but reference equality via ==, type unification, with-expressions, LINQ and EF Core projections, and when to switch to records or tuples.

Junior Theory Intermediate anonymous-types csharp ef-core linq records

🎯 Key points

  • `new { o.Id, Total = o.Amount }` creates an instance of a compiler-generated, internal, sealed class with read-only properties
  • Equals and GetHashCode compare all properties (value equality), but `==` is reference equality because no operator is generated
  • Same property names, types and order in one assembly share a single generated type
  • Property names are inferred from member access expressions (projection initialisers)
  • `with` expressions work on anonymous types since C# 10
  • They cannot be named in signatures, so they cannot leave a method except as object or dynamic
  • Ideal for local LINQ projections and EF Core SELECT shaping; use records or tuples when data crosses method boundaries

⏱ 30-second answer

An anonymous type, like new { o.Id, Total = o.Amount }, is a compiler-generated internal sealed class with read-only properties. It overrides Equals and GetHashCode to compare every property, but == is still reference equality because no operator is generated. Anonymous types with the same property names, types and order in one assembly share one type. They're perfect for local LINQ and EF Core projections. Since you can't name the type in a signature, I switch to a record or tuple as soon as the data has to leave the method.

🗣 Full answer (senior)

How I would answer this in an interview

Anonymous types are compiler-generated, unnamed classes created with new { ... }: var x = new { o.Id, o.Total };. They're perfect for local, temporary shapes — especially LINQ projections. I'd explain what the compiler generates, equality and immutability, the == vs Equals difference, when two anonymous types are the same type, their role in LINQ/EF Core, and their limits compared with tuples and records.

1. What the compiler generates

var summary = new { OrderId = o.Id, o.Total, Customer = o.Customer.Name };

The compiler generates an internal sealed class (name like <>f__AnonymousType0<...>) that is generic over the property types, with:

  • read-only properties (get only) with the given names (inferred from member access: o.Total → Total),
  • a constructor taking all values,
  • value-based Equals(object) and GetHashCode() over all properties,
  • a readable ToString(): { OrderId = 42, Total = 99.50, Customer = Ada },
  • [DebuggerDisplay] attributes.

You can only refer to the type via var (or generic inference), because it has no name you can write.

2. Equality and immutability

  • Properties are read-only, so anonymous objects are immutable (shallowly — a property holding a List<T> can still be mutated).
  • Since C# 10 you can create modified copies with with: var bigger = summary with { Total = 200m };.
  • Equals compares all property values with EqualityComparer<T>.Default; GetHashCode combines them. So anonymous types work as keys in GroupBy, Distinct, ToDictionary, Join on multiple columns:
var byRegionAndMonth = orders.GroupBy(o => new { o.Region, o.CreatedAt.Month });

3. Why == behaves differently from Equals

Anonymous types override Equals but don't overload ==. So == is reference equality:

var a = new { Id = 1, Name = "Ada" };
var b = new { Id = 1, Name = "Ada" };
a.Equals(b);   // true  - value equality
a == b;        // false - different objects

(Records, by contrast, generate both.)

4. When two anonymous types share a type

Two anonymous object expressions in the same assembly produce the same type if they have the same property names, in the same order, with the same types:

var a = new { Id = 1, Name = "x" };
var b = new { Id = 2, Name = "y" };
b = a;                                  // OK - same type
var c = new { Name = "x", Id = 1 };    // different order -> different type

Across assemblies, they're different types (and internal), which is why returning them as object to another assembly and using dynamic fails.

5. LINQ projections and EF Core

var rows = await db.Orders
    .Where(o => o.CreatedAt >= from)
    .Select(o => new { o.Id, o.Total, CustomerName = o.Customer.Name })
    .ToListAsync(ct);
  • EF Core translates the projection into a SELECT of only those columns — no entity materialisation, no tracking.
  • Anonymous types are fine as intermediate results inside a method (GroupBy keys, let-like shaping, joins).
  • For results that leave the method (returned from a service, serialised by an API, passed to another method), use a named record — anonymous types can't appear in signatures (except as object/dynamic, losing type safety).
  • Minimal APIs can return anonymous types (Results.Ok(new { id, status })) and they serialise fine — convenient for small endpoints, but you lose OpenAPI type information and contract stability.

6. Limits vs tuples and records

Anonymous type Tuple (ValueTuple) Record
Kind Class (heap) Struct (no allocation) Class or struct
Named members Yes (real properties) Names are compile-time only Yes
Usable in method signatures No Yes Yes
Expression trees / EF projections Yes Limited (tuple literals not allowed in expression trees) Yes (constructor or init)
Mutability Immutable Mutable fields Immutable by default
== Reference Value (C# 7.3) Value
Serialisation Real property names Item1/Item2 Real names
Cross-assembly Internal, not shareable Yes Yes

Rule of thumb: anonymous types for local LINQ projections, tuples for private multi-value returns, records for anything public, serialised, or shared.

7. A production example

A service method returned object containing an anonymous type, and a controller in another project used dynamic to read result.Total. It worked until the anonymous type's property was renamed in a refactor — the controller failed at run time with RuntimeBinderException. We replaced it with a record OrderSummary(int Id, decimal Total, string CustomerName) used in the EF projection; the rename became a compile error and the API's OpenAPI schema became accurate.

🔍 What the interviewer is checking

Checks knowledge of what the compiler generates and sensible usage.

Green flags: internal sealed generated class; Equals versus ==; type unification rules; LINQ/EF projections and composite keys; knows when to use records or tuples instead.

Red flags: thinks anonymous types are dynamic; returns them from APIs; unaware of read-only properties.

📚 Learn in depth

1. Generated type shape

[CompilerGenerated, DebuggerDisplay("\\{ OrderId = {OrderId}, Total = {Total} }")]
internal sealed class <>f__AnonymousType0<<OrderId>j__TPar, <Total>j__TPar>
{
    private readonly <OrderId>j__TPar <OrderId>i__Field;
    private readonly <Total>j__TPar <Total>i__Field;
    public <OrderId>j__TPar OrderId => <OrderId>i__Field;
    public <Total>j__TPar Total => <Total>i__Field;
    public override bool Equals(object? value) { ... EqualityComparer<T>.Default for each field ... }
    public override int GetHashCode() { ... combines field hashes with constants ... }
    public override string ToString() { ... }
}

Being generic over property types means one generated class definition is reused for all anonymous types with the same property names in the same order (instantiated with different types).

2. Expression trees

In IQueryable projections, new { o.Id, o.Total } becomes a NewExpression with Members information. EF Core maps each member to a column and constructs the anonymous object in the shaper. That's why anonymous types work in EF Core projections while tuple literals ((o.Id, o.Total)) can't be used in expression trees (CS8143).

3. GroupBy and composite keys

Anonymous types as keys rely on generated Equals/GetHashCode. In EF Core, GroupBy(o => new { o.Region, o.Year }) translates to GROUP BY [Region], [Year]. In LINQ to Objects, the keys are hashed via the generated GetHashCode.

4. with on anonymous types (C# 10)

anon with { Total = 0 } creates a new instance of the same anonymous type. Because properties are read-only, with is the only way to "change" one.

5. Serialisation

System.Text.Json serialises anonymous types by reading their public properties — names preserved. Deserialisation into anonymous types isn't directly supported (no parameterless constructor/setters), though you can deserialise into an anonymous-type "template" with some libraries (Newtonsoft's DeserializeAnonymousType).

6. Performance

Each anonymous object is a heap allocation. In hot LINQ pipelines over in-memory data (e.g. Select(x => new { x.A, x.B }) on millions of items), consider value tuples or structs instead to avoid allocations — but note tuples lose names at run time.

Try it yourself

  1. Create two anonymous objects with the same values and compare with == and Equals.
  2. Create anonymous types with the same properties in different order and try assigning one to the other.
  3. Project to an anonymous type in EF Core and inspect ToQueryString().
  4. Try returning an anonymous type from a method; then switch to a record.

💻 Code

namespace Advanced.AnonymousTypes;

public sealed record Sale(string Region, int Month, decimal Amount);
public sealed record SalesKey(string Region, int Month);

public static class Program
{
    public static void Main()
    {
        var a = new { Id = 1, Name = "Ava" };
        var b = new { Id = 1, Name = "Ava" };
        Console.WriteLine(a.Equals(b));                 // True: value equality
        Console.WriteLine(a == b);                      // False: reference equality, no operator ==
        Console.WriteLine(a.GetType() == b.GetType());  // True: same shape, same type

        var reordered = new { Name = "Ava", Id = 1 };
        Console.WriteLine(a.GetType() == reordered.GetType()); // False: order matters

        var renamed = a with { Name = "Noah" };          // C# 10
        Console.WriteLine(renamed);                     // { Id = 1, Name = Noah }

        List<Sale> sales = [new("AU", 1, 100m), new("AU", 1, 50m), new("NZ", 2, 70m)];

        // Composite key: works because of value equality
        var totals = sales
            .GroupBy(s => new { s.Region, s.Month })
            .Select(g => new { g.Key.Region, g.Key.Month, Total = g.Sum(s => s.Amount) })
            .ToList();
        foreach (var t in totals) Console.WriteLine($"{t.Region}/{t.Month}: {t.Total}");

        // Crossing a method boundary: switch to a named type
        IReadOnlyDictionary<SalesKey, decimal> publicResult = totals.ToDictionary(t => new SalesKey(t.Region, t.Month), t => t.Total);
        Console.WriteLine(publicResult[new SalesKey("AU", 1)]);
    }
}

⚠ Common mistakes

1. Returning anonymous types across boundaries

What happens: methods return object holding anonymous types, read via dynamic or reflection. Why it is wrong: no type safety; renames break at run time. Do instead: use named records for results that leave the method.

2. Comparing with ==

What happens: if (keyA == keyB) with anonymous types. Why it is wrong: reference equality; equal values compare false. Do instead: use Equals.

3. Relying on property order not mattering

What happens: expecting new { A, B } and new { B, A } to be the same type. Why it is wrong: order matters for type identity. Do instead: keep consistent ordering or use a named type.

4. Using tuples in EF Core projections

What happens: Select(o => (o.Id, o.Total)) in an IQueryable. Why it is wrong: tuple literals aren't allowed in expression trees. Do instead: use anonymous types or records.

5. Anonymous types in public API responses

What happens: minimal APIs return anonymous objects for important contracts. Why it is wrong: OpenAPI loses schema information; contracts change silently. Do instead: use typed response records.

6. Assuming deep immutability

What happens: anonymous types holding mutable lists treated as immutable. Why it is wrong: the list can still be mutated. Do instead: use immutable collections for nested data.

7. Allocations in hot in-memory pipelines

What happens: projecting millions of items to anonymous objects. Why it is wrong: heap allocation per item. Do instead: value tuples or structs when performance matters.

8. Using dynamic to access anonymous types from other assemblies

What happens: dynamic d = service.GetAnon(); d.Total. Why it is wrong: anonymous types are internal — binder fails across assemblies. Do instead: named types.

🏛 Senior insight: trade-offs

Anonymous types are a local tool: perfect for shaping data inside a LINQ or EF Core query, and wrong for anything that leaves the method. Knowing the == versus Equals difference and how composite keys translate to SQL GROUP BY shows depth. In code review I accept anonymous types inside queries and push back when they leak into public signatures as object or dynamic.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate says anonymous types cannot be returned from a method. Why: Tuples are the usual alternative when a shape must leave the method.
    How to answer

    C# tuples like (int Id, string Name) are System.ValueTuple structs with public, mutable fields Item1 and Item2. The names you write exist only at compile time: they're stored in a TupleElementNames attribute, so reflection, serialisers and dynamic only see Item1 and Item2. Tuples have value equality and support ==, and since names aren't part of the type, (int A, int B) converts to (int X, int Y). System.Text.Json ignores fields by default, so a tuple serialises as {}. I use tuples for private multiple returns, and records for anything public or serialised.

  2. When: Candidate mentions value equality of anonymous types. Why: Records are the named alternative with value equality.
    How to answer

    A record is a type whose identity is its data. public record Money(decimal Amount, string Currency); makes the compiler generate init-only properties, a constructor, Deconstruct, value-based Equals/GetHashCode/==, a readable ToString, and a hidden clone method used by with expressions for non-destructive mutation. record (or record class, C# 9) is a reference type; record struct (C# 10) is a value type whose positional properties are mutable unless you write readonly record struct. Equality includes an EqualityContract so a base and derived record are never equal. Watch out: with is a shallow copy, collection members compare by reference, and records are a poor fit for EF Core entities, which need identity-based equality.

More in Advanced Language Features

Last updated 30 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me