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

1367 lines
49 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Opc.Ua.Security.Certificates;
namespace Opc.Ua.Client
{
/// <summary>
/// Manages a session with a server.
/// </summary>
public static class SessionObsolete
{
extension(ISession session)
{
/// <summary>
/// Gets the TickCount in ms of the last keep alive based on
/// <see cref="ISession.LastKeepAliveTimestamp"/>.
/// </summary>
[Obsolete("Use LastKeepAliveTimestamp + TimeProvider.GetElapsedTime. " +
"LastKeepAliveTickCount is a 32-bit value that wraps every ~49.7 days.")]
public int LastKeepAliveTickCount
=> unchecked((int)(session.LastKeepAliveTimestamp *
1000L / TimeProvider.System.TimestampFrequency));
}
/// <summary>
/// Reconnects to the server after a network failure.
/// </summary>
[Obsolete("Use ReconnectAsync instead.")]
public static void Reconnect(this ISession session)
{
session.ReconnectAsync().GetAwaiter().GetResult();
}
/// <summary>
/// Reconnects to the server after a network failure using a waiting connection.
/// </summary>
[Obsolete("Use ReconnectAsync instead.")]
public static void Reconnect(this ISession session, ITransportWaitingConnection connection)
{
session.ReconnectAsync(connection).GetAwaiter().GetResult();
}
/// <summary>
/// Reconnects to the server using a new channel.
/// </summary>
[Obsolete("Use ReconnectAsync instead.")]
public static void Reconnect(this ISession session, ITransportChannel channel)
{
session.ReconnectAsync(channel).GetAwaiter().GetResult();
}
/// <summary>
/// Updates the local copy of the server's namespace uri and server uri tables.
/// </summary>
[Obsolete("Use FetchNamespaceTablesAsync instead.")]
public static void FetchNamespaceTables(this ISession session)
{
session.FetchNamespaceTablesAsync().GetAwaiter().GetResult();
}
/// <summary>
/// Updates the cache with the type and its subtypes.
/// </summary>
[Obsolete("Use FetchTypeTreeAsync instead.")]
public static void FetchTypeTree(this ISession session, ExpandedNodeId typeId)
{
session.FetchTypeTreeAsync(typeId).GetAwaiter().GetResult();
}
/// <summary>
/// Updates the cache with the types and its subtypes.
/// </summary>
[Obsolete("Use FetchTypeTreeAsync instead.")]
public static void FetchTypeTree(this ISession session, ArrayOf<ExpandedNodeId> typeIds)
{
session.FetchTypeTreeAsync(typeIds).GetAwaiter().GetResult();
}
/// <summary>
/// Returns the available encodings for a node
/// </summary>
[Obsolete("Use ReadAvailableEncodingsAsync instead.")]
public static ArrayOf<ReferenceDescription> ReadAvailableEncodings(
this ISession session,
NodeId variableId)
{
return session.ReadAvailableEncodingsAsync(variableId).GetAwaiter().GetResult();
}
/// <summary>
/// Returns the data description for the encoding.
/// </summary>
[Obsolete("Use FindDataDescriptionAsync instead.")]
public static ReferenceDescription FindDataDescription(
this ISession session,
NodeId encodingId)
{
return session.FindDataDescriptionAsync(encodingId).GetAwaiter().GetResult();
}
/// <summary>
/// Reads the values for the node attributes and returns a node object.
/// </summary>
[Obsolete("Use ReadNodeAsync instead.")]
public static Node ReadNode(this ISession session, NodeId nodeId)
{
return session.ReadNodeAsync(nodeId).GetAwaiter().GetResult();
}
/// <summary>
/// Reads the values for the node attributes and returns a node object.
/// </summary>
[Obsolete("Use ReadNodeAsync instead.")]
public static Node ReadNode(
this ISession session,
NodeId nodeId,
NodeClass nodeClass,
bool optionalAttributes = true)
{
return session.ReadNodeAsync(
nodeId,
nodeClass,
optionalAttributes).GetAwaiter().GetResult();
}
/// <summary>
/// Reads the values for the node attributes and returns a node object.
/// </summary>
[Obsolete("Use ReadNodesAsync instead.")]
public static void ReadNodes(
this ISession session,
ArrayOf<NodeId> nodeIds,
out ArrayOf<Node> nodeCollection,
out ArrayOf<ServiceResult> errors,
bool optionalAttributes = false)
{
(nodeCollection, errors) = session.ReadNodesAsync(
nodeIds,
optionalAttributes).GetAwaiter().GetResult();
}
/// <summary>
/// Reads the values for the node attributes and returns a node object collection.
/// </summary>
/// <remarks>
/// If the nodeclass for the nodes in nodeIdCollection is already known,
/// and passed as nodeClass, reads only values of required attributes.
/// Otherwise NodeClass.Unspecified should be used.
/// </remarks>
[Obsolete("Use ReadNodesAsync instead.")]
public static void ReadNodes(
this ISession session,
ArrayOf<NodeId> nodeIds,
NodeClass nodeClass,
out ArrayOf<Node> nodeCollection,
out ArrayOf<ServiceResult> errors,
bool optionalAttributes = false)
{
(nodeCollection, errors) = session.ReadNodesAsync(
nodeIds,
nodeClass,
optionalAttributes).GetAwaiter().GetResult();
}
/// <summary>
/// Reads the value for a node.
/// </summary>
[Obsolete("Use ReadValueAsync instead.")]
public static DataValue ReadValue(this ISession session, NodeId nodeId)
{
return session.ReadValueAsync(nodeId).GetAwaiter().GetResult();
}
/// <summary>
/// Reads the values for a set of variables.
/// </summary>
[Obsolete("Use ReadValuesAsync returning Variant Array")]
public static async ValueTask<(
ArrayOf<object?>,
ArrayOf<ServiceResult>
)> ReadValuesAsync(
this ISessionClient session,
ArrayOf<NodeId> variableIds,
ArrayOf<Type?> expectedTypes,
CancellationToken ct = default)
{
(ArrayOf<DataValue> dataValues, ArrayOf<ServiceResult> errorValues) =
await session.ReadValuesAsync(variableIds, ct).ConfigureAwait(false);
var errors = new ServiceResult[errorValues.Count];
object?[] values = new object?[dataValues.Count];
for (int ii = 0; ii < variableIds.Count; ii++)
{
object? value = dataValues[ii].Value;
// extract the body from extension objects.
if (value is ExtensionObject extension &&
extension.TryGetValue(out IEncodeable? encodeable))
{
value = encodeable;
}
// check expected type.
Type? expectedType = expectedTypes[ii];
if (expectedType != null &&
!expectedType.IsInstanceOfType(value))
{
errors[ii] = ServiceResult.Create(
StatusCodes.BadTypeMismatch,
"Value {0} does not have expected type: {1}.",
value!,
expectedType.Name);
continue;
}
// suitable value found.
errors[ii] = errorValues[ii];
values[ii] = value;
}
return (values, errors);
}
/// <summary>
/// Reads the value for a node an checks that it is the specified type.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
[Obsolete("Use ReadValueAsync instead.")]
public static object? ReadValue(this ISession session, NodeId nodeId, Type? expectedType)
{
DataValue dataValue = session.ReadValueAsync(nodeId).GetAwaiter().GetResult();
object? value = dataValue.Value;
if (expectedType != null)
{
if (dataValue.WrappedValue.TryGetValue(out ExtensionObject extension) &&
extension.TryGetValue(out IEncodeable? encodeable))
{
value = encodeable;
}
if (!expectedType.IsInstanceOfType(value))
{
throw ServiceResultException.Create(
StatusCodes.BadTypeMismatch,
"Server returned value unexpected type: {0}",
value != null ? value.GetType().Name : "(null)");
}
}
return value;
}
/// <summary>
/// Reads the values for a node collection. Returns diagnostic errors.
/// </summary>
[Obsolete("Use ReadValuesAsync instead.")]
public static void ReadValues(
this ISession session,
ArrayOf<NodeId> nodeIds,
out ArrayOf<DataValue> values,
out ArrayOf<ServiceResult> errors)
{
(values, errors) = session.ReadValuesAsync(
nodeIds).GetAwaiter().GetResult();
}
/// <summary>
/// Reads a byte string which is too large for the (server side) encoder to handle.
/// </summary>
/// <returns></returns>
[Obsolete("Use ReadByteStringInChunksAsync instead.")]
public static ByteString ReadByteStringInChunks(
this ISession session,
NodeId nodeId)
{
return session.ReadByteStringInChunksAsync(nodeId).GetAwaiter().GetResult();
}
/// <summary>
/// Fetches all references for the specified node.
/// </summary>
[Obsolete("Use FetchReferencesAsync instead.")]
public static ArrayOf<ReferenceDescription> FetchReferences(
this ISession session,
NodeId nodeId)
{
return session.FetchReferencesAsync(nodeId).GetAwaiter().GetResult();
}
/// <summary>
/// Fetches all references for the specified nodes.
/// </summary>
[Obsolete("Use FetchReferencesAsync instead.")]
public static void FetchReferences(
this ISession session,
ArrayOf<NodeId> nodeIds,
out ArrayOf<ArrayOf<ReferenceDescription>> referenceDescriptions,
out ArrayOf<ServiceResult> errors)
{
(referenceDescriptions, errors) = session.FetchReferencesAsync(
nodeIds).GetAwaiter().GetResult();
}
/// <summary>
/// Establishes a session with the server.
/// </summary>
[Obsolete("Use OpenAsync instead.")]
public static void Open(
this ISession session,
string sessionName,
IUserIdentity identity)
{
session.OpenAsync(sessionName, identity).GetAwaiter().GetResult();
}
/// <summary>
/// Establishes a session with the server.
/// </summary>
[Obsolete("Use OpenAsync instead.")]
public static void Open(
this ISession session,
string sessionName,
uint sessionTimeout,
IUserIdentity identity,
ArrayOf<string> preferredLocales)
{
session.OpenAsync(
sessionName,
sessionTimeout,
identity,
preferredLocales).GetAwaiter().GetResult();
}
/// <summary>
/// Establishes a session with the server.
/// </summary>
[Obsolete("Use OpenAsync instead.")]
public static void Open(
this ISession session,
string sessionName,
uint sessionTimeout,
IUserIdentity identity,
ArrayOf<string> preferredLocales,
bool checkDomain)
{
session.OpenAsync(
sessionName,
sessionTimeout,
identity,
preferredLocales,
checkDomain).GetAwaiter().GetResult();
}
/// <summary>
/// Establishes a session with the server.
/// </summary>
[Obsolete("Use OpenAsync instead.")]
public static void Open(
this ISession session,
string sessionName,
uint sessionTimeout,
IUserIdentity identity,
ArrayOf<string> preferredLocales,
bool checkDomain,
bool closeChannel)
{
session.OpenAsync(
sessionName,
sessionTimeout,
identity,
preferredLocales,
checkDomain,
closeChannel).GetAwaiter().GetResult();
}
/// <summary>
/// Updates the preferred locales used for the session.
/// </summary>
[Obsolete("Use ChangePreferredLocalesAsync instead.")]
public static void ChangePreferredLocales(
this ISession session,
ArrayOf<string> preferredLocales)
{
session.ChangePreferredLocalesAsync(preferredLocales)
.GetAwaiter()
.GetResult();
}
/// <summary>
/// Updates the user identity and/or locales used for the session.
/// </summary>
[Obsolete("Use UpdateSessionAsync instead.")]
public static void UpdateSession(
this ISession session,
IUserIdentity identity,
ArrayOf<string> preferredLocales)
{
session.UpdateSessionAsync(identity, preferredLocales)
.GetAwaiter()
.GetResult();
}
/// <summary>
/// Finds the NodeIds for the components for an instance.
/// </summary>
[Obsolete("Use FindComponentIdsAsync instead.")]
public static void FindComponentIds(this ISession session,
NodeId instanceId,
IList<string> componentPaths,
out ArrayOf<NodeId> componentIds,
out ArrayOf<ServiceResult> errors)
{
(componentIds, errors) = session.FindComponentIdsAsync(
instanceId,
componentPaths)
.GetAwaiter()
.GetResult();
}
/// <summary>
/// Reads the values for a set of variables.
/// </summary>
[Obsolete("Use Use ReadValuesAsync instead.")]
public static void ReadValues(this ISession session,
ArrayOf<NodeId> variableIds,
ArrayOf<Type?> expectedTypes,
out ArrayOf<object?> values,
out ArrayOf<ServiceResult> errors)
{
(ArrayOf<DataValue> dataValues, ArrayOf<ServiceResult> errorValues) = session.ReadValuesAsync(
variableIds)
.GetAwaiter()
.GetResult();
object?[] valuesBuffer = new object?[dataValues.Count];
var errorsBuffer = new ServiceResult[errorValues.Count];
for (int ii = 0; ii < variableIds.Count; ii++)
{
object? value = dataValues[ii].Value;
// extract the body from extension objects.
if (dataValues[ii].WrappedValue.TryGetValue(out ExtensionObject extension) &&
extension.TryGetValue(out IEncodeable? encodeable))
{
value = encodeable;
}
// check expected type.
Type? expectedType = expectedTypes[ii];
if (expectedType != null &&
!expectedType.IsInstanceOfType(value))
{
errorsBuffer[ii] = ServiceResult.Create(
StatusCodes.BadTypeMismatch,
"Value {0} does not have expected type: {1}.",
value!,
expectedType.Name);
continue;
}
// suitable value found.
valuesBuffer[ii] = value;
errorsBuffer[ii] = errorValues[ii];
}
values = valuesBuffer;
errors = errorsBuffer;
}
/// <summary>
/// Reads the display name for a set of Nodes.
/// </summary>
[Obsolete("Use Use ReadValuesAsync instead.")]
public static void ReadDisplayName(this ISession session,
ArrayOf<NodeId> nodeIds,
out ArrayOf<string> displayNames,
out ArrayOf<ServiceResult> errors)
{
(displayNames, errors) = session.ReadDisplayNameAsync(nodeIds)
.GetAwaiter()
.GetResult();
}
/// <summary>
/// Closes the client object and the underlying channel.
/// </summary>
[Obsolete("Use CloseAsync instead.")]
public static StatusCode Close(this IClientBase client)
{
return client.CloseAsync().GetAwaiter().GetResult();
}
/// <summary>
/// Disconnects from the server and frees any network resources
/// with the specified timeout.
/// </summary>
[Obsolete("Use CloseAsync instead.")]
public static StatusCode Close(this ISession session, int timeout)
{
return session.CloseAsync(timeout).GetAwaiter().GetResult();
}
/// <summary>
/// Close the session with the server and optionally closes the channel.
/// </summary>
[Obsolete("Use CloseAsync instead.")]
public static StatusCode Close(this ISession session, bool closeChannel)
{
return session.CloseAsync(closeChannel)
.GetAwaiter()
.GetResult();
}
/// <summary>
/// Disconnects from the server and frees any network resources
/// with the specified timeout.
/// </summary>
[Obsolete("Use CloseAsync instead.")]
public static StatusCode Close(
this ISession session,
int timeout,
bool closeChannel)
{
return session.CloseAsync(timeout, closeChannel)
.GetAwaiter()
.GetResult();
}
/// <summary>
/// Removes a subscription from the session.
/// </summary>
[Obsolete("Use RemoveSubscriptionAsync instead.")]
public static bool RemoveSubscription(
this ISession session,
Subscription subscription)
{
return session.RemoveSubscriptionAsync(subscription)
.GetAwaiter()
.GetResult();
}
/// <summary>
/// Remove a list of subscriptions from the session.
/// </summary>
[Obsolete("Use RemoveSubscriptionsAsync instead.")]
public static bool RemoveSubscriptions(
this ISession session,
IEnumerable<Subscription> subscriptions)
{
return session.RemoveSubscriptionsAsync(subscriptions)
.GetAwaiter()
.GetResult();
}
/// <summary>
/// Reactivates a list of subscriptions loaded from storage.
/// </summary>
[Obsolete("Use ReactivateSubscriptionsAsync instead.")]
public static bool ReactivateSubscriptions(
this ISession session,
SubscriptionCollection subscriptions,
bool sendInitialValues)
{
return session.ReactivateSubscriptionsAsync(
subscriptions,
sendInitialValues)
.GetAwaiter()
.GetResult();
}
/// <summary>
/// Transfers a list of subscriptions from another session.
/// </summary>
[Obsolete("Use TransferSubscriptionsAsync instead.")]
public static bool TransferSubscriptions(
this ISession session,
SubscriptionCollection subscriptions,
bool sendInitialValues)
{
return session.TransferSubscriptionsAsync(
subscriptions,
sendInitialValues).GetAwaiter().GetResult();
}
/// <summary>
/// Invokes the Browse service.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
[Obsolete("Use BrowseAsync instead.")]
public static ResponseHeader Browse(
this ISession session,
RequestHeader requestHeader,
ViewDescription view,
NodeId nodeToBrowse,
uint maxResultsToReturn,
BrowseDirection browseDirection,
NodeId referenceTypeId,
bool includeSubtypes,
uint nodeClassMask,
out ByteString continuationPoint,
out ArrayOf<ReferenceDescription> references)
{
(
ResponseHeader responseHeader,
ArrayOf<ByteString> continuationPoints,
ArrayOf<ArrayOf<ReferenceDescription>> referencesList,
ArrayOf<ServiceResult> errors
) =
session.BrowseAsync(
requestHeader,
view,
[nodeToBrowse],
maxResultsToReturn,
browseDirection,
referenceTypeId,
includeSubtypes,
nodeClassMask)
.GetAwaiter()
.GetResult();
Debug.Assert(errors.Count <= 1);
if (errors.Count > 0 && StatusCode.IsBad(errors[0].StatusCode))
{
throw new ServiceResultException(errors[0]);
}
Debug.Assert(referencesList.Count == 1);
Debug.Assert(continuationPoints.Count == 1);
references = referencesList[0];
continuationPoint = continuationPoints[0];
return responseHeader;
}
/// <summary>
/// Invokes the BrowseNext service.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
[Obsolete("Use BrowseNextAsync instead.")]
public static ResponseHeader BrowseNext(
this ISession session,
RequestHeader requestHeader,
bool releaseContinuationPoint,
ByteString continuationPoint,
out ByteString revisedContinuationPoint,
out ArrayOf<ReferenceDescription> references)
{
(
ResponseHeader responseHeader,
ArrayOf<ByteString> revisedContinuationPoints,
ArrayOf<ArrayOf<ReferenceDescription>> referencesList,
ArrayOf<ServiceResult> errors
) =
session.BrowseNextAsync(
requestHeader,
[continuationPoint],
releaseContinuationPoint)
.GetAwaiter()
.GetResult();
Debug.Assert(errors.Count <= 1);
if (errors.Count > 0 && StatusCode.IsBad(errors[0].StatusCode))
{
throw new ServiceResultException(errors[0]);
}
Debug.Assert(referencesList.Count == 1);
Debug.Assert(revisedContinuationPoints.Count == 1);
references = referencesList[0];
revisedContinuationPoint = revisedContinuationPoints[0];
return responseHeader;
}
/// <summary>
/// Execute browse and, if necessary, browse next in one service call.
/// Takes care of BadNoContinuationPoint and BadInvalidContinuationPoint status codes.
/// </summary>
[Obsolete("Use ManagedBrowseAsync instead.")]
public static void ManagedBrowse(
this ISession session,
RequestHeader requestHeader,
ViewDescription view,
ArrayOf<NodeId> nodesToBrowse,
uint maxResultsToReturn,
BrowseDirection browseDirection,
NodeId referenceTypeId,
bool includeSubtypes,
uint nodeClassMask,
out ArrayOf<ArrayOf<ReferenceDescription>> result,
out ArrayOf<ServiceResult> errors)
{
(result, errors) = session.ManagedBrowseAsync(
requestHeader,
view,
nodesToBrowse,
maxResultsToReturn,
browseDirection,
referenceTypeId,
includeSubtypes,
nodeClassMask)
.GetAwaiter()
.GetResult();
}
/// <summary>
/// Calls the specified method and returns the output arguments.
/// </summary>
[Obsolete("Use CallAsync instead.")]
public static ArrayOf<Variant> Call(
this ISession session,
NodeId objectId,
NodeId methodId,
params Variant[] args)
{
return session.CallAsync(
objectId,
methodId,
default,
args)
.GetAwaiter()
.GetResult();
}
/// <summary>
/// Sends a republish request.
/// </summary>
[Obsolete("Use RepublishAsync instead.")]
public static bool Republish(
this ISession session,
uint subscriptionId,
uint sequenceNumber,
out ServiceResult error)
{
(bool result, error) = session.RepublishAsync(
subscriptionId,
sequenceNumber)
.GetAwaiter()
.GetResult();
return result;
}
/// <summary>
/// Call the ResendData method on the server for all subscriptions.
/// </summary>
[Obsolete("Use ResendDataAsync instead.")]
public static bool ResendData(
this ISession session,
IEnumerable<Subscription> subscriptions,
out ArrayOf<ServiceResult> errors)
{
(bool success, errors) = session.ResendDataAsync(subscriptions)
.GetAwaiter()
.GetResult();
return success;
}
/// <summary>
/// Call the ResendData method on the server for all subscriptions.
/// </summary>
[Obsolete("Use ResendDataAsync using subscription ids instead.")]
public static async Task<(bool, ArrayOf<ServiceResult>)> ResendDataAsync(
this ISession session,
IEnumerable<Subscription> subscriptions,
CancellationToken ct = default)
{
try
{
ArrayOf<ServiceResult> errorsRo = await session.ResendDataAsync(
subscriptions.Select(s => s.Id).ToArrayOf(), ct).ConfigureAwait(false);
return (true, errorsRo);
}
catch
{
return (false, Array.Empty<ServiceResult>());
}
}
}
public partial class Session
{
/// <summary>
/// Creates a new communication session with a server by invoking the CreateSession service
/// </summary>
[Obsolete("Use ISessionFactory.CreateAsync")]
public static Task<Session> Create(
ApplicationConfiguration configuration,
ConfiguredEndpoint endpoint,
bool updateBeforeConnect,
string sessionName,
uint sessionTimeout,
IUserIdentity identity,
ArrayOf<string> preferredLocales,
CancellationToken ct = default)
{
return Create(
configuration,
endpoint,
updateBeforeConnect,
false,
sessionName,
sessionTimeout,
identity,
preferredLocales,
ct);
}
/// <summary>
/// Creates a new communication session with a server by invoking the CreateSession service
/// </summary>
[Obsolete("Use ISessionFactory.CreateAsync")]
public static Task<Session> Create(
ApplicationConfiguration configuration,
ConfiguredEndpoint endpoint,
bool updateBeforeConnect,
bool checkDomain,
string sessionName,
uint sessionTimeout,
IUserIdentity identity,
ArrayOf<string> preferredLocales,
CancellationToken ct = default)
{
return Create(
configuration,
// No reverse-connect transport in this overload; the modern API requires
// a non-nullable parameter so this obsolete bridge forwards null!.
(ITransportWaitingConnection)null!,
endpoint,
updateBeforeConnect,
checkDomain,
sessionName,
sessionTimeout,
identity,
preferredLocales,
ct);
}
/// <summary>
/// Creates a new session with a server using the specified channel
/// </summary>
[Obsolete("Use ISessionFactory.CreateAsync")]
public static Session Create(
ApplicationConfiguration configuration,
ITransportChannel channel,
ConfiguredEndpoint endpoint,
Certificate clientCertificate,
ArrayOf<EndpointDescription> availableEndpoints = default,
ArrayOf<string> discoveryProfileUris = default)
{
return Create(
// Telemetry context is nullable on this obsolete bridge but required by the
// modern API; null! preserves the pre-nullable lookup behavior.
null!,
configuration,
channel,
endpoint,
clientCertificate,
availableEndpoints,
discoveryProfileUris);
}
/// <summary>
/// Recreates a session based on a specified template.
/// </summary>
/// <param name="template">The Session object to use as template</param>
/// <returns>The new session object.</returns>
[Obsolete("Use ISessionFactory.RecreateAsync")]
public static Session Recreate(Session template)
{
return RecreateAsync(template).GetAwaiter().GetResult();
}
/// <summary>
/// Recreates a session based on a specified template.
/// </summary>
/// <param name="template">The Session object to use as template</param>
/// <param name="connection">The waiting reverse connection.</param>
/// <returns>The new session object.</returns>
[Obsolete("Use ISessionFactory.RecreateAsync")]
public static Session Recreate(Session template, ITransportWaitingConnection connection)
{
return RecreateAsync(template, connection).GetAwaiter().GetResult();
}
/// <summary>
/// Recreates a session based on a specified template using the provided channel.
/// </summary>
/// <param name="template">The Session object to use as template</param>
/// <param name="transportChannel">The waiting reverse connection.</param>
/// <returns>The new session object.</returns>
[Obsolete("Use ISessionFactory.RecreateAsync")]
public static Session Recreate(Session template, ITransportChannel transportChannel)
{
return RecreateAsync(template, transportChannel).GetAwaiter().GetResult();
}
/// <summary>
/// Recreates a session based on a specified template.
/// </summary>
[Obsolete("Use ISessionFactory.RecreateAsync")]
public static async Task<Session> RecreateAsync(
Session sessionTemplate,
CancellationToken ct = default)
{
var factory = new DefaultSessionFactory(sessionTemplate.MessageContext.Telemetry)
{
ReturnDiagnostics = sessionTemplate.ReturnDiagnostics
};
return (Session)await factory.RecreateAsync(
sessionTemplate,
ct).ConfigureAwait(false);
}
/// <summary>
/// Recreates a session based on a specified template.
/// </summary>
[Obsolete("Use ISessionFactory.RecreateAsync")]
public static async Task<Session> RecreateAsync(
Session sessionTemplate,
ITransportWaitingConnection connection,
CancellationToken ct = default)
{
var factory = new DefaultSessionFactory(sessionTemplate.MessageContext.Telemetry)
{
ReturnDiagnostics = sessionTemplate.ReturnDiagnostics
};
return (Session)await factory.RecreateAsync(
sessionTemplate,
connection,
ct).ConfigureAwait(false);
}
/// <summary>
/// Recreates a session based on a specified template using the provided channel.
/// </summary>
[Obsolete("Use ISessionFactory.RecreateAsync")]
public static async Task<Session> RecreateAsync(
Session sessionTemplate,
ITransportChannel transportChannel,
CancellationToken ct = default)
{
var factory = new DefaultSessionFactory(sessionTemplate.MessageContext.Telemetry)
{
ReturnDiagnostics = sessionTemplate.ReturnDiagnostics
};
return (Session)await factory.RecreateAsync(
sessionTemplate,
transportChannel,
ct).ConfigureAwait(false);
}
/// <summary>
/// Creates a new communication session with a server using a reverse connection.
/// </summary>
[Obsolete("Use ISessionFactory.CreateAsync")]
public static Task<Session> Create(
ApplicationConfiguration configuration,
ITransportWaitingConnection connection,
ConfiguredEndpoint endpoint,
bool updateBeforeConnect,
bool checkDomain,
string sessionName,
uint sessionTimeout,
IUserIdentity identity,
ArrayOf<string> preferredLocales,
CancellationToken ct = default)
{
return CreateAsync(
// Telemetry context is nullable on this obsolete bridge but required by the
// modern API; null! preserves the pre-nullable lookup behavior.
null!,
configuration,
connection,
endpoint,
updateBeforeConnect,
checkDomain,
sessionName,
sessionTimeout,
identity,
preferredLocales,
DiagnosticsMasks.None,
ct);
}
/// <summary>
/// Create a session
/// </summary>
[Obsolete("Use ISessionFactory.CreateAsync")]
public static Task<Session> Create(
ISessionInstantiator sessionInstantiator,
ApplicationConfiguration configuration,
ITransportWaitingConnection connection,
ConfiguredEndpoint endpoint,
bool updateBeforeConnect,
bool checkDomain,
string sessionName,
uint sessionTimeout,
IUserIdentity identity,
ArrayOf<string> preferredLocales,
CancellationToken ct = default)
{
return CreateAsync(
sessionInstantiator,
configuration,
connection,
endpoint,
updateBeforeConnect,
checkDomain,
sessionName,
sessionTimeout,
identity,
preferredLocales,
DiagnosticsMasks.None,
ct);
}
/// <summary>
/// Create a session
/// </summary>
[Obsolete("Use ISessionFactory.CreateAsync")]
public static Task<Session> Create(
ISessionInstantiator sessionInstantiator,
ApplicationConfiguration configuration,
ReverseConnectManager reverseConnectManager,
ConfiguredEndpoint endpoint,
bool updateBeforeConnect,
bool checkDomain,
string sessionName,
uint sessionTimeout,
IUserIdentity userIdentity,
ArrayOf<string> preferredLocales,
CancellationToken ct = default)
{
return CreateAsync(
sessionInstantiator,
configuration,
reverseConnectManager,
endpoint,
updateBeforeConnect,
checkDomain,
sessionName,
sessionTimeout,
userIdentity,
preferredLocales,
DiagnosticsMasks.None,
ct);
}
/// <summary>
/// Create a session
/// </summary>
[Obsolete("Use ISessionFactory.CreateAsync")]
public static Task<Session> Create(
ApplicationConfiguration configuration,
ReverseConnectManager reverseConnectManager,
ConfiguredEndpoint endpoint,
bool updateBeforeConnect,
bool checkDomain,
string sessionName,
uint sessionTimeout,
IUserIdentity userIdentity,
ArrayOf<string> preferredLocales,
CancellationToken ct = default)
{
return CreateAsync(
configuration,
reverseConnectManager,
endpoint,
updateBeforeConnect,
checkDomain,
sessionName,
sessionTimeout,
userIdentity,
preferredLocales,
ct);
}
/// <summary>
/// Creates a new session.
/// </summary>
[Obsolete("Use ISessionFactory.Create")]
public static Session Create(
ISessionInstantiator sessionInstantiator,
ApplicationConfiguration configuration,
ITransportChannel channel,
ConfiguredEndpoint endpoint,
Certificate clientCertificate,
ArrayOf<EndpointDescription> availableEndpoints = default,
ArrayOf<string> discoveryProfileUris = default)
{
ServiceMessageContext context = configuration.CreateMessageContext(false);
var factory = new DefaultSessionFactory(context.Telemetry);
return (Session)factory.Create(
channel,
configuration,
endpoint,
clientCertificate,
null,
availableEndpoints,
discoveryProfileUris);
}
/// <summary>
/// Creates a secure channel to the specified endpoint.
/// </summary>
[Obsolete("Use ISessionFactory.CreateChannelAsync")]
public static Task<ITransportChannel> CreateChannelAsync(
ApplicationConfiguration configuration,
ITransportWaitingConnection connection,
ConfiguredEndpoint endpoint,
bool updateBeforeConnect,
bool checkDomain,
CancellationToken ct = default)
{
ServiceMessageContext context = configuration.CreateMessageContext(false);
var factory = new DefaultSessionFactory(context.Telemetry);
return factory.CreateChannelAsync(
configuration,
connection,
endpoint,
updateBeforeConnect,
checkDomain,
ct);
}
/// <summary>
/// Creates a new communication session with a server using a reverse connection.
/// </summary>
[Obsolete("Use ISessionFactory.CreateAsync")]
public static async Task<Session> CreateAsync(
ISessionInstantiator sessionInstantiator,
ApplicationConfiguration configuration,
ITransportWaitingConnection connection,
ConfiguredEndpoint endpoint,
bool updateBeforeConnect,
bool checkDomain,
string sessionName,
uint sessionTimeout,
IUserIdentity identity,
ArrayOf<string> preferredLocales,
DiagnosticsMasks returnDiagnostics,
CancellationToken ct = default)
{
ServiceMessageContext context = configuration.CreateMessageContext(false);
var factory = new DefaultSessionFactory(context.Telemetry)
{
ReturnDiagnostics = returnDiagnostics
};
return (Session)await factory.CreateAsync(
configuration,
connection,
endpoint,
updateBeforeConnect,
checkDomain,
sessionName,
sessionTimeout,
identity,
preferredLocales,
ct).ConfigureAwait(false);
}
/// <summary>
/// Creates a new communication session with a server using a reverse connect manager.
/// </summary>
[Obsolete("Use ISessionFactory.CreateAsync")]
public static async Task<Session> CreateAsync(
ApplicationConfiguration configuration,
ReverseConnectManager reverseConnectManager,
ConfiguredEndpoint endpoint,
bool updateBeforeConnect,
bool checkDomain,
string sessionName,
uint sessionTimeout,
IUserIdentity userIdentity,
ArrayOf<string> preferredLocales,
CancellationToken ct = default)
{
ServiceMessageContext context = configuration.CreateMessageContext(false);
var factory = new DefaultSessionFactory(context.Telemetry);
return (Session)await factory.CreateAsync(
configuration,
reverseConnectManager,
endpoint,
updateBeforeConnect,
checkDomain,
sessionName,
sessionTimeout,
userIdentity,
preferredLocales,
ct).ConfigureAwait(false);
}
/// <summary>
/// Creates a new communication session with a server using a reverse connect manager.
/// </summary>
[Obsolete("Use ISessionFactory.CreateAsync")]
public static async Task<Session> CreateAsync(
ISessionInstantiator sessionInstantiator,
ApplicationConfiguration configuration,
ReverseConnectManager reverseConnectManager,
ConfiguredEndpoint endpoint,
bool updateBeforeConnect,
bool checkDomain,
string sessionName,
uint sessionTimeout,
IUserIdentity userIdentity,
ArrayOf<string> preferredLocales,
DiagnosticsMasks returnDiagnostics,
CancellationToken ct = default)
{
ServiceMessageContext context = configuration.CreateMessageContext(false);
var factory = new DefaultSessionFactory(context.Telemetry)
{
ReturnDiagnostics = returnDiagnostics
};
return (Session)await factory.CreateAsync(
configuration,
reverseConnectManager,
endpoint,
updateBeforeConnect,
checkDomain,
sessionName,
sessionTimeout,
userIdentity,
preferredLocales,
ct).ConfigureAwait(false);
}
}
/// <summary>
/// Obsolete traceable session, which now is supported by session
/// </summary>
[Obsolete("Use Session which also provides tracing")]
public class TraceableSession : Session
{
/// <summary>
/// Create session
/// </summary>
public TraceableSession(
ISessionChannel channel,
ApplicationConfiguration configuration,
ConfiguredEndpoint endpoint)
: base(channel, configuration, endpoint)
{
}
/// <summary>
/// Create session
/// </summary>
public TraceableSession(
ITransportChannel channel,
Session template,
bool copyEventHandlers)
: base(channel, template, copyEventHandlers)
{
}
/// <summary>
/// Create session
/// </summary>
public TraceableSession(
ITransportChannel channel,
ApplicationConfiguration configuration,
ConfiguredEndpoint endpoint,
Certificate clientCertificate,
ArrayOf<EndpointDescription> availableEndpoints = default,
ArrayOf<string> discoveryProfileUris = default)
: base(
channel,
configuration,
endpoint,
clientCertificate,
null,
availableEndpoints,
discoveryProfileUris)
{
}
/// <summary>
/// Object that creates instances of a session
/// </summary>
[Obsolete("Use DefaultSessionFactory which also provides tracing capabilities.")]
public class TraceableSessionFactory : DefaultSessionFactory
{
/// <summary>
/// The default instance of the factory.
/// </summary>
public static new readonly TraceableSessionFactory Instance = new();
/// <summary>
/// Obsolete default constructor
/// </summary>
public TraceableSessionFactory()
// Telemetry is required by the base ctor; this obsolete parameterless ctor
// forwards null! to preserve the pre-nullable instantiation pattern.
: base(null!)
{
}
}
}
/// <summary>
/// Object that creates an instance of a Session object.
/// </summary>
[Obsolete("Use ISessionFactory instead. This interface will be removed in a future release.")]
public interface ISessionInstantiator : ISessionFactory;
/// <summary>
/// Obsolete session factory api
/// </summary>
public static class SessionFactoryObsolete
{
/// <summary>
/// Create session
/// </summary>
[Obsolete("Use Create with channel and configuration reversed")]
public static ISession Create(
this ISessionFactory factory,
ApplicationConfiguration configuration,
ITransportChannel channel,
ConfiguredEndpoint endpoint,
Certificate clientCertificate,
ArrayOf<EndpointDescription> availableEndpoints = default,
ArrayOf<string> discoveryProfileUris = default)
{
return factory.Create(
channel,
configuration,
endpoint,
clientCertificate,
null,
availableEndpoints,
discoveryProfileUris);
}
}
}