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