🌟 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);
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; }
}
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" };
// 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 ✅
👉 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
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!");
}
👉 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;
}
}
public record Money(decimal Amount, string Currency);
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; }
}
👉 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);
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
}
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);
});
});
}
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:
It includes practical tips, comparisons with classes, and real-world scenarios.
📚 References
Top comments (4)
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?
The correct syntax is:
Thanks for your help! I’ve updated the article with the correct code.
This works well in the ever-changing world of APIs 😁
Exactly! Especially when dealing with DTOs in integrations , records help enforce consistency while being lightweight to update.