Records in C#
Records in C# are a specialized reference type introduced in C# 9.0, designed to represent immutable data models with concise syntax. They are particularly useful in scenarios where data encapsulation, equality comparison, and immutability are critical to the system’s design. Unlike classes, records provide built-in value-based equality, meaning two records with the same property values are considered equal, which is not the case with standard reference types. This makes records highly valuable in scenarios such as data transfer objects (DTOs), domain-driven design (DDD) aggregates, and functional programming-inspired patterns.
In C# development, records allow developers to model data with clarity while avoiding repetitive boilerplate code. For example, when implementing algorithms that depend on comparing or deduplicating data structures, records provide automatic equality and hash code generation, reducing both coding effort and potential bugs. Records also support inheritance, deconstruction, and "with-expressions" for non-destructive mutation, aligning with modern OOP principles and immutable data practices.
In this tutorial, readers will learn how to declare and use records in C#, how they differ from classes and structs, and where they fit in real-world system architecture. We will explore syntax, immutability, and equality in depth, supported by practical coding examples. By the end, learners will understand how records strengthen data modeling, improve maintainability, and support advanced software development practices in C#.
Basic Example
textusing System;
namespace RecordsDemo
{
// Define a simple record for representing a person
public record Person(string FirstName, string LastName);
class Program
{
static void Main(string[] args)
{
// Create two record instances with identical values
Person p1 = new Person("Alice", "Johnson");
Person p2 = new Person("Alice", "Johnson");
// Demonstrate value-based equality
Console.WriteLine($"Are p1 and p2 equal? {p1 == p2}");
// Demonstrate non-destructive mutation using with-expression
Person p3 = p1 with { LastName = "Smith" };
Console.WriteLine($"Modified person: {p3.FirstName} {p3.LastName}");
// Deconstruction example
var (first, last) = p1;
Console.WriteLine($"Deconstructed values: {first}, {last}");
}
}
}
In the code above, we define a record named Person using C#’s concise record syntax. Unlike a class, the record automatically generates useful functionality such as value-based equality comparison and immutability. When we instantiate p1 and p2 with the same values, the equality operator (==
) returns true, demonstrating the value-based comparison that makes records distinct from classes. If these were classes, equality would depend on reference identity rather than values.
Another key feature demonstrated is the "with-expression." When creating p3, we derive a new instance from p1 while modifying only the LastName property. This preserves immutability by leaving the original p1 unchanged while producing a new object with the desired modification. Such non-destructive mutation is particularly useful in data pipeline transformations or immutable collections.
We also show deconstruction, where record properties can be extracted into local variables, improving code readability and supporting algorithmic manipulation of record data. This feature integrates seamlessly with LINQ queries or tuple-based operations in C#.
From a practical perspective, these features simplify modeling immutable data, prevent bugs related to shared mutable state, and reduce boilerplate code. Records naturally align with advanced patterns such as functional transformations and domain-driven design. For system architecture, they can serve as DTOs, domain entities, or immutable command/query payloads, ensuring cleaner, safer, and more predictable code.
Practical Example
textusing System;
using System.Collections.Generic;
using System.Linq;
namespace RecordsAdvancedDemo
{
// Record representing a product in a catalog
public record Product(int Id, string Name, decimal Price);
// Record representing an order line
public record OrderLine(Product Product, int Quantity)
{
public decimal Total => Product.Price * Quantity;
}
// Record representing a complete order
public record Order(int OrderId, List<OrderLine> Lines)
{
public decimal OrderTotal => Lines.Sum(l => l.Total);
}
class Program
{
static void Main(string[] args)
{
// Create some products
Product apple = new Product(1, "Apple", 0.5m);
Product banana = new Product(2, "Banana", 0.3m);
// Build order lines
OrderLine line1 = new OrderLine(apple, 10);
OrderLine line2 = new OrderLine(banana, 20);
// Build order
Order order = new Order(1001, new List<OrderLine> { line1, line2 });
// Display order total
Console.WriteLine($"Order {order.OrderId} total: {order.OrderTotal:C}");
// Demonstrate immutability with with-expression
Product discountedBanana = banana with { Price = 0.25m };
OrderLine line3 = new OrderLine(discountedBanana, 15);
// Create new order with modified data
Order updatedOrder = order with { Lines = new List<OrderLine> { line1, line3 } };
Console.WriteLine($"Updated Order {updatedOrder.OrderId} total: {updatedOrder.OrderTotal:C}");
}
}
}
C# best practices and common pitfalls with records primarily revolve around immutability, equality, and performance. One best practice is leveraging the concise record syntax for DTOs and immutable entities rather than defaulting to classes, reducing boilerplate while ensuring correctness. For algorithms that rely on comparisons or hashing, using records avoids writing custom equality and GetHashCode
methods.
A common mistake is misusing records for mutable entities. Records are designed for immutability, so altering their state after creation (via mutable properties) contradicts their purpose and may lead to subtle bugs in concurrent or functional contexts. Another pitfall is misunderstanding equality semantics: two records with identical property values are equal even if they are distinct instances, which may surprise developers expecting reference equality.
Performance considerations include avoiding records for large collections of frequently mutated objects. Since "with-expressions" create copies, excessive copying can introduce inefficiencies. Debugging tips include inspecting generated equality and ToString
methods to verify correct behavior and using Visual Studio’s debugger to step into record creation logic.
From a security perspective, ensure that records used for serialization or external data exchange are validated to prevent injection or over-posting attacks. Finally, for optimization, consider struct records introduced in later C# versions when working with high-performance scenarios where value semantics and stack allocation are advantageous.
📊 Reference Table
C# Element/Concept | Description | Usage Example |
---|---|---|
Record Declaration | Defines an immutable reference type with value-based equality | public record Person(string FirstName, string LastName); |
With-Expression | Creates a modified copy of a record without altering the original | Person p2 = p1 with { LastName = "Smith" }; |
Deconstruction | Extracts property values into variables for convenient use | var (first, last) = p1; |
Equality Comparison | Compares record instances by values, not references | Console.WriteLine(p1 == p2); |
Record Inheritance | Allows extending records with additional properties | public record Employee(string FirstName, string LastName, string Role) : Person(FirstName, LastName); |
In summary, records in C# provide a powerful way to model immutable, value-based data entities. They simplify equality handling, reduce boilerplate code, and encourage clean, functional programming practices within object-oriented development. The built-in features such as deconstruction, with-expressions, and auto-generated equality logic make them particularly suitable for data pipelines, domain models, and DTOs.
Understanding records also connects to broader C# development concepts such as immutability, LINQ, serialization, and domain-driven design. By mastering records, developers can create safer, more predictable code and integrate them effectively into modern architectural styles like microservices and event-driven systems.
As next steps, learners should explore advanced topics such as struct records for high-performance scenarios, record inheritance for polymorphic models, and integration with frameworks like ASP.NET Core for DTOs and API contracts. Practical advice includes using records for entities that must remain immutable and leveraging "with-expressions" to safely evolve data.
Recommended resources include Microsoft’s official C# documentation, architectural guides on immutability, and advanced tutorials on functional patterns in C#. Building on the foundation of records, learners can progress toward mastering advanced type systems, design patterns, and concurrency-safe programming in C#.
🧠 Test Your Knowledge
Test Your Knowledge
Test your understanding of this topic with practical questions.
📝 Instructions
- Read each question carefully
- Select the best answer for each question
- You can retake the quiz as many times as you want
- Your progress will be shown at the top