Compact Native-AOT win-x64 console app DELMIA invokes to notify ScadaBridge of a recipe download via POST /api/DelmiaRecipeDownload (X-API-Key). Drop-in CLI/output parity with legacy WWNotifier; appsettings.json + SCADABRIDGE_API_KEY env var; comma-list base URLs with connect-failure-only failover.
8.0 KiB
Design — Delmia Recipe-Download Notifier (ZB.MOM.WW.ScadaBridge.DelmiaNotifier)
Date: 2026-06-26 · Status: Approved (brainstorming) · Next: implementation plan (writing-plans)
Purpose
A compact Windows console application that DELMIA Apriso shells out to on each recipe/NC-program
download to notify the plant system that a recipe was placed at a path for a machine. It is the
modern replacement for the legacy WWNotifier (see
../former-api-specs/dnc/Delmia-Integration-API.md,
Surface B), repointed from the old Wonderware /notify receiver to the ScadaBridge Inbound API
method DelmiaRecipeDownload.
It is a strict drop-in for Delmia's existing call site: same command-line flags, same YES/NO
stdout contract, same exit-code semantics — so Delmia's invocation and output parsing are unchanged.
Key decisions (from brainstorming)
| Decision | Choice |
|---|---|
| Target | ScadaBridge Inbound API POST {baseUrl}/api/DelmiaRecipeDownload |
| Auth | X-API-Key: <key> header |
| CLI/output | Exact parity with legacy WWNotifier (flags + YES/NO + exit code) |
| Config | appsettings.json next to the exe (URLs, timeout, optional log path) |
| Secret | API key from env var SCADABRIDGE_API_KEY — never in a file |
| Packaging | Self-contained Native AOT, win-x64 (fast startup for per-invocation use) |
| Failover | Comma-list of base URLs; advance only on connect failure |
| Location | New project src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier, in ZB.MOM.WW.ScadaBridge.slnx |
| Implementation | Approach A — zero-dependency single-file; hand-rolled arg parser, System.Text.Json source-gen, raw HttpClient |
| Exe name | AssemblyName = WWNotifier (literal drop-in for Delmia's existing call path) |
Architecture
Single-purpose console app, no DI/generic host. One Program.cs orchestrates four small pieces:
- Arg parser — hand-rolled, the 6 legacy flags → an in-memory request model.
- Config loader — reads
appsettings.json(viaSystem.Text.Jsonsource generator) → POCO; reads the API key from the environment. - Notifier — builds the JSON payload and runs the failover POST loop behind a small seam (interface/delegate) so the loop + result mapping are unit-testable without real HTTP.
- Result reporter — maps the outcome to the
YES/NO+ exit-code contract (stdout) and writes diagnostics (stderr + optional log file).
Zero third-party NuGet dependencies (BCL only) to keep the Native-AOT surface minimal and trim-clean.
CLI contract (drop-in parity)
| Short | Long | Required | → payload field |
|---|---|---|---|
-d |
--downloadpath |
yes | DownloadPath |
-m |
--machine |
yes | MachineCode |
-w |
--workorder |
yes | WorkOrderNumber |
-p |
--partnumber |
yes | PartNumber |
-s |
--seqop |
no | JobStepNumber |
-u |
--username |
no | Username |
- stdout: exactly
YESon success, orNOfollowed by a reason line on failure. Nothing else is written to stdout (Delmia parses it). - exit code:
0on success,-1on failure (matches the legacyEnvironment.ExitCode). - A missing required flag →
NO+ reason, exit-1, no HTTP attempt.
Configuration & secret
appsettings.json placed next to the exe, loaded directly into a small POCO with a
JsonSerializerContext source generator (no reflection-based binder):
{
"ScadaBridge": {
"BaseUrls": "http://host-a:8085,http://host-b:8085",
"TimeoutSeconds": 30,
"LogPath": "logs/delmia-notifier.log"
}
}
BaseUrls— comma-separated failover list (legacy-style). The method path/api/DelmiaRecipeDownloadis appended by the app; each entry is a base URL only.TimeoutSeconds— per-attemptHttpClient.Timeout(default 30).LogPath— optional diagnostic log file (relative to the exe). Omit to log to stderr only.- API key comes from
SCADABRIDGE_API_KEY. If unset/empty →NO+ "API key not configured", exit-1, no attempt. The key is never read from or written to a file.
Request / response
- Per attempt:
POST {baseUrl}/api/DelmiaRecipeDownload, headerX-API-Key: <key>,Content-Type: application/json, body = the flatRecipeDownloadJSON. - DTOs (local, source-gen serializable):
RecipeDownload { MachineCode, DownloadPath, WorkOrderNumber, PartNumber, JobStepNumber, Username }RecipeDownloadResult { bool Result, string ResultText }
Failover (connect-failure only)
Try each base URL in order; advance to the next only when the attempt fails to connect (no HTTP response came back). A node that responds at all is authoritative — its answer is final.
| Attempt outcome | Failover? | Final result |
|---|---|---|
| No response — connection refused/reset, DNS failure, TLS error, or timeout | Yes → next URL | only if all URLs fail to connect → NO + last connection error, exit -1 |
HTTP 2xx + Result == true |
No — stop | YES, exit 0 |
HTTP 2xx + Result == false |
No — stop | NO + ResultText, exit -1 |
| HTTP non-2xx (401/403/4xx/5xx) | No — stop | NO + status/error, exit -1 |
Deliberate consequence: a
5xxfrom the first node is reported as a failure, not rolled over to the next node — failover is strictly for unreachable nodes. (Revisit only if operations want5xx/503to also fail over.)
Error handling & logging
- stdout is reserved for the
YES/NOcontract. All diagnostics — per-URL attempt, status code, exception detail, which URL answered — go to stderr and, ifLogPathis set, an appended log file. Hand-written; no logging dependency. - Failure reasons surfaced on the
NOreason line: missing required arg, missing API key, noBaseUrlsconfigured, or "all URLs unreachable: <last error>". - The HTTP call sits behind a tiny seam so the failover/result logic is unit-tested without network.
Project layout & packaging
src/ZB.MOM.WW.ScadaBridge.DelmiaNotifier/
ZB.MOM.WW.ScadaBridge.DelmiaNotifier.csproj
Program.cs # entry, arg parse, orchestration, result reporting
Config.cs # POCO + loader
RecipeDownload.cs # request DTO
RecipeDownloadResult.cs # response DTO
NotifierJsonContext.cs # JsonSerializerContext (source gen)
appsettings.json # copied to output
.csproj:net10.0,OutputType=Exe,AssemblyName=WWNotifier,PublishAot=true,RuntimeIdentifier=win-x64,InvariantGlobalization=true, AOT/trim analyzer warnings treated as errors. Added toZB.MOM.WW.ScadaBridge.slnx.- Build note: the AOT
win-x64native exe must be published on Windows (dotnet publish -c Release -r win-x64) with the MSVC build tools present — Native AOT does not cross-compile from macOS/Linux. Manageddotnet build/dotnet teststill run cross-platform for development and CI of the logic.
Testing
tests/ZB.MOM.WW.ScadaBridge.DelmiaNotifier.Tests (xUnit), all logic-level (no AOT needed, runs
cross-platform):
- Arg parsing: all-flags, required-missing → failure, optional omitted.
- Config: comma-split of
BaseUrls, defaults (TimeoutSeconds), missing file/section. - Payload mapping: flags →
RecipeDownloadJSON (field-for-field). - Result mapping:
Result true/falseand non-2xx → correct stdout + exit code. - Failover loop (via the HTTP seam / a fake handler): connect-failure advances; first responding node
is final;
Result==falsedoes not advance; all-unreachable →NO+ last error. - Missing/empty
SCADABRIDGE_API_KEY→ fail-fast, no attempt. - Manual live smoke against
wonder-app-vd03(/api/DelmiaRecipeDownload) from the Windows build, mirroring the earliercurlverification.
Out of scope (YAGNI)
- No retry/backoff beyond the connect-failover loop; no Polly.
- No DI/generic host, no
Microsoft.Extensions.*. - No support for other inbound methods — recipe download only.
- No DPAPI/secret-store integration (env var is the agreed mechanism); revisit if required.