The logical call context
System.Runtime.Remoting.Messaging.CallContext has an illogical store (tied to one physical thread) and a logical store that flows with the call and everything it awaits, across physical thread hops.
Axlis.Sitecore.Context · Shipped · v0.1.0
Sitecore.Context.Database and HttpContext.Current are ambient statics. In classic ASP.NET, a request is not guaranteed to run start-to-finish on a single physical OS thread — it can hop threads across an await, or across certain pipeline transitions. Code that assumes "the thread I'm on now is the thread that started this request" can silently pick up null — or, worse, another request's state — once that assumption breaks. In one production incident, this surfaced as a deadlock: a site would start deploying to a lower environment and simply hang on startup, never finishing its first request.
A plain static field doesn't fix this — it's shared across every concurrent request. Neither does [ThreadStatic] — it's pinned to a single physical thread and loses its value the moment the request hops, the same failure mode as the original problem. Axlis.Sitecore.Context replaces both statics with a per-request value that actually survives thread hops without leaking between requests, and adds no locking to do it.
$ dotnet add package Axlis.Sitecore.Context.Sitecore102How it's resolved
No new infrastructure, no distributed cache — just the same per-call propagation mechanism .NET Framework itself uses, applied deliberately.
System.Runtime.Remoting.Messaging.CallContext has an illogical store (tied to one physical thread) and a logical store that flows with the call and everything it awaits, across physical thread hops.
This marker interface is the entire trick: when the stored object implements it, CallContext.SetData automatically promotes it into the logical store — even though the call site used the plain, non-"Logical"-prefixed API.
The logical call context rides on ExecutionContext, which the compiler-generated async state machine captures and restores at every suspension point — surviving a thread hop, without leaking into a caller that didn't set it.
The data a lock would protect is already isolated per logical call — no two concurrent requests ever contend for the same slot. A shared lock here would only add serialization overhead, and it's itself a deadlock precursor.
Introducing it into an existing project
Axlis.Sitecore.Context is a drop-in replacement — the API shape matches Sitecore.Context closely enough that adoption is a find-and-replace, not a redesign.
Axlis.Sitecore.Context.Sitecore102 (built against Sitecore 10.2.x).SitecoreContextHttpModule in the site's real Web.config — after Sitecore's own modules, not in an App_Config include.Sitecore.Context.Database → Axlis.Sitecore.Context.Database, one at a time.<system.webServer>
<modules>
<!-- ...Sitecore's own modules come first... -->
<add name="AxlisSitecoreContextHttpModule"
type="Axlis.Sitecore.Hosting.SitecoreContextHttpModule, Axlis.Sitecore.Context.Sitecore102" />
</modules>
</system.webServer>// Before — an ambient static that can go null mid-request
var db = Sitecore.Context.Database;
// After — thread-safe, per-request, same call shape
var db = Axlis.Sitecore.Context.Database;
// Migrating a file that still needs both, side by side:
using AxlisContext = Axlis.Sitecore.Context;
var db = AxlisContext.Database;Before you rely on it
Install into a real Sitecore 10.2.x instance with the module registered, then confirm three things: Axlis.Sitecore.Context.Database/.Request/.HttpContext are non-null mid-request, they return null (not throw) outside any request once the real Sitecore.Context fallback is also unavailable, and no state bleeds between two rapid, back-to-back requests served by the same pooled thread. There is no Sitecore FakeDb in this toolchain, so this check is manual, per release.
No. The replacement is call-site by call-site — there's no compile-time way to flag remaining Sitecore.Context usages, so a text search after migrating a module is worth doing, but nothing forces an all-or-nothing cutover.
All three properties fall back gracefully: they try the real Sitecore.Context/HttpContext.Current directly, and only return null if that is also unavailable. Nothing throws for the "nothing available yet" case.
A prior reference implementation this mechanism is ported from guarded its accessor with a process-wide lock. That lock protects nothing here — the data is already isolated per logical call, so no two unrelated requests ever contend for the same slot. A shared lock would only serialize otherwise-independent work, and is itself a deadlock precursor — precisely the failure category this package exists to remove.