Files
Marc Schier dbc09b8e22 Adopt System.TimeProvider across the stack (fixes #2458) (#3829)
# 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.
2026-06-03 22:38:32 +02:00
..

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 AllowSubTypes flag was added in OPC UA 1.05.02 as part of the StructureWithSubtypedValues feature. It is encoded as a flag on the StructureField in the DataTypeDefinition attribute. 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 AllowSubTypes flag is not a separate member of StructureField. It is inferred from the structureType being StructureWithSubtypedValues (3) combined with the field's dataType pointing to an abstract DataType. Pre-1.05.02, such fields were declared with dataType = 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 StructureType values 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 isOptional field 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. IsAbstract attribute: 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