- Published on
My favorite C# feature? Records for effortless data handling.
- Authors
- Name
- Mathias Hove
- @mathias_hove
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 towith
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 toinit;
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
}
}