Properties
C# properties: auto-properties, init-only, computed, expression-bodied, and the patterns that make DTOs and entities clean.
C# — properties
EXAMPLE
// ===== Auto-properties =====
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
// ===== Init-only (immutable after construction) =====
public class User2
{
public int Id { get; init; }
public string Name { get; init; } = string.Empty;
public string Email { get; init; } = string.Empty;
}
var u = new User2 { Id = 1, Name = "Alex", Email = "a@x.io" };
// u.Name = "Sam"; // error: init-only
// ===== Records (the modern shape for DTOs) =====
public record Order(int Id, string Customer, long TotalCents);
var o = new Order(1, "Alex", 4995);
var o2 = o with { TotalCents = 5000 };
// ===== Read-only properties =====
public class Counter
{
private int _count;
public int Count => _count; // expression-bodied getter
public void Increment() => _count++;
}
// ===== Computed (no backing field) =====
public class Order2
{
public long TotalCents { get; init; }
public decimal TotalAud => TotalCents / 100m;
}
// ===== Custom getter / setter =====
public class User3
{
private string _email = string.Empty;
public string Email
{
get => _email;
set
{
if (!value.Contains('@')) throw new ArgumentException("invalid email");
_email = value;
}
}
}
// ===== Private setter (visible only in class) =====
public class Cart
{
public List<string> Items { get; private set; } = new();
public void Add(string sku) => Items.Add(sku);
}
// ===== Static properties =====
public static class Config
{
public static string AppName { get; } = "shop";
public static int MaxRetries { get; set; } = 3;
}
// ===== Required (C# 11+) =====
public class Settings
{
public required string ApiKey { get; init; }
public string Region { get; init; } = "ap-southeast-2";
}
var s = new Settings { ApiKey = "..." };
// var bad = new Settings(); // error: ApiKey not initialised
// ===== Indexers (special properties) =====
public class Cache
{
private readonly Dictionary<string, string> _data = new();
public string this[string key]
{
get => _data[key];
set => _data[key] = value;
}
}
var c = new Cache();
c["name"] = "Alex";
var n = c["name"];
// ===== Pattern: invariants in constructor + init-only properties =====
public class Money
{
public long Cents { get; }
public string Currency { get; }
public Money(long cents, string currency = "AUD")
{
if (cents < 0) throw new ArgumentOutOfRangeException(nameof(cents));
Cents = cents; Currency = currency;
}
}
// ===== Patterns to internalise =====
// - init-only properties for immutable DTOs
// - records for value-shape classes; with-expressions for non-destructive update
// - Auto-properties for trivial data; custom setter only when validation needed
// - 'required' for fields the caller MUST initialise
// ===== Pitfalls =====
// - public setter on a list property -> consumers replace the whole list
// - get-only without backing field -> recompute each access (memoise if expensive)
// - init vs set -> init is one-shot at construction; set is general
// - Forgetting nullable annotations on properties -> NREs in callers
Why it matters
C# properties give you encapsulation with little ceremony. init-only for immutability, records for DTOs, custom setters for invariants, required for mandatory init. The discipline (init over set, records over classes when possible) keeps domain objects clean and predictable.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
public class Person {
public string Name { get; set; } // auto-property
public int Age { get; private set; } // setter restricted
public string Initial => Name.Substring(0, 1); // computed
}
Try it Yourself »
Exercise
Auto-property getter/setter.
public string Name { get;
; }
Three letters.
Discussion
Loading…