Skip to Content
Course content

25: Records for Immutable Data

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I remember a developer on my team a few years back who spent an entire Friday night chasing a bug that felt like a ghost in the machine. He had a UserSession class that was being passed through five different services. Somewhere in the pipeline, a third-party validation library was "cleaning" the data by modifying a property on the object. By the time the session reached the database layer, the data was wrong, but since the object reference was the same, none of the logs showed where the change actually happened. He was mutating state in a way that was nearly impossible to track.

That's exactly the kind of headache records were designed to solve. In C#, a record is essentially a class that is optimized for immutable data. Instead of focusing on "this object is a specific instance in memory," a record focuses on "this object represents these specific values."

Value Equality Instead of Reference Pointers

If you create two different instances of a standard class with the exact same property values, C# will tell you they are different because they live at different memory addresses. Records change that. They implement value-based equality.

public record Product(string Sku, decimal Price);

var item1 = new Product("BOOK-123", 19.99m);
var item2 = new Product("BOOK-123", 19.99m);

Console.WriteLine(item1 == item2); // True

Notice the syntax above. That's a positional record. In one single line, C# generates the properties, a constructor, and the equality logic for you. It's a massive amount of boilerplate deleted. When you use this syntax, the properties are created as init-only, meaning they can be set during creation, but never changed afterward. If you try to do item1.Price = 25.00m;, the compiler will stop you dead in your tracks. This is how we kill those "ghost bugs" I mentioned earlier; if the data can't change, it can't be accidentally corrupted.

Updating State Without Breaking the Original

Now, you might be thinking, "If I can't change a property, how do I actually update anything?" You don't update a record; you create a new version of it. C# gives us the with expression for exactly this purpose. This is called non-destructive mutation.

var originalProduct = new Product("BOOK-123", 19.99m);

// We want to raise the price, but keep the SKU the same
var updatedProduct = originalProduct with { Price = 24.99m };

Console.WriteLine(originalProduct.Price); // Still 19.99
Console.WriteLine(updatedProduct.Price);   // 24.99

I love this pattern because it makes your code predictable. If you pass originalProduct into a method, you have a 100% guarantee that the method cannot change the price of that product behind your back. If it wants to "change" something, it has to return a brand new record to you. It forces a clean, functional flow of data through your application.




📋 Practical Task

Exercise: Updating a Shipping Manifest with Non-Destructive Mutation

You are building a logistics system. You need to ensure that shipping manifests cannot be accidentally altered once they are created, but you still need the ability to update the delivery status as the package moves.

Your Task:

  • Create a positional record named ShippingManifest with the following properties: string TrackingNumber, string Destination, and string Status.
  • In your Main method, instantiate a manifest for a package going to "New York" with a status of "Pending".
  • Use the with expression to create a second record called shippedManifest that changes the status to "In Transit", while keeping the tracking number and destination the same.
  • Print both the original and the new manifest to the console to verify that the original remained "Pending" and the new one is "In Transit".
Rating
0 0

There are no comments for now.

to be the first to leave a comment.