Thing Queries¶
Query the Thing tree to find other Things and Functions relative to your position in the hierarchy.
When You Need This¶
- Your Function needs to coordinate with other Things (e.g., panel locks all doors)
- Your command handler needs to find related devices (e.g., find all readers under a door)
- You need to discover siblings, descendants, or all devices in the adapter tree
Prerequisites¶
How It Works¶
Thing Queries let you ask "give me everything matching type T relative to where I am in the tree." The framework resolves these automatically through dependency injection.
Your Thing's position in the tree
determines what each query returns:
Adapter (root)
/ \
Door1 Door2 <-- ISiblings from Door1's perspective
/ \
Reader1 REX1 <-- IDirect from Door1's perspective
|
SubDevice <-- IDescendants includes everything below
Query Types¶
Four traversal strategies, each a nested interface inside IThingQuery:
| Interface | What it returns |
|---|---|
IThingQuery.IDescendants<T> |
Entire subtree below you (recursive) |
IThingQuery.IDirect<T> |
Only immediate children |
IThingQuery.ISiblings<T> |
Other children of your parent |
IThingQuery.IAll<T> |
Entire adapter tree from root |
All four implement IEnumerable<T> -- use them directly in foreach or with LINQ.
Usage¶
Request any query as a constructor parameter. The framework injects it automatically, scoped to the requesting Thing.
Descendants -- entire subtree¶
public class PanelFunction(IThingQuery.IDescendants<DoorFunction> doors)
{
public void LockdownAll()
{
foreach (var door in doors)
door.Lock();
}
}
Panel <-- you are here
/ \
Door1 Door2
| \ |
Reader REX Reader
IDescendants<DoorFunction> returns:
Door1's DoorFunction, Door2's DoorFunction
(everything below Panel that matches)
Direct -- immediate children only¶
public class DoorController(IThingQuery.IDirect<ReaderFunction> readers)
{
public bool HasMultipleReaders => readers.Count() > 1;
}
Door <-- you are here
/ \
Reader1 REX <-- IDirect searches here
|
SubDevice <-- NOT included (not direct child)
IDirect<ReaderFunction> returns:
Reader1's ReaderFunction only
Siblings -- same parent, excluding self¶
public class DoorFunction(IThingQuery.ISiblings<DoorFunction> otherDoors)
{
public void InterlockCheck()
{
// ensure no sibling door is open before unlocking
if (otherDoors.Any(d => d.IsOpen))
return;
}
}
Panel
/ \
Door1 Door2 Door3
^
you are here
ISiblings<DoorFunction> returns:
Door2's DoorFunction, Door3's DoorFunction
(same parent, excluding yourself)
All -- entire adapter tree¶
public class DiagnosticsFunction(IThingQuery.IAll<IAccessPoint> allAccessPoints)
{
public int TotalAccessPoints => allAccessPoints.Count();
}
Adapter (root)
/ \
Door1 Door2
/ \ |
Reader REX Reader
IAll<IAccessPoint> returns:
every IAccessPoint in the entire tree
(walks up to root, then traverses everything)
What Gets Matched¶
Queries match both Things and their Functions against type T:
Door (Thing)
|-- DoorFunction <-- matched if T = DoorFunction
|-- ContactSensor <-- matched if T = ContactSensor
|
Reader (Thing) <-- matched if T = Reader
|-- ReaderFunction <-- matched if T = ReaderFunction
If you query for an interface, any Thing or Function implementing that interface is returned:
// returns all Things and Functions implementing IAccessPoint
IThingQuery.IDescendants<IAccessPoint> accessPoints
Evaluation Is Lazy¶
Queries evaluate the tree each time you enumerate them. This means:
- They always reflect the current state of the tree
- No stale data if the tree was rebuilt
- No upfront cost if you don't enumerate
public class PanelFunction(IThingQuery.IDescendants<DoorFunction> doors)
{
// tree is traversed each time you iterate
public void CheckAll()
{
foreach (var door in doors) // traverses now
door.Check();
foreach (var door in doors) // traverses again, fresh
door.Report();
}
}
Common Patterns¶
Combine with LINQ¶
public class PanelFunction(IThingQuery.IDescendants<DoorFunction> doors)
{
public DoorFunction? FindByName(string name) =>
doors.FirstOrDefault(d => d.Name == name);
public int OpenDoorCount =>
doors.Count(d => d.IsOpen);
}
Multiple queries in one class¶
public class ZoneController(
IThingQuery.IDirect<DoorFunction> myDoors,
IThingQuery.ISiblings<ZoneController> otherZones,
IThingQuery.IAll<IAccessPoint> allAccessPoints)
{
// use each query for different purposes
}
Common Mistakes¶
1. Expecting self in results¶
Queries never include the requesting Thing itself in IDescendants or ISiblings. If you need to include yourself, handle it separately.
2. Caching enumeration results¶
// WRONG - snapshot goes stale if tree rebuilds
private readonly List<DoorFunction> _cached;
public MyFunction(IThingQuery.IDescendants<DoorFunction> doors)
{
_cached = doors.ToList(); // stale after rebuild
}
// RIGHT - enumerate when needed
private readonly IThingQuery.IDescendants<DoorFunction> _doors;
public MyFunction(IThingQuery.IDescendants<DoorFunction> doors)
{
_doors = doors; // keep the query, enumerate later
}
Next: Persistent Storage - Storing device-scoped settings