Captures the brainstormed design for a new Expression trigger: a read-only boolean C# expression evaluated on attribute updates, edge-triggered for scripts and level-based for alarms, compiled against a restricted read-only globals type.
5.9 KiB
Expression Trigger for Template Scripts and Alarms — Design
Date: 2026-05-16 Status: Approved (brainstorming) — implementation plan to follow
Context
Template scripts and template alarms can only be triggered by single-attribute
conditions. Scripts support Interval, ValueChange, Conditional
({attributeName, operator, threshold} — one attribute, numeric compare),
and Call. Alarms support ValueMatch, RangeViolation, RateOfChange, and
HiLo — all single-attribute. There is no way to trigger on a relationship
between multiple attributes (e.g. "speed is high and mode is Run").
This design adds an Expression trigger: a user-supplied read-only boolean
C# expression, evaluated whenever an instance attribute updates, that fires the
script / activates the alarm when it returns true. It generalizes the existing
single-attribute Conditional trigger.
Decisions taken during brainstorming
- The trigger is a read-only boolean expression — no
External/Database/Notify/CallScriptside effects. It must be cheap and safe to run on every attribute update. - Scripts fire edge-triggered — once per
false→truetransition. - Alarms are level-based — active while the expression is true, clear when false (consistent with all existing alarm trigger types).
- Evaluation approach B — compile against a restricted read-only globals type, so read-only is enforced, not merely conventional. Reuses the existing Roslyn compilation pipeline.
Design
1. Trigger model & storage
- Scripts:
TemplateScript.TriggerType(string?) gains the value"Expression".TriggerConfigurationJSON is{ "expression": "<C#>" }. - Alarms:
AlarmTriggerTypeenum gains a memberExpression.TriggerConfigurationJSON is the same{ "expression": "<C#>" }. - The expression is a bare C# boolean expression (no
returnkeyword — Roslyn scripting returns the trailing expression's value), e.g.Attributes["Speed"] > 1000 && (string)Attributes["Mode"] == "Run". - Entity types unchanged: both
TriggerConfigurationfields staystring?.
Adding the AlarmTriggerType member touches three switch sites:
AlarmActor.ParseEvalConfig, AlarmActor.HandleAttributeValueChanged,
AlarmTriggerConfigCodec.
2. Runtime evaluation
TriggerExpressionGlobals(new,ScadaLink.SiteRuntime) — a read-only globals type exposing onlyAttributes["X"],Children["C"].Attributes["X"], andParent.Attributes["X"], backed by an in-memory snapshot dictionary. No side-effecting APIs. A missing attribute reads asnull(never throws).- The expression is compiled once via the existing Roslyn pipeline (same
forbidden-API trust checks) against
TriggerExpressionGlobals; the compiled delegate is cached on the actor. - Attribute snapshot:
ScriptActorandAlarmActoralready receive everyAttributeValueChanged. Each keeps a localDictionary<string,object?>snapshot — seeded from the instance's initial attribute set at startup, then updated on each change. The expression evaluates against the snapshot — noAskback to theInstanceActor; cheap and re-entrancy-free. - On each
AttributeValueChanged: update snapshot → run cached expression →bool.- Script (edge): track the previous result; on
false→true, run the script (spawnScriptExecutionActor, as the other triggers do). - Alarm (level): the
boolfeeds the existing binary Normal↔Active state machine — raise on→Active, clear on→Normal.
- Script (edge): track the previous result; on
- Cost per attribute update: one cached-delegate call + one bool compare.
3. Editors & analysis
ScriptTriggerEditor: addExpressiontoScriptTriggerKindandScriptTriggerConfigCodec(round-trips{ expression }).AlarmTriggerEditor: add anExpressioncase to its trigger@switch.- Both render the same expression panel: a compact
MonacoEditor(~120 px) with C# syntax,Attributes["..."]completion driven by the template's attribute metadata (self / children / parent), and live compile diagnostics. A one-line hint summarizes what fires. - Analysis: reuse the existing
Templateanalysis kind — completion and diagnostics work with no new analyzer code. Editor completion is slightly permissive (also showsInstance/External), but the runtime's restrictedTriggerExpressionGlobalsis what enforces read-only. A dedicated strict analysis kind is a possible later refinement, out of scope here.
4. Error handling & validation
- Pre-deployment: extend
ValidationServiceto compile-check expression triggers (againstTriggerExpressionGlobals); compile errors block deployment and surface like other validation errors. UnknownAttributes["..."]keys are flagged as the existing trigger-reference validation does. - Runtime — expression throws: caught; treated as
falsefor that update; a script-error event is written to the site event log. The actor never crashes. - Non-bool result: treated as
falseand logged. - Missing attribute: reads as
null(handled inTriggerExpressionGlobals). - Blank expression: the trigger is inert; validation emits a warning.
5. Testing & verification
- Unit: codec round-trip for script and alarm
{ expression }; expression compile (valid + invalid). - Runtime: deploy an instance with an expression-triggered script and
alarm; drive attribute updates (bound Test Run / CLI); confirm the script
fires only on
false→trueand the alarm raises/clears with the expression. - UI: the expression panel in both editors; save → reopen round-trip.
Implementation tasks
- #25 — Implement expression trigger model + codecs
- #26 — Implement runtime expression evaluation (blocked by #25)
- #27 — Add expression panel to the trigger editors (blocked by #25)
- #28 — Validate expression triggers pre-deployment (blocked by #25, #26)