C# Record Methods

A C# record is a reference type (or record struct) built for value-shaped data: equality by contents, a concise constructor, and non-destructive copy with with. Use records for messages, query results, and value objects. Do not use them as EF Core entities with identity unless you know the tracking implications.

Positional vs explicit

public sealed record Money(decimal Amount, string Currency);

var price = new Money(10m, "USD");
var tax = price with { Amount = 1.5m };

Money equality is by amount and currency, not by reference. decimal already makes 0.1m + 0.2m exact. Records do not choose scale or rounding (two places, away from zero or to even). Do that at the boundary.

var price = new Money(10m, "USD");
var tax = price with { Amount = 1.5m };
bool same = price == new Money(10m, "USD");
var (amount, currency) = price;

Members that matter

  • Primary constructor parameters become public init properties unless you declare otherwise.
  • with copies the record’s own fields and sets the ones you name. It is shallow: a nested List<T> or class is still the same reference.
  • Synthesized members: Equals, GetHashCode, == and !=, ToString (fine for logs, not for UI), Deconstruct, PrintMembers, and the copy with uses.
  • Inheritance: record can inherit record. Equality includes runtime type. A Dog is not equal to an Animal with the same fields.

record class vs record struct vs class

record class (default) is a heap object with value equality — cheap to pass around, still a reference. record struct is a value type; copies are copies, large structs in collections hurt. Ordinary class is the right default for entities with identity and lifecycle.

What breaks it

A record with public setters is a dictionary key, then a property changes. ContainsKey misses because the hash changed. Keep the positional Money above, whose properties are init.

public record MutableMoney
{
    public decimal Amount { get; set; }
    public string Currency { get; set; } = "";
}

static bool LookupAfterMutate()
{
    var money = new MutableMoney { Amount = 10m, Currency = "USD" };
    var map = new Dictionary<MutableMoney, string> { [money] = "price" };
    money.Amount = 11m;
    return map.ContainsKey(money);
}

with on a nested list still shares the list. b.Lines.Add changes a.Lines.

sealed record Basket(List<string> Lines);

static bool SharedList()
{
    var a = new Basket(new List<string> { "a" });
    var b = a with { };
    b.Lines.Add("b");
    return a.Lines.Contains("b");
}

Pitfalls

  • Mutable record properties (set instead of init) plus using them as dictionary keys.
  • EF Core: records can work, but identity-map + value equality is easy to get wrong. Prefer classes for aggregates.
  • Huge nested records copied with with in a hot loop.

See value objects.