C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
25: Records for Immutable Data
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
ShippingManifestwith the following properties:string TrackingNumber,string Destination, andstring Status. - In your
Mainmethod, instantiate a manifest for a package going to "New York" with a status of "Pending". - Use the
withexpression to create a second record calledshippedManifestthat 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".
There are no comments for now.