Skip to main content
SEMastery
.NET Corebeginner

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.

11 min readUpdated March 18, 2026

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.

A record's main job is to carry data and compare by what is inside.

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, and ClassName.
  • 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 ToString so printing the object shows its data.
  • Value equality, so two students with the same data are equal.
  • A with expression 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 equal

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

Class equality checks identity. Record equality checks the values inside.

The table below sums up the difference clearly.

BehaviourNormal classRecord
What == checksSame object in memorySame data inside
ToString() outputType name onlyAll property values
Copy with a changeWrite it by handBuilt-in with
Encourages immutabilityNoYes
Best used forObjects with identity and changing stateData 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

Original
Copy made
Apply changes
New record

Steps

1

Original

asha = Asha, 12, 7-B

2

Copy made

exact duplicate in memory

3

Apply changes

set ClassName = 8-B

4

New record

ashaPromoted returned

The original record is never touched; you get a fresh copy with your changes.

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.

Featurerecord classrecord structreadonly record struct
KindReference typeValue typeValue type
Default mutabilityImmutable (init-only)Read-writeImmutable
Supports inheritanceYesNoNo
Stored onHeapStack/inlineStack/inline
Good forData modelsSmall mutable valuesSmall fixed values
A quick decision path for choosing the right record kind.

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());   // True

The 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

Same type?
Compare field 1
Compare field 2
Result

Steps

1

Same type?

both must be Student

2

Compare field 1

Name matches?

3

Compare field 2

RollNumber, ClassName match?

4

Result

all match => equal

The compiler-generated Equals walks field by field.

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);   // True

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

A record flowing through a simple booking API.

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 Student and a subtype that happens to share values are not equal.
  • A record struct is mutable by default. Add readonly if 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.

Picking the right tool for a new type in C# 14.

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 with expression 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

Related Posts