Real-Time State in Cosmos
A camera has two lives at two speeds. One barely changes: its name, its location, whether it's public. The other changes constantly: where it's pointing right now, its focus, its brightness, when the current movement expires. Reads want these separately — a map wants identities, a control panel wants live positions — but they describe the same physical device. Cosmos DB is where both live, and how they're kept together-but-apart is the trick worth learning here.
The one idea to hold on to
Static and operational camera data share a single Cosmos container, told apart by a
DataType discriminator that also serves as the partition key. There is no "static
container" and "operational container." There's one container, and two repositories that each
pin every query to their own slice of it:
// CameraStaticRepository — every read is filtered to the Static slice
return await base.GetByQueryAsync(
x => cameraIds.Contains(x.Id) && x.DataType == CameraEntityDataTypes.Static);
// …and writes pass that same discriminator as the partition key
await base.UpdateAsync(cameraStaticEntity, CameraEntityDataTypes.Static);
CameraOperationalRepository does the identical thing with
CameraEntityDataTypes.Operational. Because the discriminator is the partition key, Cosmos
physically groups all the static docs together and all the operational docs together — so a
"get all cameras" scan never wades through operational churn, and a hot PTZ update never
touches a static partition. One container, two access patterns, no interference.
Every override in these repositories re-applies DataType == …. If you add a query method
and forget the filter, it silently returns both static and operational documents mixed
together — no error, just wrong results that look plausible until an operational doc shows up
where a camera identity was expected. Copy an existing method; don't write one from scratch.
The base class does the CRUD
Every Cosmos repository inherits CosmosRepository<T> where T : BaseCosmosEntity, which
provides the generic operations so the concrete repositories only add what's special:
| Operation | What it's for |
|---|---|
AddAsync / UpdateAsync (upsert) / DeleteAsync | Whole-document lifecycle |
PatchAsync(id, operations, partitionKey) | Partial update — change three fields without rewriting the doc |
GetByIdAsync / GetByQueryAsync(predicate) / GetAllAsync | Point read, LINQ query, full scan |
PatchAsync is the one that matters for live state. A PTZ move changes pan, tilt, and zoom —
patching just those fields is cheaper and avoids clobbering a concurrent brightness change on
the same document. CameraOperationalRepository exposes purpose-built patches
(PatchPtz, PatchPtzFocusBrightness, PatchFocus, PatchBrightness) plus the
movement-expiration helpers (Extend…, End…) that drive the lease countdown.
CondorEventRepository does not read its container name from CosmosDbOptions like every
other repository. It's a compile-time constant:
public static readonly string CondorEventContainerName = "condorEvent";
So you can't repoint it per environment through config the way you can the others. If you're
hunting for a CondorEventContainerName setting to change, there isn't one — it's in the
code. The Condor event docs also carry a Ttl, so Cosmos expires them automatically.
Wrapping up
Camera state in Cosmos is one container wearing two hats: a DataType discriminator doubles
as the partition key so static identities and live operational state stay physically
separated while describing the same device. Lean on CosmosRepository<T> for CRUD, reach for
PatchAsync when you're changing a few fields on hot data, and remember that the Condor-event
container name is the one that's hardcoded.
Next, the relational half of the platform — and the geospatial math behind proximity alerts: The Relational Core.