-
Notifications
You must be signed in to change notification settings - Fork 4
DbContext and engine
A database context is Scrinium's unit of work: it groups the repositories of one
domain boundary, tracks what you change, and flushes it on SaveChangesAsync. Every context is backed
by a process-wide engine. Understanding this split — and the per-scope identity map — is key
to using Scrinium correctly.
Derive from DbContext, declare your repositories, and list the model-map
collectors. Optionally override SeedAsync for seeding:
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");
protected override IEnumerable<IModelMapsCollector> ModelMapsCollectors =>
[new ModelBaseMap(), new CatMap()];
protected override Task SeedAsync() => base.SeedAsync();
}Register it with AddDbContext. You never write a constructor that
takes dependencies — Scrinium builds and wires the context.
-
DbContextEngineis a keyed singleton, one per context type. Reach it viacontext.Engine. -
DbContextis scoped — one instance per request or job — and holds the unit-of-work state.
IDbContext does not extend IDbContextEngine: engine members live under .Engine.
| Member | Use |
|---|---|
Client, Database
|
The MongoDB client and database. |
GetMongoCollection<T>(name, settings?, isReadOnly?) |
A driver collection guarded by the engine's access limitations (escape hatch). |
MapRegistry |
Registered model maps and schemas. |
Options |
This context's IDbContextOptions. |
SupportsTransactions |
True when the deployment allows transactions (replica set, sharded or load-balanced), detected from the cluster topology; false while the topology is still undiscovered. |
StartSessionAsync(ct) |
Start a driver session. |
RunWithExclusiveAccessAsync(action, lockOnRead) |
Run under exclusive access (used by seeding/migrations). |
IsExclusiveReadEnabled / IsExclusiveWriteEnabled
|
Whether an exclusive operation is locking access in this process. |
DbContextLock |
The db context lock, serializing seeding and migrations across application instances. Throws InvalidOperationException on a read-only context. |
GetResourceLock(resourceNamespace, resourceId) |
The resource lock of an application resource, as an IResourceLock object. Same read-only denial as DbContextLock. |
Identifier |
Names the context in its operation log and keys the db context lock; defaults to the context's type name. |
ProxyGenerator, SerializerRegistry, DiscriminatorRegistry, SerializerModifierAccessor, ExecutionContext, DbMaintainer, DbMigrationManager, DbContextType, IsSeededCache, Logger
|
Infrastructure components and engine metadata. |
| Member | Use |
|---|---|
repositories (your properties) + RepositoryRegistry
|
Read and write documents. |
ChildDbContexts |
The child context instances attached from this context's scope (see Child contexts below). |
ChangedModelsList |
Proxy models flagged as change candidates; the save diff decides what actually writes (see Change tracking and saving). |
SaveChangesAsync(ct) |
Persist pending changes (see Change tracking and saving). |
ExecuteInTransactionAsync(...) |
Run a block in a transaction. |
DbOperations |
Log of Scrinium's own operations (_db_ops). |
DocumentMigrationList |
Registered migrations. |
IsSeeded / SeedIfNeededAsync(lockWaitTimeout?, lockLeaseDuration?)
|
Seeding state and trigger. |
ExecuteMigrationAsync, TryStartMigrationAsync, GetLastMigrationsAsync, GetMigrationAsync, IsMigrationRunningAsync
|
Migration control. |
TryAcquireResourceLockAsync(resourceNamespace, resourceId, mode?, leaseDuration?) / IsResourceLockedAsync(resourceNamespace, resourceId)
|
Acquire and inspect the application's resource locks. |
IsMemberLoaded, LoadValuesAsync (two overloads), IsOutdatedModel
|
Summary inspection, explicit preloads and outdated-instance detection — see References and denormalization. |
StartTransientModelsScope() |
A scope evicting at dispose the models materialized inside it, bounding the memory of massive scans — see Migrations. |
TryGetLoadedModel(repository, modelId) / UnregisterLoadedModel(modelId, model)
|
Escape hatches over the identity map (below): the instance already loaded for a document, and the eviction of one instance from the next loads deduplication. |
The plumbing the library itself invokes on the context — change candidate marking, loaded model
registration, model document tracking, lazy load reaction hooks — is not part of IDbContext:
DbContext implements it explicitly on infrastructure interfaces (IProxyModelsDbContext, invoked
by the generated proxy models and public only because the proxies compile into your assembly, and
IInternalDbContext, internal to the library), so the surface you code against is application API
only.
Within one context instance (one scope), Scrinium keeps an identity map: each document materializes one model instance, EF-style.
- Loading the same document twice returns the same instance; a reference to an already-loaded document resolves to it instead of a new object.
- A created model is the instance of its document too:
CreateAsyncregisters it (the auto-created referred models included), so a load of that document in the creating scope returns the plain instance you built, and a save referencing it keeps it as the member value. - A full load upgrades a summary in place: if you first saw an entity as a denormalized summary and later load it fully, the same instance gains the extra data.
-
FindOneAsyncby id returns an already-loaded full instance, or a created one, without a database round trip; a loaded summary still queries, to be upgraded in place. - Deleting a document evicts it from the map; so does a reload that finds the document has changed type (the outdated instance is replaced).
Warning — fresh data means a new scope. Because reads are deduplicated, a second read in the same scope won't reflect concurrent changes made by other scopes until the sync point (
SaveChangesAsync) or a new scope. If you need the latest state — in a long-lived background loop, for instance — open a new DI scope.UnregisterLoadedModelis the escape hatch for a single instance.
For large read-only scans, disable the identity map (and change tracking) with the no-cache serializer modifier so it doesn't grow one entry per document — see Querying and Best practices and pitfalls.
Warning. Never inject a
DbContextinto a singleton. A singleton lives for the whole process, so it would pin one scoped context — and its identity map — forever (a captive dependency). Symptoms: ever-growing memory, stale reads, and changes saved through the wrong context. Keep every consumer scoped or transient.
For a background service (a hosted singleton), inject IServiceScopeFactory and open a fresh
scope per work cycle:
public sealed class Worker(IServiceScopeFactory scopeFactory) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stop)
{
while (!stop.IsCancellationRequested)
{
using var scope = scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<ISampleDbContext>();
// ... use db for this cycle, then dispose the scope ...
}
}
}A context can declare child contexts with ParentFor<TChild>() in its options. Parent and children
are resolved from the same DI scope, and each context keeps its own identity map, keyed by
repository — so a model always materializes once in the context that owns its
repository, whether you load it from that context directly or reach it through a
cross-context reference from the parent. Saving the parent
cascades to persist pending changes in its children (each on its own connection — child saves
don't enlist in the parent's transaction). Children can even live in a different physical database:
services.AddScriniumWithHangfire()
.AddDbContext<IMainDbContext, MainDbContext>(options =>
{
options.ConnectionString = "mongodb://localhost/main";
options.ParentFor<ISharedDbContext>(); // declare the child
})
.AddDbContext<ISharedDbContext, SharedDbContext>(options =>
{
options.ConnectionString = "mongodb://localhost/shared";
});For example, a service can keep some user state in a shared context and still persist both the user and
its shared info in a single SaveChangesAsync() on the parent.
For tests or non-ASP.NET hosts you can build an engine and attach a context yourself (BuildEngine +
AttachToEngine) instead of using AddDbContext. See the core unit tests (DbContextTest) for the
pattern.
Next: Repositories to read and write, Change tracking and saving for save semantics, or References and denormalization to relate documents.
Scrinium — source · issues (SCR) · GNU LGPL-3.0 · info@etherna.io
Getting started
Core concepts
Working with data
Serialization & mapping
Operations & maintenance
Advanced & reference