# Description Adopt [`System.TimeProvider`](https://learn.microsoft.com/dotnet/api/system.timeprovider) across the stack so that every place that creates timers, runs duration math, or schedules periodic work is mockable, deterministic in tests, and immune to wall-clock changes. The migration touches Core, Core.Types, Server, Client, PubSub, Configuration, Gds (server + client), Lds, and the Quickstarts/Console reference apps. Per-component changes follow the same pattern: 1. Add a `private readonly TimeProvider m_timeProvider;` field. 2. Accept a nullable `TimeProvider? timeProvider = null` as the **last** constructor parameter (new overload; legacy constructor stays for binary compatibility and delegates with `timeProvider: null`). 3. Initialise the field with `m_timeProvider = timeProvider ??= TimeProvider.System;`. 4. Replace `new Timer(...)`, `Stopwatch`, `HiResClock.*`, `DateTime.UtcNow`-based duration math, and `Task.Delay(delay, ct)` in timing loops with the matching `m_timeProvider` API (`CreateTimer`, `GetTimestamp` / `GetElapsedTime`, `GetUtcNow().UtcDateTime`, `Task.Delay(delay, m_timeProvider, ct)`). 5. Wire the provider through DI: `AddOpcUaServerBuilder` / `AddOpcUaClientBuilder` register `TimeProvider.System` via `TryAddSingleton<TimeProvider>()` so tests can swap in `Microsoft.Extensions.Time.Testing.FakeTimeProvider`. `HiResClock` itself is kept (so the `ApplicationConfiguration.DisableHiResClock` schema flag still round-trips), but **every public member** is now `[Obsolete]` with a single shared message pointing to the new migration-guide section. The few internal call sites that still need to set/read `HiResClock` are wrapped with `#pragma warning disable CS0618`. ### Highlights per component - **Core / Stack** — `Opc.Ua.Core`, `Opc.Ua.Core.Types`, channel pipeline (`UaSCBinaryClientChannel`, `ChannelToken`, `UaSCBinaryChannel`), `ApplicationConfiguration`, certificate validators, `JwksIssuerKeyResolver`, `TimeProviderExtensions` polyfills (`GetTimestampMilliseconds`, `GetTickCount`, `CancellationTokenSource(timeout, timeProvider)`, `Task.Delay(delay, provider, ct)` polyfill for older TFMs). - **Server** — session lockout, subscription pipeline (publish/sample/keep-alive/lifetime), `MonitoredItemQueue`, alarm sampling rate, `IntervalRunner`, server diagnostics, configuration state machines, hosted services. - **Client** — `Session`, managed/V2 subscription engines, reconnect handlers, channel keep-alive, periodic publishing, complex-type discovery polling. - **PubSub** — `IntervalRunner` pipeline, message-receive timers, MQTT/UDP keep-alive, publishers/readers. - **Gds / Lds** — `LdsServer`, `LinqApplicationsDatabase` expiry housekeeping, GDS clients (push, pull), `DirectoryCertificateStore`, `ConnectionStateMachine`, classic subscription engine. - **Quickstarts / Console samples** — `SampleNodeManager`, `ReferenceNodeManager`, `AlarmNodeManager`, `BoilerState`, `MemoryBufferState`, `PublishedValuesWrites`. Samples resolve via `(Server as ITimeProviderProvider)?.TimeProvider ?? TimeProvider.System` so existing ctor signatures stay binary-compatible. - **Tests** — `Opc.Ua.PubSub.Tests` adds `IntervalRunnerTests`; existing `SubscriptionTests`, `UaPublisherTests`, and `TestDataSystem` move off `HiResClock`. Added `Microsoft.Extensions.TimeProvider.Testing` package for `FakeTimeProvider` (project-load warning surfaces on net48/net472 only — runtime is .NET 8+). ### Migration guide New section [**Time and Timer abstraction (`TimeProvider`)**](Docs/MigrationGuide.md#time-and-timer-abstraction-timeprovider) documents: - The canonical replacement table (`HiResClock.UtcNow` → `timeProvider.GetUtcNow().UtcDateTime`, `HiResClock.TickCount64` → `timeProvider.GetTimestamp()`, `new Timer` → `timeProvider.CreateTimer`, `Task.Delay(delay, ct)` → `Task.Delay(delay, timeProvider, ct)`, …). - The `TimeProvider? timeProvider = null` + `??=` constructor pattern. - DI wiring and how to swap in `FakeTimeProvider` in tests. - Before/after snippets for `HiResClock.TickCount64`, `HiResClock.UtcNow`, and `new Timer(...)`. ## Related Issues - Fixes #2458 ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received.
OPC UA Structure Encoding Decision Tree
A reference guide for encoding and decoding structure fields in OPC UA binary encoding, based on the StructureDefinition and StructureField metadata.
Class Diagram
classDiagram
direction TB
class DataTypeDefinition {
<<abstract>>
}
class StructureDefinition {
NodeId defaultEncodingId
NodeId baseDataType
StructureType structureType
StructureField[] fields
}
class StructureField {
String name
LocalizedText description
NodeId dataType
Int32 valueRank
UInt32[] arrayDimensions
UInt32 maxStringLength
Boolean isOptional
}
class StructureType {
<<enumeration>>
Structure = 0
StructureWithOptionalFields = 1
Union = 2
StructureWithSubtypedValues = 3
}
DataTypeDefinition <|-- StructureDefinition
StructureDefinition --> "1" StructureType : structureType
StructureDefinition --> "0..*" StructureField : fields
StructureField ..> StructureField : allowSubTypes¹
note for StructureDefinition "Part 3, §8.48\nhttps://reference.opcfoundation.org/v105/Core/docs/Part3/8.48/"
note for StructureField "Part 3, §8.51\nhttps://reference.opcfoundation.org/v105/Core/docs/Part3/8.51/"
note for StructureType "Part 3, §8.50\nhttps://reference.opcfoundation.org/v105/Core/docs/Part3/8.50/"
note for DataTypeDefinition "Part 3, §8.47\nhttps://reference.opcfoundation.org/v105/Core/docs/Part3/8.47/"
¹ The
AllowSubTypesflag was added in OPC UA 1.05.02 as part of theStructureWithSubtypedValuesfeature. It is encoded as a flag on theStructureFieldin theDataTypeDefinitionattribute. See Part 3, §5.8.3.
StructureDefinition Members
| Member | Type | Description | Spec Reference |
|---|---|---|---|
defaultEncodingId |
NodeId |
NodeId of the default DataTypeEncoding node for this structure | Part 3, §8.48 |
baseDataType |
NodeId |
NodeId of the direct supertype DataType | Part 3, §8.48 |
structureType |
StructureType |
Enum controlling the overall encoding strategy (see below) | Part 3, §8.48 |
fields |
StructureField[] |
Ordered list of fields including inherited fields from supertypes | Part 3, §8.48 |
StructureField Members
| Member | Type | Description | Spec Reference |
|---|---|---|---|
name |
String |
Field name, unique within the structure | Part 3, §8.51 |
description |
LocalizedText |
Human-readable field description | Part 3, §8.51 |
dataType |
NodeId |
The DataType of the field value | Part 3, §8.51 |
valueRank |
Int32 |
Scalar (−1), OneDimension (1), or n-dimensional (>1) | Part 3, §8.51 |
arrayDimensions |
UInt32[] |
Fixed dimensions for array fields (empty if not fixed) | Part 3, §8.51 |
maxStringLength |
UInt32 |
Maximum length for String/ByteString fields (0 = unlimited) | Part 3, §8.51 |
isOptional |
Boolean |
Whether this field may be absent (only valid when structureType = StructureWithOptionalFields) |
Part 3, §8.51 |
Note: The
AllowSubTypesflag is not a separate member ofStructureField. It is inferred from thestructureTypebeingStructureWithSubtypedValues(3) combined with the field'sdataTypepointing to an abstract DataType. Pre-1.05.02, such fields were declared withdataType = Structure; post-1.05.02, the actual abstract base type is used. See OPC Foundation Forum discussion.
StructureType Enum
| Value | Name | Description | Spec Reference |
|---|---|---|---|
| 0 | Structure |
All fields are mandatory, encoded sequentially | Part 3, §8.50 |
| 1 | StructureWithOptionalFields |
Some fields may be absent; a bitmask prefix indicates presence | Part 3, §8.50 |
| 2 | Union |
Only one field is active; a switch field indicates which | Part 3, §8.50 |
| 3 | StructureWithSubtypedValues |
Fields may contain subtypes of their declared DataType, encoded as ExtensionObjects | Part 3, §8.50 |
Encoding Decision Tree
Step 1 — Structure-Level Prefix
Before encoding any fields, determine the prefix based on StructureType:
StructureType = ?
│
├─ 0: Structure
│ PREFIX: (none)
│ All fields encoded sequentially in definition order.
│ [Part 6, §5.2.7]
│
├─ 1: StructureWithOptionalFields
│ PREFIX: UInt32 EncodingMask
│ One bit per optional field (bit 0 = first optional field).
│ Mandatory fields always encoded; optional fields only if their bit = 1.
│ [Part 6, §5.2.7]
│
├─ 2: Union
│ PREFIX: UInt32 SwitchField
│ Value 0 = null (no field encoded).
│ Value N = the Nth field (1-based) is the active field.
│ Only the active field is encoded after the switch.
│ [Part 6, §5.2.7]
│
└─ 3: StructureWithSubtypedValues
PREFIX: (none)
All fields encoded sequentially.
Fields whose DataType is abstract are wrapped in ExtensionObject.
[Part 6, §5.2.7]
Spec Reference: The structure-level encoding rules for all four
StructureTypevalues are defined in Part 6, §5.2.7. See also Part 6, §5.2.2.15 for ExtensionObject binary encoding.
Step 2 — Per-Field Presence
For each StructureField, determine whether it is encoded in this instance:
For StructureField F:
│
├─ Is StructureType = Union?
│ ├─ F is the active field (matches SwitchField) → ENCODE
│ └─ F is not active → SKIP entirely
│
├─ Is F.isOptional = true? (StructureType must be 1)
│ ├─ EncodingMask bit is SET → ENCODE
│ └─ EncodingMask bit is CLEAR → SKIP entirely (no placeholder bytes)
│
└─ Otherwise (mandatory) → ENCODE (always present)
Spec Reference: Optional field handling via encoding mask is defined in Part 6, §5.2.7. The
isOptionalfield property is defined in Part 3, §8.51.
Step 3 — Field Value Encoding
When a field IS present, determine the wire format based on its DataType:
How to encode the VALUE of field F with DataType D:
│
├─ Is StructureType = StructureWithSubtypedValues (3)
│ AND is D an abstract DataType (IsAbstract = true on the DataType node)?
│ │
│ └─ YES → Encode as ExtensionObject
│ Wire format: [NodeId TypeId] [Byte Encoding] [Int32 Length] [Bytes Body]
│ TypeId = the DataTypeEncoding NodeId of the concrete instance.
│ This allows the decoder to identify and decode the actual subtype.
│ [Part 6, §5.2.2.15] [Part 3, §5.8.3]
│
├─ Is D a built-in type?
│ │ (Boolean, SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64,
│ │ Float, Double, String, DateTime, Guid, ByteString, NodeId,
│ │ ExpandedNodeId, StatusCode, QualifiedName, LocalizedText,
│ │ DiagnosticInfo, XmlElement)
│ │
│ └─ YES → Encode INLINE per built-in type rules
│ Integers: little-endian byte order
│ Floats/Doubles: IEEE 754 binary representation
│ Strings: [Int32 byteLength] [UTF-8 bytes] (−1 = null)
│ ByteStrings: [Int32 length] [bytes] (−1 = null)
│ [Part 6, §5.2.2]
│
├─ Is D an Enumeration DataType?
│ │
│ └─ YES → Encode as Int32 (the enumeration's integer value)
│ [Part 6, §5.2.4]
│
├─ Is D a concrete Structure or Union subtype (IsAbstract = false)?
│ │
│ └─ YES → Encode INLINE (recursively apply this entire decision tree)
│ The nested structure follows its own StructureType rules.
│ No ExtensionObject wrapper — the decoder knows the exact type
│ from F.dataType in the StructureDefinition.
│ [Part 6, §5.2.7]
│
└─ Is D abstract but StructureType ≠ 3?
│ (Legacy scenario, pre-1.05.02)
│
└─ The field DataType was declared as "Structure" (i=22) or another
abstract type. Encode as ExtensionObject — the TypeId in the
ExtensionObject header identifies the concrete type.
[Part 6, §5.2.2.15]
Spec Reference: Built-in type encoding: Part 6, §5.2.2. Enumeration encoding: Part 6, §5.2.4. Structure/Union encoding: Part 6, §5.2.7. ExtensionObject encoding: Part 6, §5.2.2.15.
IsAbstractattribute: Part 3, §5.8.3.
Step 4 — Array Handling
If F.valueRank ≠ −1 (i.e., the field is an array), the value encoding from Step 3 is wrapped:
F.valueRank = ?
│
├─ −1 (Scalar)
│ Encode single value per Step 3.
│ [Part 6, §5.2.5]
│
├─ 1 (OneDimension)
│ [Int32 ArrayLength] [Element₀] [Element₁] ... [ElementN₋₁]
│ ArrayLength = −1 for null arrays, 0 for empty arrays.
│ Each element encoded per Step 3.
│ [Part 6, §5.2.5]
│
└─ >1 (MultiDimension)
[Int32 TotalElements] [Element₀] ... [ElementN₋₁]
[Int32 NumDimensions] [Int32 Dim₀] [Int32 Dim₁] ...
Elements serialized in row-major order.
[Part 6, §5.2.5]
Spec Reference: Array encoding rules are defined in Part 6, §5.2.5.
Summary Tables
Structure-Level Encoding
| StructureType | Value | Prefix | Field Presence | Spec |
|---|---|---|---|---|
Structure |
0 | None | All mandatory, sequential | Part 6, §5.2.7 |
StructureWithOptionalFields |
1 | UInt32 bitmask |
Mandatory always; optional if bit set | Part 6, §5.2.7 |
Union |
2 | UInt32 switch (1-based) |
Only the active field | Part 6, §5.2.7 |
StructureWithSubtypedValues |
3 | None | All mandatory, sequential | Part 6, §5.2.7 |
Per-Field Value Encoding
| Condition | Encoding | Spec |
|---|---|---|
| Built-in type (Int32, String, etc.) | Inline per type rules | Part 6, §5.2.2 |
| Enumeration | Int32 |
Part 6, §5.2.4 |
| Concrete Structure/Union | Inline, recursive | Part 6, §5.2.7 |
Abstract DataType + StructureWithSubtypedValues |
ExtensionObject wrapper |
Part 6, §5.2.2.15 |
| Abstract DataType (legacy pre-1.05.02) | ExtensionObject wrapper |
Part 6, §5.2.2.15 |
isOptional = true, bit clear |
Not encoded (skipped) | Part 6, §5.2.7 |
Array (valueRank ≥ 1) |
[Int32 length] [elements...] |
Part 6, §5.2.5 |
Constraints
| Rule | Spec |
|---|---|
isOptional and AllowSubTypes are mutually exclusive on a field |
Part 3, §5.8.3, Forum |
isOptional = true only valid when structureType = StructureWithOptionalFields |
Part 3, §8.51 |
AllowSubTypes only valid when structureType = StructureWithSubtypedValues |
Part 3, §8.50 |
fields array includes inherited fields from the supertype chain |
Part 3, §8.48 |
DataTypeDefinition is mandatory for DataTypes derived from Structure or Union |
Part 3, §5.8.3 |
ExtensionObject Wire Format
When a field is encoded as an ExtensionObject (Part 6, §5.2.2.15):
┌──────────────┬───────────────┬──────────────┬──────────────────────┐
│ NodeId TypeId│ Byte Encoding │ Int32 Length │ Byte[] Body │
│ (variable │ 0x00 = none │ (present if │ (encoded per the │
│ length) │ 0x01 = binary │ body exists)│ concrete type's │
│ │ 0x02 = xml │ │ StructureDefinition)│
└──────────────┴───────────────┴──────────────┴──────────────────────┘
| Field | Description | Spec |
|---|---|---|
| TypeId | NodeId of the concrete type's DataTypeEncoding node (the "Default Binary" encoding node reachable via HasEncoding from the DataType node) |
Part 3, §5.8.3 |
| Encoding | 0x01 for OPC UA Binary, 0x02 for XML |
Part 6, §5.2.2.15 |
| Length | Total byte length of Body; omitted if Encoding = 0x00 |
Part 6, §5.2.2.15 |
| Body | The structure's fields encoded per its own StructureDefinition |
Part 6, §5.2.7 |
Version History
| OPC UA Version | Change | Spec Reference |
|---|---|---|
| 1.04 | DataTypeDefinition attribute introduced; StructureWithOptionalFields and Union added |
Part 3, §8.48 |
| 1.05.02 | StructureWithSubtypedValues (value 3) added; AllowSubTypes flag formalized; replaces the pre-1.04 pattern of declaring abstract fields as Structure |
Part 3, §8.50, Forum |
Full Specification References
| Document | Section | Topic | URL |
|---|---|---|---|
| OPC 10000-3 | §5.8.3 | DataType NodeClass, IsAbstract, DataTypeDefinition | Link |
| OPC 10000-3 | §8.47 | DataTypeDefinition (abstract base) | Link |
| OPC 10000-3 | §8.48 | StructureDefinition | Link |
| OPC 10000-3 | §8.49 | EnumDefinition | Link |
| OPC 10000-3 | §8.50 | StructureType enumeration | Link |
| OPC 10000-3 | §8.51 | StructureField | Link |
| OPC 10000-6 | §5.2.2 | Built-in Types Binary Encoding | Link |
| OPC 10000-6 | §5.2.4 | Enumeration Encoding | Link |
| OPC 10000-6 | §5.2.5 | Array Encoding | Link |
| OPC 10000-6 | §5.2.7 | Structure, Union, and Optional Field Encoding | Link |
| OPC 10000-6 | §5.2.2.15 | ExtensionObject Binary Encoding | Link |