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
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 priorStartTagQuery(QTB). On this 2020 box QTB fails server-side (error 1fromCMdServer::StartTagQuery::StartActiveTagnamesQueryover\\.\pipe\aahMetadataServer\console), so this path is dead here regardless of handle format. - Name-based
HistorianAccess.GetTagExtendedPropertiesByName(tagName, fetchFromServer, …)— issuesGetTepByNmdirectly with the tag name intagNames, no QTB needed. Its second arg forces a server fetch when true; when false the C++ client reads a local cache and returnserror 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/Valuepairs; empty when the tag has none).HistorianTagExtendedPropertyProtocol(serializer/parser),HistorianWcfTagExtendedPropertyClient(orchestration), goldenWcfTagExtendedPropertyProtocolTests, gated liveGetTagExtendedPropertiesAsync_AgainstLocalHistorian_ReturnsProperties(setHISTORIAN_TEP_TAGto a tag with extended properties).