Published on

My favorite C# feature? Records for effortless data handling.

Authors

C# Records: Why I Actually Use Them

Why?

Let’s be honest: writing “data holder” types in C# used to be a bit of a pain. You know the drill—Customer, OrderItem, Address—all those little classes that just sit around holding data. Constructors, Equals, GetHashCode, copy logic... it’s a lot of noise for something so simple.

When records landed in C#, I was genuinely relieved. Suddenly, all that boilerplate was gone. I could focus on what the type actually meant, not just wiring up equality and copy logic. If you care about value-based equality and want to keep your code clean, records are a breath of fresh air.

What’s a record, anyway?

Here’s the magic: a record is a type where two instances with the same data are considered equal. It’s like C# finally “gets” what you meant.

public record Person(string FirstName, string LastName);

var a = new Person("Ana", "López");
var b = new Person("Ana", "López");

Console.WriteLine(a == b); // True (value-based equality)

With a regular class, that == would be false unless you did all the equality plumbing yourself. Records just do it for you.

Copying without the mess

One of my favorite things: you can make a copy of a record and tweak just what you need, without touching the original. (This is where the with expression shines.)

public record Product(string Name, decimal Price, string Category);

var original = new Product("Laptop", 999.99m, "Electronics");
var discounted = original with { Price = 899.99m };

Console.WriteLine(original.Price);   // 999.99
Console.WriteLine(discounted.Price); // 899.99

Heads up: this is a shallow copy. If you have a property that’s a reference type, you’ll want to update that inner object too if you need a true deep copy.

You can read more about with here

Two ways to declare records

1) Positional (compact)

Perfect for small models and DTOs. I use this style when I just want to get to the point.

public record Address(string Street, string City, string Country);

// Deconstruction works out of the box:
var home = new Address("Main St 1", "Copenhagen", "Denmark");
var (street, city, country) = home;

2) Object-style (explicit)

If you want XML docs, attributes, or default values, this style is your friend.

public record Customer
{
    public string Id { get; init; } = default!;
    public string Name { get; init; } = default!;
    public string Email { get; init; } = default!;
}

var c1 = new Customer { Id = "42", Name = "Sofia", Email = "sofia@example.com" };
var c2 = c1 with { Email = "sofia@contoso.com" };

Immutability that feels natural

Records don’t force you to go immutable, but they make it so easy that you’ll probably want to. I stick to init instead of set and use computed properties when I can.

public record OrderItem
{
    public string Sku { get; init; } = default!;
    public int Quantity { get; init; }
    public decimal UnitPrice{ get; init; }
    public decimal Total => Quantity * UnitPrice;
}

What you get for free

Records auto-generate Equals, GetHashCode, and ==/!= so equality is based on the data. This is perfect for:

  • Deduplicating in collections
  • Using as dictionary keys
  • Clean assertions in tests

Record structs (C# 10+)

If you want a lightweight value type with all the record goodness, record structs are the way to go.

public readonly record struct Point(int X, int Y);

var p1 = new Point(2, 3);
var p2 = p1 with { X = 10 };
Console.WriteLine(p1 == p2); // False (values differ)

A few gotchas

  • Shallow with: For nested updates, you’ll need to with each level:
    var updated = order with {
        ShippingAddress = order.ShippingAddress with { City = "Aarhus" }
    };
    
  • Don’t over-mutate: If you add set; everywhere, you lose the benefits. Stick to init; where you can.
  • Serializer fit: Pick positional or object-style based on what your serializer (and your team) likes best.

Quick reference

  • Use records for data-centric types and value equality
  • Use classes for identity/behavior-centric types where reference equality makes sense
  • Use with for safe, non-destructive changes
  • Consider record structs for tiny, copy-by-value models

Try it (copy-paste demo)

using System;

public record Product(string Name, decimal Price, string Category);

public record CartItem
{
    public Product Product { get; init; } = default!;
    public int Quantity    { get; init; }
    public decimal LineTotal => Quantity * Product.Price;
}

public readonly record struct Point(int X, int Y);

class Program
{
    static void Main()
    {
        // Value equality
        var p1 = new Product("Headphones", 199.99m, "Audio");
        var p2 = new Product("Headphones", 199.99m, "Audio");
        Console.WriteLine(p1 == p2); // True

        // Non-destructive mutation
        var sale = p1 with { Price = 149.99m };
        Console.WriteLine($"{p1.Price} -> {sale.Price}");

        // Shallow copy note (nested record)
        var item = new CartItem { Product = p1, Quantity = 2 };
        var itemSale = item with { Product = item.Product with { Price = 149.99m } };
        Console.WriteLine(item.LineTotal);     // 399.98
        Console.WriteLine(itemSale.LineTotal); // 299.98

        // Record struct
        var a = new Point(1, 2);
        var b = a with { X = 5 };
        Console.WriteLine(a == b); // False
    }
}