NashTech Blog

How record Types Improve Data Modeling in C# (and in EF Core)

Table of Contents

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.

Picture of Hoc Nguyen Thai

Hoc Nguyen Thai

Leave a Comment

Your email address will not be published. Required fields are marked *

Suggested Article

Scroll to Top