Pattern 9: Event Publishing Best Practices¶
Prefer Function Methods¶
Use generated function methods whenever available. They publish the mapped event and update function status in one call.
var timestamp = evt.Timestamp;
await door.Opened(timestamp);
await door.LockedRemote(timestamp);
await zone.Alarm(timestamp);
await output.Activated(timestamp);
Call methods directly on the Thing. Use function properties mainly for StatusBatch polling updates.
PublishEvent Belongs to the Thing — Never Call It From Outside¶
PublishEvent is the Thing's own concern. Never call it on a Thing reference you received
from outside (channel.PublishEvent(...), door.PublishEvent(...)) in an event router, command
handler, or status-polling class.
It is public only because generated methods must reach it across the partial-class boundary —
that visibility is a technical necessity, not an invitation. Publishing from outside the Thing
bypasses the Thing's ability to update its own state in the same step, and scatters a Thing's
events across the codebase instead of keeping them on one surface. This is RULE-032.
// ❌ WRONG — router publishes on a Thing it was handed
private static ValueTask<bool> ChannelEnabled(Channel channel, HistoryEvent evt) =>
channel.PublishEvent(...);
// ✅ RIGHT — declare pq.event.system.device.enabled on Channel; the router calls the generated method
Route<Channel>(router, EventType.Door, 7, e => e.Parameter3, (c, e) => c.DeviceEnabled(Timestamp(e)));
The build reports an outside call as PQC232 — see the diagnostic page for the full explanation.
Declare The Event And The Method Appears¶
This is how an event no function action covers gets published. List the event under the device type in
adapter-registration.yaml and the generator puts a named publication method on the Thing:
// router calls the generated method — no PublishEvent anywhere in your code
await panel.ConfigurationChanged(timestamp);
The name is the fewest trailing segments of the event id that stay unique on that type, so
pq.event.access.door.forced becomes DoorForced. The event's taxonomy parameters become
method parameters: required as plain arguments, optional as nullable with a default. Pass the
values the device gave you — a dropped parameter is a thinner audit record.
See Declared Event Methods for names, parameters and inheritance.
An action of one of the type's functions that already publishes the event produces no generated method, on purpose: call the action instead.
Never Build The Event By Hand¶
Adapter code never references PqEvent and never calls EventBuilder.Event(...) — not in a
router, not in a partial of the Thing. The build reports both as
PQC203. Every call shape fits the generated method:
- a value the device reports only sometimes → pass
nullwhen it is missing - a value the protocol implies → pass a constant at the call site
// generated on Panel — do not write this yourself
public ValueTask<bool> HardwareFault(DeviceTimestamp timestamp, string faultDescription, string? faultCode = null)
// router
await panel.HardwareFault(timestamp, faultDescription: "Device licence removed");
When To Use Which¶
| Scenario | Use | Example |
|---|---|---|
| Standard function state/event | Function method | await door.Opened(ts) |
| Taxonomy event with no function behind it | Declare it in events: |
await panel.ConfigurationChanged(ts) |
| Status snapshot from polling | StatusBatch |
batch.Set(OutputState.Active) |
| Access grant/deny with identity | Function method | await reader.AccessGranted(ts, credentialId: credentialId, personId: personId) |
| Unmapped vendor event | Extension method | thing.Unknown(ts, eventType, reason) |
| Unresolved Thing | Extension method | thing.Unknown(ts, eventType, reason) with unresolved diagnostics |
| Publish from a router/command/poll | Generated method on the Thing | await channel.DeviceEnabled(ts) — never channel.PublishEvent(...) (RULE-032) |
See Also¶
- Pattern 2: Event Translation - device event mapping
- Function Call Patterns - generated function method style
- Declared Event Methods - naming, parameters, inheritance
- PQC203 - the diagnostic that forbids hand-built events
- PQC232 - the diagnostic that enforces encapsulation