Utilizing Record Types in .NET

Sheldon Cohen
4 min readJul 13, 2024

--

Record types for DTOs

C# 9.0 introduced record types, a feature for creating data-centric reference types. Records offer a simpler way to define immutable objects with value-based equality. This article explores how records differ from traditional classes and when to use them in .NET development.

What are Record Types?

Record types simplify the creation of data objects in C#. They provide a concise way to define reference types with built-in functionality for value-based equality and immutability. Here’s a basic example of a record definition:

public record Person(string Name, int Age);

This single line creates a record with two properties: Name and Age. When you define a record this way, C# automatically generates a constructor, public properties, a deconstruction method, value-based equality members, and a ToString() method.

Key Features of Records

  1. Immutability: By default, records are immutable. Once you create a record instance, its properties cannot be changed.
  2. Value-based Equality: Records use value-based equality, meaning two record instances with the same property values are considered equal.
  3. Auto-generated Methods: Records come with a built-in ToString() implementation, a constructor, and methods for equality and deconstruction.
  4. Inheritance: Records support single inheritance, allowing you to create hierarchies of record types.
  5. Non-destructive Mutation: Records introduce with expressions, enabling you to create new instances with modified properties without altering the original instance.

Comparing Records to Traditional Classes

While both records and classes are reference types in C#, they have key differences:

  • Immutability: Records are immutable by default, whereas classes are mutable.
  • Equality: Records use value-based equality, while classes use reference-based equality.
  • Syntax: Record definitions are more concise, often requiring only a single line of code.
  • Auto-generated Members: Records automatically implement useful methods and behaviors that need manual implementation in classes.

Advantages of Using Records

  1. Built-in Immutability: Records prevent unintended modifications, making your code more predictable and easier to reason about.
  2. Concise Syntax: A single line of code can define a complex data structure, enhancing readability and reducing errors.
  3. Value-based Equality: Simplifies comparisons, especially when working with collections or data objects.
  4. Non-destructive Mutation: The with expression allows easy updates while maintaining immutability.

When to Use Records

Records are ideal for representing immutable data models such as DTOs (Data Transfer Objects), configuration settings, or scenarios where encapsulating a set of related values is necessary. They are particularly useful in domain-driven design for value objects or entities with clear identity and equality semantics.

However, traditional classes may be preferred when you need mutable state, complex business logic, or more control over object behavior.

Code Examples

Basic Record Declaration:

public record Person(string Name, int Age, string Email);

Equivalent Class:

public class PersonClass
{
public string Name { get; init; }
public int Age { get; init; }
public string Email { get; init; }

public PersonClass(string name, int age, string email)
{
Name = name;
Age = age;
Email = email;
}

public override bool Equals(object obj)
{
return obj is PersonClass person &&
Name == person.Name &&
Age == person.Age &&
Email == person.Email;
}

public override int GetHashCode()
{
return HashCode.Combine(Name, Age, Email);
}
}
  • Value-based Equality:
var person1 = new Person("Alice", 30, "alice@example.com");
var person2 = new Person("Alice", 30, "alice@example.com");
Console.WriteLine(person1 == person2); // Outputs: True
  • Immutability and Non-destructive Mutation:
var original = new Person("Bob", 25, "bob@example.com");
var updated = original with { Age = 26 };
Console.WriteLine(original); // Outputs: Person { Name = Bob, Age = 25, Email = bob@example.com }
Console.WriteLine(updated); // Outputs: Person { Name = Bob, Age = 26, Email = bob@example.com }
  • Inheritance:
public record Employee(string Name, int Age, string Email, string Department) 
: Person(Name, Age, Email);

var employee = new Employee("Charlie", 35, "charlie@example.com", "IT");
Console.WriteLine(employee);
// Outputs: Employee { Name = Charlie, Age = 35, Email = charlie@example.com, Department = IT }

Best Practices for Using Records

  1. Use for Immutable Data: Ideal for DTOs, value objects, and configuration settings.
  2. Leverage Value-based Equality: Simplifies comparisons and improves code intuitiveness.
  3. Utilize Positional Syntax: For concise definitions when the structure is straightforward.
  4. Init-only Properties: For more control over property initialization or computed properties.
  5. Non-destructive Mutation: Use with expressions for easy updates while maintaining immutability.
  6. Use Inheritance Sparingly: Avoid complex hierarchies that are hard to maintain.
  7. Pattern Matching: Records work well with pattern matching, leading to more expressive and concise code.
  8. Be Mindful of Performance: Benchmark for performance-critical scenarios, as value-based equality checks can be slower than reference equality for complex objects.

Wrapping it Up

Records in C# significantly simplify the creation and handling of data-centric types. By providing built-in immutability, value-based equality, and a concise syntax, records offer developers a powerful tool for expressing data models and improving code readability.

Throughout this article, we’ve explored the key features of records, compared them to traditional classes, and examined their practical applications through code examples. We’ve seen how records can reduce boilerplate code, enhance data integrity, and simplify operations like equality comparisons and non-destructive updates.

While records aren’t a replacement for classes in all scenarios, they excel in situations where you need to represent immutable data structures or value objects. By understanding when and how to use records effectively, you can write more expressive, concise, and maintainable code.

As C# continues to evolve, records are likely to play an increasingly important role in .NET development. By incorporating records into your toolkit and following best practices, you can leverage this feature to its full potential, leading to cleaner and more robust applications.

Have some thoughts or feedback? Comment below.

--

--

Sheldon Cohen

Technology professional with 15+ years of software development