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#
UnitOfWork.Commit() // persist everything accumulated, atomically
UnitOfWork.Discard() // drop everything accumulated, in THIS unit of work onlyDescription#
What a unit of work is#
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#
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:
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#
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#
Commit per act. Ticking a to-do is the save; each verb is a complete act and commits for itself.
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.
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() 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.
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();
}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#
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 for the worked example.
Where it runs#
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 for how a body is split, and creating & saving data for the UI-facing story of building
a form around it.
See also#
- creating & saving data — creating and saving data from a UI action, and choosing between the two shapes above
- entity — the entities a unit of work persists
- try / catch / finally — what survives a refused commit, and how to record the failure
- execution side — why a commit is a server act even when written in a client body