Place hierarchy & visibility
Places have no state machine — a place is either alive or soft-deleted, like any reference-data row. What does need explaining is how the place tree is stored and rebuilt, and how the same tree drives what each user is allowed to see across the rest of the platform.
Two representations of one tree
Every place carries both an adjacency link and a nested-set range:
ParentPlaceId— the source of truth. This is what you set on create and update.LeftBower/RightBower— a derived nested-set encoding used for fast subtree queries ("this place and everything under it" becomes a single range comparison).
A subtree is every place whose bowers fall inside the ancestor's range — here,
Aisle 1's subtree is everything with bowers between 2 and 5.
Bowers are regenerated tenant-wide
The server recomputes the nested set with [dbo].[RebuildNestedSets] on:
- every place create (
createplace), and - every parent change through
updateplace.
The rebuild renumbers the whole tenant's tree, not just the branch you
touched. Practical consequence: do not cache LeftBower/RightBower across
writes — any place mutation anywhere in the tenant can change every place's
bowers. Treat bowers as a server-side query mechanism, and ParentPlaceId as
the durable shape of the tree.
Exceptions to the rebuild
| Operation | Rebuild behaviour |
|---|---|
deleteplace | Does not rebuild. The subtree is re-parented to the deleted node's parent, the deleted row's bowers are zeroed to 0/0, and the surviving bowers keep a gap until the next create or parent change closes it. |
DeleteSystemPlace | Does rebuild. |
UpdatePlaceStructure | Client-authoritative — the one endpoint where the bower values you send are persisted verbatim, with no rebuild. Only use it if you are deliberately supplying a complete, valid nested set. |
updateplace performs no existence or cycle validation on ParentPlaceId.
A move that creates a cycle (e.g. re-parenting a place under its own
descendant) returns HTTP 200 — and RebuildNestedSets silently drops the
cycle members from the nested set, leaving their stale bowers colliding with
the renumbered live tree. From that point, every bower-range consumer returns
wrong data: subtree queries, visibility expansion, missing-item sweeps, and
dashboards. The tree self-heals only when a valid re-parent triggers the next
full rebuild. Until a server-side guard lands, validate parent moves
client-side — never set ParentPlaceId to the place itself or any of its
descendants. (Verified live 2026-07-04; a server-side cycle guard is under
owner review.)
User place visibility
Users can be restricted to a subset of the place tree. A place-filtered user sees their granted places plus all descendants — the grant expands down the subtree via the same nested-set ranges described above.
- Grants are managed per user:
AddUserVisibility,BulkAddUserVisibility,DeleteUserVisibility,BulkDeleteUserVisibility,GetUserVisibilities. - Creating a place auto-grants it to the creator.
- The expanded place set is held in a server-side cache
(
UserVisibilityCacheService) and applied to queries via the visibility filters.
Hierarchy changes and visibility grants re-seed the cache asynchronously via a Hangfire job. After a place move or a grant/revoke, a filtered user can briefly see a stale subtree — a just-granted place may not appear immediately, and a just-moved branch may still show under its old scope until the reload completes.
The same cache drives visibility of items, orders, and persons — not just place lists. A user who can't see a place also can't see the items located there or the orders scoped to it, which is why a corrupted tree (see the warning above) or a stale cache surfaces as "missing" records in completely unrelated list endpoints.
Related
- Item lifecycle — item queries are filtered by the same visibility cache; item movement histories reference places.
- Order lifecycle — order visibility follows place visibility.
- Errors & response conventions — including why a not-found parent id doesn't 404.