-
Notifications
You must be signed in to change notification settings - Fork 4
Architecture overview
A tour of how Scrinium is put together — the packages, the engine/context split, and how a request flows through it.
Scrinium is a stack of layered libraries. Etherna.Scrinium.Core holds the ODM logic and performs all data
access through the MongoDB driver; the others adapt it to a host and a task runner —
Etherna.Scrinium.AspNetCore also constructs the MongoClient it hands to the engine, and the Hangfire
package delegates its job storage to the external Hangfire.Mongo. The core package also
ships a Roslyn source generator (Etherna.Scrinium.Core.Generators, under analyzers/) that emits the
model proxies at compile time in every consuming project — no setup needed.
graph TD
UI["Etherna.Scrinium.AspNetCore.UI<br/>(admin dashboard)"] -. optional .-> AN
Meta["Etherna.Scrinium<br/>(meta package)"] --> AN["Etherna.Scrinium.AspNetCore<br/>(DI integration)"]
Meta --> HF["Etherna.Scrinium.Hangfire<br/>(task runner)"]
AN --> Core["Etherna.Scrinium.Core<br/>(the framework)"]
HF --> Core
Core --> Driver[("Etherna MongoDB<br/>driver fork")]
See Packages and feeds for which one to install. The driver is the Etherna fork
(Etherna.MongoDB.*), not the official one.
This is the single most important idea in Scrinium. There are two related objects per database:
-
DbContextEngine— one per process (singleton). It owns everything built once and shared: the MongoDB client and database, theMapRegistryand versioned schemas, the discriminator registry, the serializer modifiers, the proxy generator, seeding state, exclusive access in this process, and the db context lock across instances. The serialization pipeline is wired to the engine. -
DbContext— one per scope (request or background job). It is the unit of work: it exposes the repositories, tracks changed models (ChangedModelsList), holds the per-scope identity map, and providesSaveChangesAsyncandExecuteInTransactionAsync. Each context attaches to the singleton engine of its type, reachable asIDbContext.Engine.
graph TD
Engine["DbContextEngine — singleton<br/>client · MapRegistry · schemas<br/>seeding · exclusive access · db context lock"]
A["DbContext — scope A (request)<br/>repositories · identity map<br/>changed models · SaveChanges"]
B["DbContext — scope B (Hangfire job)<br/>repositories · identity map<br/>changed models · SaveChanges"]
A -->|.Engine| Engine
B -->|.Engine| Engine
Two consequences you will feel constantly:
-
IDbContextdoes not extendIDbContextEngine. Engine-level members (Client,Database,MapRegistry,ProxyGenerator,Options, exclusive access, sessions…) live undercontext.Engine; scope-level members (repositories,SaveChangesAsync,ChangedModelsList, seed/migration facades) live on the context. -
Fresh data = a new scope. Within one scope, the identity map returns the same instance for the
same document, so a second read won't see other scopes' concurrent changes until you
SaveChangesAsync(the sync point) or open a new scope. This makes injecting a context into a singleton a bug (a captive dependency) — see Best practices and pitfalls.
Details: DbContext and engine.
Scrinium needs to know "which flow am I in?" during serialization and lazy loading — for example to
find the current context when a summary reference lazy-loads its full document. That ambient state is
the execution context: an HTTP-request context for web requests, an async-local context for
background work. It's wired automatically by AddScrinium; you only touch it on threads that have no
ambient context (some background services), where you open one explicitly. See
Execution contexts.
How models become documents:
- The
MapRegistryholds a model map per type. A map can carry several schemas, each stamped with an immutable schema id written into the document, so multiple schema versions live in one collection and old documents deserialize without a mass migration (Versioned schemas). - Reference serializers implement denormalization: a referenced entity is stored as a compact summary (at minimum its id), and unloaded members lazy-load on access (References and denormalization).
- Serializer modifiers tweak behavior for a block of operations — most importantly the no-cache modifier, which skips change tracking and the identity map for large read-only scans.
Details: Model mapping.
You read and write through repositories (IRepository<TModel, TKey>). All collection
access funnels through AccessToCollectionAsync, which is also your hook for driver-level atomic
operations (e.g. FindOneAndUpdateAsync). As you mutate tracked models, the generated proxy flags
them as change candidates; SaveChangesAsync diffs each against its captured document and persists
only the changed members in one atomic statement per model (Change tracking and saving).
When the deployment supports it, that flush runs inside an implicit transaction.
Some work must happen off the request path and is handed to a task runner (ITaskRunner, Hangfire
by default — Background tasks):
-
UpdateDocDependenciesTaskpropagates a changed summary to every document of the application that denormalized it, keeping denormalized copies in sync. -
DeleteDocDependenciesTaskapplies the origin delete policies of the references to a deleted document — removing the references, or cascading the delete. -
MigrateDbContextTaskruns a context migration under exclusive access (a dry run skips it), holding the db context lock claimed by its start so no other instance migrates or seeds the same context meanwhile.
sequenceDiagram
participant R as Request
participant S as DI scope
participant C as DbContext
participant E as DbContextEngine
participant Q as Task runner
R->>S: begin scope
S->>C: resolve DbContext (attach to Engine)
C->>E: read via repository (schemas, driver)
E-->>C: model (registered in identity map)
C->>C: mutate model (change tracking)
C->>E: SaveChangesAsync (member-level, maybe in a transaction)
C->>Q: enqueue dependency-update task
R->>S: end scope (context disposed)
Q->>E: refresh denormalized references (own scope)
- Domain models — the rules your persisted classes must follow
- DbContext and engine — the lifecycle and identity map in depth
- References and denormalization — Scrinium's flagship feature
- Best practices and pitfalls — scopes, no-cache, captive dependencies, and more
Scrinium — source · issues (SCR) · GNU LGPL-3.0 · info@etherna.io
Getting started
Core concepts
Working with data
Serialization & mapping
Operations & maintenance
Advanced & reference