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 and event classes |
| 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.
pq.command.access.lock → Access.Lock
pq.command.output.activate → Output.Activate
pq.command.push → Push (top-level, no nesting)
pq.event.access.granted → PqEvent.Access.Granted
pq.event.access.door.opened → PqEvent.Access.Door.Opened
pq.event.connection.lost → PqEvent.Connection.Lost
An event listed under a device type's events: also becomes a named publication method on
that Thing, so the router calls the occurrence by name instead of assembling an event. The
method is named after the shortest unique tail of the event id, and carries the event's
taxonomy parameters:
pq.event.access.door.forced → door.DoorForced(timestamp)
pq.event.system.configuration.changed → panel.ConfigurationChanged(timestamp, …)
A method of that name you write by hand always wins, so a richer variant stays yours. 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 builders, 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