PQC232 — Event publication must be encapsulated in the owning Thing¶
A class publishes an event through a reference to a Thing it does not own.
PublishEvent belongs to the Thing whose event it is. Only the Thing's generated methods call it. A router, a command handler, a polling class or an extension method holding a reference to a Thing must not publish on its behalf.
// reported
private static ValueTask<bool> Route(AcmeDoor door, DeviceEvent evt) =>
door.PublishEvent(forcedEvent);
// ^^^^ the router is publishing a door's event
Why the build refuses this¶
State and audit drift apart. A Thing emitting an event usually has to update its own function state in the same step — that is exactly what the generated methods do. An external PublishEvent writes the audit record but cannot touch the Thing's private state. The audit log then says the door was forced while the door's own state says otherwise.
The Thing stops describing itself. When publication lives on the Thing, everything it can emit is visible on one surface. When it lives in routers and handlers, answering "what does this door report?" means reading every file in the adapter.
Cross-cutting behaviour has nowhere to live. Severity policy, throttling, ordering, deduplication — all of it can be added once on a Thing that owns its publication. It cannot be added to thirty scattered call sites.
PublishEvent is public only because generated code has to reach it across the partial-class boundary. That visibility is a technical necessity, not an invitation.
How to fix it¶
Declare the event on the device type¶
Add the event to the type in adapter-registration.yaml:
A named publication method appears on the Thing, and the router calls that instead:
private static ValueTask<bool> Route(AcmeDoor door, DeviceEvent evt) =>
door.DoorForced(Timestamp(evt));
No PublishEvent remains in your code. The method name comes from the event id, shortened to the fewest trailing segments that stay unique on that type, so pq.event.access.door.forced becomes DoorForced. Some events carry a recommended name in the taxonomy where the short one would be unclear — pq.event.system.firmware.update.started becomes FirmwareUpdateStarted, not UpdateStarted.
If an action of one of the type's functions already publishes that event, declaring it changes nothing: the function action is the Thing's named surface for it and updates function state in the same call, so call the action — door.Opened(timestamp). Full naming, parameter and inheritance rules are in Declared Event Methods.
Do not write the method by hand. A partial that builds the event from PqEvent is reported as PQC203. Every call shape fits the generated method — the generator reads the parameters from the taxonomy and emits required ones as plain arguments, optional ones as nullable with a default:
// generated on Panel — do not write this yourself
public ValueTask<bool> HardwareFault(DeviceTimestamp timestamp, string faultDescription, string? faultCode = null)
A value the device reports only sometimes goes in as null when it is missing. A value the protocol implies goes in as a constant at the call site.
When the receiver is a framework type¶
A second wording appears when the reference is typed as a framework class rather than one of your device types:
publishes an event through 'Thing', a framework type no device type of this adapter owns
// reported with the second wording
private static ValueTask<bool> PublishUpdateStarted(Thing module, DeviceTimestamp ts) =>
module.PublishEvent(updateStartedEvent);
The fix is the same, but there is a decision to make first: which device type owns this event? A parameter typed as the framework base accepts several of your types, and none of them can carry the event until you say which one it belongs to. Declare it there, then take the concrete type as the parameter.
Note that an abstract device type of your own is not this case. A base panel type carrying the events its concrete variants share is proper ownership — declare the events on the base, and the generated methods are inherited by every type extending it. That is the ordinary wording, and the ordinary fix.
What is not reported¶
Generated methods. The generated function actions and declared event methods call PublishEvent from inside the Thing — that is the whole point. A hand-written partial that does the same is not reported here; PQC203 reports it.
Calling a Thing's named method from outside. door.Opened(timestamp), reader.AccessGranted(timestamp, personId: personId) — these generated methods are the Thing's public surface. Calling them from a router is exactly what should happen.
Framework event helpers. Helpers the framework provides for unresolved or unknown occurrences wrap publication behind a coordinated API and are not violations.
If you disagree with a report¶
Do not suppress it. A wrong report is a bug in the check — report it with the code that triggered it.