Summary#
A class may declare properties — members that read and write like a field but run a body on access. A property
is the natural home for a value derived from other fields (Label), for a write that must be validated or
transformed, and for a field you want to expose with asymmetric access. It is, exactly as in C#, a pair of methods
dressed as a field: a read obj.Prop runs the getter, a write obj.Prop = v runs the setter.
Use a plain field when a value is just stored; reach for a property when access itself is behaviour.
Signature#
class <Name> {
public <Type> <Prop> => <expr>; // computed — read-only, no storage
public <Type> <Prop> { get => <expr>; } // getter-only, explicit
public <Type> <Prop> { get => …; set => …; } // full property over a backing field (`value` is the input)
public <Type> <Prop> { get; set; } // auto-property — the platform synthesizes the backing field
public <Type> <Prop> { get; private set; } // asymmetric — read anywhere, write only inside the class
public <Type> <Prop> { get; init; } // init-only — settable during construction, then frozen
public required <Type> <Prop> { get; set; } // required — must be supplied in every `new T { … }`
}Description#
A computed property — a value derived from other fields#
The smallest property is getter-only: an expression over the instance's other fields, with no storage of its own.
Written => expr, it recomputes on every read.
class Money {
public decimal Amount;
public string Currency;
// computed on read — no backing storage
public string Label => Amount.ToString("F2") + " " + Currency;
}Reading it runs the getter over the current field values:
[Test]
void Label_Runs_The_Getter() {
var m = new Money { Amount = 9.5m, Currency = "USD" };
Assert.Equal("9.50 USD", m.Label);
}A computed property is read-only — it has no setter, so a write to it is a compile error. That is the point: it is a view of other state, not a slot you can assign.
A full property — a getter and a setter over a backing field#
When a write needs to be validated or transformed, give the property both accessors over an explicit private
field. Inside the setter, the incoming value is the implicit parameter value:
class Account {
private decimal _rate;
public decimal Rate {
get => _rate;
set {
if (value < 0m) { throw "rate cannot be negative"; }
_rate = value;
}
}
}A write runs the setter; a read runs the getter:
[Test]
void Rate_RoundTrips_Through_The_Accessors() {
var a = new Account();
a.Rate = 0.2m;
Assert.Equal(0.2m, a.Rate);
}A bad write — a.Rate = -1m — runs the setter body and throws, exactly as the setter says. The setter is the one
place the rule lives, so no caller can slip an invalid value past it.
An auto-property — the platform synthesizes the storage#
When the accessors would be trivial — read the field, write the field — write { get; set; } and the platform
synthesizes the hidden backing field for you:
class Contact {
public string Name { get; set; }
public string Email { get; set; }
}It stores and reads a value like a field — both through assignment and through an object initializer:
[Test]
void Auto_Property_Stores_A_Value() {
var c = new Contact { Name = "Ada", Email = "ada@example.com" };
Assert.Equal("Ada", c.Name);
c.Name = "Grace";
Assert.Equal("Grace", c.Name);
}Asymmetric visibility — read anywhere, write only inside#
A private set narrows the write path without touching the read path: anyone can read the property, but only the
class's own code can set it. It reuses the same private-member rule as a private method.
class Ledger {
public decimal Balance { get; private set; }
// in-class code sets it through the private setter
public void Credit(decimal amount) {
Balance = Balance + amount;
}
}From outside Ledger, ledger.Balance = 100m is a compile error — the setter is private — while ledger.Balance
reads freely. A bare { get; } behaves the same way: settable inside the class, read-only to the outside.
init — settable only during construction#
An init accessor is a setter you may run only while the object is being built — in an object initializer or the
constructor — and never after. It is how you make a value that is fixed once constructed:
class Booking {
public string Reference { get; init; }
public int Seats { get; init; }
}Set it in the object initializer; a write afterwards is a compile error:
[Test]
void Init_Is_Set_At_Construction() {
var b = new Booking { Reference = "BK-1", Seats = 2 };
Assert.Equal("BK-1", b.Reference);
Assert.Equal(2, b.Seats);
}Writing b.Reference = "BK-2" after construction does not compile — the accessor is init-only. Reach for init
when a field must be supplied when the object is made but must not change once it exists. A bodied init { … } runs
its body during construction, so it can validate or transform the incoming value exactly like a set.
required — must be supplied at every new#
Marking a member required makes the compiler insist it appears in every object initializer — a missing one is a
compile error, not a value silently left null:
class Registration {
public required string Email { get; set; }
public string? Name { get; set; } // optional (reads back null when unset) — a bare `string Name` would itself be required
}[Test]
void Required_Is_Supplied() {
var r = new Registration { Email = "ada@example.com" }; // Name is optional; Email is required
Assert.Equal("ada@example.com", r.Email);
}Omitting Email — new Registration { } — is a compile error. required pairs naturally with init for a value
that must be given once and then frozen: public required string Email { get; init; }.
A constructor that always sets a required member takes over that obligation automatically — the compiler sees the
constructor sets it, so new T(args) needs no initializer for it and no annotation:
class Membership {
public required string Owner { get; set; }
public Membership(string owner) { Owner = owner; } // sets Owner on every path — the compiler infers it
}[Test]
void Ctor_Satisfies_Required() {
var m = new Membership("Ada"); // no `{ Owner = … }` needed — the ctor sets it
Assert.Equal("Ada", m.Owner);
}The inference is sound and conservative: the constructor must set the member unconditionally — directly, or through
a method it calls. If it only sets the member inside an if/loop, that isn't provable, so the initializer is still
required. A constructor that sets some required members can leave the rest to the caller's initializer
(new Membership(owner) { OtherRequired = … }). You may still write [SetsRequiredMembers] explicitly — it is accepted
and means exactly this.
It runs on the client too#
A property is a method call underneath, so it rides the same path a method does: a getter read or
setter write inside a [Render(CSR)] component action runs in the browser, with no server round trip, as long as
its body is client-runnable. Nothing extra is needed — the accessor ships with the component.
See also#
readonlyfields —readonlyfields, the field-level counterpart to{ get; }andinit- class methods — behaviour with a receiver; a property is a field-shaped pair of these
- constructor — set up an instance's fields (and back an auto-property) at construction
- Classes — the whole
classsurface: fields, constructor, methods, properties - type visibility (public / internal) —
public/privateon members, and why a class defaults tointernal