Skip to content

Pattern 7: Address Resolution and Thing Queries

Why Address Resolution?

Core problem: Device events contain protocol-specific addresses (device ID, door number, reader index), but PQ works with Thing instances (typed objects in a hierarchy).

Solution: Address resolution maps protocol addresses → Thing instances.

Device Event:
  DeviceID: 123
  DoorID: 5
  EventCode: 0x1000 (Access Granted)

Address Resolution:
  123 → ControllerDevice
  5 → DoorDevice (child of controller)

Result:
  await PublishEvent(PqEvent.Access.Granted...) // Protocol publishes

Without address resolution: You can't route events to the correct Thing or execute commands.


Static Addressing (IProtocolAddressable)

Use case: Address known at configuration time (defined in JSON config)

YAML Configuration

# In adapter-registration.yaml
device_types:
  - type_id: "Door"
    properties:
      address:
        type: uint
        required: true
        description: "Protocol door number (1-32)"

Generated Code

Framework generates Thing class implementing IProtocolAddressable<uint>:

// Generated partial class
internal sealed partial class DoorDevice : IProtocolAddressable<uint>
{
    public uint Address { get; set; } // from JSON config
}

JSON Configuration

{
  "Devices": [
    {
      "Type": "Door",
      "DeviceId": "a1b2c3d4-...",
      "Address": 5,
      "Name": "Main Entrance"
    }
  ]
}

Dynamic Addressing (IDynamicProtocolAddressable)

Use case: Address discovered or assigned at runtime (hot-plug devices, SDK discovery)

YAML Configuration

device_types:
  - type_id: "Reader"
    dynamic_address:
      type: uint
      description: "Runtime-assigned reader index"

Generated Code

// Generated partial class
internal sealed partial class ReaderDevice : IDynamicProtocolAddressable<uint>
{
    public DynamicAddress<uint> Address { get; set; } = DynamicAddress<uint>.None;
}

Runtime Assignment

// In Protocol.cs - device discovery
public async Task DiscoverDevices(CancellationToken ct)
{
    var sdk = _transport.GetSdkClient();
    var devices = await sdk.GetDeviceList(ct);

    foreach (var deviceInfo in devices)
    {
        var reader = new ReaderDevice(_protocol, _logger)
        {
            Address = DynamicAddress.Some(deviceInfo.Id), // assign at runtime
            Name = deviceInfo.Name,
            DeviceId = Guid.NewGuid(),
            Parent = _controller
        };

        _controller.AddChild(reader);
    }
}

Finding Things by Address (Event Routing)

FindByAddress in Transport/Protocol

Pattern: Incoming events contain protocol addresses. Use FindByAddress() to route to correct Thing.

// In Transport.cs - event received from SDK
private async Task OnEventReceived(DeviceEvent evt)
{
    // resolve protocol address → Thing instance
    var device = FindByAddress(evt.DeviceID) as ControllerDevice;
    if (device is null)
    {
        Logger.LogWarning("Event for unknown device {DeviceId}", evt.DeviceID);
        return;
    }

    // now we have typed Thing - route event to Protocol
    await device.Functions.Connection.Protocol.DispatchEvent(evt, ct);
}

Multi-Level Address Resolution

Common pattern: Event has both controller ID and door ID.

private async Task OnEventReceived(DeviceEvent evt)
{
    // find controller by device ID
    var controller = FindByAddress(evt.DeviceID) as ControllerDevice;
    if (controller is null) return;

    // find door within controller's children
    var door = controller.Children
        .OfType<DoorDevice>()
        .FirstOrDefault(d => d.Address == evt.DoorID);

    if (door is null)
    {
        Logger.LogWarning("Door {DoorId} not found on device {DeviceId}",
            evt.DoorID, evt.DeviceID);
        return;
    }

    // translate device event → PQ event
    // Thing publishes its own event (not Protocol!)
    await door.PublishAccessGranted(
        evt.PersonId,
        evt.CredentialId,
        evt.Timestamp,
        ct
    );
}

IThingQuery - DI-Based Hierarchy Queries

Use case: Find Things by type in the hierarchy without direct references.

Benefit: Type-safe queries, automatic DI resolution based on requesting Thing's position.

Four Query Strategies

// 1. IDescendants<T> - recursive search in entire subtree
IThingQuery.IDescendants<DoorDevice> doors;

// 2. IDirect<T> - only direct children
IThingQuery.IDirect<ReaderDevice> readers;

// 3. ISiblings<T> - siblings (same parent, excluding self)
IThingQuery.ISiblings<InputDevice> siblingInputs;

// 4. IAll<T> - entire adapter tree from root
IThingQuery.IAll<ControllerDevice> allControllers;

Injection in Thing Class

// In DoorDevice.cs
internal sealed partial class DoorDevice(
    Protocol protocol,
    ILogger<DoorDevice> logger,
    IThingQuery.ISiblings<ReaderDevice> siblingReaders) // ← DI-injected query
{
    /// <summary>
    /// Handles door forced event - notify all readers on same controller.
    /// </summary>
    private async Task OnDoorForced(DeviceTimestamp timestamp)
    {
        await this.ForcedOpen(timestamp);

        // query siblings - all readers on same controller
        foreach (var reader in siblingReaders)
        {
            Logger.LogInformation("Door {DoorName} forced, disabling reader {ReaderName}",
                Name, reader.Name);
            // update related reader state only if the framework function exposes a matching action
        }
    }
}

Injection in Command Handler

// In DoorCommands.cs
public static class DoorCommands
{
    /// <summary>
    /// Emergency open all doors in building.
    /// </summary>
    public static async Task<DeviceCommandResult> EmergencyOpenAll(
        ControllerDevice controller,
        Access.EmergencyOpen command,
        Protocol protocol,
        IThingQuery.IDescendants<DoorDevice> allDoors) // ← DI-injected query
    {
        foreach (var door in allDoors)
        {
            await protocol.OpenDoor(door.Address, CancellationToken.None);
            await door.UnsecuredRemote(DeviceTimestamp.UtcNow, command.OperatorIdentity);
        }

        return DeviceCommandResult.Succeeded();
    }
}

Query Strategy Selection Guide

Strategy Use When Example
IDescendants Need all Things of type in subtree (recursive) Find all doors under controller
IDirect Need only direct children Find readers directly under door
ISiblings Need peers at same level Find other inputs on same controller
IAll Need all Things across entire adapter Find all controllers in building

Practical Examples

Example 1: Access Event with Door Lookup

// Event from device
private async Task OnAccessGranted(DeviceEvent evt)
{
    // resolve controller
    var controller = FindByAddress(evt.ControllerID) as ControllerDevice;
    if (controller is null) return;

    // find door by address
    var door = controller.Children
        .OfType<DoorDevice>()
        .FirstOrDefault(d => d.Address == evt.DoorAddress);

    if (door is null) return;

    // Thing publishes its own event
    await door.PublishAccessGranted(
        evt.PersonId,
        evt.CredentialId,
        evt.Timestamp,
        ct
    );

    // trigger door open (function method)
    await door.Opened(DeviceTimestamp.UtcNow);
}

Example 2: Command Routing with Address

// Command arrives from PQ server
public static async Task<DeviceCommandResult> Open(
    DoorDevice door,     // framework routes to correct Thing by DeviceId
    Access.Open command,
    Protocol protocol)
{
    // door.Address = protocol-specific address (e.g., door number 5)
    await protocol.OpenDoor(door.Address, CancellationToken.None);
    await door.UnsecuredRemote(DeviceTimestamp.UtcNow, command.OperatorIdentity);
    return DeviceCommandResult.Succeeded();
}

Example 3: Find Reader for Door Forced Event

// In DoorDevice.cs
internal sealed partial class DoorDevice(
    Protocol protocol,
    ILogger<DoorDevice> logger,
    IThingQuery.IDirect<ReaderDevice> doorReaders) // readers under THIS door
{
    private async Task OnDoorForcedOpen(DeviceTimestamp timestamp)
    {
        // update door state
        await this.ForcedOpen(timestamp);

        // publish intrusion event
        await PublishEvent(
            PqEvent.Intrusion.Forced
                .At(timestamp, this)
                .WithReason("Door forced open")
                .Build()
        );

        // disable all readers for this door
        foreach (var reader in doorReaders)
        {
            await reader.Reader.SetOffline(CancellationToken.None);
        }
    }
}

When to Use Which

Scenario Solution Configuration
Fixed hardware (known addresses) IProtocolAddressable YAML property + JSON config
Hot-plug devices IDynamicProtocolAddressable YAML dynamic_address + runtime assignment
Event routing FindByAddress() In Transport/Protocol event handlers
Cross-Thing communication IThingQuery.IDescendants DI injection in Thing/command handler
Sibling notification IThingQuery.ISiblings DI injection for same-level Things
Building-wide operations IThingQuery.IAll DI injection for global queries

Key Takeaways

  1. Address resolution is for event routing - maps protocol addresses to Thing instances
  2. IProtocolAddressable - static addresses from configuration (most common)
  3. IDynamicProtocolAddressable - runtime-assigned addresses (hot-plug scenarios)
  4. FindByAddress() - primary tool for event routing in Transport/Protocol
  5. IThingQuery - type-safe hierarchy queries via DI (NEW feature)
  6. Choose query strategy based on traversal needs (descendants/direct/siblings/all)

See Also