Skip to main content

C# Records vs Classes vs Structs: When to Use Each (2026)

Learn C# records vs classes vs structs with real examples. Master when to use each type in 2026 with best practices, pitfalls, and performance tips. Start now!

One of the most common questions C# developers ask in 2026 is simple to state but tricky to answer: C# records vs classes vs structs — which one should you actually use? Since records arrived in C# 9 and matured through C# 10, 11, 12, and 13, the decision is no longer just "value type or reference type." You now have three first-class options for modeling data, and picking the wrong one leads to subtle bugs, poor performance, and code that's painful to maintain.

In this tutorial, we'll break down the difference between records, classes, and structs in C#, show practical runnable examples, and give you a clear decision framework so you always pick the right type. By the end, you'll understand not just how each works, but why you'd choose one over the others.

The Quick Answer: C# Records vs Classes vs Structs

Before we dive deep, here's the short version that most developers searching for "when to use record c#" are looking for:

  • Use a class when you need a reference type with mutable state and identity (the default choice for most objects and services).
  • Use a record when you're modeling immutable data and care about value-based equality (DTOs, API models, configuration, events).
  • Use a struct when you have small, short-lived value types where avoiding heap allocations matters (coordinates, money, measurements).

That's the cheat sheet. But the real value is understanding the trade-offs behind value type vs reference type in C#, so let's look at each one properly.

Classes in C#: The Reference Type Default

A class is a reference type. When you create an instance, the object lives on the managed heap, and your variable holds a reference (a pointer) to it. Two variables can point to the same object, and equality is based on identity by default — not the values inside.

public class BankAccount
{
    public string Owner { get; set; }
    public decimal Balance { get; set; }

    public void Deposit(decimal amount) => Balance += amount;
}

var a = new BankAccount { Owner = "Alex", Balance = 100m };
var b = a;                 // b references the SAME object
b.Deposit(50m);

Console.WriteLine(a.Balance);   // 150 — both see the change
Console.WriteLine(a == b);      // True — same reference (identity)

var c = new BankAccount { Owner = "Alex", Balance = 150m };
Console.WriteLine(a == c);      // False — different objects, same data

Notice that a == c is False even though the data is identical. That's reference equality — the foundation of how classes behave. This is exactly what you want for objects that have identity and behavior: services, controllers, entities with a lifecycle, and anything with mutable state shared across your application.

When to use a class

  • The object has identity that matters (a specific user, a specific order in your database).
  • It carries significant behavior, not just data.
  • State changes over time and is shared by reference.
  • It's large, or you need inheritance hierarchies.

Classes remain the workhorse of C#. If you're unsure, a class is rarely a wrong answer — it's just sometimes not the best one.

C# Records: Immutable Data With Value Equality

Records are the headline feature in the C# records vs classes debate. A record is still a reference type by default (a record compiles to a class), but the compiler generates a lot of boilerplate for you and changes the equality semantics to value-based.

This means two records are equal if all their properties are equal — perfect for data that represents a value rather than an identity.

public record Person(string FirstName, string LastName, int Age);

var p1 = new Person("Jordan", "Lee", 30);
var p2 = new Person("Jordan", "Lee", 30);

Console.WriteLine(p1 == p2);          // True — value equality!
Console.WriteLine(p1.Equals(p2));     // True
Console.WriteLine(p1);                // Person { FirstName = Jordan, LastName = Lee, Age = 30 }

Compare that to the class example earlier: with a class, p1 == p2 would be False. The record gives you value equality, a readable ToString(), a deconstructor, and GetHashCode() — all for free.

Non-destructive mutation with with

Because records are designed for immutability, C# gives you the with expression to create a modified copy instead of mutating the original:

var original = new Person("Jordan", "Lee", 30);
var older = original with { Age = 31 };   // copy with one change

Console.WriteLine(original.Age);   // 30 — unchanged
Console.WriteLine(older.Age);      // 31
Console.WriteLine(original == older); // False — different values

This pattern is incredibly useful for state management, redux-style architectures, and any code where you want predictable, side-effect-free updates.

record class vs record struct

Since C# 10 you can also declare a record struct, which combines value-type storage with the record's auto-generated members. Use record struct for small immutable values where you also want the convenience records provide:

public readonly record struct Point(double X, double Y);

var origin = new Point(0, 0);
var moved = origin with { X = 5 };   // works on record structs too

Console.WriteLine(origin == new Point(0, 0)); // True — value equality
Console.WriteLine(moved);                      // Point { X = 5, Y = 0 }

The plain record keyword is shorthand for record class. Mark record struct types as readonly whenever possible to guarantee immutability and help the JIT optimize.

When to use a record

  • DTOs and API request/response models — they're pure data and benefit from value equality.
  • Domain events and messages — immutable by nature.
  • Configuration objects read once and never mutated.
  • Anywhere you'd previously write a class and then manually override Equals, GetHashCode, and ToString.

Structs in C#: Value Types and Performance

A struct is a value type. Unlike classes and records, a struct's data is stored inline — on the stack for local variables, or embedded directly inside its containing object. When you assign a struct, you copy the entire value. This is the core of the C# struct vs class distinction.

public struct Money
{
    public decimal Amount { get; init; }
    public string Currency { get; init; }
}

var wallet = new Money { Amount = 100m, Currency = "USD" };
var copy = wallet;                 // a full COPY is made
copy = copy with { Amount = 50m }; // 'with' works on structs in modern C#

Console.WriteLine(wallet.Amount);  // 100 — original is untouched
Console.WriteLine(copy.Amount);    // 50

Because structs are copied by value, there's no shared reference and (usually) no heap allocation. For small, frequently created values like points, vectors, dates, and money, this avoids garbage collection pressure and can dramatically improve performance in hot paths.

The struct trade-offs you must understand

Structs are not "faster classes." They have real costs if misused:

  • Copy cost: Large structs are expensive to copy on every assignment and method call. Microsoft's guideline is to keep structs under ~16 bytes unless you pass them by ref/in.
  • Boxing: Storing a struct in an object or non-generic collection boxes it onto the heap, erasing the performance benefit.
  • Mutable structs are dangerous: Because of copy semantics, mutating a struct stored in a collection or property often changes a copy, not the original — a classic source of bugs. Prefer readonly struct.

When to use a struct

  • The type is small and represents a single value (coordinate, color, money, timestamp).
  • It's immutable, or can be made readonly.
  • You create many instances and want to avoid heap allocations.
  • It logically behaves like a primitive — comparing by value makes sense.

Value Type vs Reference Type in C#: The Core Difference

Everything above comes down to one concept that every C# developer must master: value type vs reference type in C#.

  • Reference types (class, record class): the variable holds a reference; assignment copies the reference; multiple variables can share one object; equality is identity-based by default.
  • Value types (struct, record struct): the variable holds the data; assignment copies the whole value; each variable is independent; equality is value-based.

Records blur the line by giving reference types value-based equality while keeping reference storage. That's why records are so popular for data modeling — you get the convenient equality of a value type without the copy cost of a large struct.

Common Pitfalls and Best Practices in 2026

Here are the mistakes I see most often when teams adopt records and structs:

  • Using a record for entities with identity. An EF Core entity with a database ID should usually be a class — value equality on mutable, tracked entities causes confusion.
  • Making mutable structs. Always prefer readonly struct or readonly record struct to avoid copy-mutation bugs.
  • Large structs. If your struct has many fields, the copy cost can be worse than a class allocation. Benchmark before assuming structs are faster.
  • Forgetting records can still be mutable. A record with set accessors is mutable; use init or positional parameters for true immutability.
  • Inheritance with records. Record equality accounts for the runtime type, which can surprise you in hierarchies — keep record inheritance shallow and intentional.

A solid default policy for 2026 codebases: records for data, classes for behavior and identity, structs for small performance-critical values.

C# Records vs Classes vs Structs: Decision Cheat Sheet

  • Modeling pure data (DTO, event, config)? → record
  • Object with identity, behavior, or shared mutable state? → class
  • Small immutable value created in large numbers? → readonly struct or readonly record struct
  • Need inheritance and polymorphism? → class (or carefully, record class)
  • Need value equality without copy cost? → record class

Conclusion: Choosing Between C# Records vs Classes and Structs

The C# records vs classes vs structs decision becomes easy once you focus on intent rather than syntax. Ask yourself two questions: Does this type have identity or is it just a value? and Is it immutable data or behavior-rich state? Those two answers point you straight to the right choice.

Key takeaways:

  • Classes are reference types with identity-based equality — your default for services, entities, and mutable, behavior-rich objects.
  • Records are reference types (by default) with value-based equality, immutability support, and with expressions — ideal for DTOs, events, and data modeling.
  • Structs are value types stored inline — best for small, immutable, high-frequency values where you want to avoid heap allocations.
  • Always understand value type vs reference type in C#, prefer immutability, and benchmark before optimizing with structs.

Master these three and you'll write cleaner, faster, and more maintainable C# in 2026 and beyond. Pick the type that matches your data's intent, and the rest of your design tends to fall into place.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Send an Email via SMTP with MailKit Using .NET 6

How to Send an Email in .NET Core This tutorial show you how to send an email in .NET 6.0 using the MailKit email client library. Install MailKit via NuGet Visual Studio Package Manager Console: Install-Package MailKit How to Send an HTML Email in .NET 6.0 This code sends a simple HTML email using the Gmail SMTP service. There are instructions further below on how to use a few other popular SMTP providers - Gmail, Hotmail, Office 365. // create email message var email = new MimeMessage(); email.From.Add(MailboxAddress.Parse("from_address@example.com")); email.To.Add(MailboxAddress.Parse("to_address@example.com")); email.Subject = "Email Subject"; email.Body = new TextPart(TextFormat.Html) { Text = "<h1>Test HTML Message Body</h1>" }; // send email using var smtp = new SmtpClient(); smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls); smtp.Authenticate("[Username]", "[Password]"); smtp.Se...