Code Generation¶
You describe the device in YAML and write thin C# partials. At build time a source generator turns both into strongly-typed Things, functions, commands, and events — no reflection, no hand-written boilerplate, everything checked by the compiler.
You author Source generator Generated for you
┌───────────────────┐ ┌──────────────────┐ ┌───────────────────────┐
│ adapter- │ │ build-time, │ │ Things · Functions │
│ registration.yaml│ ────► │ no reflection │ ────► │ Commands · Events │
│ access-model.yaml │ │ │ │ Dispatcher · Program │
│ C# partials + │ │ │ │ │
│ handlers │ │ │ │ │
└───────────────────┘ └──────────────────┘ └───────────────────────┘
What you write vs. what's generated¶
| You write by hand | Generated for you |
|---|---|
| YAML — device types, properties, commands, functions | Thing base classes with property validation |
| C# partials — protocol, event translation | Function state machines, typed command classes, event publication methods |
| Command handler methods (matched by signature) | Command dispatcher, routing, DI wiring, entry point |
You never edit generated files. You extend the generated partial classes with your
protocol logic.
What triggers generation¶
The generator reads your YAML and scans your C# for a few shapes. Each one opts your adapter into a capability — no registration call needed.
| The generator detects… | …and adds |
|---|---|
A class implementing IDeviceConnection |
Connection function + lifecycle |
A static PollStatus(Thing, …, ct) method |
Status-poll capability |
An access-model.yaml file |
Access synchronization + typed access records |
capabilities.time_sync: true in YAML |
Time-synchronization capability |
A [BiometricTemplate] partial class |
Serialize / Deserialize / hash |
This is why adapters have almost no wiring code: presence of the right shape is the registration.
Handlers are matched by signature, not name¶
Command handlers are discovered by their shape, not an attribute or a magic method name. Get the signature right and the generator wires it into the dispatcher.
// matched — Task<DeviceCommandResult>, command type, CancellationToken last
public static Task<DeviceCommandResult> Lock(DoorThing door, Access.Lock cmd, Protocol p, CancellationToken ct);
public Task<DeviceCommandResult> Open(Access.Open cmd, Protocol p, CancellationToken ct); // self-handler on the Thing
// NOT matched
public void Handle(Access.Lock cmd); // wrong return type
public Task Handle(Access.Lock cmd, CancellationToken ct); // missing DeviceCommandResult
public Task<DeviceCommandResult> Handle(string cmd, CancellationToken ct); // wrong command type
Detection rules:
- Return type:
Task<DeviceCommandResult>orValueTask<DeviceCommandResult>. - The command parameter is any generated command type; a
CancellationTokenis last. - Middle parameters are resolved from DI (protocol, logger, any registered service).
- Static, separate-instance, or Thing self-handler shapes all work.
- The method name is irrelevant — the command type in the parameter list is what binds it.
The same signature-based detection drives PollStatus handlers (see
patterns/12-status-polling.md) and the canonical
command rules (patterns/04-command-handling.md).
YAML → C# naming¶
Dotted taxonomy ids become nested types; custom functions become a function + interface + state enum. The mapping is mechanical and stable.
PqEvent.*is for the generator only. Adapter code never referencesPqEventorEventBuilder— the build reports it as PQC203. Call the generated method on the Thing instead (see below).
pq.command.access.lock → Access.Lock
pq.command.output.activate → Output.Activate
pq.command.push → Push (top-level, no nesting)
# generator only — adapter code must not use these (PQC203)
pq.event.access.granted → PqEvent.Access.Granted
pq.event.access.door.opened → PqEvent.Access.Door.Opened
pq.event.connection.lost → PqEvent.Connection.Lost
The generator builds the event publication methods from the PqEvent catalog.
Adapter code publishes an event by calling a generated method on the Thing. An action of
one of the type's functions publishes the events that function owns
(pq.event.access.door.opened → door.Opened(timestamp)). Any other event listed under a
device type's events: becomes a named publication method on that Thing. The method is
named after the shortest unique tail of the event id, and carries the event's taxonomy
parameters:
pq.event.technical.fault.hardware → panel.HardwareFault(timestamp, faultDescription, …)
pq.event.system.configuration.changed → panel.ConfigurationChanged(timestamp, …)
See reference/generated-code-api.md.
Custom functions declared in your adapter YAML expand into three types:
Standard functions from the framework taxonomy follow the same shape
(door → DoorFunction · IDoorFunction · DoorState). See
reference/generated-code-api.md for the full
generated surface and how to call it.
Debugging generated code¶
Normal development never requires reading generated output — you work against the typed API. When something looks wrong, though:
- Handler not picked up? Compare its signature against the rules above — one wrong parameter and it is silently skipped.
- Types missing after a YAML edit? The generator caches aggressively — clean and rebuild.
- Wrong name? Naming comes from YAML
idfields; check the dotted path and function keys.
Generated files are read-only. Build output lands in your project's
.GeneratedFiles/folder for inspection only. Never edit it — your changes are overwritten on the next build. Extend thepartialclass instead.
See Also¶
- concepts/framework-architecture.md — what the generated code plugs into at runtime
- reference/generated-code-api.md — using the generated event methods, function shortcuts, and command classes
- patterns/04-command-handling.md — the canonical command handler rules
- patterns/12-status-polling.md — the
PollStatusdetection shape - reference/yaml-properties.md — YAML property model used across generation