Skip to content

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

  • Things - understand the Thing hierarchy
  • Functions - understand what Functions are

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