Osy#betaRun it on Osyrin: osyrin.com

Reference / Function

[CommitsAlone] — a function whose writes land by themselves

[CommitsAlone] void Record(string what) { … }

A function marked `[CommitsAlone]` runs in a unit of work of its own. What it writes is committed when it returns and stays committed whatever its caller does next — fail, discard, or carry on. A fault inside it discards only its own writes. It neither sees nor commits the caller's pending changes, so it can be called in the middle of other work and from a task. Use it for the rows that must outlive a failure: a usage meter, a reservation other callers have to see at once, an attempt log.

stable4 examples compiled by CIfunctiondatatransactions

Summary#

A function's writes normally join its caller's unit of work: they land when the caller's work is committed, and are thrown away with it when the caller fails. [CommitsAlone] is for the writes that must not follow the caller:

entity Attempt {
  [Required, MaxLength(200)] string What;
  security { allow create, read when IsAuthenticated || IsAnonymous; }
}

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

[CommitsAlone]
void NoteAttempt(string what) { new Attempt { What = what }; }

void Import(string name) {
  var customer = new Customer { Name = name };
  NoteAttempt("import " + name);                     // committed here, by itself
  if (name == "") { throw new ValidationException("A customer needs a name."); }
}

Signature#

[CommitsAlone]
<Type> <Name>(<value parameters>) { … }      // a top-level function; values in, a value out

Description#

What happens at the call?#

The function gets a fresh, empty unit of work. Its body runs in it, as the caller — the same person, the same rules. When it returns, that unit of work is committed, atomically, before the caller's next statement runs.

the function's writesthe caller's pending writes
the function returnscommitted, at onceuntouched — still pending
the function throwsdiscardeduntouched; the caller may catch and go on
the caller later fails or calls UnitOfWork.Discard()still committeddiscarded
the caller later commitsalready committedcommitted

What does it see?#

The database as it is stored. It does not see rows its caller created or changed and has not committed, and it never commits them. Once it has returned, the caller sees what it wrote like any other stored row — and that includes a row the caller already holds: a variable read before the call reads the stored value after it. A change of the caller's own that is still pending on that row is kept, on top of what was stored.

Why values, and not rows?#

A row belongs to the unit of work that read it. So the function takes and returns values — pass a row's Id and read the row inside. A row-typed parameter or return is refused when you compile, saying so.

entity Tally {
  [Unique, MaxLength(40)] string Name;
  int Count = 0;
  security { allow create, read, update when IsAuthenticated || IsAnonymous; }
}

[CommitsAlone]
int CountOne(Guid tallyId) {
  var tally = Tally.Single(t => t.Id == tallyId);
  tally.Count = tally.Count + 1;
  return tally.Count;
}

Can a task call it?#

Yes — and it is the way a task writes. A [[function-async-await#writes|task may not write]] to its caller's unit of work, because the two would race; a function with a unit of work of its own has nothing to race over. Each call commits by itself, so calls running at the same time are separate transactions: guard a value two of them may change with [[entity-concurrency-check|[ConcurrencyCheck]]] and retry the loser.

void NoteAll(List<string> items) {
  Parallel.ForEach(items, item => NoteAttempt("seen " + item));
}

Does except via still apply?#

Unchanged. A function a deny … except via names may also be [CommitsAlone]: its own statements write past that deny, a helper's do not, and the caller still needs the grant.

What about a workflow body?#

It works there, as the kit's or the app's code that wrote it. Treat the call as you would an outbound call: a run that is interrupted after the function committed and before the run recorded its own progress calls it again when it resumes. Where once matters, give the row a [Unique] key the second call collides on.

An event the function raises is committed with it, and its handlers run when the body's step has ended, with the step's other events — not when the function returns ([[function-raise#in-a-step]]).

What is refused?#

you wrotewhat the compiler says
a row parameter, a row return, or a list of rowsa row belongs to the unit of work that read it — take or return its Id
async, or a Task returnit commits when it returns, so it must have finished by then
a stream<T> producera producer has no single return to commit at
a class's methoda unit of work of its own belongs to a top-level function
[Client] beside itit runs on the server, where the commit happens — the attribute places it there, so write no side
[OwnUnitOfWork], its former name[OwnUnitOfWork] is now [CommitsAlone] — the same meaning under the new name

At run time, a function that has to wait part-way — a dialog in the browser, a workflow, a durable delay — is refused with the same reason: do the waiting in the caller and hand the function what to write.

Examples#

The rows are there after the failure, proved by a test:

[Test]
void The_attempt_outlives_the_failed_import() {
  Assert.Throws<ValidationException>(() => Import(""));
  Assert.Equal(1, Attempt.Count());          // NoteAttempt's row is stored
  Assert.Equal(0, Customer.Count());         // the import's own row went with the failure
}

[Test]
void A_counter_is_changed_by_id() {
  var tally = new Tally { Name = "visits" };
  UnitOfWork.Commit();
  Assert.Equal(1, CountOne(tally.Id));
  Assert.Equal(2, CountOne(tally.Id));
}

[Test]
void A_row_the_caller_holds_reads_what_the_function_stored() {
  var tally = new Tally { Name = "held" };
  UnitOfWork.Commit();
  CountOne(tally.Id);
  Assert.Equal(1, tally.Count);                                        // the variable from before the call
  Assert.Equal(1, Tally.Single(t => t.Name == "held").Count);          // …and the row read again
}

[Test]
void A_change_the_caller_has_pending_on_that_row_is_kept() {
  var tally = new Tally { Name = "pending" };
  UnitOfWork.Commit();
  tally.Name = "renamed";                    // the caller's own, not yet saved
  CountOne(tally.Id);
  Assert.Equal("renamed", tally.Name);
  Assert.Equal(1, tally.Count);
  UnitOfWork.Commit();
  Assert.Equal(1, Tally.Count(t => t.Name == "renamed" && t.Count == 1));
}

[Test]
void A_loop_of_tasks_writes_one_row_each() {
  NoteAll(["a", "b", "c"]);
  Assert.Equal(3, Attempt.Count());
}

See also#

Related

UnitOfWork

Every body accumulates its writes in a unit of work rather than sending them one at a time. UnitOfWork.Commit() makes…

try / catch / finally

Handles a fault. C#'s syntax, including typed catches, catch filters and finally. The rows written inside a try block…

async / await and Task

C#'s async and await, with C#'s meaning. A call to a Task-returning method that you do not await where you make it…

Do the same work for many items at once (Parallel, SemaphoreSlim)

Parallel.For, Parallel.ForEach and Parallel.ForEachAsync run one body per item, each as a task beside the others, and…

deny … except via

Names the ONLY functions whose own code may change something a deny withholds: `deny update UnitsLeft except via…

ConcurrencyCheck

Plain optimistic concurrency: refuses an update whose value was changed by somebody else after the value the write was…