Model an organisation hierarchy
Departments inside faculties inside colleges, regions inside divisions, branches, cost centres — any "units inside units" structure whose depth differs from one customer to the next. NetForge Pro ships it as a platform piece: the OrgUnit entity and OrgUnitTree, the one service allowed to change it.
Roll up a subtree
Each unit stores its ancestry as a materialised path — /{rootId}/{childId}/ — so "this unit and everything under it" is one indexed prefix scan, not a recursive query per report:
var unit = await db.Set<OrgUnit>().AsNoTracking().FirstAsync(u => u.Id == unitId, ct);
var unitIds = tree.Subtree(unit.Path).Where(u => u.IsActive).Select(u => u.Id);
var headcount = await db.Set<Staff>().CountAsync(s => unitIds.Contains(s.OrgUnitId), ct);Change the tree
Only through the tree service — it owns Path and Depth, and a hand-set value is a quietly wrong rollup rather than an exception:
| Call | What it guarantees |
|---|---|
CreateAsync(name, parentId, kind) | Path and depth from the parent chain; unique among active siblings (ignoring case); at most 10 levels deep. |
MoveAsync(id, newParentId) | The unit's parent, and every descendant's path and depth, change in one statement. Refuses a cycle or a push past the depth limit. |
RenameAsync(id, name) | Nothing beneath changes — paths hold ids, not names. |
RetireAsync(id) / ReactivateAsync(id) | Units are retired, never deleted, so history stays true. Retiring refuses while an active child remains. |
OrgUnitTree.LineageOf(unit) gives the ids from the root down to the unit, for breadcrumbs. Every query runs under the tenant filter, so a tenant can only restructure itself.
Assigning things to units
If your domain assigns people or records to units, add that check to RetireAsync too — retiring a unit something still lives in is exactly the mistake it exists to stop.
Edition
The org-unit tree is part of Pro (--optOrgUnits). See Editions.