Immutability with init & with
Use init-only setters to create immutable objects and with-expressions to produce non-destructively modified copies.
Immutability with init & with is a free C# Academy lesson on CoddyKit. This is lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C# Academy learning path, and your progress syncs across the web and the CoddyKit app. The C# Academy course includes 4 lessons in total.
What Is Immutability?
An immutable object cannot be changed after creation. Immutability prevents accidental mutation, makes objects thread-safe by default, and simplifies reasoning about code. C# provides init setters and with expressions to make immutability ergonomic.
init-Only Setters
An init accessor allows a property to be set during object initialization (constructor or object initializer) but never after. It's the immutable counterpart to set.
public class Point
{
public double X { get; init; }
public double Y { get; init; }
}
var p = new Point { X = 3.0, Y = 4.0 }; // OK — init phase
p.X = 5.0; // COMPILE ERROR — cannot set after init
// All of these are valid init-phase assignments:
var p2 = new Point(X: 1.0, Y: 2.0);
var p3 = new Point { X = 0, Y = 0 };init in Record Positional Properties
Positional record properties are init-only by default — that's what makes records immutable. The compiler generates get; init; for each positional parameter.
// This record:
public record Person(string Name, int Age);
// Is equivalent to:
public record Person
{
public string Name { get; init; }
public int Age { get; init; }
public Person(string Name, int Age) { this.Name = Name; this.Age = Age; }
// + Deconstruct, Equals, GetHashCode, ToString
}
var alice = new Person("Alice", 30);
alice.Name = "Bob"; // COMPILE ERRORwith Expressions: Non-Destructive Mutation
A with expression creates a copy of a record with specific properties changed. The original is unchanged — true immutability.
public record Person(string Name, int Age, string Email);
var alice = new Person("Alice", 30, "alice@example.com");
// Create a modified copy — alice is unchanged
var olderAlice = alice with { Age = 31 };
var renamed = alice with { Name = "Alicia", Email = "alicia@example.com" };
Console.WriteLine(alice.Name); // Alice (unchanged)
Console.WriteLine(olderAlice.Age); // 31
Console.WriteLine(renamed.Name); // Aliciawith on Non-Record Types
C# 10 allows with on any struct or class that has copy constructor semantics — but it's most natural with records. For classes you need to implement it manually.
// struct with with-expression:
public struct Temperature
{
public double Celsius { get; init; }
public double Fahrenheit => Celsius * 9 / 5 + 32;
}
var t1 = new Temperature { Celsius = 20 };
var t2 = t1 with { Celsius = 25 }; // copy with change
Console.WriteLine(t1.Celsius); // 20 — unchanged
Console.WriteLine(t2.Celsius); // 25Chaining with Expressions
Chain multiple with expressions to build up complex transformations, each step creating a new immutable value from the previous one.
public record Order(int Id, string Status, decimal Total, DateTime UpdatedAt);
var order = new Order(42, "Pending", 99.99m, DateTime.UtcNow);
// Apply a promotion discount then mark as confirmed
var finalOrder = order
with { Total = order.Total * 0.9m } // 10% off
with { Status = "Confirmed" }
with { UpdatedAt = DateTime.UtcNow };
Console.WriteLine(order.Status); // Pending (original unchanged)
Console.WriteLine(finalOrder.Status); // ConfirmedComputed Properties on Immutable Types
Derived properties on immutable types are naturally pure — they compute from the fixed property values and always return the same result for the same input.
public record Money(decimal Amount, string Currency)
{
public Money Add(Money other)
{
if (Currency != other.Currency)
throw new InvalidOperationException("Currency mismatch");
return this with { Amount = Amount + other.Amount };
}
public Money Multiply(decimal factor) =>
this with { Amount = Amount * factor };
public override string ToString() =>
$"{Amount:F2} {Currency}";
}
var price = new Money(10.00m, "USD");
var tax = price.Multiply(0.08m);
var total = price.Add(tax);
Console.WriteLine(total); // 10.80 USDImmutable Collections
Combine records with ImmutableList<T> and other types from System.Collections.Immutable for fully immutable data structures.
using System.Collections.Immutable;
public record ShoppingCart(
string UserId,
ImmutableList<CartItem> Items)
{
public ShoppingCart AddItem(CartItem item) =>
this with { Items = Items.Add(item) };
public ShoppingCart RemoveItem(int productId) =>
this with { Items = Items.RemoveAll(i => i.ProductId == productId) };
public decimal Total => Items.Sum(i => i.Price * i.Quantity);
}Thread Safety from Immutability
Immutable objects are inherently thread-safe — no synchronization needed when sharing them across threads because their state cannot change after construction.
// Immutable config record shared across all threads safely
public record AppConfig(
string ConnectionString,
int MaxRetries,
TimeSpan Timeout);
// Register as singleton — safe because record is immutable
builder.Services.AddSingleton(
new AppConfig(
ConnectionString: config["DB"]!,
MaxRetries: 3,
Timeout: TimeSpan.FromSeconds(30)));
// Any thread can read this simultaneously without locksReal-World: Functional Event Sourcing
Immutable records pair naturally with event sourcing: each domain event is immutable, and state is derived by folding events — no mutation, no surprises.
public record OrderState(
int Id,
string Status = "Draft",
decimal Total = 0m);
public static OrderState Apply(OrderState state, object evt) => evt switch
{
OrderPlaced e => state with { Status = "Pending", Total = e.Total },
OrderShipped _ => state with { Status = "Shipped" },
OrderCancelled _ => state with { Status = "Cancelled" },
_ => state
};
// Fold events to get current state:
var state = events.Aggregate(
new OrderState(id),
Apply);Quick Check
What does a 'with' expression do to the original record?
Recap: Immutability with init & with
Key takeaways:
initaccessor: settable only during initialization, never after- Record positional properties are
init-only by default withexpression: creates a modified copy — original is unchanged- Chain
withexpressions for multi-step transformations - Immutable types are thread-safe without synchronization
- Combine with
ImmutableList<T>for fully immutable object graphs
Learn C# with an AI tutor — free
Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.
- Courses
- 93
- Lessons
- 346
Frequently Asked Questions
Is the “Immutability with init & with” lesson free?
Yes — the full text of “Immutability with init & with” is free to read here on the web. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C# Academy course, upgrade to CoddyKit PRO. The C# Academy course includes 4 lessons in total.
What will I learn in “Immutability with init & with”?
Use init-only setters to create immutable objects and with-expressions to produce non-destructively modified copies. You practise C# Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start C# Academy?
No prior experience is required. C# Academy on CoddyKit is structured for beginners through advanced learners, so you can start here or from the beginning and move at your own pace. This is lesson 2 of 4.
How long does the “Immutability with init & with” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this C# Academy lesson?
Yes. Every C# Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Record Types: Basics & Syntax
- Immutability with init & with
- Value Equality & Deconstruction
- Records in Domain-Driven Design