# 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}

A row you have created or modified is 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;
}
```

### 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 at **compile** time instead. `data-write-never-committed` is a MUST-tier lint
finding that names the verb that writes, and it stays quiet as soon as something in that flow commits — so the two
correct shapes below both satisfy it.

### 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.

### 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
[execution side](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
- [execution side](https://osysharp.com/reference/function/execution-side/) — why a commit is a server act even when written in a client body
