-
Notifications
You must be signed in to change notification settings - Fork 4
CRUD operations
How to create, read, update and delete documents through a repository. Reads that return many documents (LINQ, pagination) are on Querying; the save semantics behind change tracking are on Change tracking and saving.
var cat = new Cat("Milo", new DateTime(2021, 4, 1));
await db.Cats.CreateAsync(cat); // inserts, assigns the Id
await db.Cats.CreateAsync([cat1, cat2]); // batch insertNote. A just-created instance is not a proxy, but it is tracked and it is the instance of its document on the scope:
CreateAsynccaptures its model document, so mutations made after the create persist on the nextSaveChangesAsync, and registers it on the identity map, so a same-scope find of it returns the created instance itself — no re-read needed. See Change tracking and saving.
New referred models linked to the creating instance — models whose
id is still null — are created too, into their own repositories, before the insert serializes
the references.
Inside a transaction that aborts, the create is undone in the scope too: the assigned
ids return to null (those of the auto created referred models included) and the instances leave the
identity map and the change tracking, so the same instance creates anew — by the automatic retry of a
transient failure, or by your own code.
FindOneAsync returns the entity and throws ScriniumEntityNotFoundException if it's missing;
TryFindOneAsync returns null instead. Both accept an id or a predicate:
Cat cat = await db.Cats.FindOneAsync(id);
Cat? one = await db.Cats.TryFindOneAsync(id);
Cat byName = await db.Cats.FindOneAsync(c => c.Name == "Milo");Within a scope, a find by id is served from the identity map — no database round trip — when a full instance is already loaded or created. A loaded summary still hits the database, to be upgraded in place to the full document.
Scrinium has no UpdateAsync. You load a tracked entity, mutate it (directly or through its
domain methods — see Domain models), and flush the unit of work:
var cat = await db.Cats.FindOneAsync(id);
cat.Rename("Miloh");
await db.SaveChangesAsync(); // persists every changed model of this contextdb.SaveChangesAsync() saves all tracked changes; repository.SaveChangesAsync(model) saves one
specific model. By default only the changed members are written, atomically. Full semantics —
member-level updates, concurrency, schema guarding — are on Change tracking and saving.
ReplaceAsync writes the whole document, regardless of change tracking. Use it as an explicit
escape hatch (e.g. after a heavy rebuild). It refreshes denormalized
references by default:
await db.Cats.ReplaceAsync(cat); // whole-document replace
await db.Cats.ReplaceAsync(cat, updateDependentDocuments: false);The instance doesn't need to be the tracked one you loaded: replacing with a brand-new instance carrying the same id works too — including one of a different type of the repository's model hierarchy, which upgrades the stored document to the new type in place.
await db.Cats.DeleteAsync(id); // by id
await db.Cats.DeleteAsync(cat); // by instance (evicts from identity map)A domain delete also propagates in background to the documents referencing the deleted model, applying the origin delete policy each reference declares — by default the reference is removed.
DeleteManyAsync is a raw bulk delete by filter — fast, but it skips the delete propagation and
does not touch any scope's identity map:
long removed = await db.Cats.DeleteManyAsync(c => c.Birthday < cutoff);Warning. After
DeleteManyAsync, instances already loaded in a scope keep being returned by finds on that scope (useUnregisterLoadedModelif that matters). Saving their changes won't recreate the deleted documents. The references pointing at the deleted documents stay dangling too: the missing origin references scan (see Repositories) finds and removes them.
For concurrency-safe counters, sets and conditional updates, operate on the server atomically instead
of load-mutate-save. Reach the driver collection with AccessToCollectionAsync, or use the built-in
atomic helpers.
The strongest pattern encodes the invariant in the filter, so the update can't race. A balance debit is the classic example — an overdraw simply matches no document:
// amount < 0 for a debit; the filter guarantees the balance never goes negative.
await db.Wallets.AccessToCollectionAsync(collection =>
collection.FindOneAndUpdateAsync(
Builders<Wallet>.Filter.And(
Builders<Wallet>.Filter.Eq(w => w.Id, walletId),
Builders<Wallet>.Filter.Gte(w => w.Balance, -amount)),
Builders<Wallet>.Update.Inc(w => w.Balance, amount)));Built-in helpers cover the common shapes without dropping to the driver collection:
| Method | Does |
|---|---|
TryFindOneAndUpdateAsync(filter, update, options) |
Atomic find-and-update, returns the pre/post model. |
TryFindOneAndSetFieldAsync / TryFindOneAndAddToSetAsync
|
Set a field / add to a set atomically. |
UpdateManyAsync(filter, update) |
Bulk $set/$inc/… over matching documents. |
UpsertAsync / UpsertSetFieldAsync / UpsertIncrementAsync / UpsertAddToSetAsync
|
Find-and-modify with insert of onInsertModel when absent. |
Warning. The server-side write bypasses the unit of work: a model already loaded in the same scope won't reflect a change you make this way — read it fresh in a new scope when you need the updated value. A model returned by the
TryFindOneAnd*helpers still materializes on the scope: registered and tracked when not loaded yet, or the already-loaded (unrefreshed) instance when one is. The upsert helpers instead detach the pre-image they return, and skip the auto-creation of new referred models: anonInsertModelreferencing a model without id throws instead of persisting a broken reference.
The upsert helpers write the onInsertModel through update instructions, one per element of its
serialized form (its id excluded). An element the update writes stays out, and an element the update
writes inside splits into its sub elements, down to the updated field: a nested update keeps the
siblings of the field it touches, so the document created when nothing matches carries the same
elements CreateAsync writes for that model, with the update applied.
Update field names are paths, and the element names composing them — the top level ones, and
those of a split element — have to work as path segments. When they can't, the upsert refuses the
model with a detailed InvalidOperationException instead of writing a document different from the
one an insert writes:
The onInsertModel serializes |
Why the upsert can't write it |
|---|---|
an element whose name contains a . — a member mapped onto a field named that way, a dictionary key, a custom serializer naming elements from data |
the field would land nested instead of literal |
| an element with an empty name | no update path addresses it |
an array, a scalar or a null where the update writes inside |
it can't be set beside the updated field, and the update alone would create a nested document in its place |
Keep such a member out of the onInsertModel and write it with CreateAsync, or give it a value the
update can complete — an empty dictionary rather than a null, for instance. A name of any shape
inside an element the update leaves whole is stored as an insert stores it.
Next: Querying for reads that return many documents, Change tracking and saving for save internals, or Transactions to group writes atomically.
Scrinium — source · issues (SCR) · GNU LGPL-3.0 · info@etherna.io
Getting started
Core concepts
Working with data
Serialization & mapping
Operations & maintenance
Advanced & reference