0
0
C Sharp (C#)programming~15 mins

With expressions for immutable copies in C Sharp (C#) - Deep Dive

Choose your learning style9 modes available
Overview - With expressions for immutable copies
What is it?
With expressions in C# allow you to create a new object by copying an existing one but changing some of its properties. This is especially useful for immutable types, where you cannot change the original object after creation. Instead of writing code to copy all properties manually, with expressions do it in a simple, clear way. They help keep your data safe and your code clean.
Why it matters
Without with expressions, changing an immutable object means creating a new one manually, which is error-prone and verbose. This can lead to bugs and harder-to-read code. With expressions solve this by making it easy to create modified copies without changing the original. This helps programs avoid accidental changes and makes working with data safer and clearer.
Where it fits
Before learning with expressions, you should understand classes, objects, and immutability in C#. After this, you can explore records and pattern matching, which often use with expressions for clean, functional-style code.
Mental Model
Core Idea
With expressions create a new object by copying an existing one but changing only specified parts, keeping the rest the same.
Think of it like...
Imagine you have a cookie cutter shape made of dough. Instead of making a new shape from scratch, you press the cutter again but change the dough slightly in one spot. The new cookie is almost the same but with your small change.
Original Object
┌─────────────────────┐
│ PropertyA: Value1   │
│ PropertyB: Value2   │
│ PropertyC: Value3   │
└─────────────────────┘
          │
          │ with expression changes PropertyB
          ▼
New Object
┌─────────────────────┐
│ PropertyA: Value1   │
│ PropertyB: NewValue │
│ PropertyC: Value3   │
└─────────────────────┘
Build-Up - 7 Steps
1
FoundationUnderstanding immutable objects
🤔
Concept: Immutable objects cannot be changed after creation, so any change requires making a new object.
In C#, an immutable object is one where all properties are readonly or have no setters. For example, a record type or a class with only get-only properties. Once created, you cannot change its data directly.
Result
You learn that to 'change' an immutable object, you must create a new one with the desired differences.
Understanding immutability is key because with expressions only make sense when objects cannot be changed directly.
2
FoundationCopying objects manually
🤔
Concept: Before with expressions, copying an object with some changes meant writing code to copy each property.
Suppose you have a Person class with Name and Age. To change Age, you create a new Person and copy Name manually: var newPerson = new Person(oldPerson.Name, newAge); This is tedious and error-prone for many properties.
Result
You see that manual copying is repetitive and can cause mistakes if you forget a property.
Knowing this pain shows why a simpler syntax like with expressions is valuable.
3
IntermediateUsing record types for immutability
🤔
Concept: C# record types are designed for immutable data and support with expressions natively.
Records automatically create immutable properties and a copy method. For example: record Person(string Name, int Age); var person1 = new Person("Alice", 30); var person2 = person1 with { Age = 31 }; This creates a new Person with Age changed, Name same.
Result
You can create modified copies easily without manual copying.
Understanding records unlocks the full power of with expressions in C#.
4
IntermediateSyntax of with expressions
🤔Before reading on: do you think with expressions modify the original object or create a new one? Commit to your answer.
Concept: With expressions create a new object by copying the original and changing specified properties using a special syntax.
The syntax is: var newObj = oldObj with { Property1 = newValue1, Property2 = newValue2 }; This does not change oldObj but returns a new object with changes.
Result
You get a new object with updated properties, original stays unchanged.
Knowing that with expressions never mutate the original prevents bugs related to unexpected changes.
5
IntermediateLimitations of with expressions
🤔Before reading on: do you think with expressions work with any class or only with records? Commit to your answer.
Concept: With expressions only work with record types or types that implement a special method called Clone or similar.
If you try with expressions on a normal class without support, you get a compile error. Records generate this support automatically.
Result
You learn that with expressions are tied to record types or custom implementations.
Understanding this limitation helps you choose the right type for your data.
6
AdvancedHow with expressions handle nested objects
🤔Before reading on: do you think with expressions perform deep or shallow copies? Commit to your answer.
Concept: With expressions perform shallow copies, meaning nested objects are shared, not copied deeply.
If a record has a property that is a reference type (like a list), the new object shares the same nested object unless you manually copy it. Example: record Order(int Id, List Items); var order1 = new Order(1, new List{"apple"}); var order2 = order1 with { }; Both share the same Items list.
Result
You realize changes to nested objects affect all copies unless handled carefully.
Knowing shallow copy behavior prevents bugs with shared mutable state in supposedly immutable objects.
7
ExpertCustomizing with expressions behavior
🤔Before reading on: do you think you can change how with expressions copy objects? Commit to your answer.
Concept: You can customize how with expressions create copies by overriding the Clone method or using positional records with custom copy constructors.
Records generate a protected virtual method called 'Clone' that with expressions use internally. By overriding Clone, you can control copying behavior, for example to do deep copies or validate data. Example: public record Person(string Name, int Age) { protected virtual Person Clone() => this with { }; } You can override Clone to customize.
Result
You gain control over copying, enabling advanced scenarios like deep copy or logging.
Understanding internal cloning lets you extend with expressions beyond default shallow copies.
Under the Hood
When you use a with expression on a record, the compiler generates a method that creates a new object copying all fields from the original. It then applies the changes you specify. Internally, this uses a 'Clone' method that returns a copy of the object. The new object is a separate instance with the updated properties, leaving the original untouched.
Why designed this way?
This design balances immutability with ease of use. Instead of forcing manual copying, the compiler automates it, reducing errors and boilerplate. Records and with expressions were introduced together in C# 9 to support functional programming styles and safer data handling.
Original Object
┌─────────────────────────────┐
│ Properties: A, B, C         │
│                             │
│  Clone() method creates copy│
└─────────────┬───────────────┘
              │
              ▼
New Object (copy)
┌─────────────────────────────┐
│ Properties: A, B', C        │
│ (B changed by with expr)    │
└─────────────────────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Does a with expression change the original object or create a new one? Commit to your answer.
Common Belief:With expressions modify the original object directly.
Tap to reveal reality
Reality:With expressions always create a new object and leave the original unchanged.
Why it matters:Believing the original changes can cause bugs where code unexpectedly shares state or loses data integrity.
Quick: Do with expressions perform deep copies of nested objects? Commit to your answer.
Common Belief:With expressions create deep copies of all nested objects automatically.
Tap to reveal reality
Reality:With expressions perform shallow copies; nested objects are shared between copies unless manually cloned.
Why it matters:Assuming deep copy leads to bugs when nested mutable objects are changed unexpectedly across copies.
Quick: Can you use with expressions on any class type? Commit to your answer.
Common Belief:With expressions work on all classes, not just records.
Tap to reveal reality
Reality:With expressions only work on record types or classes that implement special cloning support.
Why it matters:Trying to use with expressions on unsupported types causes compile errors and confusion.
Quick: Are with expressions just syntax sugar for manual copying? Commit to your answer.
Common Belief:With expressions are just a shortcut and do not add new capabilities.
Tap to reveal reality
Reality:With expressions integrate with compiler-generated cloning and immutability features, enabling safer and clearer code beyond simple copying.
Why it matters:Underestimating their power leads to missing out on idiomatic, robust C# patterns.
Expert Zone
1
With expressions rely on compiler-generated Clone methods, but you can override these to implement deep copying or validation.
2
Records with inheritance can have complex with expression behavior, especially when base and derived records have different properties.
3
Using with expressions with mutable reference type properties requires careful design to avoid unintended shared state.
When NOT to use
Avoid with expressions when working with mutable classes that do not support cloning or when deep copying is required but not implemented. Instead, use explicit copy constructors or factory methods that handle deep copies.
Production Patterns
In production, with expressions are used heavily with records to implement immutable data transfer objects (DTOs), configuration objects, and state snapshots in functional programming styles. They enable concise updates to data without side effects.
Connections
Functional programming immutability
With expressions build on the idea of immutable data common in functional programming.
Understanding immutability in functional programming helps grasp why with expressions promote safer, side-effect-free code.
Copy constructors in OOP
With expressions automate the pattern of copy constructors used to create modified copies of objects.
Knowing copy constructors clarifies what with expressions do behind the scenes and why they simplify object copying.
Version control systems
With expressions conceptually resemble creating a new version of a file with small changes, similar to commits in version control.
Seeing with expressions as versioning helps understand their role in preserving history and avoiding destructive changes.
Common Pitfalls
#1Assuming with expressions modify the original object.
Wrong approach:var newPerson = oldPerson with { Age = 40 }; // Then expecting oldPerson.Age to be 40
Correct approach:var newPerson = oldPerson with { Age = 40 }; // oldPerson remains unchanged, newPerson has Age 40
Root cause:Misunderstanding that with expressions create new objects rather than mutate existing ones.
#2Using with expressions on normal classes without record support.
Wrong approach:class Person { public string Name { get; } public int Age { get; } } var p2 = p1 with { Age = 25 }; // Compile error
Correct approach:record Person(string Name, int Age); var p2 = p1 with { Age = 25 }; // Works
Root cause:Not realizing with expressions require record types or custom cloning support.
#3Expecting deep copy of nested objects automatically.
Wrong approach:var order2 = order1 with { }; order2.Items.Add("banana"); // Changes order1.Items too
Correct approach:var order2 = order1 with { Items = new List(order1.Items) }; order2.Items.Add("banana"); // order1.Items unchanged
Root cause:Not understanding that with expressions do shallow copies only.
Key Takeaways
With expressions create new objects by copying existing ones and changing specified properties, preserving immutability.
They work primarily with record types in C#, which are designed for immutable data.
With expressions perform shallow copies, so nested mutable objects are shared unless explicitly copied.
Understanding with expressions helps write safer, clearer code by avoiding accidental mutations.
Advanced users can customize copying behavior by overriding cloning methods in records.