DEV Community

Cover image for Why Records in C# Are Game-Changing
Spyros Ponaris
Spyros Ponaris

Posted on • Edited on

Why Records in C# Are Game-Changing

🌟 Why Records in C# Are Great

If you’ve been working with C# lately, you’ve probably encountered records, a feature introduced in C# 9 that many developers have come to love.

They’re elegant, powerful, and remove a lot of boilerplate when modeling data.

But what actually makes them great? Here’s why records aren’t just nice — they’re game-changing.


🧠 What Are Records?

At their core, records are reference types designed for immutable data modeling. Think of them as classes optimized for holding data — with extra features built in.

You can define a record in two main ways:

✅ 1. Positional Syntax (Concise and Clean)

public record Person(string FirstName, string LastName);
Enter fullscreen mode Exit fullscreen mode

This one-liner gives you:

  • Constructor
  • Equals(), GetHashCode(), and ToString() overrides
  • init-only properties (immutable)
  • Deconstruction support

✨ Ouaaa...

In order to implement all that in a regular class, you'd need to write a ton of code.
Records do it all for you, automatically and cleanly.

✅ 2. Classic Syntax (More Flexible)

public record Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
}
Enter fullscreen mode Exit fullscreen mode

Need mutability? You can use set instead of init, but that sacrifices immutability and thread safety.

💡 What is init?

init is a special accessor introduced in C# 9 that allows properties to be set only during initialization — making them immutable afterward.

var person = new Person { FirstName = "Alice", LastName = "Green" };
Enter fullscreen mode Exit fullscreen mode

// person.FirstName = "Bob"; ❌ Not allowed

✅ Cleaner than readonly fields
✅ Safer than public setters
✅ Perfect for records, DTOs, and configuration objects

👉 Microsoft Docs – Init-only Setters

🎯 Why Records Are Actually Great
✅ 1. Value-Based Equality — Finally Done Right
With classes, equality checks compare references.
With records, it’s based on content:

var a = new Person("Alice", "Smith");
var b = new Person("Alice", "Smith");
Console.WriteLine(a == b); // True ✅
Enter fullscreen mode Exit fullscreen mode

👉 Microsoft Docs – Records

✅ 2. Immutability by Default — and Thread-Safe by Nature
Records are immutable by default, leading to safer, more predictable code:

var p1 = new Person("John", "Doe");
var p2 = p1 with { FirstName = "Jane" }; // Creates a copy
Enter fullscreen mode Exit fullscreen mode

Since their state can't be changed after creation, records are thread-safe for read operations — perfect for:

  • Parallel tasks
  • Background services
  • Blazor state containers
  • Event sourcing

👉 Immutability in C#

✅ 3. Concise and Readable Code
Records eliminate boilerplate. You define what matters — the compiler handles the rest.

public record Invoice(string Id, DateTime Date, decimal Amount);

Clean. Lightweight. Clear.

✅ 4. Deconstruction and Pattern Matching
Records play perfectly with modern C# features:

var (first, last) = new Person("Ana", "Lopez");

if (p1 is { FirstName: "John" })
{
    Console.WriteLine("Hello John!");
}
Enter fullscreen mode Exit fullscreen mode

👉 Pattern Matching in C#

✅ 5. Functional Programming Friendly
Records align beautifully with functional principles:

  • No side effects
  • Easy transformations
  • Clear data flow

var updatedInvoice = invoice with { Amount = invoice.Amount + 10 };
👉 Welcome to C# 9 – Records

✅ 6. Supports Inheritance, Structs, and More

You’re not locked into one pattern:

  • record struct — value-type records
  • readonly record struct — fully immutable
  • Abstract/sealed record inheritance supported

👉 What's New in C# 10 – Record Structs

🧱 Records and the Value Object Pattern
In Domain-Driven Design (DDD), Value Objects are:

  • Immutable
  • Without identity
  • Compared by value

✅ Records = Ideal for Value Objects

public record Email
{
    public string Address { get; }

    public Email(string address)
    {
        if (!address.Contains("@"))
            throw new ArgumentException("Invalid email", nameof(address));

        Address = address;
    }
}
Enter fullscreen mode Exit fullscreen mode
public record Money(decimal Amount, string Currency);
Enter fullscreen mode Exit fullscreen mode

You get clean, reusable types with built-in equality and optional validation.

public record Product
{
    public Guid Id { get; init; }
    public string Name { get; init; }
    public Money Price { get; init; }
}
Enter fullscreen mode Exit fullscreen mode

👉 Martin Fowler – Value Object

🚫 When NOT to Use Records
Avoid records if:

  • You need fully mutable entities (e.g., for EF Core)
  • You rely on reference identity
  • You have complex inheritance hierarchies

🛠 Using Records as Value Objects in EF Core

Records are great for modeling value objects in Domain-Driven Design — and EF Core supports them perfectly with OwnsOne.

Let’s say you have a simple Money value object:

public record Money(decimal Amount, string Currency);
Enter fullscreen mode Exit fullscreen mode

You can embed it in an entity like this:

public class Product
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public Money Price { get; set; } // ✅ Value Object
}
Enter fullscreen mode Exit fullscreen mode

Then, configure it in your DbContext:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>(builder =>
    {
        builder.HasKey(p => p.Id);

        builder.OwnsOne(p => p.Price, money =>
        {
            money.Property(m => m.Amount)
                .HasColumnName("PriceAmount")
                .HasPrecision(18, 2);

            money.Property(m => m.Currency)
                .HasColumnName("PriceCurrency")
                .HasMaxLength(3);
        });
    });
}
Enter fullscreen mode Exit fullscreen mode

This makes your domain model clean, immutable, and EF-friendly — all thanks to record + OwnsOne.

🔧 Real-World Use Cases

✅ DTOs for APIs
✅ Immutable configs
✅ Logging and audit trails
✅ Domain Value Objects
✅ State models in Blazor, Redux, Fluxor
✅ Event-driven architecture

🧠 Final Thoughts

C# records are more than just syntactic sugar — they promote a mindset of immutability, clarity, and value-based design.

They:

  • Reduce bugs
  • Simplify data models
  • Improve thread safety
  • Save tons of development time

Whether you're building APIs, desktop apps, or microservices, records make your C# experience cleaner and smarter.

Once you go record, you’ll never want to class again.

📎 Want to Know More About Records?

If you're curious to dive deeper into C# records, check out my other article:

👉 Exploring Records in C#

It includes practical tips, comparisons with classes, and real-world scenarios.

📚 References

Microsoft Docs – Records

Init-only Setters in C# 9

Immutability

Pattern Matching

Dev Blog – Welcome to C# 9

Martin Fowler – Value Object

Top comments (4)

Collapse
 
fernando_gmez_45a9866b66 profile image
Fernando Gómez • Edited

Hi great article, thanks! How do you get this syntax to compile?

public record Email(string Address)
{
public Email
{
if (!Address.Contains("@"))
throw new ArgumentException("Invalid email");
}
}

When I try it I get an error "Invalid token '{' in a member declaration, and I'm using C# 13 and .NET 9 (the latest at the moment). Am I missing something?

Collapse
 
stevsharp profile image
Spyros Ponaris

The correct syntax is:

public record Email
{
    public string Address { get; }

    public Email(string address)
    {
        if (!address.Contains("@"))
            throw new ArgumentException("Invalid email", nameof(address));

        Address = address;
    }
}
Enter fullscreen mode Exit fullscreen mode

Thanks for your help! I’ve updated the article with the correct code.

Collapse
 
auyeungdavid_2847435260 profile image
David Au Yeung

This works well in the ever-changing world of APIs 😁

Collapse
 
stevsharp profile image
Spyros Ponaris

Exactly! Especially when dealing with DTOs in integrations , records help enforce consistency while being lightweight to update.