-
Notifications
You must be signed in to change notification settings - Fork 4
Execution contexts
Scrinium needs a bit of ambient per-flow state during serialization and lazy loading — for example to know the current db context when a summary reference lazy-loads its full document. That state is the execution context. It's wired automatically for web requests and Hangfire jobs; you only open one yourself for the few operations that require an ambient one (below).
public interface IExecutionContext
{
IDictionary<object, object?>? Items { get; }
}A per-flow key/value bag. Scrinium stores per-operation handlers in it: the current db context, the
resolved source repository, active serializer modifiers, the ambient
transaction session, the exclusive-access allowance, the
dry run marker, and the resource lock leases of the flow. Enabling a
serializer modifier or entering an exclusive-access flow throws
ExecutionContextNotFoundException when Items is null (no ambient context) — when you must open
one yourself is below.
One context, one flow. An execution context carries the ambient state of a single flow: the handlers a flow registers are the ones it resolves. Inside a web request the ambient state is the request's own —
HttpContext.Itemsbacks it on every branch — so it can't be isolated per parallel branch: open modifier, session or exclusive-access scopes only around sequential work there. On an async-local-backed flow (a background thread, a Hangfire job) give each parallel branch its own context withInitAsyncLocalContext()(below).
AddScrinium registers an IExecutionContext that selects the right backing store: the current
HttpContext.Items for web requests, and an async-local context otherwise. Hangfire jobs get their
own async-local context from the filter AddScriniumWithHangfire installs. So inside a request or a
Hangfire job, everything just works.
Ordinary operations — CRUD, queries, lazy loads — never require a pre-opened context: each one self-creates an isolated context when none is ambient. You open one yourself only for the operations that must share the ambient context across calls:
- enabling a serializer modifier (no-cache, reference
readOnlyId), whoseusingscope must span the reads it modifies; -
exclusive-access flows —
RunWithExclusiveAccessAsync, and so seeding or migrations driven by your own code (SeedDbContexts()opens one per context for you); - keeping one ambient context across a whole unit of work instead of one per operation.
On a background thread with no ambient context — a hosted BackgroundService loop, a startup
task — open an isolated one for the duration of the work:
using Etherna.Scrinium.Core.ExecContext.AsyncLocal;
using (AsyncLocalContext.Instance.InitAsyncLocalContext())
using (db.Engine.SerializerModifierAccessor.EnableCacheSerializerModifier(noCache: true))
{
// the modifier requires the ambient execution context opened above
var cats = await db.Cats.QueryElementsAsync(q => q.ToListAsync());
}InitAsyncLocalContext() creates a new isolated context (even if one is inherited from an ancestor
flow) and returns a handler; disposing it restores the previous context.
Pair it with a scope. A background service opens a DI scope per work cycle (for a fresh DbContext); add
InitAsyncLocalContext()around the cycle when the work uses a serializer modifier or exclusive access — see Best practices and pitfalls.
Next: Best practices and pitfalls for the background-service pattern, or Querying for the no-cache modifier that relies on this context.
Scrinium — source · issues (SCR) · GNU LGPL-3.0 · info@etherna.io
Getting started
Core concepts
Working with data
Serialization & mapping
Operations & maintenance
Advanced & reference