# UnitOfWork

> Every body accumulates its writes in a unit of work rather than sending them one at a time. UnitOfWork.Commit() makes everything accumulated so far durable, atomically — all of it lands or none of it does, and UnitOfWork.Discard() throws that accumulation away instead. Reads inside the same body already see the pending writes, so nothing needs saving before it can be used.

<!-- id: function-unit-of-work · area: function · stability: stable · html: https://osysharp.com/reference/function/unit-of-work/ -->

## Summary        {#summary}

Every body accumulates its writes in a **unit of work** rather than sending them one at a time.
`UnitOfWork.Commit()` makes everything accumulated so far durable, **atomically** — all of it lands or none of it
does — and `UnitOfWork.Discard()` throws that accumulation away instead. Reads inside the same body already see the
pending writes, so nothing needs saving before it can be used.

## Signature      {#signature}

```osy syntax
UnitOfWork.Commit()     // persist everything accumulated, atomically
UnitOfWork.Discard()    // drop everything accumulated, in THIS unit of work only
```

## Description    {#description}

### What a unit of work is   {#what}

Writing `new Order { … }`, assigning a property, or calling `.Delete()` does not talk to the database. It records
the change in the unit of work that the current body is running inside. `UnitOfWork.Commit()` is what sends the
accumulated changes, and it sends them as one atomic act: if any part fails — an invariant, a constraint, a
security rule — **nothing** is written.

That is the reason the verb names the unit of work rather than the row. There is no per-row save, because a save is
never about one row: it is about everything the body has done so far.

### Reads already see pending writes   {#read-your-writes}

**Every read answers about the rows the database holds together with the changes this unit of work has staged.**
That is the whole rule, and it holds for all three kinds of change — a row created, a row deleted, a field edited —
and for every way of asking: `ToList`, `Count`, `Any`, `All`, `Sum`, `Min`, `Max`, `Average`, `First`, `Last`,
`Single`, `Where`, `OrderBy` with `Take`/`Skip`, through a parent's child collection (`report.Lines`) or as a query
you write out yourself. There is no member that reads only the committed data, so there is nothing to remember about
which one you are holding.

A row you have created or modified is therefore visible to the rest of the body immediately, including through
queries. This is the property that makes a body readable — you write what you mean in the order you mean it, and the
last line makes it durable:

```osy title="a query sees a row that has not been committed yet" test app=function-unit-of-work
entity Invoice {
  [Required, MaxLength(20)] string Code;
  decimal Total;
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

decimal AddAndTotal(string code, decimal amount) {
  var inv = new Invoice { Code = code, Total = amount };

  // Not committed yet — and already found by an ordinary query, because the query
  // reads the unit of work's view of the data, not the database's.
  var sum = Invoice.Sum(i => i.Total);

  UnitOfWork.Commit();
  return sum;
}
```

**So you never need to commit in order to read your own work back**, and doing so to "make the total come out right"
is a workaround for nothing — it also splits one atomic act into two, which is a real loss: after the first commit,
a later failure can no longer undo what the first one wrote.

The one thing worth knowing is where the work happens. With nothing staged, a total is computed **by the database**
— one `SELECT SUM(…)`, no rows sent back. Once this unit of work is holding an unsaved change to that data the
database can no longer answer correctly (it has not been told), so the rows are read and the total is worked out
here instead. Measured on 6,000 rows: 1.2 ms folded, 18.2 ms once a single row is dirty. That is paid only while
something is unsaved and only for the type you changed — but it is the reason to keep a body's writes and its
big reports apart rather than interleaving them.

### Committing is not automatic, and nothing warns you at run time   {#commit-required}

A body that writes and never commits loses the write silently. There is no exception and no log line: the change
was recorded in a unit of work that was then discarded. In a UI this is especially convincing, because the screen
updates from the pending write and *looks* saved.

The platform therefore catches it before run time instead. `data-write-never-committed` is a MUST-tier lint
finding that names the verb that writes and where the write is; `osy validate` prints it in full as
`DATA_NEVER_SAVED` even when the source is otherwise green, and `osy test` prints it under a green tally, because a
test that only looks at the screen passes. It stays quiet as soon as something in that flow commits — so the two
correct shapes below both satisfy it.

The commonest way in is a function taking a ROW, called from a page: it joins the page's unit of work rather than
committing when it returns (see [function](https://osysharp.com/reference/function/declaration/)). Pages are getting an explicit unit-of-work mode; until
then, commit in the function or the action.

### The two correct shapes   {#shapes}

**Commit per act.** Ticking a to-do *is* the save; each verb is a complete act and commits for itself.

```osy title="each call is a complete act" test app=function-unit-of-work
entity Todo {
  [Required, MaxLength(120)] string Title;
  bool IsDone;
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

void ToggleTodo(Todo t) {
  t.IsDone = !t.IsDone;
  UnitOfWork.Commit();
}
```

**Commit once, at the end.** A form accumulates freely and commits when the user saves. Every write between the
first and the commit is part of the same atomic act — which is what makes a half-saved form impossible.

```osy title="many writes, one atomic commit" test app=function-unit-of-work
entity Customer {
  [Required, MaxLength(120)] string Name;
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

entity Address {
  [Required] Customer Owner;
  [Required, MaxLength(200)] string Line1;
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

void Register(string name, string line1) {
  var c = new Customer { Name = name };
  var a = new Address { Owner = c, Line1 = line1 };
  UnitOfWork.Commit();      // both rows, or neither
}
```

### Throwing the accumulation away — `UnitOfWork.Discard()`   {#discard}

`Discard()` is the other half of the same decision: it drops everything the unit of work has accumulated instead of
persisting it. The rows return to what the server last confirmed, and the body carries on.

```osy title="abandon the pending edits without abandoning the body" test app=function-unit-of-work
entity Draft {
  [Required, MaxLength(200)] string Body;
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

void StartOver(Draft d, string replacement) {
  d.Body = "";
  UnitOfWork.Discard();          // that edit is gone
  d.Body = replacement;          // this one is not — the unit of work continues
  UnitOfWork.Commit();
}
```

A row the discarded unit of work **created** is a different matter from one it merely edited. The edit above has
something to fall back to — the value the server last confirmed — but a row that only ever existed in the discarded
work has nothing: it never reached the database and never will. Reading or writing one is refused by name rather
than answered with `null` or staged where nothing will commit it:

```osy title="a row the discard threw away has nothing to fall back to" syntax
var d = new Draft { Body = "…" };
UnitOfWork.Discard();
d.Body = "changed";     // refused: the Draft row … was created and then DISCARDED
```

*"…everything this unit of work had staged was thrown away — by an explicit `UnitOfWork.Discard()`, or by the
cleanup that follows a `Throws`/`Denied` assert catching a commit fault."* Create it **after** the discard, or keep
the work you want out of the unit of work you are about to throw away.

**`Commit` and `Discard` are deliberately not symmetric, and the asymmetry is load-bearing.** A commit reaches
OUTWARD — persisting is the outermost unit of work's job, so an inner scope's edits have to reach it. A discard
clears exactly ONE unit of work, the one you are in, and stops. If it reached outward too, closing an inner surface
would take the surrounding page's unsaved work with it.

### Failure leaves nothing behind   {#failure}

If a commit is refused, the writes it carried are gone — including the ones that were individually valid. A body
that wants to record something about the failure must do that work **after** catching it, and commit again; see
[try / catch / finally](https://osysharp.com/reference/function/try-catch/) for the worked example.

**The one exception is a page's own save.** A `UnitOfWork.Commit()` written in an action of a page, or of a component
on it (a `SaveBar`, a Save button), is the person's save of what they edited: refused, it keeps every edit on the page,
still unsaved and still shown, so they fix what the refusal names and save again ([[ui-page-unit-of-work#held-save]]).
A commit inside a function — the page's or a kit's — keeps the rule above.

### Where it runs   {#execution-side}

`UnitOfWork.Commit()` is a server act. Called from a UI action it hands off to the server, persists, and returns —
the client's pending edits become durable at that moment. Nothing about the spelling changes between a server
function and a UI action, which is the point: the same sentence means the same thing in both. See
[Crossing the network](https://osysharp.com/reference/function/execution-side/) for how a body is split, and [creating & saving data](https://osysharp.com/reference/ui/data-mutation/) for the UI-facing story of building
a form around it.

## See also       {#see-also}
- [creating & saving data](https://osysharp.com/reference/ui/data-mutation/) — creating and saving data from a UI action, and choosing between the two shapes above
- [entity](https://osysharp.com/reference/entity/declaration/) — the entities a unit of work persists
- [try / catch / finally](https://osysharp.com/reference/function/try-catch/) — what survives a refused commit, and how to record the failure
- [Crossing the network](https://osysharp.com/reference/function/execution-side/) — why a commit is a server act even when written in a client body
- [UnitOfWork savepoints — Mark, Undo, Redo, RollbackTo](https://osysharp.com/reference/function/unit-of-work-savepoints/) — Mark / Undo / Redo / RollbackTo: stepping back through the accumulation instead of throwing all of it away
