# entity members

> The typed members an entity holds — text, numbers, dates, booleans, Guids, enums and references. A member's type spelling carries its nullability: a bare `string`, enum, `DateTime` or `Guid` is REQUIRED, a bare value type reads its zero, a reference is optional, and `?` makes any member optional. See [[types-optional-and-required]].

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

## Summary        {#summary}
An entity's members are declared C#-style — `decimal Total;` — one per line, with optional attributes in front. Each
becomes a column. Whether a member is **required or optional is carried by its type spelling**, not a separate keyword:
a bare `string`, enum, `DateTime` or `Guid` is **required**, a bare value type (`int`, `bool`, `decimal`) reads its
zero, a **reference is optional**, and the `?` suffix makes any member optional. The full model — and *when* a required
member is checked — is [Optional and required members](https://osysharp.com/reference/types/optional-and-required/); the essentials are below.

## Signature      {#signature}
```osy syntax
entity <Name> {
  <attributes>? <Type> <MemberName>;
}
```

## Description    {#description}

### Which types may a member hold?   {#types}

**This table is the whole storable set** — [Every type, in one list](https://osysharp.com/reference/types/vocabulary/) is the same list with the non-storable types (the
collections, `Action`/`Func`, the component-parameter wrappers) beside it. If a type is not here and you did not
declare it yourself, an entity cannot hold it.

| Type | Holds | Notes |
|---|---|---|
| `string` | text | give it a `[MaxLength(n)]` — see below |
| `int` · `long` | whole numbers | `long` for ids and counters past 2³¹ — [long](https://osysharp.com/reference/types/long/) |
| `double` | measurements | a binary float — **never money** |
| `decimal` | money and exact quantities | **use this for money**, never a floating type — [decimal](https://osysharp.com/reference/types/decimal/) |
| `bool` | true / false | |
| `DateTime` | a point in time | stored UTC — [DateTime](https://osysharp.com/reference/types/datetime/) |
| `DateOnly` | a calendar date, no time | a SQL `date` — [DateOnly and TimeOnly](https://osysharp.com/reference/types/date-and-time/) |
| `TimeOnly` | a time of day, no date | a SQL `time` — [DateOnly and TimeOnly](https://osysharp.com/reference/types/date-and-time/) |
| `TimeSpan` | a duration | [TimeSpan (durations)](https://osysharp.com/reference/types/timespan/) |
| `Guid` | an identifier | |
| `Json` | an arbitrary JSON document | for genuinely open-shaped data — [Json](https://osysharp.com/reference/types/json/) |
| `Markdown` | a section-addressable markdown document | text plus a rendering contract — [Markdown](https://osysharp.com/reference/types/markdown/) |
| `RichText` | formatted prose | a rich-text document |
| `Vector` | an embedding, for similarity search | `[MaxLength(n)]` sets the dimensions — [[Searchable]](https://osysharp.com/reference/memory/searchable/) |
| `TimeZoneInfo` | a time zone, stored as its IANA id (`Europe/Stockholm`) | [Time zones — TimeZoneInfo](https://osysharp.com/reference/stdlib/zones/) |
| `Culture` | a BCP-47 culture token (`sv-SE`) | [Culture formatting — ToString(format, culture)](https://osysharp.com/reference/stdlib/culture-formatting/) |
| `byte[]` | binary | the one array form that is a scalar, not a collection |
| an `enum` you declared | a fixed set of values | see [enum](https://osysharp.com/reference/enum/declaration/) |
| another `entity` | a reference to one row | see [relations](https://osysharp.com/reference/entity/relations/) |
| `<Entity>[]` | the children pointing back at this row | see [relations](https://osysharp.com/reference/entity/relations/) |

```osy title="the scalar types" test app=entity-properties
entity Invoice {
  [MaxLength(40)] string Number;
  decimal Total;                 // money is ALWAYS decimal
  int LineCount;
  bool Paid;
  DateTime IssuedAt;
  Guid ExternalRef;
}
```

The date/time trio and `Json` are **ordinary columns like any other** — there is no conversion to write and no
`DateTime` to fall back on. Reach for `DateOnly` when the time of day is not a fact about the row (a birthday, an
invoice date, the day a slot is booked for), and `TimeOnly` when the date is not (opening hours, a daily cutoff):

```osy title="dates without times, times without dates, and open-shaped data" test app=entity-properties
entity Appointment {
  DateOnly Day;                  // a calendar date — no time of day to get wrong
  TimeOnly StartsAt;             // a time of day — no date attached
  TimeSpan Runs;                 // how long it lasts
  Json Extras;                   // genuinely open-shaped data, stored as a document
}

DateOnly WhenIsIt(Appointment a) { return a.Day; }   // and it reads back as itself
```

**A member may be named for its own type**, exactly as `Color Color;` is in C# — and it is the natural spelling for
the commonest fields (`Room Room`, `Status Status`, `Type Type`). No prefix, no suffix, no second word:

```osy title="a member named for its own type — C#'s `Color Color`" test app=entity-properties
enum Room { Kitchen, Bathroom, Bedroom }

entity Kiln {
  [Required, MaxLength(80)] string Name;
  [Required] Room Room;          // the member and its type share a name — legal, and the name to use
}

Room WhereIsIt(Kiln p) { return p.Room; }   // and it reads back with no ceremony
```

### Is this member required, or may it be null?   {#optional}
Whether a member is required is decided by **how you spell its type** — there is no separate keyword. What a *bare*
(non-`?`) member means depends on whether its type has a natural zero (full treatment: [Optional and required members](https://osysharp.com/reference/types/optional-and-required/)):

- A **bare `string`, enum, `DateTime`/`DateOnly`/`TimeOnly`, `Guid` or `Json`** — a type with **no honest zero** — is
  **required**: the platform invents no value for it, so you must supply one. (An enum is *not* silently defaulted to
  its first member — reordering the members would change the stored default — so a bare enum is required too.)
- A **bare value-type scalar** — `int`, `long`, `bool`, `decimal`, `TimeSpan` — reads its **zero** (`0`, `false`, `0m`)
  when unset, exactly as a C# field does. It is never null; declare it `int?` for a real "unset". (Because it is never
  null, comparing one to `null` — `priority == null` — is a compile error that points you at the `?` form.)
- A **bare entity reference is optional** (reads back `null`) — the everyday shape is *create the row, then pick the
  related record* — and you write `[Required]` to demand one.
- The **`?` suffix** makes any member optional; it reads back `null` when nobody set it.

**When is a required member checked?** An entity is a **draft until commit**, so `new Ticket {}` compiles — you seed an
empty draft, bind each field to a form input, and the required members are validated **at commit**, naming any that are
still unset. (A `class` has no commit step, so its required members are checked at `new` instead — see
[class properties](https://osysharp.com/reference/class/properties/).) This is what makes the everyday create-form work.

```osy title="required by spelling; optional with ?" test app=entity-properties
entity Contact {
  string Name;              // REQUIRED — a bare string has no honest zero (checked at commit)
  string? Phone;            // optional — a contact without a phone is fine; reads back null
  DateTime? LastSpokeAt;    // `?` is not a string thing: ANY type takes it, value types included
  int? Doorstep;            // `int?` is genuinely absent, which is what `0` could never say
}

string PhoneOrDash(Contact c) {
  return c.Phone ?? "—";    // null-coalescing on the optional field, exactly as in C#
}
```

⚑ **EVERY C# NULLABLE TYPE IS SUPPORTED.** `?` is not a string affordance — it is the C# rule, and it holds for
value types and reference types alike: `DateTime?`, `int?`, `decimal?`, `bool?`, `Guid?`, `TimeOnly?`, an enum,
your own `class`. There is no list to check against, which is why this page states the rule rather than
enumerating one.

⚠ **It is written down because the shape is usually only ever SHOWN on a string**, and a reader who has seen
`string?` and nothing else has to guess. The guess costs a field: measured 2026-09-01, a model reasoning aloud —
*"`DateTime?` — is nullable DateTime supported? The docs mention `string?` for nullable"* — and it dropped the
field rather than find out.

A bare **reference** is the exception — optional by default, so write `[Required]` to demand one:
`[Required] Customer Reporter;`. See [relations](https://osysharp.com/reference/entity/relations/) and [Optional and required members](https://osysharp.com/reference/types/optional-and-required/).

### How do I give a member a default value?   {#defaults}
A member may declare a default, which applies when the row is created without one — `= true`, `= 0m`, or an enum
member. It is the honest way to say *"this is what a new one looks like"*, instead of remembering to set it at every
creation site:

```osy title="what a new row looks like" test app=entity-properties
enum AccountStatus { Active, Suspended, Closed }

entity Account {
  [Required, MaxLength(200)] string Name;
  bool IsActive = true;                          // a new account is active
  AccountStatus Status = AccountStatus.Active;   // …and its status says so
  decimal Balance = 0m;
  [Required] string ApiKey = Security.RandomId(32);   // a fresh unguessable key per account
  DateTime CreatedFor = DateTime.UtcNow.AddDays(30);     // …and a computed date, evaluated at creation
}

void Open(string name) {
  var a = new Account { Name = name };
  // IsActive is true, Status is Active, Balance is 0 — none of them written here;
  // ApiKey is a fresh 32-char id and CreatedFor is 30 days out — each EVALUATED for this new row
}
```

A default is not limited to a constant. It can be **any expression** — a call like `Security.RandomId(32)`, a
computed value like `DateTime.UtcNow.AddDays(30)`, arithmetic — and it is **evaluated afresh for each row at creation**,
exactly like a C# field initializer. So two accounts opened in the same breath get two *different* `ApiKey`s; the
expression runs per row, not once. (A constant/enum default behaves the same as always.)

A default can also be one of the **constant members** C# gives the built-in types — `Guid.Empty`, `string.Empty`,
`int.MinValue`/`int.MaxValue`, `long.MinValue`/`long.MaxValue`, `decimal.Zero`/`One`/`MinusOne`/`MinValue`/`MaxValue`,
`double.MinValue`/`double.MaxValue`, `DateTime.MinValue`/`DateTime.MaxValue`, `DateOnly.MinValue`/`DateOnly.MaxValue`,
`TimeOnly.MinValue`/`TimeOnly.MaxValue` and `TimeSpan.Zero`/`TimeSpan.MinValue`/`TimeSpan.MaxValue`. Each stands for a
value that never changes, so it is stored exactly as the literal it names, on an entity and on a `class` alike:

```osy title="a constant member as a default" test app=entity-properties
public class Cursor {
  public Guid After = Guid.Empty;        // start from the beginning
  public string Filter = string.Empty;
  public int Limit = int.MaxValue;
  public DateOnly Until = DateOnly.MaxValue;   // open-ended
  public TimeSpan Timeout = TimeSpan.MaxValue;
}

entity Reading {
  [Required, MaxLength(80)] string Sensor;
  Guid Batch = Guid.Empty;               // no batch assigned yet
  DateTime LastSeen = DateTime.MinValue; // never seen
  decimal Offset = decimal.Zero;
}
```

The constant must fit the member the way C# requires: `long Big = int.MaxValue;` widens and is fine, while
`int Count = long.MaxValue;` or `string Name = Guid.Empty;` is refused with both types named. `DateTime.MaxValue` is
the largest instant a stored date can hold, `9999-12-31 23:59:59.999999` — the same value it has in an expression.
For the same reason `TimeOnly.MaxValue` is `23:59:59.999999` and `TimeSpan.MaxValue` / `MinValue` are
`±10675199.02:48:05.4775800`. A boundary value is stored as the value it names, so a new row, a row an added member
backfilled, and a query that compares against the constant all agree.
Any other dotted default that is not an enum member or one of these constants is refused, and the message lists every
accepted form.

Reach for a default whenever "unset" and "the normal value" are the same thing. It removes a whole class of bug:
the creation site somebody added last week that forgot to set `IsActive`, and the row that has been invisible ever
since.

### Text needs a length   {#maxlength}
`string` with no `[MaxLength]` is unbounded. That is fine for a body of prose and wrong for a code, a name or a
status — give those a length, and the database enforces it:

```osy title="bounded and unbounded text" test app=entity-properties
entity Article {
  [MaxLength(200)] string Title;   // bounded — a title has a sane maximum
  string Body;                     // unbounded — prose
}
```

### Money is `decimal`   {#money}
There is no `float` or `double` member type, and that is on purpose: binary floating point cannot represent `0.10`,
so totals drift by fractions of a cent and eventually a customer notices. `decimal` is exact.

### How do I index a member, or rename its column?   {#storage-attributes}
A handful of attributes shape how a member is *stored* rather than what it may hold. Most rows never need them:

- **`[Index]`** asks the database to index the member, so queries that filter or sort by it stay fast as the table
  grows. Put it on the members you actually query by.
- **`[ExternalName]`** overrides the database column name when it must differ from the member — `[ExternalName("col")]`
  maps onto a pre-existing or externally-owned schema.
- **`[Virtual]`** marks a member **in-memory only**: it is never persisted, for a value you compute and carry but do
  not store.
- **`[Mentions]`** declares the entities a rich-text member may `@`-mention, so an editor can offer and resolve them.

Every entity already has an automatic `Id` — read it like any other member. Nothing else is added: a creation time, a
creator and the last change are fields you declare with `[CreatedAt]`/`[CreatedBy]`/`[ModifiedAt]`/`[ModifiedBy]`, and
the platform writes them ([[CreatedAt] [CreatedBy] [ModifiedAt] [ModifiedBy]](https://osysharp.com/reference/entity/row-stamps/)).

```osy title="indexing and an external column name" test app=entity-properties
entity LegacyCustomer {
  [Required, Index] string Status;              // queried often → indexed
  [ExternalName("cust_ref")] string Reference;  // the column is named cust_ref in the database
  [Virtual] int ScoreThisSession;               // computed and carried, never stored
}
```

### How do I describe a member to whoever calls it?   {#documenting}
A `///` doc comment on a member is its **description**, and `[Example]` gives it a representative **value**. Neither
changes what is stored — both travel with the member into the JSON schema a published function or an MCP tool
exposes, so the caller reading that schema sees what the field is for and what a real one looks like instead of a
bare type name.

```osy title="a member that explains itself" test app=entity-properties-documented
entity Order {
  /// The human-facing reference printed on the invoice.
  [Required, MaxLength(50), Example("ORD-1042")] string Reference;

  /// Where the goods are going.
  [MaxLength(200), Example("Storgatan 1, 111 51 Stockholm")] string? ShipTo;
}
```

## See also       {#see-also}
- [entity](https://osysharp.com/reference/entity/declaration/) — the type these members live in
- [constraints](https://osysharp.com/reference/entity/constraints/) — `[Required]`, `[Unique]`, `[MaxLength]`, `[Min]`/`[Max]`, `[Pattern]`
- [relations](https://osysharp.com/reference/entity/relations/) — members that point at another entity
- [enum](https://osysharp.com/reference/enum/declaration/) — a member with a fixed set of values
