Files
ScadaBridge/docs/plans/2026-06-26-delmia-recipe-notifier-design.md
T
Joseph Doherty 0008ca891c docs(delmia-notifier): design for DelmiaNotifier console app (WWNotifier modern replacement)
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.
2026-06-26 04:55:40 -04:00

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:

  1. Arg parser — hand-rolled, the 6 legacy flags → an in-memory request model.
  2. Config loader — reads appsettings.json (via System.Text.Json source generator) → POCO; reads the API key from the environment.
  3. 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.
  4. 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 YES on success, or NO followed by a reason line on failure. Nothing else is written to stdout (Delmia parses it).
  • exit code: 0 on success, -1 on failure (matches the legacy Environment.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/DelmiaRecipeDownload is appended by the app; each entry is a base URL only.
  • TimeoutSeconds — per-attempt HttpClient.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, header X-API-Key: <key>, Content-Type: application/json, body = the flat RecipeDownload JSON.
  • 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 5xx from 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 want 5xx/503 to also fail over.)

Error handling & logging

  • stdout is reserved for the YES/NO contract. All diagnostics — per-URL attempt, status code, exception detail, which URL answered — go to stderr and, if LogPath is set, an appended log file. Hand-written; no logging dependency.
  • Failure reasons surfaced on the NO reason line: missing required arg, missing API key, no BaseUrls configured, 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 to ZB.MOM.WW.ScadaBridge.slnx.
  • Build note: the AOT win-x64 native 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. Managed dotnet build / dotnet test still 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 → RecipeDownload JSON (field-for-field).
  • Result mapping: Result true/false and non-2xx → correct stdout + exit code.
  • Failover loop (via the HTTP seam / a fake handler): connect-failure advances; first responding node is final; Result==false does 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 earlier curl verification.

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.