Skip to main content

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.

Gotcha: the discriminator is not optional

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:

OperationWhat it's for
AddAsync / UpdateAsync (upsert) / DeleteAsyncWhole-document lifecycle
PatchAsync(id, operations, partitionKey)Partial update — change three fields without rewriting the doc
GetByIdAsync / GetByQueryAsync(predicate) / GetAllAsyncPoint 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.

Gotcha: one container name is hardcoded, not configured

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.