# 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` (`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).