C# has long prided itself on being a balanced language — object-oriented at its core, yet steadily embracing functional programming constructs. With the introduction of record types in C# 9, and their evolution in later versions including C# 14, developers have gained a powerful new tool for modeling immutable data with minimal ceremony. But beyond the syntax sugar lies a deep paradigm shift in how we think about value semantics, equality, and data modeling.
This article takes you through the why, how, and what next of using record in C#, diving into the semantics, the capabilities, and the nuances that differentiate it from traditional classes and structs. Whether you're working with DTOs, DDD entities, or functional pipelines, record types offer expressive, declarative modeling that can simplify and elevate your code.
What Is a record?
At its core, a record is a reference type with value-based equality semantics. This means two record instances with the same data are considered equal, regardless of whether they’re the same reference in memory.
Traditionally, reference types (classes) in C# use reference equality by default. This means two objects are equal only if they point to the same memory address. record changes that, comparing objects by their values — making them ideal for data transfer, modeling state, and representing immutable entities.
Here’s the simplest example
public record User(string Name, int Age);
var u1 = new User("Alice", 30);
var u2 = new User("Alice", 30);
Console.WriteLine(u1 == u2); // True — value-based equality
In contrast, had User been a class, the comparison would return False, since they are two different references. This behavior is a deliberate and powerful shift that aligns C# more closely with functional languages like F#, where immutability and structural equality are default.
Types of records: Positional vs. Nominal
C# supports two primary forms of records.
1. Positional Records
These are defined using a concise constructor-like syntax directly in the declaration. They automatically provide Deconstruct(), ToString(), and Equals() methods.
public record Product(string Name, decimal Price);
This form is incredibly concise and perfect for data-centric applications where the focus is on holding state rather than behavior. They are ideal for DTOs, messages in messaging systems, or values passed across layers.
2. Nominal Records (With Explicit Properties)
You can also declare a record with manually defined properties, giving you more control over behaviors like validation, formatting, or custom accessors.
public record Order
{
public string OrderId { get; init; }
public DateTime Date { get; init; }
}
This variant is useful when you need richer behavior, annotations, or more customization in your data models, such as domain-driven design (DDD) aggregates or view models.
Immutability and With-Expressions
One of the most elegant features of record types is non-destructive mutation via with expressions. Instead of mutating an instance, you can create a new instance based on an existing one, changing only the values you specify.
var original = new User("Alice", 30);
var updated = original with { Age = 31 };
Console.WriteLine(original); // User { Name = Alice, Age = 30 }
Console.WriteLine(updated); // User { Name = Alice, Age = 31 }
This pattern is perfect for functional programming, immutable state management, and concurrent systems, where shared mutable state is often a source of bugs. With-expressions support clarity and intent, enabling a cleaner, safer style of programming.
Value Equality vs Reference Equality
Understanding the difference between value-based equality (used in record) and reference-based equality (used in class) is essential to avoid unexpected behavior. For instance:
public class Person(string Name);
public record Citizen(string Name);
var p1 = new Person("Bob");
var p2 = new Person("Bob");
var c1 = new Citizen("Bob");
var c2 = new Citizen("Bob");
Console.WriteLine(p1 == p2); // False
Console.WriteLine(c1 == c2); // True
This distinction is not just academic — it influences everything from unit tests and dictionary lookups to LINQ queries and UI state comparisons. Knowing when your type will be compared by structure vs. identity is crucial in large codebases.
Inheritance and Sealing
Records support inheritance, but by default, a record is sealed. You can create record hierarchies using the record class or record struct syntax, but it's important to understand that equality comparisons are type-sensitive.
public record Animal(string Name);
public record Dog(string Name, string Breed) : Animal(Name);
Animal a = new Dog("Rex", "Labrador");
Animal b = new Dog("Rex", "Labrador");
Console.WriteLine(a == b); // True — same type and values
Records introduce subtleties in polymorphism and equality when inheritance is involved. Base records cannot easily override derived behaviors, and with expressions return the same runtime type as the original — preserving immutability while maintaining type safety.
Records in Real-World Scenarios
Here are a few practical domains where record types shine.
1. Modeling Immutable Domain Events
In event-sourced systems, each change in state is a discrete, immutable event. Records are perfect for representing these events clearly and immutably.
public record OrderShipped(Guid OrderId, DateTime ShippedAt);
2. Functional Pipelines
When data flows through transformations (e.g., in LINQ or data pipelines), you often need to return modified versions of the same structure. with expressions enable this elegantly.
var transformed = inputData with { Status = "Processed" };
3. Minimal APIs and Data Contracts
ASP.NET Core’s minimal API style pairs beautifully with records for request/response types, reducing ceremony and improving expressiveness.
What’s New in C# 14 for Records?
C# 14 refines several aspects of record usability, especially when combined with primary constructors and required members. You can now.
- Use primary constructors with full support for non-record types, making the gap smaller between record and class.
- Mix required members with record declarations for safer initialization.
- Rely on collection expressions and natural lambda types when composing record-based models, especially in APIs.
Together, these changes make records feel more like a first-class data modeling language inside C# — one that can compete with the conciseness of TypeScript or Kotlin, but with full C# robustness.
Conclusion: Records as a Mindset
Understanding record types is more than learning a syntax feature — it’s about shifting how we design and think about data in our applications. Records encourage immutability, clarity, and value semantics, all of which lead to more reliable and maintainable code.
Whether you’re modeling events, building APIs, or crafting domain models, records offer a streamlined and expressive way to encapsulate state without unnecessary complexity. They are not just an alternative to classes — they represent a fundamental shift toward a more declarative, intention-revealing style of programming.
As C# continues to evolve, embracing records will help you write code that is not just shorter, but smarter.