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.

Exploring C# Records and Their Use Cases

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.

elmah.io: Error logging and Uptime Monitoring for your web apps

This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.

See how we can help you monitor your website for crashes Monitor your website