C# 9.0 Deep Dive: Records, Init-Only, and Patterns

C# 9.0 is arguably the most significant update to the language since LINQ in C# 3.0. It introduces a functional paradigm shift with Records, cementing immutability as a first-class citizen. In this article, we will go beyond the syntax and explore how these features change the way we design Domain Models and DTOs.

The Problem with Classes

For 20 years, we have written Data Transfer Objects (DTOs) like this:

public class CustomerDto 
{
    public int Id { get; set; }
    public string Name { get; set; }
    
    // Boilerplate equality
    public override bool Equals(object obj) { ... }
    public override int GetHashCode() { ... }
}

There are two problems here:
1. Mutability: Anyone can change `Name` at any time, leading to race conditions in multi-threaded apps.
2. Identity Semantics: Classes use reference equality. `new CustomerDto { Id=1 } != new CustomerDto { Id=1 }`. This breaks testing and caching logic.

Enter Records

A `record` is a reference type with value semantics. It is immutable by default.

public record CustomerDto(int Id, string Name);

That one line generates:
– Constructor
– Properties with `init` setters
– Value-based `Equals` and `GetHashCode`
– A `ToString()` that prints the values (not the type name)
– Deconstructor support

Non-Destructive Mutation

Since records are immutable, how do we modify them? We don’t. We create copies using the `with` expression.

var c1 = new CustomerDto(1, "Alice");
var c2 = c1 with { Name = "Bob" }; // c1 remains Alice, c2 is Bob. ID is copied.

This is internally compiled to a cloning method. It is highly efficient and thread-safe because no shared state is ever mutated.


Discover more from C4: Container, Code, Cloud & Context

Subscribe to get the latest posts sent to your email.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.