Files
histsdk/docs/reverse-engineering/wcf-tag-extended-properties.md
T
Joseph Doherty c1b1b3d23b R1.11 DelTep capture + R1.3/R1.4/R1.12/R1.13 bounded out
DelTep (extended-property delete) — wire format captured + serializer
golden-proven, but live delete is server-blocked and NOT exposed publicly:
- Captured the DelTep inBuff via a cross-session trick (harness add-tep gains
  --tep-skip-add + read-for-sync before --tep-delete; Capture-DeleteTagExtended
  Properties.ps1 / decode-del-tep-capture.py). Layout = same group framing as
  AddTEx but property-name-only (no 0x43 value) + 0x00 group trailer.
- SerializeDeleteRequest + 4 golden tests pin the server-accepted buffer.
- A decisive experiment shows SDK-added properties ARE deletable (the native
  client read-syncs and deletes one), so SDK-add is complete; the SDK's own
  DelTep is rejected by CHistStorage::DeleteTagExtendedProperties even with
  byte-identical inBuff, matching mode/handle, GetTgByNm+GetTepByNm prime, open
  channel, and 60s retries. Root cause: the native multiplexes services over one
  connection (per-connection working set); the SDK's per-service WCF channels
  don't reproduce it. Kept as documented-but-blocked internal orchestrator path;
  no public HistorianClient delete API.

Bounded out with evidence (no code; docs + roadmap + probe):
- R1.12 localized-property write — no op on 2020 (mirror of R1.6); no
  *LocalizedPropert*/TagLocalized* symbol in any current/*.dll.
- R1.13 non-analog tag create — GATED; native AddTag rejects every non-analog
  type client-side (ValidationFailed, before any WCF op): SingleByteString,
  DoubleByteString, Int1 all fail, Float works. No Discrete type in the native
  enum, no TagType setter. No wire request to capture.
- R1.3 timezone + R1.4 EventStorageMode — re-confirmed 2023R2/gRPC-only from
  the Runtime DB schema (no timezone param, no EventStorageMode anywhere) and a
  parameter-op probe (GetSystemParameter + GETRP return null/throw for every
  candidate; only HistorianVersion works).

238 unit tests pass; full solution builds with 0 warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
2026-06-21 11:26:21 -04:00

5.8 KiB

Tag extended properties over 2020 WCF — GetTepByNm (HCAL R1.5)

Status: DONE + live-verified (2026-06-20). HistorianClient.GetTagExtendedPropertiesAsync(tag) reads a tag's extended (user-defined) properties over the 2020 WCF op aa/Retr/GetTagExtendedPropertiesFromName (GetTepByNm). Live-verified end-to-end from the pure-managed .NET 10 client against the local 2020 Historian.

The op

GetTepByNm is on the Retrieval service (IRetrievalServiceContract4):

bool GetTagExtendedPropertiesFromName(
    string handle,                       // Open2 storage-session GUID, UPPERCASE dash-no-braces
    byte[]  tagNames,                    // [MessageParameter pRequestBuff-style]
    ref uint sequence,                   // paging cursor (0 on first call)
    out byte[] tagExtendedProperties,    // result buffer
    out byte[] errorBuffer)

It is a string-handle op — reachable from the managed client because the handle is the Open2 storage-session GUID formatted storageSessionId.ToString("D").ToUpperInvariant() (the same handle format that unlocked GETRP/GETHI/ExeC; see wcf-string-handle-wall.md). The Retrieval service version handshake (Retr.GetV) is primed first, as the native client does.

Why the name-based path (not the TagQuery path)

There are two managed entry points:

  • Index-based TagQuery.GetTagExtendedPropertyInfo(start, count, …) — requires a prior StartTagQuery (QTB). On this 2020 box QTB fails server-side (error 1 from CMdServer::StartTagQuery::StartActiveTagnamesQuery over \\.\pipe\aahMetadataServer\console), so this path is dead here regardless of handle format.
  • Name-based HistorianAccess.GetTagExtendedPropertiesByName(tagName, fetchFromServer, …) — issues GetTepByNm directly with the tag name in tagNames, no QTB needed. Its second arg forces a server fetch when true; when false the C++ client reads a local cache and returns error 41 (Requested item not found) without any WCF round-trip. The SDK reproduces the name-based path.

Wire format (captured)

scripts/Capture-TagExtendedProperties.ps1 (NativeTraceHarness tag-extended-properties scenario + instrument-wcf-{write,read}message) → decode with scripts/decode-tag-properties-capture.py. Golden-pinned in WcfTagExtendedPropertyProtocolTests.

Request — tagNames buffer

uint32 count
per name: uint32 charCount + UTF-16LE chars

(For one tag: 01 00 00 00 + LL 00 00 00 + UTF-16 name.)

Response — tagExtendedProperties buffer

uint32 tagCount
per tag:
    byte   groupMarker            (observed 0x01)
    0x09 + uint16 byteLen + ASCII tagName        (compact-ASCII string)
    uint32 propertyCount
    per property:
        byte   propMarker         (observed 0x02 — likely the value type)
        0x09 + uint16 byteLen + ASCII propertyName
        0x43 + uint16 payloadLen + uint16 charCount + UTF-16LE value   (VT_BSTR CRetVariant)
    byte   trailingMarker         (observed 0x01)

payloadLen counts the charCount field (2 bytes) + the UTF-16 value bytes. Only the string value variant (0x43) is evidence-backed; other variant types throw ProtocolEvidenceMissingException (same discipline as GETRP). The single-byte 0x01/0x02/0x01 markers are pinned as observed constants from a single capture; their full semantics are not independently disambiguated.

Example (sanitized — real capture used a dev tag/value):

tag "Reactor.Temp1" → property "Location" = "Plant/AreaA"

Paging

GetTepByNm is sequence-paged like GetNextQueryResultBuffer: call with sequence = 0, parse the buffer, then re-call with the returned sequence. A small result returns everything on the first call; the next call returns an empty/nil buffer (with a benign CClientUtil::FillBufferFromVector terminator) — that is the stop signal. The SDK loops until the buffer carries no rows.

R1.6 (localized properties) — no distinct op on 2020

There is no GetTagLocalizedPropertiesFromName / GetTlpByNm op or GetTagLocalizedPropertiesByName method in current/aahClientManaged.dll — the only "localized" surfaces are ClientApp.GetLocalizedText and SMessageTextMap.GetLocalizedMessage (error-message / UI-text localization), not tag properties. So R1.6 collapses into R1.5: extended properties (GetTepByNm) are the user-defined tag-property read surface on 2020. R1.6 is closed as "no separate op," not left throwing.

R1.12 (localized-property write) — no distinct op on 2020 (mirror of R1.6)

Symmetric to R1.6 on the write side: there is no AddTagLocalizedProperties / DeleteTagLocalizedProperties (or any *LocalizedPropert* / TagLocalized*) symbol in any current/*.dll. A full symbol sweep of the shipped client DLLs surfaces only GetLocalizedText, GetLocalizedMessage, and LocalizedResourcesDir — all UI/error-message-text localization, not tag data. (The sweep does find the real write op AddTagExtendedProperties and the whole AddTag* family, so the absence of a localized op is a true negative, not a grep miss.) R1.12 is therefore closed as "no separate op" — the same conclusion as R1.6's read side. Extended-property write (R1.11 AddTEx) is the user-defined tag-property write surface on 2020; localized properties are a 2023 R2 / gRPC-only concept. Not left throwing.

Shipped surface

  • HistorianClient.GetTagExtendedPropertiesAsync(tag)IReadOnlyList<HistorianTagExtendedProperty> (Name/Value pairs; empty when the tag has none).
  • HistorianTagExtendedPropertyProtocol (serializer/parser), HistorianWcfTagExtendedPropertyClient (orchestration), golden WcfTagExtendedPropertyProtocolTests, gated live GetTagExtendedPropertiesAsync_AgainstLocalHistorian_ReturnsProperties (set HISTORIAN_TEP_TAG to a tag with extended properties).