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. It may be called from inside that Thing — unqualified, or through this — and from nowhere else. 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(PqEvent.Access.Door.Forced.At(Timestamp(evt), door).Build());
// ^^^^ 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¶
This is the fix to reach for first. 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.
Write the method by hand¶
Reach for this when the generated wrapper is not enough — typically because the event carries parameters the declaration cannot express. Add a partial for the Thing:
public partial class AcmePanel
{
/// <summary>
/// Publishes a configuration change with a human-readable description.
/// </summary>
public ValueTask<bool> ConfigurationChanged(DeviceTimestamp timestamp, string description) =>
PublishEvent(
PqEvent.System.Configuration.Changed.At(timestamp, this)
.WithParameter("changeDescription", description)
.Build());
}
A hand-written method always wins: when a name is already declared by hand, no wrapper is generated for it, so your richer version stands.
Either way the PublishEvent call moves onto the Thing and the caller invokes something named after what happened.
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(PqEvent.System.Firmware.Update.Started.At(ts, module).Build());
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¶
Publishing your own event. PublishEvent(...) or this.PublishEvent(...) inside the Thing's own class is the compliant shape — that is the whole point.
Calling a Thing's named method from outside. door.Opened(timestamp), reader.AccessGranted(timestamp, personId) — generated or hand-written, these 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.