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¶
- Address resolution is for event routing - maps protocol addresses to Thing instances
- IProtocolAddressable - static addresses from configuration (most common)
- IDynamicProtocolAddressable - runtime-assigned addresses (hot-plug scenarios)
- FindByAddress() - primary tool for event routing in Transport/Protocol
- IThingQuery - type-safe hierarchy queries via DI (NEW feature)
- Choose query strategy based on traversal needs (descendants/direct/siblings/all)
See Also¶
- Pattern 2: Event Translation - Using resolved Things to publish events
- Pattern 4: Command Handling - Framework routes commands to correct Thing by DeviceId
- Pattern 1: Access Synchronization - Address allocation for access entities