Pattern 20: Scenario List¶
When to Use¶
Your integration needs to know which correlation scenarios exist in PQ — for example, to offer them in the external system's own configuration or to show a scenario name in its log. The framework keeps the list current for you.
Declare the capability in adapter-registration.yaml:
Without it, IScenarioCatalog is not registered and resolving it from DI fails.
IScenarioCatalog¶
Pq.Adapters.Framework.Capabilities.Scenarios.IScenarioCatalog is available through DI once the
capability is declared. There is one instance per adapter process, shared by all devices.
| Member | Description |
|---|---|
Task<IReadOnlyList<ScenarioInfo>> GetScenarios(CancellationToken) |
All scenarios, disabled ones included |
event EventHandler? Changed |
Raised after the list was reloaded |
ScenarioInfo is a record: Id (Guid), Name (string), Enabled (bool). The list contains
every scenario; filter on Enabled yourself if you only want active ones.
Lifecycle¶
- Lazy. Nothing happens until the first
GetScenarioscall. That call subscribes to scenario changes and loads the list from the PQ server. - Kept current. Whenever a scenario is created, renamed or otherwise changed, deleted, enabled or
disabled, the framework reloads the whole list and raises
Changed. CallGetScenariosagain to read it. - Failure. If the first load fails,
GetScenariosthrows; the next call retries. If a reload fails, the previous list is kept and the next scenario change retries. - Limitation. A change made while the adapter is disconnected from the message bus is picked up with the next scenario change, not on reconnect.
Example¶
Push the list to the external system after connecting and after every change. Subscribe from a long-lived service (not from a Thing), and unsubscribe when it stops — the catalog lives for the whole process and would otherwise keep your handler alive.
public sealed class ScenarioPublisher(IScenarioCatalog catalog, Protocol protocol) : IDisposable
{
public async Task Start(CancellationToken ct)
{
catalog.Changed += OnChanged;
await Push(ct);
}
public void Dispose() => catalog.Changed -= OnChanged;
private async void OnChanged(object? sender, EventArgs e)
{
try
{
await Push(CancellationToken.None);
}
catch (Exception ex)
{
// log: the next change pushes the full list again
}
}
private async Task Push(CancellationToken ct)
{
var scenarios = await catalog.GetScenarios(ct);
await protocol.SendScenarioList(scenarios.Where(s => s.Enabled), ct);
}
}
Protocol.SendScenarioList stands for your own protocol call. Always send the full list: it is small,
and a full list makes a missed or repeated change harmless.
Scenario Notify Command¶
To let PQ tell your external system that a scenario fired, support the framework command
pq.command.scenario.notify. List it on the device type that represents the external system:
In the scenario editor, the operator adds a Send command action, picks that device and the Notify Scenario command. There is nothing else to fill in: the server sets every parameter from the scenario that fired.
| Parameter | Type | Content |
|---|---|---|
ScenarioName |
string |
Scenario name as it is when the scenario fires |
ScenarioId |
Guid? |
Scenario identifier |
Persons |
EntityRef[]? |
Every distinct person named by the events that fired the scenario |
Devices |
EntityRef[]? |
Every distinct device named by the events that fired the scenario |
EntityRef (Pq.Domain.Api.Adapter) is a record: Id (Guid), Name (string). For a scheduled or
manual run there are no firing events, so both lists are empty.
The command does not need capabilities: scenarios — that flag is only for the scenario list. Write the
handler like any other:
public async Task<DeviceCommandResult> Notify(
PartnerSystem system,
Scenario.Notify command,
Protocol protocol,
CancellationToken ct)
{
await protocol.SendScenarioFired(command.ScenarioName, command.Persons ?? [], command.Devices ?? [], ct);
return DeviceCommandResult.Succeeded();
}
Protocol.SendScenarioFired stands for your own protocol call.