# Extended-property write over 2020 WCF — AddTEx (HCAL R1.11) **Status: ✅ Add DONE + live-verified (2026-06-21). Delete (DelTep) deferred — see below.** `HistorianClient.AddTagExtendedPropertiesAsync` / `AddTagExtendedPropertyAsync` writes user-defined extended properties onto an existing tag via the 2020 WCF **`AddTEx`** (AddTagExtendedProperties) op, and they read back via the R1.5 `GetTagExtendedPropertiesAsync` path. Verified end-to-end from the pure-managed .NET 10 client against the local 2020 Historian (create tag → add property → read back → delete tag). ## The op ``` bool AddTagExtendedProperties(string handle, byte[] inBuff, out byte[] errorBuffer) // AddTEx ``` On `IHistoryServiceContract2` (History service). Requires a **write-enabled** connection (Open2 mode `0x401`) and the uppercase storage-session GUID handle — the SDK reuses the write orchestrator's open + priming chain (the same one used by EnsT2/DelT). The tag is referenced by name inside `inBuff`; no extra per-connection tag registration was needed (the server resolves it). ## The inBuff — the exact inverse of the R1.5 read response The native `AddTagExtendedProperties(TagExtendedPropertyGroupList, out err)` packs its groups into the `AddTEx` `inBuff` with the **same framing the R1.5 `GetTepByNm` response uses**, so the write serializer is the inverse of `HistorianTagExtendedPropertyProtocol.ParseResponse`: ``` uint32 groupCount (= 1) byte 0x01 (group marker) 0x09 + uint16 byteLen + ASCII tagName (compact-ASCII string) uint32 propertyCount repeated propertyCount times: byte 0x02 (property marker) 0x09 + uint16 byteLen + ASCII propertyName 0x43 + uint16 payloadLen + uint16 charCount + UTF-16LE value (VT_BSTR variant; payloadLen = 2 + charCount*2) byte 0x01 (group trailer) byte 0x00 (buffer terminator) ``` ⚠️ **The trailing `0x01 0x00`** matters: the group trailer is `0x01` (as in the read parser) **plus a final `0x00` buffer terminator**. Omitting the `0x00` makes `inBuff` one byte short and the server throws `SErrorException` in `aahClientAccessPoint::CHistStorage::AddTagExtendedProperties` (AddTEx returns false). The read parser tolerates the extra byte because it only consumes one trailing byte per group. Only the string (`0x43` VT_BSTR) value variant is evidence-backed (matching the read path). The raw instrument capture mangles the final byte with MDAS chunk markers, so the golden fixture pins the **clean** byte[] the SDK handed the channel (dumped via `AVEVA_HISTORIAN_TEP_DUMP`) — the exact buffer the live server accepted. ## Delete (DelTep) — deferred `DeleteTagExtendedProperties` (`DelTep`) is **not shipped yet**. The native `DeleteTagExtendedPropertiesByName(tag, propertyNames, deleteFromServer, out err)` performs a **client-side sync check** and returns error **229 ("Tag extended property not synchronized with server")** when deleting a just-added property — so the `DelTep` request never reached the wire and its inBuff could not be captured. Capturing it needs a property that is already server-synchronized (add it in one session, then delete in a later one). Left for a follow-up rather than shipping a guessed buffer. ## Shipped surface - `HistorianClient.AddTagExtendedPropertiesAsync(tag, IReadOnlyList)` and `AddTagExtendedPropertyAsync(tag, name, value)`. - `HistorianTagExtendedPropertyProtocol.SerializeAddRequest` (the inBuff serializer; lives beside the R1.5 read parser); orchestrator path in `HistorianWcfTagWriteOrchestrator`. - Golden `WcfTagExtendedPropertyWriteProtocolTests` (pins the server-accepted buffer + layout); gated live test `AddTagExtendedPropertiesAsync_AgainstLocalHistorian_WritesAndReadsBack`. ## Capture / decode tooling `scripts/Capture-AddTagExtendedProperties.ps1` (native-harness `add-tep` scenario + instrument-wcf-{write,read}message; sandbox-guarded create→add→[optional delete]) and `scripts/decode-add-tep-capture.py`.