Records in C#

Master C# records for immutable data modeling, value-based equality, and cleaner DTOs in modern .NET applications

C#
Data Types
Immutability
Modern C#
18 min read

Overview

Records fundamentally change how we model data in C#. Before records, creating a proper data class required implementing Equals(), GetHashCode(), ToString(), and often a copy constructor - easily 50+ lines for a simple data holder. Records reduce this to a single line while also signaling intent: "this type represents data."

Understanding records matters because they're becoming the standard for DTOs, API models, and domain events in modern .NET. They work seamlessly with pattern matching, integrate with Entity Framework Core (with some considerations), and enable cleaner functional programming patterns.

Core Concepts in Detail

Record Declaration Styles

There are two ways to declare records:

// 1. Positional syntax - concise, auto-generates constructor and properties
public record Person(string FirstName, string LastName, int Age);

// 2. Standard syntax - more control, like a regular class
public record Employee
{
    public string Id { get; init; }
    public string Name { get; init; }

    // Can add computed properties and methods
    public string DisplayName => $"{Name} ({Id})";
}

// 3. Hybrid - positional with additional members
public record Product(string Sku, string Name)
{
    public decimal Price { get; init; }  // Additional property
}

Value Equality in Practice

Records implement IEquatable<T> and work correctly in collections:

public record Point(int X, int Y);

var p1 = new Point(3, 4);
var p2 = new Point(3, 4);

// All equality checks work as expected
Console.WriteLine(p1 == p2);            // True
Console.WriteLine(p1.Equals(p2));       // True
Console.WriteLine(ReferenceEquals(p1, p2)); // False (different instances)

// Works correctly as dictionary keys
var dict = new Dictionary<Point, string> { [p1] = "First" };
Console.WriteLine(dict[p2]);  // "First" (p2 equals p1)

// Duplicates removed in HashSet
var set = new HashSet<Point> { p1, p2 };
Console.WriteLine(set.Count);  // 1

Inheritance with Records

Records can inherit from other records:

public record Animal(string Name);
public record Dog(string Name, string Breed) : Animal(Name);

var dog1 = new Dog("Rex", "German Shepherd");
var dog2 = new Dog("Rex", "German Shepherd");
var animal = new Animal("Rex");

Console.WriteLine(dog1 == dog2);    // True (same type, same values)
Console.WriteLine(dog1 == animal);  // False (different types)

When to Use vs. When to Avoid

| Scenario | Use It? | Why | |----------|---------|-----| | DTOs / API models | ✅ Yes | Immutability and value equality are ideal | | Domain events | ✅ Yes | Events should be immutable facts | | Configuration objects | ✅ Yes | Config shouldn't change at runtime | | Entities with identity | ❌ No | Entities are identified by ID, not all values | | Mutable state | ❌ No | Records are designed for immutability | | EF Core entities | ⚠️ Depends | Works but requires configuration |

Common Patterns

Pattern 1: API Response Models

public record ApiResponse<T>(T Data, bool Success, string? Error = null)
{
    public static ApiResponse<T> Ok(T data) => new(data, true);
    public static ApiResponse<T> Fail(string error) => new(default!, false, error);
}

// Usage
var success = ApiResponse<User>.Ok(user);
var failure = ApiResponse<User>.Fail("Not found");

Pattern 2: Domain Events

public abstract record DomainEvent(DateTime OccurredAt);

public record OrderCreated(Guid OrderId, decimal Total, DateTime OccurredAt)
    : DomainEvent(OccurredAt);

public record OrderShipped(Guid OrderId, string TrackingNumber, DateTime OccurredAt)
    : DomainEvent(OccurredAt);

Common Mistakes

Mistake: Expecting Records to Be Value Types

// ❌ Wrong assumption
public record Point(int X, int Y);
var p1 = new Point(1, 2);
var p2 = p1;
// p1 and p2 reference the SAME object (records are reference types)

// ✅ Understanding
Console.WriteLine(ReferenceEquals(p1, p2));  // True - same reference
// Records have value EQUALITY, not value TYPE semantics
// Use 'record struct' if you need value type behavior

Why: Records are reference types. They have value equality but are still allocated on the heap.

Mistake: Trying to Mutate Records

// ❌ Won't compile
var person = new Person("Alice", 30);
person.Age = 31;  // Error: init-only property

// ✅ Use with-expression
var older = person with { Age = 31 };

Why: Positional record properties are init-only by design for immutability.

Mistake: Using Records as EF Core Entities Without Setup

// ❌ EF Core struggles with this
public record Product(int Id, string Name, decimal Price);

// ✅ Add parameterless constructor for EF
public record Product(int Id, string Name, decimal Price)
{
    private Product() : this(0, "", 0) { }  // For EF Core
}

Why: EF Core needs a parameterless constructor for materialization.

Practical Example

Scenario: Building an API that returns user profile data with clean testing.

public record UserProfileResponse(
    int Id,
    string Username,
    string Email,
    IReadOnlyList<string> Roles
)
{
    public static UserProfileResponse FromEntity(User user) => new(
        user.Id,
        user.Username,
        user.Email,
        user.Roles.Select(r => r.Name).ToList()
    );
}

// Testing is clean due to value equality
[Fact]
public void GetProfile_ReturnsCorrectData()
{
    var expected = new UserProfileResponse(1, "alice", "alice@test.com", new[] { "User" });
    var actual = _service.GetProfile(1);

    Assert.Equal(expected, actual);  // Value equality - just works!
}

Interview Questions

  1. What's the difference between a record and a class?

    • Records provide value-based equality and auto-generate Equals, GetHashCode, ToString. Classes use reference equality by default. Records are ideal for data; classes for behavior and identity.
  2. Are records value types or reference types?

    • Records are reference types (heap allocated). They have value semantics for equality, but they're not value types. record struct (C# 10) is the value type version.
  3. Can you inherit from a record?

    • Yes, but only another record can inherit from a record. Classes can't inherit from records, and records can't inherit from classes.
  4. How do with-expressions work?

    • The compiler generates a copy constructor. The with expression clones all properties, then applies your changes. The original is unchanged.
  5. When would you choose record over class?

    • Use records for DTOs, API models, events, configuration - any immutable data. Use classes for entities with identity, mutable objects, or complex behavior.

Interview Tips

Tip: Always mention value equality first when discussing records - it's the main differentiator from classes.

Tip: Be ready for "Are records value types?" - the answer is no. This is a common misconception interviewers test for.

Related Topics

  • Immutability: Records are C#'s primary tool for immutable reference types
  • Pattern Matching: Records work excellently with switch expressions
  • Value Types vs Reference Types: Records bridge the gap with value semantics on reference types
  • DTOs: Records are becoming the standard for data transfer objects