Getting Started with C# Records: A Beginner's Friendly Guide
Learn C# records the easy way: value equality, with expressions, positional syntax, and record struct, explained with simple real-life examples.
Think about your school ID card. It has your name, your roll number, and your class written on it. If your friend has an ID card with the exact same name, roll number, and class, you would say "these two cards hold the same information." You do not care that they are two separate pieces of plastic. You care about what is written on them.
That simple idea is the heart of C# records. A record is a type whose job is to carry data, and two records are treated as equal when they hold the same data inside. In this guide we will go step by step, in plain English, and build up your understanding with small examples and pictures.
What problem do records solve?
Before records, if you wanted a small data holder in C#, you wrote a class. But a normal class has a habit that surprises beginners. When you compare two class objects, C# checks whether they are the same object in memory, not whether they hold the same values.
Imagine two tiffin boxes packed with the exact same food. A normal class would say "these are different boxes" even though the food inside is identical. A record says "the food is the same, so for my purpose these are equal."
Records were added in C# 9 and have grown ever since. Today, with C# 14 shipping in .NET 10 (the current LTS release), records are a normal, everyday tool that .NET developers reach for all the time.
Your first record
Here is the smallest useful record. We will model a Student.
public record Student(string Name, int RollNumber, string ClassName);That single line does a surprising amount of work. The compiler reads the part in brackets, called positional syntax, and writes a lot of code for you behind the scenes:
- A constructor that takes
Name,RollNumber, andClassName. - Three public properties, one for each value, that are init-only (you set them once when you create the object, then they stay fixed).
- A nice
ToStringso printing the object shows its data. - Value equality, so two students with the same data are equal.
- A
withexpression so you can copy and change a little.
Let us use it:
var asha = new Student("Asha", 12, "7-B");
var asha2 = new Student("Asha", 12, "7-B");
Console.WriteLine(asha); // Student { Name = Asha, RollNumber = 12, ClassName = 7-B }
Console.WriteLine(asha == asha2); // True -> same data, so equalNotice two things. First, printing asha shows all the data, not a confusing memory address. Second, asha == asha2 is True, because both hold the same values. With a normal class, that comparison would have been False.
How records compare: value equality
This is the single most important idea, so let us slow down and picture it.
With a normal class, equality is about identity. Two variables are equal only when they point to the very same object. With a record, equality is about content. Two records are equal when they are the same type and every field matches.
The table below sums up the difference clearly.
| Behaviour | Normal class | Record |
|---|---|---|
What == checks | Same object in memory | Same data inside |
ToString() output | Type name only | All property values |
| Copy with a change | Write it by hand | Built-in with |
| Encourages immutability | No | Yes |
| Best used for | Objects with identity and changing state | Data holders |
The with expression: copy and change
Records are usually immutable. That means once you create one, you do not change its values. But real programs need to make new versions of data all the time. The with expression solves this neatly.
A with expression makes a copy of an existing record, then changes only the properties you mention. The original stays untouched.
var asha = new Student("Asha", 12, "7-B");
// Asha moves to a new class. We make a new record, we do not edit the old one.
var ashaPromoted = asha with { ClassName = "8-B" };
Console.WriteLine(asha.ClassName); // 7-B (unchanged)
Console.WriteLine(ashaPromoted.ClassName); // 8-B (the new copy)Think of it like a photocopy of a form where you white-out one line and rewrite it. The original form is still safe in the drawer.
How a with expression works
Steps
Original
asha = Asha, 12, 7-B
Copy made
exact duplicate in memory
Apply changes
set ClassName = 8-B
New record
ashaPromoted returned
This "do not change the original, make a new one" style is called non-destructive mutation. It keeps your code safe, because no other part of your program can be surprised by a value changing under its feet.
Record class vs record struct
So far our Student was a record class (a reference type). C# also lets you make a record struct (a value type). The keyword record struct is your tool here.
// A reference type. The default record.
public record Point2D(int X, int Y);
// A value type. Good for small data on hot paths.
public readonly record struct Pixel(byte R, byte G, byte B);When should you pick which? Here is a simple rule.
- Use a record class when the type is data and you want value equality. This is the everyday default.
- Use a readonly record struct when the data is small (roughly 16 bytes or less), never changes, and is created in large numbers in performance-sensitive code, where avoiding a heap allocation helps.
There is one trap to remember. A record class made with positional syntax has init-only properties, so it is immutable. A record struct has read-write properties by default. If you want the struct to be immutable too, add readonly, which is why the Pixel example above says readonly record struct.
| Feature | record class | record struct | readonly record struct |
|---|---|---|---|
| Kind | Reference type | Value type | Value type |
| Default mutability | Immutable (init-only) | Read-write | Immutable |
| Supports inheritance | Yes | No | No |
| Stored on | Heap | Stack/inline | Stack/inline |
| Good for | Data models | Small mutable values | Small fixed values |
You are not limited to positional syntax
Positional records are short and lovely, but you can also write a record with a full body, just like a class. This is handy when you need extra properties, validation, or methods.
public record Order
{
public required string Id { get; init; }
public required decimal Amount { get; init; }
// You can add your own methods too.
public bool IsLarge() => Amount > 10000m;
}
var order = new Order { Id = "ORD-1", Amount = 12500m };
Console.WriteLine(order.IsLarge()); // TrueThe required keyword (added in C# 11) makes sure nobody can create an Order without giving an Id and Amount. Records and required work very well together for safe data models.
How records build their equality
You might wonder how the magic equality actually happens. The compiler generates a hidden Equals method and a GetHashCode method that walk through every field and compare them. It also generates == and != operators. You do not write any of this. The diagram below shows the flow when you compare two records.
What happens during record equality
Steps
Same type?
both must be Student
Compare field 1
Name matches?
Compare field 2
RollNumber, ClassName match?
Result
all match => equal
Because equality and GetHashCode are based on values, records work beautifully as keys in a Dictionary or items in a HashSet. Two records with the same data will land in the same bucket, which is exactly what you want for a data holder.
var seen = new HashSet<Student>();
seen.Add(new Student("Asha", 12, "7-B"));
// Same data, so the set already contains it.
bool already = seen.Contains(new Student("Asha", 12, "7-B"));
Console.WriteLine(already); // TrueA real example: an API request model
Records shine in web apps. When your ASP.NET Core API receives JSON, you often map it into a small immutable object. A record is perfect for that, because the request data should not change after it arrives.
public record CreateBookingRequest(
string CustomerName,
DateOnly TravelDate,
string FromCity,
string ToCity);This reads like a clear contract. Anyone looking at it instantly knows what a booking request carries. If a later step needs a slightly different version (say a corrected city name), with gives a safe copy without touching the original request.
Small things that trip up beginners
A few gentle warnings so you do not get stuck.
- Records can still hold mutable things. If a record has a
List<int>property, the list itself can change. Value equality only compares the reference of that list, not its contents. Prefer immutable collections inside records when you can. - Inheritance changes equality. Two records are equal only when their runtime type matches. A
Studentand a subtype that happens to share values are not equal. - A record struct is mutable by default. Add
readonlyif you want the safe, fixed behaviour you get from record classes. - Do not over-use records. If a type has lots of behaviour and a changing internal state, a normal class is the better fit. Records are for data first.
Records vs classes vs structs at a glance
Here is one final picture to lock the choices in your memory.
Quick recap
- A record is a type built to carry data. The compiler writes equality,
ToString, and copying for you. - Records use value equality: two records are equal when they hold the same data, unlike normal classes which compare by memory identity.
- The positional syntax
record Student(string Name, int Roll)is the short, common way to define one. - The
withexpression makes a copy with small changes, keeping the original safe (non-destructive mutation). - A record class is a reference type and immutable by default. A record struct is a value type and read-write unless you add
readonly. - Records work great as dictionary keys, hash set items, and API request models.
- Reach for a normal class when your object has identity and changing behaviour rather than just data.
References and further reading
- C# record types - Microsoft Learn
- Records - C# reference - Microsoft Learn
- Use record types tutorial - Microsoft Learn
- How to define value equality for a type - Microsoft Learn
Related Posts
How to Apply Functional Programming in C#: A Beginner's Guide
Learn functional programming in C# the simple way: pure functions, immutability, records, LINQ, pattern matching, and composition with friendly examples.
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.
C# init-only and required Properties: A Beginner's Guide
Learn C# init-only and required properties with simple analogies, diagrams, and code. Build safe, immutable objects that are filled correctly every time.
How to Write Elegant Code with C# Pattern Matching
Learn C# pattern matching the easy way. Use is, switch expressions, property and list patterns to write clean, readable, and elegant .NET code.
Functional Programming in C#: The Practical Parts You Will Actually Use
A warm, beginner-friendly guide to functional programming in C#: records, immutability, pattern matching, switch expressions, pure functions, and LINQ.
New Features in C# 13: A Friendly Beginner's Guide
Learn the new features in C# 13 with simple words, real-life examples, diagrams, and code you can read in minutes. Great for beginners.