Summary#
An entity is a persisted type: a table of rows the app stores, queries and secures. It is the noun your
application is about — Order, Customer, Invoice. Declaring one is enough to get a table, an identity, an audit
trail and a query surface; you write the members, the platform provides the rest.
If the value never needs to be stored — a computed shape, a DTO, the result of a calculation — use a
class instead. A class lives in memory and has no table.
Signature#
entity <Name> {
<attributes> <Type> <Member>; // data
invariant <condition>; // row-level rules
security { … } // who may read and write it
}Description#
Which members do I get for free?#
Every entity already has these. Do not declare them — a member of the same name is a compile error, because the platform is already providing one:
| Member | Type | What it is |
|---|---|---|
Id | Guid | the row's identity, assigned on create |
CreatedAt · ModifiedAt | DateTime | when the row was written |
CreatedBy · ModifiedBy | Guid | who wrote it |
So an entity with two declared members has seven columns. You read them like any other member — order.CreatedAt
works without you writing it.
How do I create a row, and when is it saved?#
A row is created with new, exactly as in C#. It is persisted when the function commits — there is no Save() call
to forget:
entity Customer {
[Required] string Name;
string Email;
}
void Register(string name, string email) {
var c = new Customer { Name = name, Email = email };
// no Save() — the row is written when the function commits
}Reading is a query over the type itself — you name the entity and write LINQ against it:
int ActiveCustomers() {
return Customer.Where(c => c.Email != null).ToList().Count;
}
Customer ByName(string name) {
return Customer.Single(c => c.Name == name); // exactly one, or it faults
}The predicate is compiled into the database query — it is not a filter over rows you already fetched — so the shape of what you can ask for is worth knowing before you write your first loop:
| I want to… | Read |
|---|---|
| filter, and pick one row or count them | Where / Single / Count — Where · Single · FirstOrDefault · Count · Any |
| run the query and get the rows | ToList — ToList, and why you should call it once |
| show page 3 of 40 | Skip / Take (paging) — OrderBy · Skip · Take |
| drop duplicates | Distinct — and why it does nothing on whole rows |
| combine two result sets | Union / Concat / Intersect / Except — Union · Concat · Intersect · Except |
| match against a list I have in hand | Dynamic IN (list.Contains in a query) — list.Contains(x) inside a query |
| walk a parent's children | relations — through the collection, never a filtered query |
Reaching for a foreach to do work the query could have done is the most common way an application that was fast on
a laptop becomes slow in production.
Entity or class?#
entity | class | |
|---|---|---|
| Stored in the database | yes — it has a table | no — in memory only |
Has an Id and audit columns | yes, from the platform | no; it has only what you declare |
Queryable (Where, Single, …) | yes | no |
Securable (security { … }) | yes | no — it holds no rows to protect |
| Good for | the nouns you persist | DTOs, computed shapes, JSON bodies |
The rule of thumb: if you will ever query it, it is an entity.
Security: an entity nobody can read is the DEFAULT#
This is the part to read twice, because it is where the platform will surprise you — deliberately.
The posture is deny-all. An entity that declares no security { } block is denied to every user request. Not
"readable by signed-in users", not "readable by its owner" — denied. Your beautifully modelled Order entity is, until
you say otherwise, an entity that no user of your application can read, write or count.
That is the correct default, and it is chosen on purpose. The failure mode of forgetting a rule is "nobody can do it" — which someone reports within the minute — rather than "everybody can", which nobody reports until it is somebody else's headline. A door that fails shut is a door you can trust.
So declaring who may read an entity is part of declaring the entity. It is not a hardening pass you schedule for later; there is no working application before you have done it.
[Principal] entity User {
[Required] string Name;
}
entity Note {
User Owner;
string Body;
security { allow read where Owner == user; }
}Note what the block does not contain: there is no default deny. Everything is denied already, so a
security { } block is a list of grants — you only ever write what is allowed. (To lock an entity completely,
write no block at all.)
Two rules, two different questions, and the distinction is the whole model:
wherefilters by the row — the owner sees their own notes.whengates by the principal — staff see every note.
And note what you cannot do: a bare allow read; is a compile error on an app that has a principal. The platform
makes you say who may read — allow read when IsAuthenticated;, or IsAuthenticated || IsAnonymous if you really
do mean the whole internet. Opening a door is allowed; opening one by accident is not.
Read these three, in this order, before you ship anything:
- secure by default (deny-all) — the deny-all posture, and what it does and does not cover
- security { } — the
security { }block in full:where,when, and namedpolicypredicates - runas — because a security rule you have not tested is a rule you only believe you wrote
That last one is not a flourish. Security is the one area where the code compiling, the tests passing and the feature working tell you nothing about whether it is correct — the only way to know that Bob cannot read Alice's note is to become Bob and try.
See also#
- entity members — the member types an entity can hold
- relations — pointing one entity at another, and reading the children back
- constraints —
[Required],[Unique],[MaxLength], ranges and patterns - invariant — row-level rules enforced when the row is written
- constructor — the in-memory counterpart, for values you never store
- secure by default (deny-all) — the deny-all posture: what an entity permits before you say anything
- security { } — the
security { }block in full - Where / Single / Count — querying the rows, and the rest of the LINQ surface
- runas — proving your rules deny the people they should