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 — 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, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
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
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, and the C# Academy course includes 4 lessons in total. 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.
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; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.
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