How record Types Improve Data Modeling in C# (and in EF Core)
When C# 9 introduced record types, many developers saw them as just “fancier classes.”
But in practice, records have changed how we model data — making our DTOs, domain entities, and configuration objects cleaner, safer, and more expressive.
In this article, we’ll explore how record types improve immutability, value equality, and data modeling, then see how they behave inside Entity Framework Core (EF Core) — including their limitations and best practices.
1. What Is a record in C#?
A record is a reference type like a class, but designed primarily for immutable data and value-based equality.
It’s perfect for data transfer objects (DTOs), request/response models, and domain value objects — places where the data content matters more than the object identity.
public record Product(string Code, string Name, decimal Price);
This single line defines:
- Properties (
Code,Name,Price) - A constructor
- Value-based equality (
Equals,GetHashCode) - A readable
ToString()method
2. Immutability Made Easy
When you use a record, the compiler automatically creates init-only properties, ensuring immutability by default.
public record Product
{
public string Code { get; init; }
public string Name { get; init; }
public decimal Price { get; init; }
}
Once created, you can’t modify the object:
var p1 = new Product { Code = "A01", Name = "Widget", Price = 10.5m };
// ❌ p1.Price = 20.0m; // Compile-time error!
To “update” data, use the with expression, which creates a copy:
var discounted = p1 with { Price = 8.5m };
Console.WriteLine(discounted);
// Output: Product { Code = A01, Name = Widget, Price = 8.5 }
✅ Why it’s useful:
- Prevents accidental data mutation
- Works naturally in multi-threaded or functional-style code
- Ideal for clean, predictable DTOs or configuration models
3. Value Equality vs Reference Equality
Unlike classes, which compare by reference, records compare by value — all property values must match.
var a = new Product("A01", "Widget", 10.5m);
var b = new Product("A01", "Widget", 10.5m);
Console.WriteLine(a == b); // True
But for classes:
public class ProductClass
{
public string Code { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
var c1 = new ProductClass { Code = "A01", Name = "Widget", Price = 10.5m };
var c2 = new ProductClass { Code = "A01", Name = "Widget", Price = 10.5m };
Console.WriteLine(c1 == c2); // False
✅ Why it matters:
records behave like value objects (great for DDD).- Great for comparisons, caching, or deduplication logic.
- Reduces boilerplate equality code.
4. Perfect for DTOs and API Models
Records are ideal for DTOs in REST APIs or message contracts.
They provide compact syntax, easy serialization, and built-in immutability.
Example: Using Records in ASP.NET Core
DTO definition:
public record CreateOrderDto(string CustomerId, List Items);
public record OrderItemDto(string ProductCode, int Quantity);
Controller usage:
[HttpPost("orders")]
public IActionResult CreateOrder([FromBody] CreateOrderDto dto)
var totalItems = dto.Items.Sum(i => i.Quantity);
return Ok(new { Message = $"Order received with {totalItems} items." });
}
This pattern is clean, expressive, and safe — once the DTO is constructed, its data can’t be changed unexpectedly.
5. Extending Records in Domain Models
Records can also represent domain value objects in Clean Architecture:
public record Money(decimal Amount, string Currency)
{
public Money ConvertTo(string newCurrency, decimal rate)
=> new Money(Amount * rate, newCurrency);
}
You can extend or override behavior while keeping equality and immutability consistent.
6. Using Records with Entity Framework Core
So, what happens when we use records inside EF Core?
Problem: EF Core Expects Mutable Entities
EF Core needs:
- A parameterless constructor
- Settable properties to populate data during materialization
But immutable records (with init or constructor-only properties) don’t fit that well — especially in older EF versions.
public record Product
{
public int Id { get; init; }
public string Name { get; init; } = string.Empty;
public decimal Price { get; init; }
}
This works fine in memory, but EF Core might throw:
> System.InvalidOperationException: No suitable constructor found for entity type ‘Product’.
because it can’t figure out how to set the init-only properties.
Option 1: Use Mutable Records for EF Entities
Simplify by allowing `set` accessors:
public record Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}
This behaves like a class, but still gets value-based equality, ToString(), and Deconstruct() support.
✅ Works with EF Core
❌ Loses full immutability
Option 2: Use Records for Value Objects, Not Entities
A better design: keep your entities as classes, but use records as owned value objects.
public record Money(decimal Amount, string Currency);
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public Money Price { get; set; } = new(0, "USD");
}
Then configure it:
modelBuilder.Entity().OwnsOne(p => p.Price);
✅ Keeps EF happy
✅ Retains immutability
✅ Ideal for DDD-style modeling
Option 3: EF Core 8+ Constructor Binding Support
Starting from EF Core 8, you can map record constructors directly:
public record Product(int Id, string Name, decimal Price);
modelBuilder.Entity().HasKey(p => p.Id);
EF Core now understands positional constructors, so it can hydrate record instances properly — but:
- EF still cannot track updates to immutable instances.
- Updating requires replacing the record entirely.
So this is best for read models or projections, not tracked entities.
7. Records vs Classes — When to Use Each
| Feature | class |
record |
|---|---|---|
| Equality | Reference-based | Value-based |
| Mutability | Usually mutable | Usually immutable |
with expression |
❌ | ✅ |
| Ideal for | Business logic, tracked entities | DTOs, value objects, projections |
| EF Compatibility | Full | Limited (mutable or constructor-binding only) |
8. Practical Guidelines
✅ Use records for:
- DTOs and API contracts
- Value objects (
Money,Address,Coordinate) - Immutable read models
❌ Avoid records for:
- Tracked entities in EF Core (unless using mutable properties)
- Highly stateful domain entities
Conclusion
C# `record` types promote safer, cleaner, and more declarative modeling.
They make your data types self-documenting, immutable by default, and equality-friendly.
In EF Core, records still work well — just be aware of the limitations:
- EF needs settable or constructor-bound properties.
- Records shine as value objects or read models, not as tracked entities.
Used wisely, they can make your .NET codebase more expressive and bug-resistant.