Skip to content

The search box knows all the secrets -- try it!

Fisher is part of the Critter Stack ecosystem.

JasperFx Logo JasperFx provides formal support for Fisher and other Critter Stack libraries. Please check our Support Plans for more details.

Optimistic Concurrency ​

Two styles, and they are alternatives: a type carries one column or the other. Declaring both is refused at configuration time rather than letting the storage descriptor pick one silently.

ColumnGuard
Guid versionguid_versionThe stored version must equal the one you loaded
Numeric revisionrevisionThe supplied revision must be strictly greater than the stored one

Guid versions ​

cs
opts.Schema.For<VersionedOrder>().UseOptimisticConcurrency();

snippet source | anchor

Or store-wide with opts.Policies.AllDocumentsUseOptimisticConcurrency().

Or by implementing IVersioned:

cs
public class VersionedOrder : JasperFx.Metadata.IVersioned
{
    public Guid Id { get; set; }
    public Guid Version { get; set; }
}

snippet source | anchor

TIP

IVersioned turns optimistic concurrency on, as on both siblings — with it off the column is neither written nor read, so mapping a member onto it would mean nothing. The converse does not hold: UseOptimisticConcurrency() alone maps nothing, because there is no member named.

A losing write throws ConcurrencyException at SaveChangesAsync.

TIP

How the guard is read is worth knowing: the upsert carries a where on the version and ends RETURNING id, and when the guard does not match SQLite returns no row and leaves the row untouched — which is exactly what the operation's postprocessing reads as a concurrency failure. Verified against SQLite 3.51 before anything was built on it.

Numeric revisions ​

cs
opts.Schema.For<RevisionedOrder>().UseNumericRevisions();

snippet source | anchor

Or by implementing JasperFx.IRevisioned:

cs
public class RevisionedOrder : IRevisioned
{
    public Guid Id { get; set; }
    public int Version { get; set; }
}

snippet source | anchor

cs
session.Store(doc, revision: 4);       // fails unless 4 > the stored revision
session.UpdateRevision(doc, 4);
session.TryUpdateRevision(doc, 4);     // no exception if it loses

0 means auto — increment whatever is stored.

TIP

The two routes behave identically, but a DSL-configured type has no Version member to carry an expectation on. So for one of those a plain Store(doc) always means auto, and guarding a write means naming the revision: UpdateRevision(doc, 4). For an IRevisioned type the member is both the expectation and where the new revision is written back, which is what makes Store(doc) guard on its own — the sharp edge below.

The sharp edge ​

WARNING

The semantics are Marten's, deliberately, and they have a sharp edge. Store passes the document's own Version as the expected revision, and the guard requires it to be strictly greater than the stored one.

So re-storing an instance that still carries the revision it was written at is a ConcurrencyException, not an increment. The way forward is UpdateRevision(doc, Version + 1), or resetting Version to 0 for auto.

Polecat diverged to an equality rule for its own pipeline's parity; following it here would mean writing SQL the shared operations do not describe, and would silently disagree with Marten about what an explicit revision means.

The revision is always read back ​

Even when no member is mapped to it — asymmetric with a Guid version, which is dropped from the query-only projection.

That is on purpose: the revision you will guard the next write with is the one the database just computed, so a read that withheld it would leave every explicit store guessing.

TIP

The column is INTEGER, and that is load-bearing. A TEXT affinity would sort revision 10 below revision 9 and turn the "must be greater" guard into nonsense.

A projected document's revision is its stream version ​

When a projection maintains an IRevisioned document, the stored revision is the version the events put it at, as on Marten and Polecat. For a single-stream projection that is the stream version, and for a multi-stream projection it is the last event's sequence. LoadAsync and a LINQ Select(x => x.Version) return the same number, so you can check (Id, Version) to see whether a projected document changed before loading all of it. Advanced.RebuildSingleStreamAsync writes the same revision.

Fisher 1.14.0 and earlier counted writes in that column instead: one save of two events stored revision 1, while the document body said 2.

Which to use ​

Guid versions are the safer default and need nothing from the caller. Numeric revisions are worth it when the revision is part of your API — an HTTP client sending If-Match: 4 reads better than one sending a Guid, and Fisher.AspNetCore can serve an ETag from either.

Event stream concurrency ​

Streams have their own guard. See Appending Events — including why AppendExclusive fails here where the siblings wait.

Released under the MIT License.