Skip to main content
SEMastery
.NET Corebeginner

Records, Anonymous Types, and Non-Destructive Mutation in C#

Learn C# records, anonymous types, and non-destructive mutation with the with expression using simple words, real-life examples, and clear diagrams.

11 min readUpdated January 14, 2026

Think about a railway ticket. Once it is printed, you do not scribble on it to change your seat. If your seat changes, the counter prints a fresh ticket with the new seat number. Your old ticket still shows the old details, and the new ticket shows the new ones. Nobody erases anything.

That little idea, "make a new copy instead of editing the original," is the heart of three close friends in C#: records, anonymous types, and non-destructive mutation. In this guide we will meet all three slowly, with simple words and small examples. By the end, you will know when to reach for each one.

The three ideas at a glance

Before we go deep, here is the big picture. These three features all share one habit: they treat data as something you copy and compare, not something you constantly poke and change.

FeatureWhat it isHas a name you can use?
RecordA type built to carry data, compares by valueYes, you give it a name
Anonymous typeA quick read-only object with no written nameNo, only lives in one method
Non-destructive mutationMaking a changed copy with withIt is an action, not a type

Now let us look at each one as a kind teacher would, one small step at a time.

Records: a type made for data

A record is a special kind of type whose main job is to hold data. You still write a name for it, like Student or Money, but the compiler quietly does a lot of helpful work for you.

Here is a small record.

public record Student(string Name, int RollNumber, string Class);
 
var asha = new Student("Asha", 12, "6A");
var asha2 = new Student("Asha", 12, "6A");
 
// Records compare by value, so this prints True.
Console.WriteLine(asha == asha2);
 
// You also get a clean ToString for free.
Console.WriteLine(asha);
// Output: Student { Name = Asha, RollNumber = 12, Class = 6A }

Notice two gifts the compiler gave us. First, asha == asha2 is True even though they are two separate objects, because a record compares by the values inside. Second, printing the record shows all its data neatly. With a normal class you would have to write that code yourself.

A record compares two objects by the data inside them, not by their memory location.

Records are also immutable by default when you use the short positional syntax above. That means once a Student is made, you cannot change its Name. This sounds limiting at first, but it is actually a safety feature. If nobody can secretly change your data, your program is much easier to trust.

Anonymous types: a quick holder with no name

Sometimes you need a small object for a short moment, and writing a whole named type feels like too much work. This is where an anonymous type helps. It is an object with properties, but you never write a name for the type. The compiler makes a hidden name behind the scenes.

You create one with the new keyword and curly braces, but no type name.

var book = new { Title = "Panchatantra", Pages = 120 };
 
Console.WriteLine(book.Title);  // Panchatantra
Console.WriteLine(book.Pages);  // 120

The variable must be var, because you cannot write the type's name yourself. Every property is read-only, so book.Pages = 200; would not compile. Anonymous types also compare by value, just like records.

Anonymous types shine most with LINQ, when you want to pick out just a few fields from a bigger list.

var students = new[]
{
    new Student("Asha", 12, "6A"),
    new Student("Ravi", 15, "6B"),
};
 
// Keep only the name and class, drop the roll number.
var summary = students.Select(s => new { s.Name, s.Class });
 
foreach (var item in summary)
    Console.WriteLine($"{item.Name} is in {item.Class}");

The big catch is that an anonymous type cannot leave its method. You cannot return it, you cannot pass it as a parameter, and you cannot store it in a field, because none of those places can name the type. It is a helper for one small spot in your code.

An anonymous type is born and used inside one method, then thrown away.

Non-destructive mutation: change by copying

Now we come back to our railway ticket. Suppose you have a Student record and the student moves from class 6A to 6B. Records are immutable, so you cannot edit the old one. Instead, you make a new copy that is the same in every way except the class. This is called non-destructive mutation, and you do it with the with expression.

var asha = new Student("Asha", 12, "6A");
 
// Make a copy that changes only the Class.
var ashaMoved = asha with { Class = "6B" };
 
Console.WriteLine(asha.Class);       // 6A  (unchanged)
Console.WriteLine(ashaMoved.Class);  // 6B  (the new copy)

The word "non-destructive" means nothing was destroyed. The original asha is untouched. You simply got a fresh object with one field changed, exactly like the railway counter printing a new ticket.

Here is the flow of what happens, step by step.

How the with expression works

Original
Copy
Apply changes
New record

Steps

1

Original

Start with asha (6A)

2

Copy

Compiler clones every field

3

Apply changes

Set Class to 6B

4

New record

Return ashaMoved, asha stays

The with expression copies the old record, then changes only the fields you list.

Why a class cannot do this

The with expression only works on records and record structs. The reason is simple. When you write a record, the compiler quietly builds a copy constructor and a clone method for you. The with expression uses that hidden copy constructor to make the new object.

A normal class does not get that free copy constructor, so with will not compile on a plain class. If you want this nice copy behavior, use a record.

The with expression calls the hidden copy constructor that the compiler builds for records.

A small but important warning: shallow copy

There is one thing every beginner should know. The with expression makes a shallow copy. That means if your record holds a reference to another object, the copy points to the same inner object, not a fresh one.

public record Classroom(string Name, List<string> Students);
 
var roomA = new Classroom("6A", new List<string> { "Asha" });
var roomB = roomA with { Name = "6A-copy" };
 
// Both share the SAME list!
roomB.Students.Add("Ravi");
 
Console.WriteLine(roomA.Students.Count); // 2  (surprise!)

Both roomA and roomB share one list, so adding to one shows up in the other. This is not a bug in C#. It is just how shallow copying works. The fix is to give the copy its own new list when you need full separation. A good habit is to keep your record fields as simple, immutable values whenever you can.

Records vs anonymous types: how to choose

Both records and anonymous types compare by value and are read-only friendly. So when do you pick which one? This table makes it clear.

QuestionUse a recordUse an anonymous type
Do you need a name to reuse?YesNo
Return it from a method?YesNo
Pass it as a parameter?YesNo
Just a quick LINQ shape?OverkillPerfect
Want a with copy?YesLimited

A simple rule of thumb: if the data is a real idea in your program that lives in more than one place, like a Money, an Address, or an API request, make it a record. If you just need to bundle a couple of fields for a moment inside one method, an anonymous type is lighter and faster to write.

Putting it all together

Let us see all three friends working in one tiny example. We have an order, we make a changed copy, and we shape a quick view of it for printing.

public record Order(int Id, string Item, int Quantity, decimal Price);
 
var order = new Order(1, "Notebook", 2, 50m);
 
// Non-destructive mutation: customer wants 5 notebooks now.
var updated = order with { Quantity = 5 };
 
// Anonymous type: a quick view with just two fields.
var view = new { updated.Item, Total = updated.Quantity * updated.Price };
 
Console.WriteLine($"{view.Item}: {view.Total}"); // Notebook: 250
 
// The original order is still safe and unchanged.
Console.WriteLine(order.Quantity); // 2

In a few lines we used a record for the real data, the with expression to make a safe changed copy, and an anonymous type to shape a small view. Each tool did the job it is best at.

Choosing the right tool

Need data type
Reuse it?
Pick tool

Steps

1

Need data type

You want to hold some fields

2

Reuse it?

Used in many places or returned?

3

Pick tool

Yes -> record, No -> anonymous type

A simple decision path for records, anonymous types, and with.

A note on the wider .NET world

These features are stable and ready to use today. Records arrived in C# 9, record structs in C# 10, and the language has kept polishing them since. With C# 14 shipping in .NET 10, the current LTS release, records are a normal everyday tool. Looking ahead, C# 15 in the .NET 11 preview is adding union types, which pair very nicely with records for modeling data that can be one of several shapes. So the time you spend learning records now keeps paying off.

A real-life style example

Let us imagine a simple shop app. A customer adds items, changes the quantity, and we never want to lose the original order in case they cancel. Records and the with expression make this safe and clean, because every change becomes a brand new copy while the old one stays as a record of history.

public record CartItem(string Name, int Qty, decimal Price);
 
var item = new CartItem("Pen", 1, 10m);
 
// Customer bumps the quantity. We keep the first version too.
var history = new List<CartItem> { item };
var updated = item with { Qty = 3 };
history.Add(updated);
 
// Now history holds both the old and the new state.
foreach (var snapshot in history)
    Console.WriteLine($"{snapshot.Name} x {snapshot.Qty}");
// Pen x 1
// Pen x 3

Because each with makes a fresh copy, we naturally build a small history of changes. This pattern is very common in real apps where you want to undo a change or show what the data looked like before. You get this almost for free, simply by choosing records over mutable classes.

Common beginner mistakes

A few traps catch new learners. Watch out for these.

  • Trying to use with on a plain class. It only works on records and record structs.
  • Forgetting that with makes a shallow copy, so shared lists or objects can surprise you.
  • Trying to return an anonymous type from a method. You cannot, because it has no name.
  • Editing a record property after creation. Positional records are immutable, so make a copy with with instead.
  • Using an anonymous type for a real, reusable idea. Promote it to a record when it grows up.

Quick recap

  • A record is a named type made to carry data. It compares by value and gives you a free ToString.
  • An anonymous type is a quick read-only object with no name you can write. It is great for LINQ but cannot leave its method.
  • Non-destructive mutation means making a changed copy with the with expression instead of editing the original.
  • The with expression works only on records and record structs, because the compiler builds a hidden copy constructor for them.
  • The with expression makes a shallow copy, so be careful with shared inner objects like lists.
  • Choose a record for reusable data, and an anonymous type for quick one-method shapes.

References and further reading

Related Posts