Exploring C# Records and Their Use Cases
In C#, classes historically define types like in any other Object-Oriented Language. Classes define models, encapsulate data and behavior, and organize code. A new block organizer came into play with the launch of C# 9—Record. This article will explore C# records with practical code examples. I will also dive deep into understanding the difference between class and record and when to use each.
What is C# class
A class is a blueprint or user-defined type representing a template for creating an object. An object is a concrete instantiation based on a class. C# builds object-oriented principles by encapsulating and abstracting fields and behaviors. It also defines a model structure on which objects are created.
What is C# Record
C# records, introduced in C# 9.0, declare value-based equality types. Unlike type declarations in class, records define immutable data types. That means properties cannot change their values. It focuses on data in type declaration and offers a simpler syntax.
public record Student(string Name, int Age);
Or
public record Animal
{
public string Name{ get; init; }
public string Breed{ get; init; }
}
Class vs Record
Class and Record have significant differences in usage and declaration. Let's dive deep.
Immutability
Class: By default, classes are mutable, and their fields and properties can be modified.
Records: By default, records are immutable, and the values of their properties cannot be changed.
Equality Comparison
Class: The class compares the equality of two objects on their reference. Two class instances are considered equal only if they refer to the same memory location (or if the Equals
method has been overridden to do something else).
Records: Records provide value-based equality. Two records are equal if they have the same set of properties and have the same values in the corresponding properties. However, the types of two instances must be the same.
public class Student
{
public string Name { get; set; }
public int Age { get; set; }
}
public record Student(string Name, int Age);
var student1 = new Student { Name = "Tracey", Age = 19 };
var student2 = new Student { Name = "Tracey", Age = 19 };
bool result = student1 == student2; // False (each instance has different references)
var student1 = new Student("Tracey", 19);
var student2 = new Student("Tracey", 19);
bool result = student1 == student2; // True (same set of values )
Concise Syntax
Class: Requires more boilerplate code, especially for constructors, property get and set, and equality comparison.
Record: Performs data models with a concise syntax by using built-in support for property assignment, equality comparison, and other features like with
expressions.
with
Expression or Non-destructive Mutation
Class: There is no support for with
expression in class.
Record: Supports the with expression, used to create a copy of an existing record with specific properties changed while keeping the original record the same as the existing record.
var student1 = new Student("Israr", 20);
var student2 = student1 with { Age = 30 }; // student2 is a copy of student1 with Age changed
Use Cases of C# Records
Now, we have understood what a record is and its key differences from the class. Let's move to the significant question, “When do you prefer records over class?” We will think of scenarios that lead to the requirement of using records instead of the traditional type.
Data Transfer Objects (DTOs)
Scenario: When sending or receiving data, especially over APIs or between services, DTOs pass data around without behavior (methods). These objects often require only value-based equality, immutability, and easy creation.
How Records Help
Concise Definition: You can define DTOs in a single line.
Immutability: Records ensure the data is not accidentally mutated after creation.
Equality: Value-based equality allows easy comparison of two DTOs, which is useful when checking whether two received responses are the same.
Example
public record OrderDto(int Id, string CustomerName, decimal TotalAmount);
The record OrderDto is concise and simpler than its counterpart class definition. It automatically provides a value-based equality comparison, which is required for a DTO. It offers immutability and a ToString()
method that helps with debugging.
Benefit: The type definition becomes very concise. There is no need to write custom constructors, equality checks, or GetHashCode()
methods.
Define Immutable Types
Scenario: We often need to set some values used within the application. However, these values, such as configuration settings, snapshots, or historical data, do not change once defined. For such scenarios, Records are a hero.
How Records Help
Concise Definition: As we are more concerned with the values for configuration, historical data, and snapshots, records provide a concise solution.
Immutability: Records ensure the data is not accidentally mutated after creation. This is what such types require.
Example
public record Configuration(string BaseUrl, string ConnectionString);
var initialConfig = new Configuration("https://api.myapp.com", “Server=myServerName,myPortNumber;Database=myDataBase;User Id=myUsername;Password=myPassword;”);
Benefit: The records ensure values stay consistent. It also allows easy type creation.
Snapshots and data versioning
Scenario: We need to manage versions of a system’s state, each representing data at a given time.
How Records Help
Concise Definition: As we are more concerned with the values for configuration, historical data, and snapshots, records provide a concise solution.
Immutability: Records ensure each version or snapshot is written once and cannot be altered.
Example
public record FileVersion(string Title, string Content, DateTime VersionDate);
var v1 = new FileVersion("File 1", "Initial Content", DateTime.Now);
var v2 = firstVersion with { Content = "Updated Content", VersionDate = DateTime.Now };
Benefit: Immutability is highly concerned when representing a version. Hence, records provide immutability to the version data.
Testing and Debugging
Scenario: In testing or debugging, we need to compare the results with the expected values. Here again, the value-based equality feature comes into play.
How Records Help
Automatic ToString()
: When a user creates a records type, it automatically generates a helpful ToString()
that prints out all property values. This method makes it easier to log in during testing and debugging.
Equality for Unit Testing: Value-based equality ensures that object comparisons in unit tests are accurate and intuitive.
Example:
var product1 = new Product("Laptop", 249.99M);
Console.WriteLine(product1);
// Prints: Product { Name = Laptop, Price = 199.99 }
Benefit: It reduces boilerplate code for logging during testing and debugging and automates the writing to ToString
methods for models.
Conclusion
C# records are the new addition to the C# type declaration. Unlike their counterparts, records are immutable and provide a value-based equality type. In this blog, I discussed the difference between class and records and explored use cases when records came into play.