-
Notifications
You must be signed in to change notification settings - Fork 4
Repositories
A repository gives typed access to one collection of entities. You declare repositories on your DbContext; every read and write goes through them.
Construct a Repository<TModel, TKey> with the collection name and expose it (behind an interface):
public interface ISampleDbContext : IDbContext
{
IRepository<Cat, string> Cats { get; }
}
public class SampleDbContext : DbContext, ISampleDbContext
{
public IRepository<Cat, string> Cats { get; } = new Repository<Cat, string>("cats");
// ...
}TModel must be an entity (IEntityModel<TKey>); TKey is its id type. To configure indexes,
opt into whole-document saves, or deny writes on the collection, build it from a
RepositoryOptions<TModel> instead of a bare name:
public IRepository<Cat, string> Cats { get; } = new Repository<Cat, string>(
new RepositoryOptions<Cat>("cats")
{
IndexBuilders =
[
(Builders<Cat>.IndexKeys.Ascending(c => c.Name), new CreateIndexOptions<Cat> { Unique = true })
],
SaveWithDocumentReplace = false // default: save only changed members
});See Indexes for index configuration, Change tracking and saving for SaveWithDocumentReplace,
and Read-only access for the IsReadOnly option.
You can declare more than one repository over the same model type (or over a type hierarchy); when you do, references to that type need to know which repository is their origin — see References and denormalization.
| Category | Members | Page |
|---|---|---|
| Create |
CreateAsync(model), CreateAsync(models)
|
CRUD operations |
| Read one |
FindOneAsync(id|predicate), TryFindOneAsync(id|predicate)
|
CRUD operations |
| Query |
QueryElementsAsync(...), QueryPaginatedElementsAsync(...), FindAsync(...)
|
Querying |
| Update (tracked) | mutate a tracked model, then SaveChangesAsync
|
Change tracking and saving |
| Update (atomic) |
AccessToCollectionAsync, TryFindOneAnd*, UpdateManyAsync, Upsert*
|
CRUD operations |
| Replace | ReplaceAsync(model) |
CRUD operations |
| Delete |
DeleteAsync(id|model), DeleteManyAsync(filter)
|
CRUD operations |
| Indexes |
RepositoryOptions.IndexBuilders, GetDefinedIndexModelsAsync, BuildNewIndexesAsync, DeleteOldIndexesAsync
|
Indexes |
| Counts |
EstimatedDocumentCountAsync(), CountDocumentsBySchemaIdAsync()
|
below |
| Deprecated schemas | BuildDeprecatedSchemaDocumentsMigration() |
Migrations |
| Deprecated schema id elements |
CountDeprecatedSchemaIdDocumentsAsync(), MigrateDeprecatedSchemaIdDocumentsAsync()
|
below |
| Missing origin references |
FindMissingOriginReferencesAsync(), RepairMissingOriginReferencesAsync()
|
below |
Every repository also exposes ModelType, KeyType, Name, IsReadOnly, ModelIdToString, and
its DbContext.
EstimatedDocumentCountAsync reads the collection size from its metadata, at a constant cost whatever
the collection holds. The number is an estimate: it can drift after an unclean shutdown, and on a
sharded cluster it includes orphaned documents.
var estimatedCount = await Cats.EstimatedDocumentCountAsync();CountDocumentsBySchemaIdAsync groups the documents by the model map schema id
they carry, resolved from the _s element or from the deprecated _m one. Documents carrying no
schema id element are counted aside, and schema ids that no map declares are reported too — together,
they are the documents no registered schema accounts for:
var (documentsBySchemaId, documentsWithoutSchemaId) = await Cats.CountDocumentsBySchemaIdAsync();
foreach (var (schemaId, documentsCount) in documentsBySchemaId)
Console.WriteLine($"{schemaId}: {documentsCount}");Warning. MongoDB serves this grouping with a full collection scan — indexing the schema id element does not change that — so the cost grows with the collection. Size the collection with
EstimatedDocumentCountAsyncbefore counting a large one. The Admin dashboard exposes both, counting one collection per request.
Both work on read-only repositories and db contexts: counting is a read.
A document written before the schema id element took its _s name carries it in the deprecated _m
element (ModelMapSchema.DeprecatedIdElementName), still recognized while reading it — see
Versioned schemas. CountDeprecatedSchemaIdDocumentsAsync counts those documents, matching the
element at their root: a document whose root carries _s was written whole by a version writing
it, its sub-documents included, so the root tells the whole document.
var documentsToMigrate = await Cats.CountDeprecatedSchemaIdDocumentsAsync();MigrateDeprecatedSchemaIdDocumentsAsync rewrites them. Renaming the root element wouldn't be
enough — a document nests a schema id as deep as its sub-documents and its
summaries go — so each counted document is deserialized and written
back whole with its current active schema, which stamps _s at every level. It is a
migration restricted to those documents, and behaves like one: a document failing
deserialization or write is skipped and reported in the returned MigrationResult, keeping the
content it has, and the documents referencing the migrated ones are not updated.
var result = await Cats.MigrateDeprecatedSchemaIdDocumentsAsync();
Console.WriteLine($"{result.MigratedDocuments} migrated, {result.TotDocumentErrors} failing");The count works on read-only repositories and db contexts — counting is a read —
while the migration throws UnauthorizedAccessException there, like every write.
Warning. The count scans the collection, and the migration reads and rewrites every document it selects: both belong to on-demand maintenance. The Admin dashboard counts them with the model schemas of a collection, on request, and rewrites them through the Rewrite deprecated schemas option of a migration start, which covers every document left on a deprecated schema.
A reference whose origin document doesn't exist anymore has
nothing to load (see the missing origin documents section of that page). What dangles comes from the
deletes the origin delete propagation doesn't cover; this scan
finds it.
FindMissingOriginReferencesAsync scans the collection: for every reference element
path of the registered schemas — secondary ones included, since a reference written by a deprecated
schema still points to its origin document — it reads the distinct referenced ids server side and
verifies them, as stored, against the origin repository of the reference:
var report = await Cats.FindMissingOriginReferencesAsync();
foreach (var pathReport in report.PathReports)
Console.WriteLine(
$"{pathReport.ElementPath} -> {string.Join(", ", pathReport.OriginRepositoryNames)}: " +
$"{pathReport.MissingOriginIdsCount} missing origins, " +
$"{pathReport.ReferencingDocumentsCount} documents affected");Each MissingOriginReferencesPathReport carries the reference element path, the origin repositories
the ids were verified against — a source declared on a child db context resolves like a lazy load
would — the full count of the missing origin ids with a listing capped at
MissingOriginReferencesPathReport.MaxTrackedMissingOriginIds entries (100), the count of the
documents referencing the listed ids — a lower bound when the cap drops some — with their ids listed
under their own cap (MaxTrackedReferencingDocumentIds, 100), and the OriginDelete its mapping
declares. Null references are not reported: they address no origin document. UnverifiableElementPaths lists apart the reference paths
the scan can't verify — a dictionary written with its keys as element names, a fixed array position in
the path (an ArrayOfArrays dictionary value), or an origin repository that doesn't resolve on the
current scope.
RepairMissingOriginReferencesAsync runs the same scan and repairs each path the way its mapping
declares a deleted origin is propagated — the dangling references it finds are exactly the ones that
propagation never reached:
OriginDelete |
What the repair does |
|---|---|
KeepReference |
Leaves the path alone, and doesn't even scan it: those references dangle by design. |
RemoveReference |
Pulls a reference hosted as an array item out of its array, sets any other one to null — reading as a null reference from then on. A raw bulk repair: it writes server side without loading models. |
DeleteReferencingDocument |
Deletes the referencing documents through the repository domain delete, batch by batch inside a transient models scope, so they propagate their own reference policies in turn. |
Every update matches the missing origin id itself, so a reference concurrently rewritten to another document is left alone, and unverifiable paths stay untouched.
var repair = await Cats.RepairMissingOriginReferencesAsync();
foreach (var pathRepair in repair.PathRepairs)
Console.WriteLine(
$"{pathRepair.ElementPath} ({pathRepair.RepairMode}): " +
$"{pathRepair.MissingOriginIdsCount} missing origins, " +
$"{pathRepair.UpdatedDocumentsCount} documents updated, " +
$"{pathRepair.DeletedDocumentsCount} deleted");repairModesByElementPath overrides the declared policy of the paths it names, and a name that isn't
a verifiable reference of the collection fails with a detailed ArgumentException instead of applying
to nothing. dryRun executes the whole repair with its collection writes simulated, and progressAsync
reports what a path brought so far while it runs.
The find works on read-only repositories and db contexts — scanning is a read —
while the repair throws UnauthorizedAccessException there, like every write.
Warning. The scan reads every referenced id of the collection — one aggregation per reference path, plus chunked existence reads on the origin collections — so its cost grows with the collection and its references. Size the collection with
EstimatedDocumentCountAsyncbefore scanning a large one. The Admin dashboard exposes the find on request, and runs the repair as an operation, one collection at a time.
AccessToCollectionAsync hands you the driver IMongoCollection<TModel> surface — guarded by the
engine's access limitations — for operations Scrinium doesn't wrap; most usefully atomic server-side
updates (see the balance example in CRUD operations):
var count = await Cats.AccessToCollectionAsync(collection =>
collection.CountDocumentsAsync(Builders<Cat>.Filter.Empty));By default (handleImplicitDbExecutionContext: true) it opens a db execution context around the
operation, so documents materialized inside it register on the current scope (identity map and change
tracking). Enlistment in an ambient transaction is automatic, with or without the
flag, for every operation on the context's collections that can run in a transaction (change stream
watches and estimated document counts stay session-less).
Next: CRUD operations to write data, Querying to read it.
Scrinium — source · issues (SCR) · GNU LGPL-3.0 · info@etherna.io
Getting started
Core concepts
Working with data
Serialization & mapping
Operations & maintenance
Advanced & reference