๐ 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(string Address)
{
public Email
{
if (!Address.Contains("@"))
throw new ArgumentException("Invalid email");
}
}
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 (2)
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.