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 Foundation UA .NET Standard Library - Console Reference Publisher

Introduction

This OPC application was created to provide the sample code for creating Publisher applications using the OPC Foundation UA .NET Standard PubSub Library. There is a .NET Core 3.1 (2.1) console version of the Publisher which runs on any OS supporting .NET Standard 2.1. The Reference Publisher is configured to run in parallel with the Console Reference Subscriber

How to build and run the console OPC UA Reference Publisher from Visual Studio

  1. Open the solution UA.slnx with Visual Studio 2026.
  2. Choose the project ConsoleReferencePublisher in the Solution Explorer and set it with a right click as Startup Project.
  3. Hit F5 to build and execute the sample.

How to build and run the console OPC UA Reference Publisher on Windows, Linux and iOS

This section describes how to run the ConsoleReferencePublisher.

Please follow instructions in this article to setup the dotnet command line environment for your platform.

Start the Publisher

  1. Open a command prompt.
  2. Navigate to the folder Applications/ConsoleReferencePublisher.
  3. To run the Publisher sample execute:

dotnet run --project ConsoleReferencePublisher.csproj --framework net10.0

The Publisher will start and publish network messages that can be consumed by the Reference Subscriber. Publisher Initialization

Command Line Arguments for ConsoleReferencePublisher

ConsoleReferencePublisher can be executed using the following command line arguments:

  • -h|help - Shows usage information
  • -m|mqtt_json - Creates a connection using there MQTT with Json encoding Profile. This is the default option.
  • -u|udp_uadp - Creates a connection using there UDP with UADP encoding Profile.

To run the Publisher sample using a connection with MQTT with Json encoding execute:

  dotnet run --project ConsoleReferencePublisher.csproj --framework net10.0

or

  dotnet run --project ConsoleReferencePublisher.csproj --framework net10.0 -m

To run the Publisher sample using a connection with the UDP with UADP encoding execute:

  dotnet run --project ConsoleReferencePublisher.csproj --framework net10.0 -u

Programmer's Guide

To create a new OPC UA Publisher application:

Publisher Initialization

The following four steps are required to implement a functional Publisher:

  1. Create Publisher Configuration.

      // Create configuration using MQTT protocol and JSON Encoding
            PubSubConfigurationDataType pubSubConfiguration = CreatePublisherConfiguration_MqttJson();
    

    Or use the alternative configuration object for UDP with UADP encoding

      // Create configuration using UDP protocol and UADP Encoding 
            PubSubConfigurationDataType pubSubConfiguration = CreatePublisherConfiguration_UdpUadp();
    

    The CreatePublisherConfiguration methods can be found in ConsoleReferencePublisher/Program.cs file.

  2. Create an instance of the UaPubSubApplication Class using the configuration data from step 1.

            // Create an instance of UaPubSubApplication
            UaPubSubApplication uaPubSubApplication = UaPubSubApplication.Create(pubSubConfiguration);
    
  3. Provide the data to be published based on the configuration of published data sets. This step is described in the Publisher Data section.

  4. Start PubSub application

            // Start the publisher
            uaPubSubApplication.Start();
    

After this step the Publisher will publish data as configured.

Publisher Configuration

The Publisher configuration is a subset of the PubSub Configuration. A functional Publisher application needs to have a configuration (PubSubConfigurationDataType instance) that contains a list of published data sets (PublishedDataSetDataType instances) and at least one connection (PubSubConnectionDataType instance) with at least one writer group configuration (WriterGroupDataType instance). The writer group contains at least one data set writer (DataSetWriterDataType instance) pointing to a published data set from the current configuration.

The diagram shows the subset of classes involved in an OPC UA Publisher configuration.

PublisherConfigClasses

Publisher Data

The UaPubSubApplication Class provides a property of type IUaPubSubDataStore called DataStore. In ConsoleReferencePublisher there is no custom implementation provided for IUaPubSubDataStore therefore the pub sub application object is initialized using the default implementation of this interface, an instance of UaPubSubDataStore.

The code responsible for generating the data values to be published is located in the PublishedValuesWrites file from the ConsoleReferencePublisher project. It maintains a list of all the fields from the table below and uses a timer for writing the values to UaPubSubApplication.DataStore using the WritePublishedDataItem() method from DataStore class. The data values simulator component is initialized like:

// Start values simulator
PublishedValuesWrites valuesSimulator = new PublishedValuesWrites(uaPubSubApplication);
valuesSimulator.Start();

The Publisher component from ConsoleReferencePublisher application will use the data generated by PublishedValuesWrites to create the NetworkMessages that will be published as configured in Publisher Configuration.

Note:
The current PubSub implementation only supports PublishedDataItemsDataType as DataSetSource of a PublishedDataSetDataType from the configuration. Events will be added in a future version.

The ConsoleReferencePublisher application is configured to use the following data sets and will generate values as specified in the table below if the default configuration method is used:

PublishedDataSet 'Simple' - NamespaceIndex = 2

Name DataType ValueRank Behavior
BoolToggle Boolean Scalar Toggles every 3 seconds
Int32 Int32 Scalar Counts (1 per second) from 0 to 10,000 and then resets
Int32Fast Int32Fast Scalar Counts (100 per second) from 0 to 10,000 and then resets
DateTime DateTime Scalar Current time refreshed with every packet sent

The CreatePublishedDataSetSimple() method from Program.cs creates a PublishedDataSetDataType configuration object that contains the metadata information for 'Simple' DataSet.

PublishedDataSet 'AllTypes' - NamespaceIndex = 3

Name DataType ValueRank Behavior
BoolToggle Boolean Scalar Toggles every second
Byte Byte Scalar Counts (1 per second) from 0 to type-max and then resets
Int16 Int16 Scalar Counts (1 per second) from 0 to type-max and then resets
Int32 Int32 Scalar Counts (1 per second) from 0 to type-max and then resets
SByte SByte Scalar Counts (1 per second) from 0 to type-max and then resets
UInt16 UInt16 Scalar Counts (1 per second) from 0 to type-max and then resets
UInt32 UInt32 Scalar Counts (1 per second) from 0 to type-max and then resets
UInt64 UInt64 Scalar Counts (1 per second) from 0 to type-max and then resets
Float Float Scalar Counts (1 per second) from 0 to type-max and then resets
Double Double Scalar Counts (1 per second) from 0 to type-max and then resets
String String Scalar Spells the aviation alphabet (Alpha, Bravo …) (1 per second)
ByteString ByteString Scalar 1 new random ByteString per second
Guid Guid Scalar 1 new random Guid per second
DateTime DateTime Scalar Current time refreshed with every packet sent
UInt32Array UInt32 OneDimension Counts (1 per second on every element) from 0 to type-max and then resets. The count starting point for each value should differ

The CreatePublishedDataSetAllTypes() method from Program.cs creates a PublishedDataSetDataType configuration object that contains the metadata information for 'AllTypes' DataSet.