# class properties

> A class member that reads and writes like a field but runs a body on access — a computed value, a validating setter, or an auto-property whose storage the platform synthesizes. It is a method dressed as a field access, so it works everywhere a class does: server, client, and across a suspension.

<!-- id: class-properties · area: class · stability: stable · html: https://osysharp.com/reference/class/properties/ -->

## Summary        {#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](https://osysharp.com/reference/class/methods/) when a value is just stored; reach for a property when *access* itself is behaviour.

## Signature      {#signature}
```osy syntax
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    {#description}

### A computed property — a value derived from other fields   {#computed}
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.

```osy title="a value derived from other fields" test app=class-properties
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:

```osy title="the getter runs on read" run app=class-properties
[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   {#get-set}
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`:

```osy title="a validating setter over a backing field" test app=class-properties
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:

```osy title="the setter runs on write, the getter on read" run app=class-properties
[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   {#auto}
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:

```osy title="an auto-property" test app=class-properties
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:

```osy title="an auto-property stores a value" run app=class-properties
[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   {#asymmetric}
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.

```osy title="read anywhere, write only inside the class" test app=class-properties
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   {#init}
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:

```osy title="a value fixed at construction" test app=class-properties
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:

```osy title="init is set at construction, then frozen" run app=class-properties
[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`   {#required}
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:

```osy title="a member the caller must supply" test app=class-properties
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
}
```

```osy title="required is enforced at the call site" run app=class-properties
[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:

```osy title="a constructor that satisfies required" test app=class-properties
class Membership {
  public required string Owner { get; set; }
  public Membership(string owner) { Owner = owner; }   // sets Owner on every path — the compiler infers it
}
```

```osy title="the constructor satisfies the requirement" run app=class-properties
[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.

### A member that lives in the browser   {#client-handles}
A class may hold the values that only exist on the client — the ones a drawing app builds and keeps:

```osy syntax
class Valley {
  Mesh ground;          // geometry, on the GPU
  Mesh hill;
  Gradient sky;         // a fill the 2D context owns
  Surface stars;        // an offscreen canvas
  Random rng;           // a random stream

  public void Build() { ground = Mesh.Plane(160, 40); }
  public void Paint() { Draw.Mesh(ground, 0, 0, 0, "#79b85c"); }
}
```

⭐ **This is what lets a drawing app be organised at all.** A class method can run the `Draw.*` verbs, so each part
of a scene can own its own geometry and know how to paint itself, in its own file — and the page is left with what
only it knows: the physics, the input, the score.

⚠️ **On a `class`, not on an `entity`.** These values live in the browser, so there is nothing for a stored row to
hold; declaring one on a persisted `entity` is refused, and the message says so.

### It runs on the client too   {#client}
A property is a method call underneath, so it rides the same path a [method](https://osysharp.com/reference/class/methods/) 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       {#see-also}
- [`readonly` fields](https://osysharp.com/reference/class/readonly/) — `readonly` fields, the field-level counterpart to `{ get; }` and `init`
- [class methods](https://osysharp.com/reference/class/methods/) — behaviour with a receiver; a property is a field-shaped pair of these
- [constructor](https://osysharp.com/reference/class/constructors/) — set up an instance's fields (and back an auto-property) at construction
- [Classes](https://osysharp.com/reference/class/index/) — the whole `class` surface: fields, constructor, methods, properties
- [type visibility (public / internal)](https://osysharp.com/reference/types/visibility/) — `public`/`private` on members, and why a class defaults to `internal`
