diff --git a/Applications/ConsoleReferenceClient/ClientSamples.cs b/Applications/ConsoleReferenceClient/ClientSamples.cs
index 83e2dc187b..a1b4d1cf4b 100644
--- a/Applications/ConsoleReferenceClient/ClientSamples.cs
+++ b/Applications/ConsoleReferenceClient/ClientSamples.cs
@@ -258,7 +258,7 @@ public async Task BrowseAsync(ISession session, CancellationToken ct = default)
try
{
// Create a Browser object
- var browser = new Browser(session, m_telemetry)
+ var browser = new Browser(session)
{
// Set browse parameters
BrowseDirection = BrowseDirection.Forward,
@@ -1192,7 +1192,7 @@ private static Task FetchReferenceIdTypesAsync(
///
/// Output all values as JSON.
///
- public async Task<(DataValueCollection, IList)> ReadAllValuesAsync(
+ public async Task> ReadAllValuesAsync(
IUAClient uaClient,
NodeIdCollection variableIds,
CancellationToken ct = default)
@@ -1278,7 +1278,7 @@ private static Task FetchReferenceIdTypesAsync(
}
} while (retrySingleRead);
- return (values, errors);
+ return ResultSet.From(values, errors);
}
///
diff --git a/Applications/ConsoleReferenceClient/Program.cs b/Applications/ConsoleReferenceClient/Program.cs
index 9199c87e34..c842c8a198 100644
--- a/Applications/ConsoleReferenceClient/Program.cs
+++ b/Applications/ConsoleReferenceClient/Program.cs
@@ -570,8 +570,10 @@ r is VariableNode v &&
if (jsonvalues && variableIds != null)
{
- (DataValueCollection allValues, IList results)
- = await samples
+ (
+ IReadOnlyList allValues,
+ IReadOnlyList results
+ ) = await samples
.ReadAllValuesAsync(uaClient, variableIds, ct)
.ConfigureAwait(false);
}
diff --git a/Applications/ConsoleReferenceClient/UAClient.cs b/Applications/ConsoleReferenceClient/UAClient.cs
index 173295ccf8..ae91b691d9 100644
--- a/Applications/ConsoleReferenceClient/UAClient.cs
+++ b/Applications/ConsoleReferenceClient/UAClient.cs
@@ -245,7 +245,7 @@ public async Task ConnectAsync(
endpointConfiguration);
// Create the session factory. - we could take it as parameter or as member
- var sessionFactory = new TraceableSessionFactory(m_telemetry);
+ var sessionFactory = new DefaultSessionFactory(m_telemetry);
// Create the session
ISession session = await sessionFactory
diff --git a/Libraries/Opc.Ua.Client.ComplexTypes/TypeResolver/NodeCacheResolver.cs b/Libraries/Opc.Ua.Client.ComplexTypes/TypeResolver/NodeCacheResolver.cs
index b5421d9f7c..06248905d4 100644
--- a/Libraries/Opc.Ua.Client.ComplexTypes/TypeResolver/NodeCacheResolver.cs
+++ b/Libraries/Opc.Ua.Client.ComplexTypes/TypeResolver/NodeCacheResolver.cs
@@ -52,7 +52,9 @@ public class NodeCacheResolver : IComplexTypeResolver
/// Initializes the type resolver with a session to load the custom type information.
///
public NodeCacheResolver(ISession session, ITelemetryContext telemetry)
- : this(new LruNodeCache(session, telemetry), telemetry)
+ : this(session, new LruNodeCache(
+ new NodeCacheContext(session),
+ telemetry), telemetry)
{
}
@@ -64,7 +66,10 @@ public NodeCacheResolver(
ISession session,
TimeSpan cacheExpiry,
ITelemetryContext telemetry)
- : this(new LruNodeCache(session, telemetry, cacheExpiry), telemetry)
+ : this(session, new LruNodeCache(
+ new NodeCacheContext(session),
+ telemetry,
+ cacheExpiry), telemetry)
{
}
@@ -76,7 +81,11 @@ public NodeCacheResolver(
TimeSpan cacheExpiry,
int capacity,
ITelemetryContext telemetry)
- : this(new LruNodeCache(session, telemetry, cacheExpiry, capacity), telemetry)
+ : this(session, new LruNodeCache(
+ new NodeCacheContext(session),
+ telemetry,
+ cacheExpiry,
+ capacity), telemetry)
{
}
@@ -84,9 +93,12 @@ public NodeCacheResolver(
/// Initializes the type resolver with a session and lru cache to load the
/// custom type information with the specified expiry and cache size.
///
- public NodeCacheResolver(ILruNodeCache lruNodeCache, ITelemetryContext telemetry)
+ public NodeCacheResolver(
+ ISession session,
+ ILruNodeCache lruNodeCache,
+ ITelemetryContext telemetry)
{
- m_session = lruNodeCache.Session;
+ m_session = session;
m_lruNodeCache = lruNodeCache;
m_logger = telemetry.CreateLogger();
FactoryBuilder = m_session.Factory.Builder;
diff --git a/Libraries/Opc.Ua.Client/Browser.cs b/Libraries/Opc.Ua.Client/Browser.cs
index a5c8d7df3c..d7698ec1f4 100644
--- a/Libraries/Opc.Ua.Client/Browser.cs
+++ b/Libraries/Opc.Ua.Client/Browser.cs
@@ -28,218 +28,196 @@
* ======================================================================*/
using System;
+using System.Collections.Generic;
+using System.IO;
using System.Runtime.Serialization;
using System.Threading;
using System.Threading.Tasks;
+using System.Xml;
using Microsoft.Extensions.Logging;
namespace Opc.Ua.Client
{
///
- /// Stores the options to use for a browse operation.
+ /// Address space browsing functionality.
///
- [DataContract(Namespace = Namespaces.OpcUaXsd)]
public class Browser
{
///
- /// Creates an unattached instance of a browser.
+ /// Creates new instance of a browser and attaches it to a session.
///
- public Browser()
+ public Browser(
+ ITelemetryContext telemetry,
+ BrowserOptions? options = null)
{
- Initialize();
+ m_telemetry = telemetry;
+ m_logger = m_telemetry.CreateLogger();
+ State = options ?? new BrowserOptions();
}
///
/// Creates new instance of a browser and attaches it to a session.
///
- public Browser(ISession session)
- : this(session, session.MessageContext.Telemetry)
+ public Browser(
+ ISessionClient session,
+ BrowserOptions? options = null)
{
+ m_telemetry = session.MessageContext.Telemetry;
+ m_logger = m_telemetry.CreateLogger();
+ State = options ?? new BrowserOptions();
+ Session = session;
}
///
- /// Creates new instance of a browser and attaches it to a session.
+ /// Creates an unattached instance of a browser.
///
- public Browser(ISession session, ITelemetryContext telemetry)
+ [Obsolete("Use constructor with ISessionClient or ITelemetryContext.")]
+ public Browser(BrowserOptions? options = null)
{
- m_telemetry = telemetry;
-
- Initialize();
-
- m_session = session;
+ m_logger = m_telemetry.CreateLogger();
+ State = options ?? new BrowserOptions();
}
///
/// Creates a copy of a browser.
///
+ [Obsolete("Use the constructor that accepts BrowserOptions instead.")]
public Browser(Browser template)
{
- if (template != null)
- {
- m_logger = template.m_logger;
- m_telemetry = template.m_telemetry;
- }
-
- Initialize();
-
- if (template != null)
+ if (template == null)
{
- m_logger = template.m_logger;
- m_telemetry = template.m_telemetry;
- m_session = template.m_session;
- m_view = template.m_view;
- m_maxReferencesReturned = template.m_maxReferencesReturned;
- m_browseDirection = template.m_browseDirection;
- m_referenceTypeId = template.m_referenceTypeId;
- m_includeSubtypes = template.m_includeSubtypes;
- m_nodeClassMask = template.m_nodeClassMask;
- m_resultMask = template.m_resultMask;
- m_continueUntilDone = template.m_continueUntilDone;
+ throw new ArgumentNullException(nameof(template));
}
+ m_logger = template.m_logger;
+ m_telemetry = template.m_telemetry;
+ State = template.State;
+ Session = template.Session;
+ ContinueUntilDone = template.ContinueUntilDone;
}
///
- /// Sets all private fields to default values.
+ /// Browwser state
///
- private void Initialize()
- {
- m_session = null;
- m_view = null;
- m_maxReferencesReturned = 0;
- m_browseDirection = BrowseDirection.Forward;
- m_referenceTypeId = null;
- m_includeSubtypes = true;
- m_nodeClassMask = 0;
- m_resultMask = (uint)BrowseResultMask.All;
- m_continueUntilDone = false;
- m_browseInProgress = false;
-
- m_logger ??= Telemetry.CreateLogger();
- }
+ public BrowserOptions State { get; private set; }
///
/// The session that the browse is attached to.
///
- public ISession Session
+ public ISessionClient? Session
{
get => m_session;
set
{
- CheckBrowserState();
+ if (value is ISession session)
+ {
+ MaxNodesPerBrowse =
+ session.OperationLimits.MaxNodesPerBrowse;
+ MaxBrowseContinuationPoints =
+ session.ServerCapabilities.MaxBrowseContinuationPoints;
+ ContinuationPointPolicy =
+ session.ContinuationPointPolicy;
+ }
m_session = value;
}
}
///
- /// Enables owners to set the telemetry context
+ /// The view to use for the browse operation.
///
- public ITelemetryContext Telemetry
+ public RequestHeader? RequestHeader
{
- get => m_telemetry;
- set
- {
- CheckBrowserState();
- m_telemetry = value;
- m_logger = value.CreateLogger(this);
- }
+ get => State.RequestHeader;
+ set => State = State with { RequestHeader = value };
+ }
+
+ ///
+ /// The continuation point strategy to use
+ ///
+ public ContinuationPointPolicy ContinuationPointPolicy
+ {
+ get => State.ContinuationPointPolicy;
+ set => State = State with { ContinuationPointPolicy = value };
+ }
+
+ ///
+ /// Max nodes to browse in a single operation
+ ///
+ public uint MaxNodesPerBrowse
+ {
+ get => State.MaxNodesPerBrowse;
+ set => State = State with { MaxNodesPerBrowse = value };
+ }
+
+ ///
+ /// Max continuation point limit to use
+ ///
+ public ushort MaxBrowseContinuationPoints
+ {
+ get => State.MaxBrowseContinuationPoints;
+ set => State = State with { MaxBrowseContinuationPoints = value };
}
///
/// The view to use for the browse operation.
///
- [DataMember(Order = 1)]
- public ViewDescription View
+ public ViewDescription? View
{
- get => m_view;
- set
- {
- CheckBrowserState();
- m_view = value;
- }
+ get => State.View;
+ set => State = State with { View = value };
}
///
/// The maximum number of references to return in a single browse operation.
///
- [DataMember(Order = 2)]
public uint MaxReferencesReturned
{
- get => m_maxReferencesReturned;
- set
- {
- CheckBrowserState();
- m_maxReferencesReturned = value;
- }
+ get => State.MaxReferencesReturned;
+ set => State = State with { MaxReferencesReturned = value };
}
///
/// The direction to browse.
///
- [DataMember(Order = 3)]
public BrowseDirection BrowseDirection
{
- get => m_browseDirection;
- set
- {
- CheckBrowserState();
- m_browseDirection = value;
- }
+ get => State.BrowseDirection;
+ set => State = State with { BrowseDirection = value };
}
///
/// The reference type to follow.
///
- [DataMember(Order = 4)]
public NodeId ReferenceTypeId
{
- get => m_referenceTypeId;
- set
- {
- CheckBrowserState();
- m_referenceTypeId = value;
- }
+ get => State.ReferenceTypeId;
+ set => State = State with { ReferenceTypeId = value };
}
///
/// Whether subtypes of the reference type should be included.
///
- [DataMember(Order = 5)]
public bool IncludeSubtypes
{
- get => m_includeSubtypes;
- set
- {
- CheckBrowserState();
- m_includeSubtypes = value;
- }
+ get => State.IncludeSubtypes;
+ set => State = State with { IncludeSubtypes = value };
}
///
/// The classes of the target nodes.
///
- [DataMember(Order = 6)]
- public int NodeClassMask
+ public uint NodeClassMask
{
- get => Utils.ToInt32(m_nodeClassMask);
- set
- {
- CheckBrowserState();
- m_nodeClassMask = Utils.ToUInt32(value);
- }
+ get => Utils.ToUInt32(State.NodeClassMask);
+ set => State = State with { NodeClassMask = Utils.ToInt32(value) };
}
///
/// The results to return.
///
- [DataMember(Order = 6)]
public uint ResultMask
{
- get => m_resultMask;
- set
- {
- CheckBrowserState();
- m_resultMask = value;
- }
+ get => State.ResultMask;
+ set => State = State with { ResultMask = value };
}
///
@@ -254,15 +232,7 @@ public event BrowserEventHandler MoreReferences
///
/// Whether subsequent continuation points should be processed automatically.
///
- public bool ContinueUntilDone
- {
- get => m_continueUntilDone;
- set
- {
- CheckBrowserState();
- m_continueUntilDone = value;
- }
- }
+ public bool ContinueUntilDone { get; set; }
///
/// Browses the specified node.
@@ -281,137 +251,439 @@ public async ValueTask BrowseAsync(
NodeId nodeId,
CancellationToken ct = default)
{
- if (m_session == null)
- {
+ BrowserOptions state = State;
+ ISessionClient session = Session ??
throw new ServiceResultException(
StatusCodes.BadServerNotConnected,
"Cannot browse if not connected to a server.");
- }
- try
+ // construct request.
+ var nodeToBrowse = new BrowseDescription
{
- m_browseInProgress = true;
+ NodeId = nodeId,
+ BrowseDirection = state.BrowseDirection,
+ ReferenceTypeId = state.ReferenceTypeId,
+ IncludeSubtypes = state.IncludeSubtypes,
+ NodeClassMask = Utils.ToUInt32(state.NodeClassMask),
+ ResultMask = state.ResultMask
+ };
- // construct request.
- var nodeToBrowse = new BrowseDescription
- {
- NodeId = nodeId,
- BrowseDirection = m_browseDirection,
- ReferenceTypeId = m_referenceTypeId,
- IncludeSubtypes = m_includeSubtypes,
- NodeClassMask = m_nodeClassMask,
- ResultMask = m_resultMask
- };
-
- var nodesToBrowse = new BrowseDescriptionCollection { nodeToBrowse };
-
- // make the call to the server.
- BrowseResponse browseResponse = await m_session.BrowseAsync(
- null,
- m_view,
- m_maxReferencesReturned,
- nodesToBrowse,
- ct).ConfigureAwait(false);
+ var nodesToBrowse = new BrowseDescriptionCollection { nodeToBrowse };
+
+ // make the call to the server.
+ BrowseResponse browseResponse = await session.BrowseAsync(
+ state.RequestHeader,
+ state.View,
+ state.MaxReferencesReturned,
+ nodesToBrowse,
+ ct).ConfigureAwait(false);
- BrowseResultCollection results = browseResponse.Results;
- DiagnosticInfoCollection diagnosticInfos = browseResponse.DiagnosticInfos;
- ResponseHeader responseHeader = browseResponse.ResponseHeader;
+ BrowseResultCollection results = browseResponse.Results;
+ DiagnosticInfoCollection diagnosticInfos = browseResponse.DiagnosticInfos;
+ ResponseHeader responseHeader = browseResponse.ResponseHeader;
- // ensure that the server returned valid results.
- ClientBase.ValidateResponse(results, nodesToBrowse);
- ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToBrowse);
+ // ensure that the server returned valid results.
+ ClientBase.ValidateResponse(results, nodesToBrowse);
+ ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToBrowse);
- // check if valid.
- if (StatusCode.IsBad(results[0].StatusCode))
- {
- throw ServiceResultException.Create(
- results[0].StatusCode,
- 0,
- diagnosticInfos,
- responseHeader.StringTable);
- }
+ // check if valid.
+ if (StatusCode.IsBad(results[0].StatusCode))
+ {
+ throw ServiceResultException.Create(
+ results[0].StatusCode,
+ 0,
+ diagnosticInfos,
+ responseHeader.StringTable);
+ }
- // fetch initial set of references.
- byte[] continuationPoint = results[0].ContinuationPoint;
- ReferenceDescriptionCollection references = results[0].References;
+ // fetch initial set of references.
+ byte[] continuationPoint = results[0].ContinuationPoint;
+ ReferenceDescriptionCollection references = results[0].References;
- try
+ try
+ {
+ // process any continuation point.
+ while (continuationPoint != null)
{
- // process any continuation point.
- while (continuationPoint != null)
+ ReferenceDescriptionCollection additionalReferences;
+
+ if (!ContinueUntilDone && m_MoreReferences != null)
{
- ReferenceDescriptionCollection additionalReferences;
+ var args = new BrowserEventArgs(references);
+ m_MoreReferences(this, args);
- if (!m_continueUntilDone && m_MoreReferences != null)
+ // cancel browser and return the references fetched so far.
+ if (args.Cancel)
{
- var args = new BrowserEventArgs(references);
- m_MoreReferences(this, args);
-
- // cancel browser and return the references fetched so far.
- if (args.Cancel)
+ session = Session;
+ if (session != null)
{
(_, continuationPoint) = await BrowseNextAsync(
+ session,
continuationPoint,
true,
ct).ConfigureAwait(false);
- return references;
}
-
- m_continueUntilDone = args.ContinueUntilDone;
+ return references;
}
- (additionalReferences, continuationPoint) = await BrowseNextAsync(
- continuationPoint,
- false,
- ct).ConfigureAwait(false);
- if (additionalReferences != null && additionalReferences.Count > 0)
- {
- references.AddRange(additionalReferences);
- }
- else
- {
- m_logger.LogWarning(
- "Browser: Continuation point exists, but the browse results are null/empty.");
- break;
- }
+ ContinueUntilDone = args.ContinueUntilDone;
+ }
+
+ // See if the session was updated
+ session = Session ??
+ throw new ServiceResultException(
+ StatusCodes.BadServerNotConnected,
+ "Cannot browse if not connected to a server.");
+ (additionalReferences, continuationPoint) = await BrowseNextAsync(
+ session,
+ continuationPoint,
+ false,
+ ct).ConfigureAwait(false);
+ if (additionalReferences != null && additionalReferences.Count > 0)
+ {
+ references.AddRange(additionalReferences);
+ }
+ else
+ {
+ m_logger.LogWarning(
+ "Browser: Continuation point exists, but the browse results are null/empty.");
+ break;
}
}
- catch (OperationCanceledException) when (continuationPoint?.Length > 0)
+ }
+ catch (OperationCanceledException) when (continuationPoint?.Length > 0)
+ {
+ session = Session;
+ if (session != null)
{
- (_, _) = await BrowseNextAsync(continuationPoint, true, default).ConfigureAwait(false);
+ (_, _) = await BrowseNextAsync(
+ session,
+ continuationPoint,
+ true,
+ default).ConfigureAwait(false);
}
- // return the results.
- return references;
}
- finally
+ // return the results.
+ return references;
+ }
+
+ ///
+ /// 1. if, during a Browse or BrowseNext, service call one of the status codes
+ /// BadNoContinuationPoint or BadContinuationPointInvalid is returned,
+ /// the node is browsed again.
+ /// 2. tries to avoid the status code BadNoContinuationPoint by creating
+ /// packages of size at most MaxNodesPerBrowse, taken from the Operationlimits
+ /// retrieved from the server at client startup.
+ /// 3. with the help of a new property of the session the new method calls
+ /// Browse for at most
+ /// min(MaxNodesPerBrowse, MaxBrowseContinuationPoints)
+ /// nodes in one call, to further reduce the risk of the status code
+ /// BadNoContinuationPoint
+ /// 4. calls BrowseNext, if necessary, before working on a new package. This is
+ /// the reason that the packages have to be created directly in this method
+ /// and package creation cannot be delegated to the SessionClientBatched class.
+ /// This call sequence avoids the cannibalization of continuation points from
+ /// previously worked on packages, at least if no concurrent browse operation
+ /// is started. (The server is supposed to manage the continuation point quota
+ /// on session level.The reference server, which is used in the tests, does
+ /// this correctly).
+ /// 5. the maximum number of browse continuation points is retrieved from
+ /// the server capabilities in a new method, which may also be viewed as
+ /// a prototype for evaluation of all server capabilities (this is not an
+ /// operation limit).
+ ///
+ /// The nodes to browse
+ /// Cancellation token to use
+ ///
+ ///
+ public async ValueTask> BrowseAsync(
+ IReadOnlyList nodesToBrowse,
+ CancellationToken ct = default)
+ {
+ BrowserOptions state = State;
+ ISessionClient session = Session ??
+ throw new ServiceResultException(
+ StatusCodes.BadServerNotConnected,
+ "Cannot browse if not connected to a server.");
+
+ int count = nodesToBrowse.Count;
+ var result = new List(count);
+ var errors = new List(count);
+
+ // first attempt for implementation: create the references for the output in advance.
+ // optimize later, when everything works fine.
+ for (int i = 0; i < nodesToBrowse.Count; i++)
{
- m_browseInProgress = false;
+ result.Add([]);
+ errors.Add(new ServiceResult(StatusCodes.Good));
}
+ // in the first pass, we browse all nodes from the input.
+ // Some nodes may need to be browsed again, these are then fed into the next pass.
+ var nodesToBrowseForPass = new List(count);
+ nodesToBrowseForPass.AddRange(nodesToBrowse);
+
+ var resultForPass = new List(count);
+ resultForPass.AddRange(result);
+
+ var errorsForPass = new List(count);
+ errorsForPass.AddRange(errors);
+
+ int passCount = 0;
+
+ do
+ {
+ int badNoCPErrorsPerPass = 0;
+ int badCPInvalidErrorsPerPass = 0;
+ int otherErrorsPerPass = 0;
+ uint maxNodesPerBrowse = MaxNodesPerBrowse;
+
+ if (ContinuationPointPolicy == ContinuationPointPolicy.Balanced &&
+ MaxBrowseContinuationPoints > 0)
+ {
+ maxNodesPerBrowse =
+ MaxBrowseContinuationPoints < maxNodesPerBrowse
+ ? MaxBrowseContinuationPoints
+ : maxNodesPerBrowse;
+ }
+
+ // split input into batches
+ int batchOffset = 0;
+
+ var nodesToBrowseForNextPass = new List();
+ var referenceDescriptionsForNextPass
+ = new List();
+ var errorsForNextPass = new List();
+
+ // loop over the batches
+ foreach (List nodesToBrowseBatch in nodesToBrowseForPass
+ .Batch>(maxNodesPerBrowse))
+ {
+ int nodesToBrowseBatchCount = nodesToBrowseBatch.Count;
+
+ ResultSet results = await BrowseAsync(
+ session,
+ state.RequestHeader,
+ state.View,
+ nodesToBrowseBatch,
+ state.MaxReferencesReturned,
+ state.BrowseDirection,
+ state.ReferenceTypeId,
+ state.IncludeSubtypes,
+ state.NodeClassMask,
+ ct)
+ .ConfigureAwait(false);
+
+ int resultOffset = batchOffset;
+ for (int ii = 0; ii < nodesToBrowseBatchCount; ii++)
+ {
+ StatusCode statusCode = results.Errors[ii].StatusCode;
+ if (StatusCode.IsBad(statusCode))
+ {
+ bool addToNextPass = false;
+ if (statusCode == StatusCodes.BadNoContinuationPoints)
+ {
+ addToNextPass = true;
+ badNoCPErrorsPerPass++;
+ }
+ else if (statusCode == StatusCodes.BadContinuationPointInvalid)
+ {
+ addToNextPass = true;
+ badCPInvalidErrorsPerPass++;
+ }
+ else
+ {
+ otherErrorsPerPass++;
+ }
+
+ if (addToNextPass)
+ {
+ nodesToBrowseForNextPass.Add(
+ nodesToBrowseForPass[resultOffset]);
+ referenceDescriptionsForNextPass.Add(
+ resultForPass[resultOffset]);
+ errorsForNextPass.Add(errorsForPass[resultOffset]);
+ }
+ }
+
+ resultForPass[resultOffset].Clear();
+ resultForPass[resultOffset].AddRange(results.Results[ii]);
+ errorsForPass[resultOffset] = results.Errors[ii];
+ errors[resultOffset] = results.Errors[ii];
+ resultOffset++;
+ }
+
+ batchOffset += nodesToBrowseBatchCount;
+ }
+
+ resultForPass = referenceDescriptionsForNextPass;
+ errorsForPass = errorsForNextPass;
+ nodesToBrowseForPass = nodesToBrowseForNextPass;
+
+ if (badCPInvalidErrorsPerPass > 0)
+ {
+ m_logger.LogDebug(
+ "ManagedBrowse: in pass {Pass}, {Count} error(s) occured with a status code {StatusCode}.",
+ passCount,
+ badCPInvalidErrorsPerPass,
+ nameof(StatusCodes.BadContinuationPointInvalid));
+ }
+ if (badNoCPErrorsPerPass > 0)
+ {
+ m_logger.LogDebug(
+ "ManagedBrowse: in pass {Pass}, {Count} error(s) occured with a status code {StatusCode}.",
+ passCount,
+ badNoCPErrorsPerPass,
+ nameof(StatusCodes.BadNoContinuationPoints));
+ }
+ if (otherErrorsPerPass > 0)
+ {
+ m_logger.LogDebug(
+ "ManagedBrowse: in pass {Pass}, {Count} error(s) occured with a status code {StatusCode}.",
+ passCount,
+ otherErrorsPerPass,
+ $"different from {nameof(StatusCodes.BadNoContinuationPoints)} or {nameof(StatusCodes.BadContinuationPointInvalid)}");
+ }
+ if (otherErrorsPerPass == 0 &&
+ badCPInvalidErrorsPerPass == 0 &&
+ badNoCPErrorsPerPass == 0)
+ {
+ m_logger.LogTrace("ManagedBrowse completed with no errors.");
+ }
+
+ passCount++;
+ } while (nodesToBrowseForPass.Count > 0);
+ return new ResultSet(result, errors);
}
///
- /// Checks the state of the browser.
+ /// Call the browse service asynchronously and call browse next,
+ /// if applicable, immediately afterwards. Observe proper treatment
+ /// of specific service results, specifically
+ /// BadNoContinuationPoint and BadContinuationPointInvalid
///
- ///
- private void CheckBrowserState()
+ private static async ValueTask> BrowseAsync(
+ ISessionClient session,
+ RequestHeader? requestHeader,
+ ViewDescription? view,
+ List nodeIds,
+ uint maxResultsToReturn,
+ BrowseDirection browseDirection,
+ NodeId referenceTypeId,
+ bool includeSubtypes,
+ int nodeClassMask,
+ CancellationToken ct = default)
{
- if (m_browseInProgress)
+ if (requestHeader != null)
{
- throw new ServiceResultException(
- StatusCodes.BadInvalidState,
- "Cannot change browse parameters while a browse operation is in progress.");
+ requestHeader.RequestHandle = 0;
+ }
+
+ var result = new List(nodeIds.Count);
+ (
+ _,
+ ByteStringCollection continuationPoints,
+ IList referenceDescriptions,
+ IList errors
+ ) = await session.BrowseAsync(
+ requestHeader,
+ view,
+ nodeIds,
+ maxResultsToReturn,
+ browseDirection,
+ referenceTypeId,
+ includeSubtypes,
+ (uint)nodeClassMask,
+ ct)
+ .ConfigureAwait(false);
+
+ result.AddRange(referenceDescriptions);
+
+ // process any continuation point.
+ List previousResults = result;
+ var errorAnchors = new List>();
+ var previousErrors = new List>();
+ foreach (ServiceResult error in errors)
+ {
+ previousErrors.Add(new ReferenceWrapper { Reference = error });
+ errorAnchors.Add(previousErrors[^1]);
+ }
+
+ var nextContinuationPoints = new ByteStringCollection();
+ var nextResults = new List();
+ var nextErrors = new List>();
+
+ for (int ii = 0; ii < nodeIds.Count; ii++)
+ {
+ if (continuationPoints[ii] != null &&
+ !StatusCode.IsBad(previousErrors[ii].Reference.StatusCode))
+ {
+ nextContinuationPoints.Add(continuationPoints[ii]);
+ nextResults.Add(previousResults[ii]);
+ nextErrors.Add(previousErrors[ii]);
+ }
+ }
+ while (nextContinuationPoints.Count > 0)
+ {
+ if (requestHeader != null)
+ {
+ requestHeader.RequestHandle = 0;
+ }
+ (
+ _,
+ ByteStringCollection revisedContinuationPoints,
+ IList browseNextResults,
+ IList browseNextErrors
+ ) = await session.BrowseNextAsync(
+ requestHeader,
+ nextContinuationPoints,
+ false,
+ ct).ConfigureAwait(false);
+
+ for (int ii = 0; ii < browseNextResults.Count; ii++)
+ {
+ nextResults[ii].AddRange(browseNextResults[ii]);
+ nextErrors[ii].Reference = browseNextErrors[ii];
+ }
+
+ previousResults = nextResults;
+ previousErrors = nextErrors;
+
+ nextResults = [];
+ nextErrors = [];
+ nextContinuationPoints = [];
+
+ for (int ii = 0; ii < revisedContinuationPoints.Count; ii++)
+ {
+ if (revisedContinuationPoints[ii] != null &&
+ !StatusCode.IsBad(browseNextErrors[ii].StatusCode))
+ {
+ nextContinuationPoints.Add(revisedContinuationPoints[ii]);
+ nextResults.Add(previousResults[ii]);
+ nextErrors.Add(previousErrors[ii]);
+ }
+ }
}
+ var finalErrors = new List(errorAnchors.Count);
+ foreach (ReferenceWrapper errorReference in errorAnchors)
+ {
+ finalErrors.Add(errorReference.Reference);
+ }
+
+ return new ResultSet(result, finalErrors);
}
///
/// Fetches the next batch of references.
///
+ ///
/// The continuation point.
/// if set to true the browse operation is cancelled.
/// The cancellation token.
/// The next batch of references
///
- private async ValueTask<(ReferenceDescriptionCollection, byte[])> BrowseNextAsync(
+ private static async ValueTask<(ReferenceDescriptionCollection, byte[])> BrowseNextAsync(
+ ISessionClient session,
byte[] continuationPoint,
bool cancel,
CancellationToken ct = default)
@@ -419,7 +691,7 @@ private void CheckBrowserState()
var continuationPoints = new ByteStringCollection { continuationPoint };
// make the call to the server.
- BrowseNextResponse browseResponse = await m_session.BrowseNextAsync(
+ BrowseNextResponse browseResponse = await session.BrowseNextAsync(
null,
cancel,
continuationPoints,
@@ -448,19 +720,45 @@ private void CheckBrowserState()
return (results[0].References, results[0].ContinuationPoint);
}
- private ILogger m_logger;
- private ITelemetryContext m_telemetry;
- private ISession m_session;
- private ViewDescription m_view;
- private uint m_maxReferencesReturned;
- private BrowseDirection m_browseDirection;
- private NodeId m_referenceTypeId;
- private bool m_includeSubtypes;
- private uint m_nodeClassMask;
- private uint m_resultMask;
- private event BrowserEventHandler m_MoreReferences;
- private bool m_continueUntilDone;
- private bool m_browseInProgress;
+ ///
+ /// Creates the browser from a persisted stream
+ ///
+ public static Browser? Load(Stream stream, ITelemetryContext telemetry)
+ {
+ // secure settings
+ XmlReaderSettings settings = Utils.DefaultXmlReaderSettings();
+ using var reader = XmlReader.Create(stream, settings);
+ var serializer = new DataContractSerializer(typeof(BrowserOptions));
+ using IDisposable scope = AmbientMessageContext.SetScopedContext(telemetry);
+ var options = (BrowserOptions?)serializer.ReadObject(reader);
+ return new Browser(telemetry, options);
+ }
+
+ ///
+ /// Saves the state to the stream
+ ///
+ public void Save(Stream stream)
+ {
+ // secure settings
+ using IDisposable scope = AmbientMessageContext.SetScopedContext(m_telemetry);
+ var serializer = new DataContractSerializer(typeof(BrowserOptions));
+ serializer.WriteObject(stream, State);
+ }
+
+ ///
+ /// Used to pass on references to the Service results in the loop in ManagedBrowseAsync.
+ ///
+ ///
+ private class ReferenceWrapper
+ {
+ public required T Reference { get; set; }
+ }
+
+ private readonly ILogger m_logger;
+ private readonly ITelemetryContext? m_telemetry;
+ private ISessionClient? m_session;
+
+ private event BrowserEventHandler? m_MoreReferences;
}
///
diff --git a/Libraries/Opc.Ua.Client/BrowserOptions.cs b/Libraries/Opc.Ua.Client/BrowserOptions.cs
new file mode 100644
index 0000000000..af6186613b
--- /dev/null
+++ b/Libraries/Opc.Ua.Client/BrowserOptions.cs
@@ -0,0 +1,137 @@
+/* ========================================================================
+ * Copyright (c) 2005-2020 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.Runtime.Serialization;
+using System.Text.Json.Serialization;
+
+namespace Opc.Ua.Client
+{
+ [JsonSerializable(typeof(BrowserOptions))]
+ internal partial class BrowserOptionsContext : JsonSerializerContext;
+
+ ///
+ /// Stores the options to use for a browse operation. Can be serialized and
+ /// deserialized.
+ ///
+ [DataContract(Namespace = Namespaces.OpcUaXsd)]
+ public record class BrowserOptions
+ {
+ ///
+ /// Request header to use for the browse operations.
+ ///
+ [DataMember(Order = 0)]
+ public RequestHeader? RequestHeader { get; init; }
+
+ ///
+ /// The view to use for the browse operation.
+ ///
+ [DataMember(Order = 1)]
+ public ViewDescription? View { get; init; }
+
+ ///
+ /// The maximum number of references to return in a single browse operation.
+ ///
+ [DataMember(Order = 2)]
+ public uint MaxReferencesReturned { get; init; }
+
+ ///
+ /// The direction to browse.
+ ///
+ [DataMember(Order = 3)]
+ public BrowseDirection BrowseDirection { get; init; } = BrowseDirection.Forward;
+
+ ///
+ /// The reference type to follow.
+ ///
+ [DataMember(Order = 4)]
+ public NodeId ReferenceTypeId { get; init; } = NodeId.Null;
+
+ ///
+ /// Whether subtypes of the reference type should be included.
+ ///
+ [DataMember(Order = 5)]
+ public bool IncludeSubtypes { get; init; } = true;
+
+ ///
+ /// The classes of the target nodes.
+ ///
+ [DataMember(Order = 6)]
+ public int NodeClassMask { get; init; }
+
+ ///
+ /// The results to return.
+ ///
+ [DataMember(Order = 7)]
+ public uint ResultMask { get; init; } = (uint)BrowseResultMask.All;
+
+ ///
+ /// gets or set the policy which is used to prevent the allocation
+ /// of too many Continuation Points in the browse operation
+ ///
+ [DataMember(Order = 8)]
+ public ContinuationPointPolicy ContinuationPointPolicy { get; init; }
+
+ ///
+ /// Max nodes to browse in a single operation.
+ ///
+ [DataMember(Order = 9)]
+ public uint MaxNodesPerBrowse { get; set; }
+
+ ///
+ /// Max continuation points to use when ContinuationPointPolicy is set
+ /// to Balanced.
+ ///
+ [DataMember(Order = 10)]
+ public ushort MaxBrowseContinuationPoints { get; set; }
+ }
+
+ ///
+ /// controls how the browser treats continuation points if the server has
+ /// restrictions on their number.
+ ///
+ public enum ContinuationPointPolicy
+ {
+ ///
+ /// Ignore how many Continuation Points are in use already.
+ /// Rebrowse nodes for which BadNoContinuationPoint or
+ /// BadInvalidContinuationPoint was raised. Can be used
+ /// whenever the server has no restrictions no the maximum
+ /// number of continuation points
+ ///
+ Default,
+
+ ///
+ /// Restrict the number of nodes which are browsed in a
+ /// single service call to the maximum number of
+ /// continuation points the server can allocae
+ /// (if set to a value different from 0)
+ ///
+ Balanced
+ }
+}
diff --git a/Libraries/Opc.Ua.Client/CoreClientUtils.cs b/Libraries/Opc.Ua.Client/CoreClientUtils.cs
index 544bb4c5e1..8e751f56f9 100644
--- a/Libraries/Opc.Ua.Client/CoreClientUtils.cs
+++ b/Libraries/Opc.Ua.Client/CoreClientUtils.cs
@@ -129,7 +129,7 @@ public static async ValueTask> DiscoverServersAsync(
///
/// Finds the endpoint that best matches the current settings.
///
- public static ValueTask SelectEndpointAsync(
+ public static ValueTask SelectEndpointAsync(
ApplicationConfiguration application,
ITransportWaitingConnection connection,
bool useSecurity,
@@ -148,7 +148,7 @@ public static ValueTask SelectEndpointAsync(
///
/// Finds the endpoint that best matches the current settings.
///
- public static async ValueTask SelectEndpointAsync(
+ public static async ValueTask SelectEndpointAsync(
ApplicationConfiguration application,
ITransportWaitingConnection connection,
bool useSecurity,
@@ -187,7 +187,7 @@ public static async ValueTask SelectEndpointAsync(
/// The telemetry context to use to create obvservability instruments
/// Cancellation token to cancel operation with
/// The best available endpoint.
- public static ValueTask SelectEndpointAsync(
+ public static ValueTask SelectEndpointAsync(
ApplicationConfiguration application,
string discoveryUrl,
bool useSecurity,
@@ -213,7 +213,7 @@ public static ValueTask SelectEndpointAsync(
/// The telemetry context to use to create obvservability instruments
/// Cancellation token to cancel operation with
/// The best available endpoint.
- public static async ValueTask SelectEndpointAsync(
+ public static async ValueTask SelectEndpointAsync(
ApplicationConfiguration application,
string discoveryUrl,
bool useSecurity,
@@ -234,24 +234,26 @@ public static async ValueTask SelectEndpointAsync(
var url = new Uri(client.Endpoint.EndpointUrl);
EndpointDescriptionCollection endpoints =
await client.GetEndpointsAsync(null, ct).ConfigureAwait(false);
- EndpointDescription selectedEndpoint = SelectEndpoint(
+ EndpointDescription? selectedEndpoint = SelectEndpoint(
application,
url,
endpoints,
useSecurity,
telemetry);
- Uri endpointUrl = Utils.ParseUri(selectedEndpoint.EndpointUrl);
- if (endpointUrl != null && endpointUrl.Scheme == uri.Scheme)
+ if (selectedEndpoint != null)
{
- var builder = new UriBuilder(endpointUrl)
+ Uri? endpointUrl = Utils.ParseUri(selectedEndpoint.EndpointUrl);
+ if (endpointUrl != null && endpointUrl.Scheme == uri.Scheme)
{
- Host = uri.IdnHost,
- Port = uri.Port
- };
- selectedEndpoint.EndpointUrl = builder.ToString();
+ var builder = new UriBuilder(endpointUrl)
+ {
+ Host = uri.IdnHost,
+ Port = uri.Port
+ };
+ selectedEndpoint.EndpointUrl = builder.ToString();
+ }
}
-
return selectedEndpoint;
}
@@ -259,14 +261,14 @@ public static async ValueTask SelectEndpointAsync(
/// Select the best supported endpoint from an
/// EndpointDescriptionCollection, with or without security.
///
- public static EndpointDescription SelectEndpoint(
+ public static EndpointDescription? SelectEndpoint(
ApplicationConfiguration configuration,
Uri url,
EndpointDescriptionCollection endpoints,
bool useSecurity,
ITelemetryContext telemetry)
{
- EndpointDescription selectedEndpoint = null;
+ EndpointDescription? selectedEndpoint = null;
// select the best endpoint to use based on the selected URL and the UseSecurity checkbox.
for (int ii = 0; ii < endpoints.Count; ii++)
diff --git a/Libraries/Opc.Ua.Client/CoreClientUtilsObsolete.cs b/Libraries/Opc.Ua.Client/CoreClientUtilsObsolete.cs
index 3aeeb18e3e..c51d536644 100644
--- a/Libraries/Opc.Ua.Client/CoreClientUtilsObsolete.cs
+++ b/Libraries/Opc.Ua.Client/CoreClientUtilsObsolete.cs
@@ -27,6 +27,8 @@
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.Threading;
diff --git a/Libraries/Opc.Ua.Client/NodeCache/ILruNodeCache.cs b/Libraries/Opc.Ua.Client/NodeCache/ILruNodeCache.cs
index 177a3656b9..84d9b74c7d 100644
--- a/Libraries/Opc.Ua.Client/NodeCache/ILruNodeCache.cs
+++ b/Libraries/Opc.Ua.Client/NodeCache/ILruNodeCache.cs
@@ -39,9 +39,9 @@ namespace Opc.Ua.Client
public interface ILruNodeCache
{
///
- /// The session used by the node cache
+ /// The namespaces used in the server
///
- ISession Session { get; }
+ NamespaceTable NamespaceUris { get; }
///
/// Get node from cache
@@ -58,7 +58,7 @@ ValueTask> GetNodesAsync(
///
/// Get node using browse path
///
- ValueTask GetNodeWithBrowsePathAsync(
+ ValueTask GetNodeWithBrowsePathAsync(
NodeId nodeId,
QualifiedNameCollection browsePath,
CancellationToken ct = default);
diff --git a/Libraries/Opc.Ua.Client/NodeCache/INodeCache.cs b/Libraries/Opc.Ua.Client/NodeCache/INodeCache.cs
index 62cbbf6115..298b1f04a2 100644
--- a/Libraries/Opc.Ua.Client/NodeCache/INodeCache.cs
+++ b/Libraries/Opc.Ua.Client/NodeCache/INodeCache.cs
@@ -54,8 +54,8 @@ public interface INodeCache : IAsyncNodeTable, IAsyncTypeTable
/// fetches missing nodes from server.
///
/// The node identifier collection.
- /// Cancelation token to cancel operation with
- Task> FindAsync(
+ /// Cancellation token to cancel operation with
+ Task> FindAsync(
IList nodeIds,
CancellationToken ct = default);
@@ -63,8 +63,8 @@ Task> FindAsync(
/// Fetches a node from the server and updates the cache.
///
/// Node id to fetch.
- /// Cancelation token to cancel operation with
- Task FetchNodeAsync(
+ /// Cancellation token to cancel operation with
+ Task FetchNodeAsync(
ExpandedNodeId nodeId,
CancellationToken ct = default);
@@ -72,8 +72,8 @@ Task FetchNodeAsync(
/// Fetches a node collection from the server and updates the cache.
///
/// The node identifier collection.
- /// Cancelation token to cancel operation with
- Task> FetchNodesAsync(
+ /// Cancellation token to cancel operation with
+ Task> FetchNodesAsync(
IList nodeIds,
CancellationToken ct = default);
@@ -81,7 +81,7 @@ Task> FetchNodesAsync(
/// Adds the supertypes of the node to the cache.
///
/// Node id to fetch.
- /// Cancelation token to cancel operation with
+ /// Cancellation token to cancel operation with
Task FetchSuperTypesAsync(
ExpandedNodeId nodeId,
CancellationToken ct = default);
@@ -111,29 +111,22 @@ Task> FindReferencesAsync(
///
/// Returns a display name for a node.
///
- ValueTask GetDisplayTextAsync(
+ ValueTask GetDisplayTextAsync(
INode node,
CancellationToken ct = default);
///
/// Returns a display name for a node.
///
- ValueTask GetDisplayTextAsync(
+ ValueTask GetDisplayTextAsync(
ExpandedNodeId nodeId,
CancellationToken ct = default);
///
/// Returns a display name for the target of a reference.
///
- ValueTask GetDisplayTextAsync(
+ ValueTask GetDisplayTextAsync(
ReferenceDescription reference,
CancellationToken ct = default);
-
- ///
- /// Builds the relative path from a type to a node.
- ///
- NodeId BuildBrowsePath(
- ILocalNode node,
- IList browsePath);
}
}
diff --git a/Libraries/Opc.Ua.Client/NodeCache/INodeCacheContext.cs b/Libraries/Opc.Ua.Client/NodeCache/INodeCacheContext.cs
new file mode 100644
index 0000000000..bd3c5ab8b8
--- /dev/null
+++ b/Libraries/Opc.Ua.Client/NodeCache/INodeCacheContext.cs
@@ -0,0 +1,137 @@
+// ------------------------------------------------------------
+// Copyright (c) 2005-2020 The OPC Foundation, Inc. All rights reserved.
+// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
+// ------------------------------------------------------------
+
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Opc.Ua.Client
+{
+ ///
+ /// Interface between node cache and a session
+ ///
+ public interface INodeCacheContext
+ {
+ ///
+ /// Gets the table of namespace uris known to the server.
+ ///
+ NamespaceTable NamespaceUris { get; }
+
+ ///
+ /// Get the table with the server uris known to the server.
+ ///
+ StringTable ServerUris { get; }
+
+ ///
+ /// Reads the values for the node attributes and returns a node object
+ /// collection.
+ ///
+ ///
+ /// 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.
+ ///
+ /// Request header to use
+ /// The nodeId collection to read.
+ /// If optional attributes should
+ /// not be read.
+ /// The cancellation token.
+ /// The node collection and associated errors.
+ ValueTask> FetchNodesAsync(
+ RequestHeader? requestHeader,
+ IReadOnlyList nodeIds,
+ bool skipOptionalAttributes = false,
+ CancellationToken ct = default);
+
+ ///
+ /// Reads the values for the node attributes and returns a node object
+ /// collection.
+ ///
+ ///
+ /// 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.
+ ///
+ /// Request header to use
+ /// The nodeId collection to read.
+ /// The nodeClass of all nodes in the collection.
+ /// Set to NodeClass.Unspecified if the nodeclass is unknown.
+ /// Set to true if optional
+ /// attributes should omitted.
+ /// The cancellation token.
+ /// The node collection and associated errors.
+ ValueTask> FetchNodesAsync(
+ RequestHeader? requestHeader,
+ IReadOnlyList nodeIds,
+ NodeClass nodeClass,
+ bool skipOptionalAttributes = false,
+ CancellationToken ct = default);
+
+ ///
+ /// Reads the values for the node attributes and returns a node object.
+ ///
+ ///
+ /// If the nodeclass is known, only the supported attribute values are
+ /// read.
+ ///
+ /// Request header to use
+ /// The nodeId.
+ /// The nodeclass of the node to read.
+ /// Skip reading optional attributes.
+ /// The cancellation token for the request.
+ ValueTask FetchNodeAsync(
+ RequestHeader? requestHeader,
+ NodeId nodeId,
+ NodeClass nodeClass = NodeClass.Unspecified,
+ bool skipOptionalAttributes = false,
+ CancellationToken ct = default);
+
+ ///
+ /// Fetches all references for the specified node.
+ ///
+ /// Request header to use
+ /// The node id.
+ ///
+ ValueTask FetchReferencesAsync(
+ RequestHeader? requestHeader,
+ NodeId nodeId,
+ CancellationToken ct = default);
+
+ ///
+ /// Fetches all references for the specified nodes.
+ ///
+ /// Request header to use
+ /// The node id collection.
+ ///
+ /// A list of reference collections and the errors reported by the
+ /// server.
+ ValueTask> FetchReferencesAsync(
+ RequestHeader? requestHeader,
+ IReadOnlyList nodeIds,
+ CancellationToken ct = default);
+
+ ///
+ /// Reads the value for a node.
+ ///
+ /// Request header to use
+ /// The node Id.
+ /// The cancellation token for the request.
+ ValueTask FetchValueAsync(
+ RequestHeader? requestHeader,
+ NodeId nodeId,
+ CancellationToken ct = default);
+
+ ///
+ /// Reads the values for a node collection. Returns diagnostic errors.
+ ///
+ /// Request header to use
+ /// The node Id.
+ /// The cancellation token for the request.
+ ValueTask> FetchValuesAsync(
+ RequestHeader? requestHeader,
+ IReadOnlyList nodeIds,
+ CancellationToken ct = default);
+ }
+}
diff --git a/Libraries/Opc.Ua.Client/NodeCache/LruNodeCache.cs b/Libraries/Opc.Ua.Client/NodeCache/LruNodeCache.cs
index 571793657a..5026f45a43 100644
--- a/Libraries/Opc.Ua.Client/NodeCache/LruNodeCache.cs
+++ b/Libraries/Opc.Ua.Client/NodeCache/LruNodeCache.cs
@@ -57,15 +57,15 @@ public sealed class LruNodeCache : ILruNodeCache
/// Create cache
///
public LruNodeCache(
- ISession session,
+ INodeCacheContext context,
ITelemetryContext telemetry,
TimeSpan? cacheExpiry = null,
int capacity = 4096,
bool withMetrics = false)
{
+ m_context = context;
cacheExpiry ??= TimeSpan.FromMinutes(5);
- Session = session;
BitFaster.Caching.Lru.Builder.AtomicAsyncConcurrentLruBuilder nodesBuilder =
new ConcurrentLruBuilder()
.WithAtomicGetOrAdd()
@@ -116,7 +116,7 @@ public LruNodeCache(
public ICacheMetrics? ReferencesMetrics => m_refs.Metrics.Value;
///
- public ISession Session { get; }
+ public NamespaceTable NamespaceUris => m_context.NamespaceUris;
///
public ValueTask GetNodeAsync(NodeId nodeId, CancellationToken ct)
@@ -128,10 +128,13 @@ ValueTask FindAsyncCore(NodeId nodeId, CancellationToken ct)
{
return m_nodes.GetOrAddAsync(
nodeId,
- async (nodeId, context) =>
- await context.session.ReadNodeAsync(nodeId, context.ct)
- .ConfigureAwait(false),
- (session: Session, ct));
+ async (nodeId, context) => await context.ctx.FetchNodeAsync(
+ null,
+ nodeId,
+ NodeClass.Unspecified,
+ ct: context.ct)
+ .ConfigureAwait(false),
+ (ctx: m_context, ct));
}
}
@@ -174,8 +177,10 @@ ValueTask FindAsyncCore(NodeId nodeId, CancellationToken ct)
{
return m_values.GetOrAddAsync(
nodeId,
- (nodeId, context) => context.session.ReadValueAsync(nodeId, context.ct),
- (session: Session, ct));
+ async (nodeId, context) =>
+ await context.ctx.FetchValueAsync(null, nodeId, context.ct)
+ .ConfigureAwait(false),
+ (ctx: m_context, ct));
}
}
@@ -502,9 +507,9 @@ private ValueTask> GetOrAddReferencesAsync(
nodeId,
async (nodeId, context) =>
{
- ReferenceDescriptionCollection references = await context
- .session.FetchReferencesAsync(nodeId, context.ct)
- .ConfigureAwait(false);
+ ReferenceDescriptionCollection references =
+ await context.ctx.FetchReferencesAsync(null, nodeId, context.ct)
+ .ConfigureAwait(false);
foreach (ReferenceDescription? reference in references)
{
// transform absolute identifiers.
@@ -512,12 +517,12 @@ private ValueTask> GetOrAddReferencesAsync(
{
reference.NodeId = ExpandedNodeId.ToNodeId(
reference.NodeId,
- context.session.NamespaceUris);
+ context.ctx.NamespaceUris);
}
}
return references;
},
- (session: Session, ct));
+ (ctx: m_context, ct));
}
///
@@ -532,9 +537,9 @@ private async ValueTask> FetchRemainingAsync(
// fetch nodes and references from server.
var localIds = new NodeIdCollection(remainingIds);
- (IList? nodes, IList? readErrors) = await Session
- .ReadNodesAsync(localIds, NodeClass.Unspecified, ct: ct)
- .ConfigureAwait(false);
+ (IReadOnlyList? nodes, IReadOnlyList? readErrors) =
+ await m_context.FetchNodesAsync(null, localIds, ct: ct)
+ .ConfigureAwait(false);
Debug.Assert(nodes.Count == localIds.Count);
Debug.Assert(readErrors.Count == localIds.Count);
@@ -567,8 +572,8 @@ private async ValueTask> FetchRemainingAsync(
Debug.Assert(result.Count(r => r == null) == remainingIds.Count);
// fetch nodes and references from server.
- (DataValueCollection? values, IList? readErrors) = await Session
- .ReadValuesAsync(remainingIds, ct: ct)
+ (IReadOnlyList? values, IReadOnlyList? readErrors) =
+ await m_context.FetchValuesAsync(null, remainingIds, ct: ct)
.ConfigureAwait(false);
Debug.Assert(values.Count == remainingIds.Count);
@@ -629,7 +634,7 @@ private NodeId GetSuperTypeFromReferences(List references)
{
return references
.Where(r => !r.IsForward && r.ReferenceTypeId == ReferenceTypeIds.HasSubtype)
- .Select(r => ExpandedNodeId.ToNodeId(r.NodeId, Session.NamespaceUris))
+ .Select(r => ExpandedNodeId.ToNodeId(r.NodeId, NamespaceUris))
.DefaultIfEmpty(NodeId.Null)
.First();
}
@@ -642,7 +647,7 @@ private NodeId ToNodeId(ExpandedNodeId expandedNodeId)
{
return expandedNodeId.IsAbsolute
? NodeId.Null
- : ExpandedNodeId.ToNodeId(expandedNodeId, Session.NamespaceUris);
+ : ExpandedNodeId.ToNodeId(expandedNodeId, NamespaceUris);
}
///
@@ -683,6 +688,7 @@ public int GetHashCode(NodeId obj)
private readonly IAsyncCache m_nodes;
private readonly IAsyncCache> m_refs;
private readonly IAsyncCache m_values;
+ private readonly INodeCacheContext m_context;
}
}
#endif
diff --git a/Libraries/Opc.Ua.Client/NodeCache/LruNodeCacheExtensions.cs b/Libraries/Opc.Ua.Client/NodeCache/LruNodeCacheExtensions.cs
index 744f33d93f..9ce1c156a9 100644
--- a/Libraries/Opc.Ua.Client/NodeCache/LruNodeCacheExtensions.cs
+++ b/Libraries/Opc.Ua.Client/NodeCache/LruNodeCacheExtensions.cs
@@ -48,7 +48,7 @@ public static ValueTask GetNodeAsync(
ExpandedNodeId expandedNodeId,
CancellationToken ct = default)
{
- var nodeId = ExpandedNodeId.ToNodeId(expandedNodeId, cache.Session.NamespaceUris);
+ var nodeId = ExpandedNodeId.ToNodeId(expandedNodeId, cache.NamespaceUris);
return cache.GetNodeAsync(nodeId, ct);
}
@@ -63,7 +63,7 @@ public static ValueTask> GetNodesAsync(
var nodeIds = expandedNodeIds
.Select(expandedNodeId => ExpandedNodeId.ToNodeId(
expandedNodeId,
- cache.Session.NamespaceUris))
+ cache.NamespaceUris))
.ToList();
return cache.GetNodesAsync(nodeIds, ct);
}
@@ -76,7 +76,7 @@ public static ValueTask GetValueAsync(
ExpandedNodeId expandedNodeId,
CancellationToken ct = default)
{
- var nodeId = ExpandedNodeId.ToNodeId(expandedNodeId, cache.Session.NamespaceUris);
+ var nodeId = ExpandedNodeId.ToNodeId(expandedNodeId, cache.NamespaceUris);
return cache.GetValueAsync(nodeId, ct);
}
@@ -91,7 +91,7 @@ public static ValueTask> GetValuesAsync(
var nodeIds = expandedNodeIds
.Select(expandedNodeId => ExpandedNodeId.ToNodeId(
expandedNodeId,
- cache.Session.NamespaceUris))
+ cache.NamespaceUris))
.ToList();
return cache.GetValuesAsync(nodeIds, ct);
}
@@ -107,7 +107,7 @@ public static ValueTask> GetReferencesAsync(
bool includeSubtypes = true,
CancellationToken ct = default)
{
- var nodeId = ExpandedNodeId.ToNodeId(expandedNodeId, cache.Session.NamespaceUris);
+ var nodeId = ExpandedNodeId.ToNodeId(expandedNodeId, cache.NamespaceUris);
return cache.GetReferencesAsync(
nodeId,
referenceTypeId,
@@ -130,7 +130,7 @@ public static ValueTask> GetReferencesAsync(
var nodeIds = expandedNodeIds
.Select(expandedNodeId => ExpandedNodeId.ToNodeId(
expandedNodeId,
- cache.Session.NamespaceUris))
+ cache.NamespaceUris))
.ToList();
return cache.GetReferencesAsync(
nodeIds,
@@ -154,7 +154,7 @@ public static ValueTask> GetReferencesAsync(
var nodeIds = expandedNodeIds
.Select(expandedNodeId => ExpandedNodeId.ToNodeId(
expandedNodeId,
- cache.Session.NamespaceUris))
+ cache.NamespaceUris))
.ToList();
return cache.GetReferencesAsync(
nodeIds,
@@ -172,7 +172,7 @@ public static ValueTask GetSuperTypeAsync(
ExpandedNodeId expandedNodeId,
CancellationToken ct = default)
{
- var nodeId = ExpandedNodeId.ToNodeId(expandedNodeId, cache.Session.NamespaceUris);
+ var nodeId = ExpandedNodeId.ToNodeId(expandedNodeId, cache.NamespaceUris);
return cache.GetSuperTypeAsync(nodeId, ct);
}
@@ -184,7 +184,7 @@ public static bool IsTypeOf(
ExpandedNodeId subTypeId,
NodeId superTypeId)
{
- var nodeId = ExpandedNodeId.ToNodeId(subTypeId, cache.Session.NamespaceUris);
+ var nodeId = ExpandedNodeId.ToNodeId(subTypeId, cache.NamespaceUris);
return cache.IsTypeOf(nodeId, superTypeId);
}
@@ -199,7 +199,7 @@ public static async Task GetBuiltInTypeAsync(
NodeId typeId = datatypeId;
while (!NodeId.IsNull(typeId))
{
- if (typeId != null && typeId.NamespaceIndex == 0 && typeId.IdType == IdType.Numeric)
+ if (typeId.NamespaceIndex == 0 && typeId.IdType == IdType.Numeric)
{
var id = (BuiltInType)(int)(uint)typeId.Identifier;
if (id is > BuiltInType.Null and <= BuiltInType.Enumeration and not BuiltInType.DiagnosticInfo)
diff --git a/Libraries/Opc.Ua.Client/NodeCache/NodeCache.cs b/Libraries/Opc.Ua.Client/NodeCache/NodeCache.cs
index 582779db18..1b7d683e6f 100644
--- a/Libraries/Opc.Ua.Client/NodeCache/NodeCache.cs
+++ b/Libraries/Opc.Ua.Client/NodeCache/NodeCache.cs
@@ -46,12 +46,15 @@ public class NodeCache : INodeCache, IDisposable
///
/// Initializes the object with default values.
///
- public NodeCache(ISession session, ITelemetryContext telemetry)
+ public NodeCache(INodeCacheContext context, ITelemetryContext telemetry)
{
- m_session = session ?? throw new ArgumentNullException(nameof(session));
+ m_context = context ?? throw new ArgumentNullException(nameof(context));
m_logger = telemetry.CreateLogger();
- m_typeTree = new TypeTable(m_session.NamespaceUris);
- m_nodes = new NodeTable(m_session.NamespaceUris, m_session.ServerUris, m_typeTree);
+ m_typeTree = new TypeTable(m_context.NamespaceUris);
+ m_nodes = new NodeTable(
+ m_context.NamespaceUris,
+ m_context.ServerUris,
+ m_typeTree);
m_uaTypesLoaded = false;
m_cacheLock = new ReaderWriterLockSlim();
}
@@ -63,7 +66,6 @@ protected virtual void Dispose(bool disposing)
{
if (disposing)
{
- m_session = null;
m_cacheLock?.Dispose();
}
}
@@ -76,10 +78,10 @@ public void Dispose()
}
///
- public NamespaceTable NamespaceUris => m_session.NamespaceUris;
+ public NamespaceTable NamespaceUris => m_context.NamespaceUris;
///
- public StringTable ServerUris => m_session.ServerUris;
+ public StringTable ServerUris => m_context.ServerUris;
///
IAsyncTypeTable IAsyncNodeTable.TypeTree => this;
@@ -90,7 +92,7 @@ public void Dispose()
public ITypeTable TypeTree => this.AsTypeTable();
///
- public async ValueTask FindAsync(
+ public async ValueTask FindAsync(
ExpandedNodeId nodeId,
CancellationToken ct = default)
{
@@ -138,7 +140,7 @@ public async ValueTask FindAsync(
}
///
- public async Task> FindAsync(
+ public async Task> FindAsync(
IList nodeIds,
CancellationToken ct = default)
{
@@ -149,7 +151,7 @@ public async Task> FindAsync(
}
int count = nodeIds.Count;
- var nodes = new List(count);
+ var nodes = new List(count);
var fetchNodeIds = new ExpandedNodeIdCollection();
int ii;
@@ -169,7 +171,7 @@ public async Task> FindAsync(
}
// do not return temporary nodes created after a Browse().
- if (node != null && node?.GetType() != typeof(Node))
+ if (node != null && node.GetType() != typeof(Node))
{
nodes.Add(node);
}
@@ -186,7 +188,7 @@ public async Task> FindAsync(
}
// fetch missing nodes from server.
- IList fetchedNodes;
+ IList fetchedNodes;
try
{
fetchedNodes = await FetchNodesAsync(fetchNodeIds, ct).ConfigureAwait(false);
@@ -199,7 +201,7 @@ public async Task> FindAsync(
}
ii = 0;
- foreach (Node fetchedNode in fetchedNodes)
+ foreach (Node? fetchedNode in fetchedNodes)
{
while (ii < count && nodes[ii] != null)
{
@@ -221,7 +223,7 @@ public async Task> FindAsync(
}
///
- public async ValueTask FindAsync(
+ public async ValueTask FindAsync(
ExpandedNodeId sourceId,
NodeId referenceTypeId,
bool isInverse,
@@ -252,7 +254,7 @@ public async ValueTask FindAsync(
foreach (IReference reference in references)
{
- INode target = await FindAsync(reference.TargetId, ct)
+ INode? target = await FindAsync(reference.TargetId, ct)
.ConfigureAwait(false);
if (target == null)
@@ -275,11 +277,11 @@ public async ValueTask FindSuperTypeAsync(
ExpandedNodeId typeId,
CancellationToken ct = default)
{
- INode type = await FindAsync(typeId, ct).ConfigureAwait(false);
+ INode? type = await FindAsync(typeId, ct).ConfigureAwait(false);
if (type == null)
{
- return null;
+ return NodeId.Null;
}
m_cacheLock.EnterReadLock();
@@ -325,7 +327,7 @@ public async ValueTask> FindAsync(
foreach (IReference reference in references)
{
- INode target =
+ INode? target =
await FindAsync(reference.TargetId, ct).ConfigureAwait(false);
if (target == null)
@@ -344,11 +346,11 @@ public async ValueTask FindSuperTypeAsync(
NodeId typeId,
CancellationToken ct = default)
{
- INode type = await FindAsync(typeId, ct).ConfigureAwait(false);
+ INode? type = await FindAsync(typeId, ct).ConfigureAwait(false);
if (type == null)
{
- return null;
+ return NodeId.Null;
}
m_cacheLock.EnterReadLock();
@@ -363,9 +365,9 @@ public async ValueTask FindSuperTypeAsync(
}
///
- public async Task FetchNodeAsync(ExpandedNodeId nodeId, CancellationToken ct)
+ public async Task FetchNodeAsync(ExpandedNodeId nodeId, CancellationToken ct)
{
- var localId = ExpandedNodeId.ToNodeId(nodeId, m_session.NamespaceUris);
+ var localId = ExpandedNodeId.ToNodeId(nodeId, m_context.NamespaceUris);
if (localId == null)
{
@@ -373,13 +375,13 @@ public async Task FetchNodeAsync(ExpandedNodeId nodeId, CancellationToken
}
// fetch node from server.
- Node source = await m_session.ReadNodeAsync(localId, ct).ConfigureAwait(false);
+ Node source = await m_context.FetchNodeAsync(null, localId, ct: ct).ConfigureAwait(false);
try
{
// fetch references from server.
- ReferenceDescriptionCollection references = await m_session
- .FetchReferencesAsync(localId, ct)
+ ReferenceDescriptionCollection references = await m_context
+ .FetchReferencesAsync(null, localId, ct)
.ConfigureAwait(false);
m_cacheLock.EnterUpgradeableReadLock();
@@ -427,7 +429,7 @@ public async Task FetchNodeAsync(ExpandedNodeId nodeId, CancellationToken
}
///
- public async Task> FetchNodesAsync(
+ public async Task> FetchNodesAsync(
IList nodeIds,
CancellationToken ct)
{
@@ -438,14 +440,14 @@ public async Task> FetchNodesAsync(
}
var localIds = new NodeIdCollection(
- nodeIds.Select(nodeId => ExpandedNodeId.ToNodeId(nodeId, m_session.NamespaceUris)));
+ nodeIds.Select(nodeId => ExpandedNodeId.ToNodeId(nodeId, m_context.NamespaceUris)));
// fetch nodes and references from server.
- (IList sourceNodes, IList readErrors) = await m_session
- .ReadNodesAsync(localIds, NodeClass.Unspecified, ct: ct)
+ (IReadOnlyList sourceNodes, IReadOnlyList readErrors) = await m_context
+ .FetchNodesAsync(null, localIds, NodeClass.Unspecified, ct: ct)
.ConfigureAwait(false);
- (IList referenceCollectionList, IList fetchErrors) =
- await m_session.FetchReferencesAsync(localIds, ct).ConfigureAwait(false);
+ (IReadOnlyList referenceCollectionList, IReadOnlyList fetchErrors) =
+ await m_context.FetchReferencesAsync(null, localIds, ct).ConfigureAwait(false);
int ii = 0;
for (ii = 0; ii < count; ii++)
@@ -496,7 +498,7 @@ public async Task> FetchNodesAsync(
InternalWriteLockedAttach(sourceNodes[ii]);
}
- return sourceNodes;
+ return [.. sourceNodes];
}
///
@@ -530,9 +532,9 @@ public async Task> FindReferencesAsync(
var targetIds = new ExpandedNodeIdCollection(
references.Select(reference => reference.TargetId));
- IList result = await FindAsync(targetIds, ct).ConfigureAwait(false);
+ IList result = await FindAsync(targetIds, ct).ConfigureAwait(false);
- foreach (INode target in result)
+ foreach (INode? target in result)
{
if (target != null)
{
@@ -556,8 +558,8 @@ public async Task> FindReferencesAsync(
return targets;
}
var targetIds = new ExpandedNodeIdCollection();
- IList sources = await FindAsync(nodeIds, ct).ConfigureAwait(false);
- foreach (INode source in sources)
+ IList sources = await FindAsync(nodeIds, ct).ConfigureAwait(false);
+ foreach (INode? source in sources)
{
if (source is not Node node)
{
@@ -583,8 +585,8 @@ public async Task> FindReferencesAsync(
}
}
- IList result = await FindAsync(targetIds, ct).ConfigureAwait(false);
- foreach (INode target in result)
+ IList result = await FindAsync(targetIds, ct).ConfigureAwait(false);
+ foreach (INode? target in result)
{
if (target != null)
{
@@ -605,11 +607,11 @@ public async Task FetchSuperTypesAsync(ExpandedNodeId nodeId, CancellationToken
}
// follow the tree.
- ILocalNode subType = source;
+ ILocalNode? subType = source;
while (subType != null)
{
- ILocalNode superType = null;
+ ILocalNode? superType = null;
// Get super type (should be 1 or none)
IList references = await FindReferencesAsync(
@@ -641,7 +643,7 @@ public async ValueTask IsKnownAsync(
ExpandedNodeId typeId,
CancellationToken ct = default)
{
- INode type = await FindAsync(typeId, ct).ConfigureAwait(false);
+ INode? type = await FindAsync(typeId, ct).ConfigureAwait(false);
if (type == null)
{
@@ -664,7 +666,7 @@ public async ValueTask IsKnownAsync(
NodeId typeId,
CancellationToken ct = default)
{
- INode type = await FindAsync(typeId, ct).ConfigureAwait(false);
+ INode? type = await FindAsync(typeId, ct).ConfigureAwait(false);
if (type == null)
{
@@ -736,7 +738,7 @@ public async ValueTask IsTypeOfAsync(
return false;
}
- ILocalNode supertype = subtype;
+ ILocalNode? supertype = subtype;
while (supertype != null)
{
@@ -781,7 +783,7 @@ public async ValueTask IsTypeOfAsync(
return false;
}
- ILocalNode supertype = subtype;
+ ILocalNode? supertype = subtype;
while (supertype != null)
{
@@ -811,11 +813,11 @@ public async ValueTask IsTypeOfAsync(
}
///
- public ValueTask FindReferenceTypeNameAsync(
+ public ValueTask FindReferenceTypeNameAsync(
NodeId referenceTypeId,
CancellationToken ct = default)
{
- QualifiedName typeName;
+ QualifiedName? typeName;
m_cacheLock.EnterReadLock();
try
{
@@ -825,7 +827,7 @@ public ValueTask FindReferenceTypeNameAsync(
{
m_cacheLock.ExitReadLock();
}
- return new ValueTask(typeName);
+ return new ValueTask(typeName);
}
///
@@ -1032,7 +1034,7 @@ public async ValueTask FindDataTypeIdAsync(
if (references.Count > 0)
{
- return ExpandedNodeId.ToNodeId(references[0].TargetId, m_session.NamespaceUris);
+ return ExpandedNodeId.ToNodeId(references[0].TargetId, m_context.NamespaceUris);
}
return NodeId.Null;
@@ -1064,7 +1066,7 @@ public async ValueTask FindDataTypeIdAsync(
if (references.Count > 0)
{
- return ExpandedNodeId.ToNodeId(references[0].TargetId, m_session.NamespaceUris);
+ return ExpandedNodeId.ToNodeId(references[0].TargetId, m_context.NamespaceUris);
}
return NodeId.Null;
@@ -1122,7 +1124,7 @@ public void Clear()
}
///
- public async ValueTask GetDisplayTextAsync(
+ public async ValueTask GetDisplayTextAsync(
INode node,
CancellationToken ct = default)
{
@@ -1138,7 +1140,7 @@ public async ValueTask GetDisplayTextAsync(
return node.ToString();
}
- string displayText = null;
+ string? displayText = null;
// use the modelling rule to determine which parent to follow.
NodeId modellingRule = target.ModellingRule;
@@ -1165,7 +1167,7 @@ public async ValueTask GetDisplayTextAsync(
// use the first parent if modelling rule is new.
if (modellingRule == Objects.ModellingRule_Mandatory)
{
- displayText = await GetDisplayTextAsync(
+ displayText = parent == null ? null : await GetDisplayTextAsync(
parent,
ct).ConfigureAwait(false);
break;
@@ -1192,7 +1194,7 @@ public async ValueTask GetDisplayTextAsync(
}
///
- public async ValueTask GetDisplayTextAsync(
+ public async ValueTask GetDisplayTextAsync(
ExpandedNodeId nodeId,
CancellationToken ct = default)
{
@@ -1201,7 +1203,7 @@ public async ValueTask GetDisplayTextAsync(
return string.Empty;
}
- INode node = await FindAsync(
+ INode? node = await FindAsync(
nodeId,
ct).ConfigureAwait(false);
@@ -1216,7 +1218,7 @@ public async ValueTask GetDisplayTextAsync(
}
///
- public async ValueTask GetDisplayTextAsync(
+ public async ValueTask GetDisplayTextAsync(
ReferenceDescription reference,
CancellationToken ct = default)
{
@@ -1225,7 +1227,7 @@ public async ValueTask GetDisplayTextAsync(
return string.Empty;
}
- INode node = await FindAsync(
+ INode? node = await FindAsync(
reference.NodeId,
ct).ConfigureAwait(false);
@@ -1239,18 +1241,6 @@ public async ValueTask GetDisplayTextAsync(
return reference.ToString();
}
- ///
- public NodeId BuildBrowsePath(
- ILocalNode node,
- IList browsePath)
- {
- NodeId typeId = null;
-
- browsePath.Add(node.BrowseName);
-
- return typeId;
- }
-
private void InternalWriteLockedAttach(ILocalNode node)
{
m_cacheLock.EnterWriteLock();
@@ -1267,7 +1257,7 @@ private void InternalWriteLockedAttach(ILocalNode node)
private readonly ReaderWriterLockSlim m_cacheLock = new();
private readonly ILogger m_logger;
- private ISession m_session;
+ private readonly INodeCacheContext m_context;
private readonly TypeTable m_typeTree;
private readonly NodeTable m_nodes;
private bool m_uaTypesLoaded;
diff --git a/Libraries/Opc.Ua.Client/NodeCache/NodeCacheContext.cs b/Libraries/Opc.Ua.Client/NodeCache/NodeCacheContext.cs
new file mode 100644
index 0000000000..919903baac
--- /dev/null
+++ b/Libraries/Opc.Ua.Client/NodeCache/NodeCacheContext.cs
@@ -0,0 +1,1086 @@
+// ------------------------------------------------------------
+// Copyright (c) 2005-2020 The OPC Foundation, Inc. All rights reserved.
+// Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
+// ------------------------------------------------------------
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Opc.Ua.Client
+{
+ ///
+ /// Node cache context provides
+ ///
+ public sealed class NodeCacheContext : INodeCacheContext
+ {
+ ///
+ /// Create node cache context
+ ///
+ ///
+ public NodeCacheContext(ISessionClient session)
+ {
+ m_session = session;
+ }
+
+ ///
+ public NamespaceTable NamespaceUris => m_session.MessageContext.NamespaceUris;
+
+ ///
+ public StringTable ServerUris => m_session.MessageContext.ServerUris;
+
+ ///
+ public async ValueTask FetchReferencesAsync(
+ RequestHeader? requestHeader,
+ NodeId nodeId,
+ CancellationToken ct = default)
+ {
+ var browser = new Browser(m_session, new BrowserOptions
+ {
+ RequestHeader = requestHeader,
+ BrowseDirection = BrowseDirection.Both,
+ ReferenceTypeId = NodeId.Null,
+ IncludeSubtypes = true,
+ NodeClassMask = 0
+ });
+ ResultSet results =
+ await browser.BrowseAsync([nodeId], ct).ConfigureAwait(false);
+ return results.Results[0];
+ }
+
+ ///
+ public ValueTask> FetchReferencesAsync(
+ RequestHeader? requestHeader,
+ IReadOnlyList nodeIds,
+ CancellationToken ct = default)
+ {
+ if (nodeIds.Count == 0)
+ {
+ return new ValueTask>(
+ ResultSet.Empty);
+ }
+ var browser = new Browser(m_session, new BrowserOptions
+ {
+ RequestHeader = requestHeader,
+ BrowseDirection = BrowseDirection.Both,
+ ReferenceTypeId = NodeId.Null,
+ IncludeSubtypes = true,
+ NodeClassMask = 0
+ });
+ return browser.BrowseAsync(nodeIds, ct);
+ }
+
+ ///
+ public async ValueTask> FetchNodesAsync(
+ RequestHeader? requestHeader,
+ IReadOnlyList nodeIds,
+ bool skipOptionalAttributes = false,
+ CancellationToken ct = default)
+ {
+ if (nodeIds.Count == 0)
+ {
+ return ResultSet.Empty;
+ }
+
+ var nodeCollection = new NodeCollection(nodeIds.Count);
+ var itemsToRead = new ReadValueIdCollection(nodeIds.Count);
+
+ // first read only nodeclasses for nodes from server.
+ itemsToRead =
+ [
+ .. nodeIds.Select(nodeId => new ReadValueId {
+ NodeId = nodeId,
+ AttributeId = Attributes.NodeClass })
+ ];
+
+ ReadResponse readResponse = await m_session.ReadAsync(
+ null,
+ 0,
+ TimestampsToReturn.Neither,
+ itemsToRead,
+ ct)
+ .ConfigureAwait(false);
+
+ DataValueCollection nodeClassValues = readResponse.Results;
+ DiagnosticInfoCollection diagnosticInfos = readResponse.DiagnosticInfos;
+
+ ClientBase.ValidateResponse(nodeClassValues, itemsToRead);
+ ClientBase.ValidateDiagnosticInfos(diagnosticInfos, itemsToRead);
+
+ // second determine attributes to read per nodeclass
+ var attributesPerNodeId = new List?>(nodeIds.Count);
+ var serviceResults = new List(nodeIds.Count);
+ var attributesToRead = new ReadValueIdCollection();
+
+ CreateAttributesReadNodesRequest(
+ readResponse.ResponseHeader,
+ itemsToRead,
+ nodeClassValues,
+ diagnosticInfos,
+ attributesToRead,
+ attributesPerNodeId,
+ nodeCollection,
+ serviceResults,
+ skipOptionalAttributes);
+
+ if (attributesToRead.Count > 0)
+ {
+ readResponse = await m_session.ReadAsync(
+ null,
+ 0,
+ TimestampsToReturn.Neither,
+ attributesToRead,
+ ct)
+ .ConfigureAwait(false);
+
+ DataValueCollection values = readResponse.Results;
+ diagnosticInfos = readResponse.DiagnosticInfos;
+
+ ClientBase.ValidateResponse(values, attributesToRead);
+ ClientBase.ValidateDiagnosticInfos(diagnosticInfos, attributesToRead);
+
+ ProcessAttributesReadNodesResponse(
+ readResponse.ResponseHeader,
+ attributesToRead,
+ attributesPerNodeId,
+ values,
+ diagnosticInfos,
+ nodeCollection,
+ serviceResults);
+ }
+
+ return ResultSet.From(nodeCollection, serviceResults);
+ }
+
+ ///
+ public async ValueTask> FetchNodesAsync(
+ RequestHeader? requestHeader,
+ IReadOnlyList nodeIds,
+ NodeClass nodeClass,
+ bool skipOptionalAttributes = false,
+ CancellationToken ct = default)
+ {
+ if (nodeIds.Count == 0)
+ {
+ return ResultSet.Empty;
+ }
+
+ if (nodeClass == NodeClass.Unspecified)
+ {
+ return await FetchNodesAsync(
+ requestHeader,
+ nodeIds,
+ skipOptionalAttributes, ct).ConfigureAwait(false);
+ }
+
+ var nodeCollection = new NodeCollection(nodeIds.Count);
+
+ // determine attributes to read for nodeclass
+ var attributesPerNodeId = new List?>(nodeIds.Count);
+ var attributesToRead = new ReadValueIdCollection();
+
+ CreateNodeClassAttributesReadNodesRequest(
+ nodeIds,
+ nodeClass,
+ attributesToRead,
+ attributesPerNodeId,
+ nodeCollection,
+ skipOptionalAttributes);
+
+ ReadResponse readResponse = await m_session.ReadAsync(
+ requestHeader,
+ 0,
+ TimestampsToReturn.Neither,
+ attributesToRead,
+ ct)
+ .ConfigureAwait(false);
+
+ DataValueCollection values = readResponse.Results;
+ DiagnosticInfoCollection diagnosticInfos = readResponse.DiagnosticInfos;
+
+ ClientBase.ValidateResponse(values, attributesToRead);
+ ClientBase.ValidateDiagnosticInfos(diagnosticInfos, attributesToRead);
+
+ List serviceResults = new ServiceResult[nodeIds.Count].ToList();
+ ProcessAttributesReadNodesResponse(
+ readResponse.ResponseHeader,
+ attributesToRead,
+ attributesPerNodeId,
+ values,
+ diagnosticInfos,
+ nodeCollection,
+ serviceResults);
+
+ return ResultSet.From(nodeCollection, serviceResults);
+ }
+
+ ///
+ public async ValueTask FetchNodeAsync(
+ RequestHeader? requestHeader,
+ NodeId nodeId,
+ NodeClass nodeClass = NodeClass.Unspecified,
+ bool skipOptionalAttributes = false,
+ CancellationToken ct = default)
+ {
+ // build list of attributes.
+ IDictionary attributes = CreateAttributes(
+ nodeClass,
+ skipOptionalAttributes);
+
+ // build list of values to read.
+ var itemsToRead = new ReadValueIdCollection();
+ foreach (uint attributeId in attributes.Keys)
+ {
+ var itemToRead = new ReadValueId { NodeId = nodeId, AttributeId = attributeId };
+ itemsToRead.Add(itemToRead);
+ }
+
+ // read from server.
+ ReadResponse readResponse = await m_session.ReadAsync(
+ null,
+ 0,
+ TimestampsToReturn.Neither,
+ itemsToRead,
+ ct)
+ .ConfigureAwait(false);
+
+ DataValueCollection values = readResponse.Results;
+ DiagnosticInfoCollection diagnosticInfos = readResponse.DiagnosticInfos;
+
+ ClientBase.ValidateResponse(values, itemsToRead);
+ ClientBase.ValidateDiagnosticInfos(diagnosticInfos, itemsToRead);
+
+ return ProcessReadResponse(
+ readResponse.ResponseHeader,
+ attributes,
+ itemsToRead,
+ values,
+ diagnosticInfos);
+ }
+
+ ///
+ public async ValueTask FetchValueAsync(
+ RequestHeader? requestHeader,
+ NodeId nodeId,
+ CancellationToken ct = default)
+ {
+ var itemToRead = new ReadValueId
+ {
+ NodeId = nodeId,
+ AttributeId = Attributes.Value
+ };
+ var itemsToRead = new ReadValueIdCollection { itemToRead };
+
+ // read from server.
+ ReadResponse readResponse = await m_session.ReadAsync(
+ null,
+ 0,
+ TimestampsToReturn.Both,
+ itemsToRead,
+ ct)
+ .ConfigureAwait(false);
+
+ DataValueCollection values = readResponse.Results;
+ DiagnosticInfoCollection diagnosticInfos = readResponse.DiagnosticInfos;
+
+ ClientBase.ValidateResponse(values, itemsToRead);
+ ClientBase.ValidateDiagnosticInfos(diagnosticInfos, itemsToRead);
+
+ if (StatusCode.IsBad(values[0].StatusCode))
+ {
+ ServiceResult result = ClientBase.GetResult(
+ values[0].StatusCode,
+ 0,
+ diagnosticInfos,
+ readResponse.ResponseHeader);
+ throw new ServiceResultException(result);
+ }
+
+ return values[0];
+ }
+
+ ///
+ public async ValueTask> FetchValuesAsync(
+ RequestHeader? requestHeader,
+ IReadOnlyList nodeIds,
+ CancellationToken ct = default)
+ {
+ if (nodeIds.Count == 0)
+ {
+ return ResultSet.Empty;
+ }
+
+ // read all values from server.
+ var itemsToRead = new ReadValueIdCollection(
+ nodeIds.Select(
+ nodeId => new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Value }));
+
+ // read from server.
+ var errors = new List(itemsToRead.Count);
+
+ ReadResponse readResponse = await m_session.ReadAsync(
+ null,
+ 0,
+ TimestampsToReturn.Both,
+ itemsToRead,
+ ct)
+ .ConfigureAwait(false);
+
+ DataValueCollection values = readResponse.Results;
+ DiagnosticInfoCollection diagnosticInfos = readResponse.DiagnosticInfos;
+
+ ClientBase.ValidateResponse(values, itemsToRead);
+ ClientBase.ValidateDiagnosticInfos(diagnosticInfos, itemsToRead);
+
+ foreach (DataValue value in values)
+ {
+ ServiceResult result = ServiceResult.Good;
+ if (StatusCode.IsBad(value.StatusCode))
+ {
+ result = ClientBase.GetResult(
+ values[0].StatusCode,
+ 0,
+ diagnosticInfos,
+ readResponse.ResponseHeader);
+ }
+ errors.Add(result);
+ }
+
+ return ResultSet.From(values, errors);
+ }
+
+ ///
+ /// Creates a read request with attributes determined by the NodeClass.
+ ///
+ private static void CreateAttributesReadNodesRequest(
+ ResponseHeader responseHeader,
+ ReadValueIdCollection itemsToRead,
+ DataValueCollection nodeClassValues,
+ DiagnosticInfoCollection diagnosticInfos,
+ ReadValueIdCollection attributesToRead,
+ List?> attributesPerNodeId,
+ NodeCollection nodeCollection,
+ List errors,
+ bool skipOptionalAttributes)
+ {
+ NodeClass? nodeClass;
+ for (int ii = 0; ii < itemsToRead.Count; ii++)
+ {
+ var node = new Node { NodeId = itemsToRead[ii].NodeId };
+ if (!DataValue.IsGood(nodeClassValues[ii]))
+ {
+ nodeCollection.Add(node);
+ errors.Add(
+ new ServiceResult(
+ nodeClassValues[ii].StatusCode,
+ ii,
+ diagnosticInfos,
+ responseHeader.StringTable));
+ attributesPerNodeId.Add(null);
+ continue;
+ }
+
+ // check for valid node class.
+ nodeClass = nodeClassValues[ii].Value as NodeClass?;
+
+ if (nodeClass == null)
+ {
+ if (nodeClassValues[ii].Value is int nc)
+ {
+ nodeClass = (NodeClass)nc;
+ }
+ else
+ {
+ nodeCollection.Add(node);
+ errors.Add(
+ ServiceResult.Create(
+ StatusCodes.BadUnexpectedError,
+ "Node does not have a valid value for NodeClass: {0}.",
+ nodeClassValues[ii].Value));
+ attributesPerNodeId.Add(null);
+ continue;
+ }
+ }
+
+ node.NodeClass = nodeClass.Value;
+
+ Dictionary attributes = CreateAttributes(
+ node.NodeClass,
+ skipOptionalAttributes);
+ foreach (uint attributeId in attributes.Keys)
+ {
+ var itemToRead = new ReadValueId
+ {
+ NodeId = node.NodeId,
+ AttributeId = attributeId
+ };
+ attributesToRead.Add(itemToRead);
+ }
+
+ nodeCollection.Add(node);
+ errors.Add(ServiceResult.Good);
+ attributesPerNodeId.Add(attributes);
+ }
+ }
+
+ ///
+ /// Builds the node collection results based on the attribute values of the read response.
+ ///
+ /// The response requestHeader of the read request.
+ /// The collection of all attributes to read passed in the read request.
+ /// The attributes requested per NodeId
+ /// The attribute values returned by the read request.
+ /// The diagnostic info returned by the read request.
+ /// The node collection which holds the results.
+ /// The service results for each node.
+ private static void ProcessAttributesReadNodesResponse(
+ ResponseHeader responseHeader,
+ ReadValueIdCollection attributesToRead,
+ List?> attributesPerNodeId,
+ DataValueCollection values,
+ DiagnosticInfoCollection diagnosticInfos,
+ NodeCollection nodeCollection,
+ List errors)
+ {
+ int readIndex = 0;
+ for (int ii = 0; ii < nodeCollection.Count; ii++)
+ {
+ IDictionary? attributes = attributesPerNodeId[ii];
+ if (attributes == null)
+ {
+ continue;
+ }
+
+ int readCount = attributes.Count;
+ var subRangeAttributes = new ReadValueIdCollection(
+ attributesToRead.GetRange(readIndex, readCount));
+ var subRangeValues = new DataValueCollection(values.GetRange(readIndex, readCount));
+ DiagnosticInfoCollection subRangeDiagnostics =
+ diagnosticInfos.Count > 0
+ ? [.. diagnosticInfos.GetRange(readIndex, readCount)]
+ : diagnosticInfos;
+ try
+ {
+ nodeCollection[ii] = ProcessReadResponse(
+ responseHeader,
+ attributes,
+ subRangeAttributes,
+ subRangeValues,
+ subRangeDiagnostics);
+ errors[ii] = ServiceResult.Good;
+ }
+ catch (ServiceResultException sre)
+ {
+ errors[ii] = sre.Result;
+ }
+ readIndex += readCount;
+ }
+ }
+
+ ///
+ /// Creates a Node based on the read response.
+ ///
+ ///
+ private static Node ProcessReadResponse(
+ ResponseHeader responseHeader,
+ IDictionary attributes,
+ ReadValueIdCollection itemsToRead,
+ DataValueCollection values,
+ DiagnosticInfoCollection diagnosticInfos)
+ {
+ // process results.
+ NodeClass? nodeClass = null;
+
+ for (int ii = 0; ii < itemsToRead.Count; ii++)
+ {
+ uint attributeId = itemsToRead[ii].AttributeId;
+
+ // the node probably does not exist if the node class is not found.
+ if (attributeId == Attributes.NodeClass)
+ {
+ if (!DataValue.IsGood(values[ii]))
+ {
+ throw ServiceResultException.Create(
+ values[ii].StatusCode,
+ ii,
+ diagnosticInfos,
+ responseHeader.StringTable);
+ }
+
+ // check for valid node class.
+ nodeClass = values[ii].Value as NodeClass?;
+ if (nodeClass == null)
+ {
+ if (values[ii].Value is int nc)
+ {
+ nodeClass = (NodeClass)nc;
+ }
+ else
+ {
+ throw ServiceResultException.Unexpected(
+ "Node does not have a valid value for NodeClass: {0}.",
+ values[ii].Value);
+ }
+ }
+ }
+ else if (!DataValue.IsGood(values[ii]))
+ {
+ // check for unsupported attributes.
+ if (values[ii].StatusCode == StatusCodes.BadAttributeIdInvalid)
+ {
+ continue;
+ }
+
+ // ignore errors on optional attributes
+ if (StatusCode.IsBad(values[ii].StatusCode) &&
+ attributeId
+ is Attributes.AccessRestrictions
+ or Attributes.Description
+ or Attributes.RolePermissions
+ or Attributes.UserRolePermissions
+ or Attributes.UserWriteMask
+ or Attributes.WriteMask
+ or Attributes.AccessLevelEx
+ or Attributes.ArrayDimensions
+ or Attributes.DataTypeDefinition
+ or Attributes.InverseName
+ or Attributes.MinimumSamplingInterval)
+ {
+ continue;
+ }
+
+ // all supported attributes must be readable.
+ if (attributeId != Attributes.Value)
+ {
+ throw ServiceResultException.Create(
+ values[ii].StatusCode,
+ ii,
+ diagnosticInfos,
+ responseHeader.StringTable);
+ }
+ }
+
+ attributes[attributeId] = values[ii];
+ }
+
+ Node node;
+ DataValue? value;
+ switch (nodeClass)
+ {
+ case NodeClass.Object:
+ var objectNode = new ObjectNode();
+
+ value = attributes[Attributes.EventNotifier];
+
+ if (value == null)
+ {
+ throw ServiceResultException.Unexpected(
+ "Object does not support the EventNotifier attribute.");
+ }
+
+ objectNode.EventNotifier = value.GetValueOrDefault();
+ node = objectNode;
+ break;
+ case NodeClass.ObjectType:
+ var objectTypeNode = new ObjectTypeNode();
+
+ value = attributes[Attributes.IsAbstract];
+
+ if (value == null)
+ {
+ throw ServiceResultException.Unexpected(
+ "ObjectType does not support the IsAbstract attribute.");
+ }
+
+ objectTypeNode.IsAbstract = value.GetValueOrDefault();
+ node = objectTypeNode;
+ break;
+ case NodeClass.Variable:
+ var variableNode = new VariableNode();
+
+ // DataType Attribute
+ value = attributes[Attributes.DataType];
+
+ if (value == null)
+ {
+ throw ServiceResultException.Unexpected(
+ "Variable does not support the DataType attribute.");
+ }
+
+ variableNode.DataType = (NodeId)value.GetValue(typeof(NodeId));
+
+ // ValueRank Attribute
+ value = attributes[Attributes.ValueRank];
+
+ if (value == null)
+ {
+ throw ServiceResultException.Unexpected(
+ "Variable does not support the ValueRank attribute.");
+ }
+
+ variableNode.ValueRank = value.GetValueOrDefault();
+
+ // ArrayDimensions Attribute
+ value = attributes[Attributes.ArrayDimensions];
+
+ if (value != null)
+ {
+ if (value.Value == null)
+ {
+ variableNode.ArrayDimensions = Array.Empty();
+ }
+ else
+ {
+ variableNode.ArrayDimensions = (uint[])value.GetValue(typeof(uint[]));
+ }
+ }
+
+ // AccessLevel Attribute
+ value = attributes[Attributes.AccessLevel];
+
+ if (value == null)
+ {
+ throw ServiceResultException.Unexpected(
+ "Variable does not support the AccessLevel attribute.");
+ }
+
+ variableNode.AccessLevel = value.GetValueOrDefault();
+
+ // UserAccessLevel Attribute
+ value = attributes[Attributes.UserAccessLevel];
+
+ if (value == null)
+ {
+ throw ServiceResultException.Unexpected(
+ "Variable does not support the UserAccessLevel attribute.");
+ }
+
+ variableNode.UserAccessLevel = value.GetValueOrDefault();
+
+ // Historizing Attribute
+ value = attributes[Attributes.Historizing];
+
+ if (value == null)
+ {
+ throw ServiceResultException.Unexpected(
+ "Variable does not support the Historizing attribute.");
+ }
+
+ variableNode.Historizing = value.GetValueOrDefault();
+
+ // MinimumSamplingInterval Attribute
+ value = attributes[Attributes.MinimumSamplingInterval];
+
+ if (value != null)
+ {
+ variableNode.MinimumSamplingInterval = Convert.ToDouble(
+ attributes[Attributes.MinimumSamplingInterval]?.Value,
+ CultureInfo.InvariantCulture);
+ }
+
+ // AccessLevelEx Attribute
+ value = attributes[Attributes.AccessLevelEx];
+
+ if (value != null)
+ {
+ variableNode.AccessLevelEx = value.GetValueOrDefault();
+ }
+
+ node = variableNode;
+ break;
+ case NodeClass.VariableType:
+ var variableTypeNode = new VariableTypeNode();
+
+ // IsAbstract Attribute
+ value = attributes[Attributes.IsAbstract];
+
+ if (value == null)
+ {
+ throw ServiceResultException.Unexpected(
+ "VariableType does not support the IsAbstract attribute.");
+ }
+
+ variableTypeNode.IsAbstract = value.GetValueOrDefault();
+
+ // DataType Attribute
+ value = attributes[Attributes.DataType];
+
+ if (value == null)
+ {
+ throw ServiceResultException.Unexpected(
+ "VariableType does not support the DataType attribute.");
+ }
+
+ variableTypeNode.DataType = (NodeId)value.GetValue(typeof(NodeId));
+
+ // ValueRank Attribute
+ value = attributes[Attributes.ValueRank];
+
+ if (value == null)
+ {
+ throw ServiceResultException.Unexpected(
+ "VariableType does not support the ValueRank attribute.");
+ }
+
+ variableTypeNode.ValueRank = value.GetValueOrDefault();
+
+ // ArrayDimensions Attribute
+ value = attributes[Attributes.ArrayDimensions];
+
+ if (value != null && value.Value != null)
+ {
+ variableTypeNode.ArrayDimensions = (uint[])value.GetValue(typeof(uint[]));
+ }
+
+ node = variableTypeNode;
+ break;
+ case NodeClass.Method:
+ var methodNode = new MethodNode();
+
+ // Executable Attribute
+ value = attributes[Attributes.Executable];
+
+ if (value == null)
+ {
+ throw ServiceResultException.Unexpected(
+ "Method does not support the Executable attribute.");
+ }
+
+ methodNode.Executable = value.GetValueOrDefault();
+
+ // UserExecutable Attribute
+ value = attributes[Attributes.UserExecutable];
+
+ if (value == null)
+ {
+ throw ServiceResultException.Unexpected(
+ "Method does not support the UserExecutable attribute.");
+ }
+
+ methodNode.UserExecutable = value.GetValueOrDefault();
+
+ node = methodNode;
+ break;
+ case NodeClass.DataType:
+ var dataTypeNode = new DataTypeNode();
+
+ // IsAbstract Attribute
+ value = attributes[Attributes.IsAbstract];
+
+ if (value == null)
+ {
+ throw ServiceResultException.Unexpected(
+ "DataType does not support the IsAbstract attribute.");
+ }
+
+ dataTypeNode.IsAbstract = value.GetValueOrDefault();
+
+ // DataTypeDefinition Attribute
+ value = attributes[Attributes.DataTypeDefinition];
+
+ if (value != null)
+ {
+ dataTypeNode.DataTypeDefinition = value.Value as ExtensionObject;
+ }
+
+ node = dataTypeNode;
+ break;
+ case NodeClass.ReferenceType:
+ var referenceTypeNode = new ReferenceTypeNode();
+
+ // IsAbstract Attribute
+ value = attributes[Attributes.IsAbstract];
+
+ if (value == null)
+ {
+ throw ServiceResultException.Unexpected(
+ "ReferenceType does not support the IsAbstract attribute.");
+ }
+
+ referenceTypeNode.IsAbstract = value.GetValueOrDefault();
+
+ // Symmetric Attribute
+ value = attributes[Attributes.Symmetric];
+
+ if (value == null)
+ {
+ throw ServiceResultException.Unexpected(
+ "ReferenceType does not support the Symmetric attribute.");
+ }
+
+ referenceTypeNode.Symmetric = value.GetValueOrDefault();
+
+ // InverseName Attribute
+ value = attributes[Attributes.InverseName];
+
+ if (value != null && value.Value != null)
+ {
+ referenceTypeNode.InverseName = (LocalizedText)value.GetValue(
+ typeof(LocalizedText));
+ }
+
+ node = referenceTypeNode;
+ break;
+ case NodeClass.View:
+ var viewNode = new ViewNode();
+
+ // EventNotifier Attribute
+ value = attributes[Attributes.EventNotifier];
+
+ if (value == null)
+ {
+ throw ServiceResultException.Unexpected(
+ "View does not support the EventNotifier attribute.");
+ }
+
+ viewNode.EventNotifier = value.GetValueOrDefault();
+
+ // ContainsNoLoops Attribute
+ value = attributes[Attributes.ContainsNoLoops];
+
+ if (value == null)
+ {
+ throw ServiceResultException.Unexpected(
+ "View does not support the ContainsNoLoops attribute.");
+ }
+
+ viewNode.ContainsNoLoops = value.GetValueOrDefault();
+
+ node = viewNode;
+ break;
+ case NodeClass.Unspecified:
+ throw ServiceResultException.Unexpected(
+ "Node does not have a valid value for NodeClass: {0}.",
+ nodeClass.Value);
+ default:
+ throw ServiceResultException.Unexpected(
+ $"Unexpected NodeClass: {nodeClass}.");
+ }
+
+ // NodeId Attribute
+ value = attributes[Attributes.NodeId];
+
+ if (value == null)
+ {
+ throw ServiceResultException.Unexpected(
+ "Node does not support the NodeId attribute.");
+ }
+
+ node.NodeId = (NodeId)value.GetValue(typeof(NodeId));
+ node.NodeClass = nodeClass.Value;
+
+ // BrowseName Attribute
+ value = attributes[Attributes.BrowseName];
+
+ if (value == null)
+ {
+ throw ServiceResultException.Unexpected(
+ "Node does not support the BrowseName attribute.");
+ }
+
+ node.BrowseName = (QualifiedName)value.GetValue(typeof(QualifiedName));
+
+ // DisplayName Attribute
+ value = attributes[Attributes.DisplayName];
+
+ if (value == null)
+ {
+ throw ServiceResultException.Unexpected(
+ "Node does not support the DisplayName attribute.");
+ }
+
+ node.DisplayName = (LocalizedText)value.GetValue(typeof(LocalizedText));
+
+ // all optional attributes follow
+
+ // Description Attribute
+ if (attributes.TryGetValue(Attributes.Description, out value) &&
+ value != null &&
+ value.Value != null)
+ {
+ node.Description = (LocalizedText)value.GetValue(typeof(LocalizedText));
+ }
+
+ // WriteMask Attribute
+ if (attributes.TryGetValue(Attributes.WriteMask, out value) && value != null)
+ {
+ node.WriteMask = value.GetValueOrDefault();
+ }
+
+ // UserWriteMask Attribute
+ if (attributes.TryGetValue(Attributes.UserWriteMask, out value) && value != null)
+ {
+ node.UserWriteMask = value.GetValueOrDefault();
+ }
+
+ // RolePermissions Attribute
+ if (attributes.TryGetValue(Attributes.RolePermissions, out value) && value != null)
+ {
+ if (value.Value is ExtensionObject[] rolePermissions)
+ {
+ node.RolePermissions = [];
+
+ foreach (ExtensionObject rolePermission in rolePermissions)
+ {
+ node.RolePermissions.Add(rolePermission.Body as RolePermissionType);
+ }
+ }
+ }
+
+ // UserRolePermissions Attribute
+ if (attributes.TryGetValue(Attributes.UserRolePermissions, out value) && value != null)
+ {
+ if (value.Value is ExtensionObject[] userRolePermissions)
+ {
+ node.UserRolePermissions = [];
+
+ foreach (ExtensionObject rolePermission in userRolePermissions)
+ {
+ node.UserRolePermissions.Add(rolePermission.Body as RolePermissionType);
+ }
+ }
+ }
+
+ // AccessRestrictions Attribute
+ if (attributes.TryGetValue(Attributes.AccessRestrictions, out value) && value != null)
+ {
+ node.AccessRestrictions = value.GetValueOrDefault();
+ }
+
+ return node;
+ }
+
+ ///
+ /// Create a dictionary of attributes to read for a nodeclass.
+ ///
+ ///
+ private static Dictionary CreateAttributes(
+ NodeClass nodeClass,
+ bool skipOptionalAttributes)
+ {
+ // Attributes to read for all types of nodes
+ var attributes = new Dictionary(Attributes.MaxAttributes)
+ {
+ { Attributes.NodeId, null },
+ { Attributes.NodeClass, null },
+ { Attributes.BrowseName, null },
+ { Attributes.DisplayName, null }
+ };
+
+ switch (nodeClass)
+ {
+ case NodeClass.Object:
+ attributes.Add(Attributes.EventNotifier, null);
+ break;
+ case NodeClass.Variable:
+ attributes.Add(Attributes.DataType, null);
+ attributes.Add(Attributes.ValueRank, null);
+ attributes.Add(Attributes.ArrayDimensions, null);
+ attributes.Add(Attributes.AccessLevel, null);
+ attributes.Add(Attributes.UserAccessLevel, null);
+ attributes.Add(Attributes.Historizing, null);
+ attributes.Add(Attributes.MinimumSamplingInterval, null);
+ attributes.Add(Attributes.AccessLevelEx, null);
+ break;
+ case NodeClass.Method:
+ attributes.Add(Attributes.Executable, null);
+ attributes.Add(Attributes.UserExecutable, null);
+ break;
+ case NodeClass.ObjectType:
+ attributes.Add(Attributes.IsAbstract, null);
+ break;
+ case NodeClass.VariableType:
+ attributes.Add(Attributes.IsAbstract, null);
+ attributes.Add(Attributes.DataType, null);
+ attributes.Add(Attributes.ValueRank, null);
+ attributes.Add(Attributes.ArrayDimensions, null);
+ break;
+ case NodeClass.ReferenceType:
+ attributes.Add(Attributes.IsAbstract, null);
+ attributes.Add(Attributes.Symmetric, null);
+ attributes.Add(Attributes.InverseName, null);
+ break;
+ case NodeClass.DataType:
+ attributes.Add(Attributes.IsAbstract, null);
+ attributes.Add(Attributes.DataTypeDefinition, null);
+ break;
+ case NodeClass.View:
+ attributes.Add(Attributes.EventNotifier, null);
+ attributes.Add(Attributes.ContainsNoLoops, null);
+ break;
+ case NodeClass.Unspecified:
+ // build complete list of attributes.
+ attributes.Add(Attributes.DataType, null);
+ attributes.Add(Attributes.ValueRank, null);
+ attributes.Add(Attributes.ArrayDimensions, null);
+ attributes.Add(Attributes.AccessLevel, null);
+ attributes.Add(Attributes.UserAccessLevel, null);
+ attributes.Add(Attributes.MinimumSamplingInterval, null);
+ attributes.Add(Attributes.Historizing, null);
+ attributes.Add(Attributes.EventNotifier, null);
+ attributes.Add(Attributes.Executable, null);
+ attributes.Add(Attributes.UserExecutable, null);
+ attributes.Add(Attributes.IsAbstract, null);
+ attributes.Add(Attributes.InverseName, null);
+ attributes.Add(Attributes.Symmetric, null);
+ attributes.Add(Attributes.ContainsNoLoops, null);
+ attributes.Add(Attributes.DataTypeDefinition, null);
+ attributes.Add(Attributes.AccessLevelEx, null);
+ break;
+ default:
+ throw ServiceResultException.Unexpected(
+ $"Unexpected NodeClass: {nodeClass}.");
+ }
+
+ if (!skipOptionalAttributes)
+ {
+ attributes.Add(Attributes.Description, null);
+ attributes.Add(Attributes.WriteMask, null);
+ attributes.Add(Attributes.UserWriteMask, null);
+ attributes.Add(Attributes.RolePermissions, null);
+ attributes.Add(Attributes.UserRolePermissions, null);
+ attributes.Add(Attributes.AccessRestrictions, null);
+ }
+
+ return attributes;
+ }
+
+ ///
+ /// Creates a read request with attributes determined by the NodeClass.
+ ///
+ private static void CreateNodeClassAttributesReadNodesRequest(
+ IReadOnlyList nodeIds,
+ NodeClass nodeClass,
+ ReadValueIdCollection attributesToRead,
+ List?> attributesPerNodeId,
+ NodeCollection nodeCollection,
+ bool skipOptionalAttributes)
+ {
+ for (int ii = 0; ii < nodeIds.Count; ii++)
+ {
+ var node = new Node { NodeId = nodeIds[ii], NodeClass = nodeClass };
+
+ Dictionary attributes = CreateAttributes(
+ node.NodeClass,
+ skipOptionalAttributes);
+ foreach (uint attributeId in attributes.Keys)
+ {
+ var itemToRead = new ReadValueId
+ {
+ NodeId = node.NodeId,
+ AttributeId = attributeId
+ };
+ attributesToRead.Add(itemToRead);
+ }
+
+ nodeCollection.Add(node);
+ attributesPerNodeId.Add(attributes);
+ }
+ }
+
+ private readonly ISessionClient m_session;
+ }
+}
diff --git a/Libraries/Opc.Ua.Client/NodeCache/NodeCacheObsolete.cs b/Libraries/Opc.Ua.Client/NodeCache/NodeCacheObsolete.cs
index 7fcf7cdbda..c15343270b 100644
--- a/Libraries/Opc.Ua.Client/NodeCache/NodeCacheObsolete.cs
+++ b/Libraries/Opc.Ua.Client/NodeCache/NodeCacheObsolete.cs
@@ -27,6 +27,8 @@
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
+#nullable disable
+
using System;
using System.Collections.Generic;
diff --git a/Libraries/Opc.Ua.Client/Opc.Ua.Client.csproj b/Libraries/Opc.Ua.Client/Opc.Ua.Client.csproj
index 06b3fa7dbe..a661ffd4fb 100644
--- a/Libraries/Opc.Ua.Client/Opc.Ua.Client.csproj
+++ b/Libraries/Opc.Ua.Client/Opc.Ua.Client.csproj
@@ -9,6 +9,7 @@
MIT
true
+ enable
$(DefineConstants);SIGNASSEMBLY
diff --git a/Libraries/Opc.Ua.Client/ReverseConnectManager.cs b/Libraries/Opc.Ua.Client/ReverseConnectManager.cs
index d489f67392..714291593a 100644
--- a/Libraries/Opc.Ua.Client/ReverseConnectManager.cs
+++ b/Libraries/Opc.Ua.Client/ReverseConnectManager.cs
@@ -27,6 +27,8 @@
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
+#nullable disable
+
using System;
using System.Collections.Generic;
using System.IO;
diff --git a/Libraries/Opc.Ua.Client/Session/DefaultSessionFactory.cs b/Libraries/Opc.Ua.Client/Session/DefaultSessionFactory.cs
new file mode 100644
index 0000000000..75621d9c2e
--- /dev/null
+++ b/Libraries/Opc.Ua.Client/Session/DefaultSessionFactory.cs
@@ -0,0 +1,438 @@
+/* ========================================================================
+ * Copyright (c) 2005-2022 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.Security.Cryptography.X509Certificates;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Opc.Ua.Client
+{
+ ///
+ /// Object that creates instances of an Opc.Ua.Client.Session object.
+ ///
+ public class DefaultSessionFactory : ISessionFactory
+ {
+ ///
+ /// The default instance of the factory.
+ ///
+ [Obsolete("Use new DefaultSessionFactory instead.")]
+ public static readonly DefaultSessionFactory Instance = new(null!);
+
+ ///
+ public ITelemetryContext Telemetry { get; init; }
+
+ ///
+ public DiagnosticsMasks ReturnDiagnostics { get; set; }
+
+ ///
+ /// Obsolete default constructor
+ ///
+ [Obsolete("Use DefaultSessionFactory(ITelemetryContext) instead.")]
+ public DefaultSessionFactory()
+ : this(null!)
+ {
+ }
+
+ ///
+ /// Force use of the default instance.
+ ///
+ public DefaultSessionFactory(ITelemetryContext telemetry)
+ {
+ Telemetry = telemetry;
+ }
+
+ ///
+ public virtual Task CreateAsync(
+ ApplicationConfiguration configuration,
+ ConfiguredEndpoint endpoint,
+ bool updateBeforeConnect,
+ string sessionName,
+ uint sessionTimeout,
+ IUserIdentity? identity,
+ IList? preferredLocales,
+ CancellationToken ct = default)
+ {
+ return CreateAsync(
+ configuration,
+ endpoint,
+ updateBeforeConnect,
+ false,
+ sessionName,
+ sessionTimeout,
+ identity,
+ preferredLocales,
+ ct);
+ }
+
+ ///
+ public virtual Task CreateAsync(
+ ApplicationConfiguration configuration,
+ ConfiguredEndpoint endpoint,
+ bool updateBeforeConnect,
+ bool checkDomain,
+ string sessionName,
+ uint sessionTimeout,
+ IUserIdentity? identity,
+ IList? preferredLocales,
+ CancellationToken ct = default)
+ {
+ return CreateAsync(
+ configuration,
+ null,
+ endpoint,
+ updateBeforeConnect,
+ checkDomain,
+ sessionName,
+ sessionTimeout,
+ identity,
+ preferredLocales,
+ ReturnDiagnostics,
+ ct);
+ }
+
+ ///
+ public virtual Task CreateAsync(
+ ApplicationConfiguration configuration,
+ ITransportWaitingConnection connection,
+ ConfiguredEndpoint endpoint,
+ bool updateBeforeConnect,
+ bool checkDomain,
+ string sessionName,
+ uint sessionTimeout,
+ IUserIdentity? identity,
+ IList? preferredLocales,
+ CancellationToken ct = default)
+ {
+ return CreateAsync(
+ configuration,
+ connection,
+ endpoint,
+ updateBeforeConnect,
+ checkDomain,
+ sessionName,
+ sessionTimeout,
+ identity,
+ preferredLocales,
+ ReturnDiagnostics,
+ ct);
+ }
+
+ ///
+ public virtual async Task CreateAsync(
+ ApplicationConfiguration configuration,
+ ReverseConnectManager reverseConnectManager,
+ ConfiguredEndpoint endpoint,
+ bool updateBeforeConnect,
+ bool checkDomain,
+ string sessionName,
+ uint sessionTimeout,
+ IUserIdentity? userIdentity,
+ IList? preferredLocales,
+ CancellationToken ct = default)
+ {
+ if (reverseConnectManager == null)
+ {
+ return await CreateAsync(
+ configuration,
+ endpoint,
+ updateBeforeConnect,
+ checkDomain,
+ sessionName,
+ sessionTimeout,
+ userIdentity,
+ preferredLocales,
+ ct).ConfigureAwait(false);
+ }
+
+ ITransportWaitingConnection? connection;
+ do
+ {
+ connection = await reverseConnectManager
+ .WaitForConnectionAsync(
+ endpoint.EndpointUrl,
+ endpoint.ReverseConnect?.ServerUri,
+ ct)
+ .ConfigureAwait(false);
+
+ if (updateBeforeConnect)
+ {
+ await endpoint.UpdateFromServerAsync(
+ endpoint.EndpointUrl,
+ connection,
+ endpoint.Description.SecurityMode,
+ endpoint.Description.SecurityPolicyUri,
+ Telemetry,
+ ct).ConfigureAwait(false);
+ updateBeforeConnect = false;
+ connection = null;
+ }
+ } while (connection == null);
+
+ return await CreateAsync(
+ configuration,
+ connection,
+ endpoint,
+ false,
+ checkDomain,
+ sessionName,
+ sessionTimeout,
+ userIdentity,
+ preferredLocales,
+ ct).ConfigureAwait(false);
+ }
+
+ ///
+ public virtual async Task CreateChannelAsync(
+ ApplicationConfiguration configuration,
+ ITransportWaitingConnection? connection,
+ ConfiguredEndpoint endpoint,
+ bool updateBeforeConnect,
+ bool checkDomain,
+ CancellationToken ct = default)
+ {
+ endpoint.UpdateBeforeConnect = updateBeforeConnect;
+
+ EndpointDescription endpointDescription = endpoint.Description;
+
+ // create the endpoint configuration (use the application configuration to provide default values).
+ EndpointConfiguration endpointConfiguration = endpoint.Configuration;
+
+ if (endpointConfiguration == null)
+ {
+ endpoint.Configuration = endpointConfiguration = EndpointConfiguration.Create(
+ configuration);
+ }
+
+ // create message context.
+ ServiceMessageContext messageContext = configuration.CreateMessageContext(true);
+
+ // update endpoint description using the discovery endpoint.
+ if (endpoint.UpdateBeforeConnect && connection == null)
+ {
+ await endpoint.UpdateFromServerAsync(messageContext.Telemetry, ct).ConfigureAwait(false);
+ endpointDescription = endpoint.Description;
+ endpointConfiguration = endpoint.Configuration;
+ }
+
+ // checks the domains in the certificate.
+ if (checkDomain &&
+ endpoint.Description.ServerCertificate != null &&
+ endpoint.Description.ServerCertificate.Length > 0)
+ {
+ configuration.CertificateValidator?.ValidateDomains(
+ CertificateFactory.Create(endpoint.Description.ServerCertificate),
+ endpoint);
+ }
+
+ X509Certificate2? clientCertificate = null;
+ X509Certificate2Collection? clientCertificateChain = null;
+ if (endpointDescription.SecurityPolicyUri != SecurityPolicies.None)
+ {
+ clientCertificate = await Session.LoadInstanceCertificateAsync(
+ configuration,
+ endpointDescription.SecurityPolicyUri,
+ messageContext.Telemetry,
+ ct)
+ .ConfigureAwait(false);
+ clientCertificateChain = await Session.LoadCertificateChainAsync(
+ configuration,
+ clientCertificate,
+ ct)
+ .ConfigureAwait(false);
+ }
+
+ // initialize the channel which will be created with the server.
+ if (connection != null)
+ {
+ return await UaChannelBase.CreateUaBinaryChannelAsync(
+ configuration,
+ connection,
+ endpointDescription,
+ endpointConfiguration,
+ clientCertificate,
+ clientCertificateChain,
+ messageContext,
+ ct).ConfigureAwait(false);
+ }
+
+ return await UaChannelBase.CreateUaBinaryChannelAsync(
+ configuration,
+ endpointDescription,
+ endpointConfiguration,
+ clientCertificate,
+ clientCertificateChain,
+ messageContext,
+ ct).ConfigureAwait(false);
+ }
+
+ ///
+ public virtual async Task RecreateAsync(
+ ISession sessionTemplate,
+ CancellationToken ct = default)
+ {
+ if (sessionTemplate is not Session template)
+ {
+ throw new ArgumentException(
+ "The ISession provided is not of a supported type.",
+
+ nameof(sessionTemplate));
+ }
+
+ template.ReturnDiagnostics = ReturnDiagnostics;
+ return await template.RecreateAsync(ct).ConfigureAwait(false);
+ }
+
+ ///
+ public virtual async Task RecreateAsync(
+ ISession sessionTemplate,
+ ITransportWaitingConnection connection,
+ CancellationToken ct = default)
+ {
+ if (sessionTemplate is not Session template)
+ {
+ throw new ArgumentException(
+ "The ISession provided is not of a supported type.",
+
+ nameof(sessionTemplate));
+ }
+
+ template.ReturnDiagnostics = ReturnDiagnostics;
+ return await template.RecreateAsync(connection, ct).ConfigureAwait(false);
+ }
+
+ ///
+ public virtual async Task RecreateAsync(
+ ISession sessionTemplate,
+ ITransportChannel transportChannel,
+ CancellationToken ct = default)
+ {
+ if (sessionTemplate is not Session template)
+ {
+ throw new ArgumentException(
+ "The ISession provided is not of a supported type.",
+
+ nameof(sessionTemplate));
+ }
+ template.ReturnDiagnostics = ReturnDiagnostics;
+ return await template.RecreateAsync(transportChannel, ct)
+ .ConfigureAwait(false);
+ }
+
+ ///
+ public virtual ISession Create(
+ ITransportChannel channel,
+ ApplicationConfiguration configuration,
+ ConfiguredEndpoint endpoint,
+ X509Certificate2? clientCertificate = null,
+ X509Certificate2Collection? clientCertificateChain = null,
+ EndpointDescriptionCollection? availableEndpoints = null,
+ StringCollection? discoveryProfileUris = null)
+ {
+ return new Session(
+ channel,
+ configuration,
+ endpoint,
+ clientCertificate,
+ clientCertificateChain,
+ availableEndpoints,
+ discoveryProfileUris)
+ {
+ ReturnDiagnostics = ReturnDiagnostics
+ };
+ }
+
+ ///
+ /// Creates a new communication session with a server using a reverse connection.
+ ///
+ /// The configuration for the client application.
+ /// The client endpoint for the reverse connect.
+ /// The endpoint for the server.
+ /// If set to true the discovery endpoint is used to
+ /// update the endpoint description before connecting.
+ /// If set to true then the domain in the certificate must match
+ /// the endpoint used.
+ /// The name to assign to the session.
+ /// The timeout period for the session.
+ /// The user identity to associate with the session.
+ /// The preferred locales.
+ /// The return diagnostics to use on this session
+ /// The cancellation token.
+ /// The new session object.
+ private async Task CreateAsync(
+ ApplicationConfiguration configuration,
+ ITransportWaitingConnection? connection,
+ ConfiguredEndpoint endpoint,
+ bool updateBeforeConnect,
+ bool checkDomain,
+ string sessionName,
+ uint sessionTimeout,
+ IUserIdentity? identity,
+ IList? preferredLocales,
+ DiagnosticsMasks returnDiagnostics,
+ CancellationToken ct = default)
+ {
+ // initialize the channel which will be created with the server.
+ ITransportChannel channel = await CreateChannelAsync(
+ configuration,
+ connection,
+ endpoint,
+ updateBeforeConnect,
+ checkDomain,
+ ct)
+ .ConfigureAwait(false);
+
+ // create the session object.
+ ISession session = Create(channel, configuration, endpoint, null);
+ session.ReturnDiagnostics = returnDiagnostics;
+
+ // create the session.
+ try
+ {
+ await session
+ .OpenAsync(
+ sessionName,
+ sessionTimeout,
+ identity ?? new UserIdentity(),
+ preferredLocales,
+ checkDomain,
+ ct)
+ .ConfigureAwait(false);
+ }
+ catch (Exception)
+ {
+ session.Dispose();
+ throw;
+ }
+
+ return session;
+ }
+ }
+}
diff --git a/Libraries/Opc.Ua.Client/Session/Factory/DefaultSessionFactory.cs b/Libraries/Opc.Ua.Client/Session/Factory/DefaultSessionFactory.cs
deleted file mode 100644
index 45177bd6c0..0000000000
--- a/Libraries/Opc.Ua.Client/Session/Factory/DefaultSessionFactory.cs
+++ /dev/null
@@ -1,342 +0,0 @@
-/* ========================================================================
- * Copyright (c) 2005-2022 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.Security.Cryptography.X509Certificates;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Opc.Ua.Client
-{
- ///
- /// Object that creates instances of an Opc.Ua.Client.Session object.
- ///
- public class DefaultSessionFactory : ISessionFactory, ISessionInstantiator
- {
- ///
- /// The default instance of the factory.
- ///
- [Obsolete("Use new DefaultSessionFactory instead.")]
- public static readonly DefaultSessionFactory Instance = new(null);
-
- ///
- public ITelemetryContext Telemetry { get; init; }
-
- ///
- public DiagnosticsMasks ReturnDiagnostics { get; set; }
-
- ///
- /// Obsolete default constructor
- ///
- [Obsolete("Use DefaultSessionFactory(ITelemetryContext) instead.")]
- public DefaultSessionFactory()
- : this(null)
- {
- }
-
- ///
- /// Force use of the default instance.
- ///
- public DefaultSessionFactory(ITelemetryContext telemetry)
- {
- Telemetry = telemetry;
- }
-
- ///
- public virtual Task CreateAsync(
- ApplicationConfiguration configuration,
- ConfiguredEndpoint endpoint,
- bool updateBeforeConnect,
- string sessionName,
- uint sessionTimeout,
- IUserIdentity identity,
- IList preferredLocales,
- CancellationToken ct = default)
- {
- return CreateAsync(
- configuration,
- endpoint,
- updateBeforeConnect,
- false,
- sessionName,
- sessionTimeout,
- identity,
- preferredLocales,
- ct);
- }
-
- ///
- public virtual async Task CreateAsync(
- ApplicationConfiguration configuration,
- ConfiguredEndpoint endpoint,
- bool updateBeforeConnect,
- bool checkDomain,
- string sessionName,
- uint sessionTimeout,
- IUserIdentity identity,
- IList preferredLocales,
- CancellationToken ct = default)
- {
- return await Session
- .CreateAsync(
- this,
- configuration,
- (ITransportWaitingConnection)null,
- endpoint,
- updateBeforeConnect,
- checkDomain,
- sessionName,
- sessionTimeout,
- identity,
- preferredLocales,
- ReturnDiagnostics,
- ct)
- .ConfigureAwait(false);
- }
-
- ///
- public virtual async Task CreateAsync(
- ApplicationConfiguration configuration,
- ITransportWaitingConnection connection,
- ConfiguredEndpoint endpoint,
- bool updateBeforeConnect,
- bool checkDomain,
- string sessionName,
- uint sessionTimeout,
- IUserIdentity identity,
- IList preferredLocales,
- CancellationToken ct = default)
- {
- return await Session
- .CreateAsync(
- this,
- configuration,
- connection,
- endpoint,
- updateBeforeConnect,
- checkDomain,
- sessionName,
- sessionTimeout,
- identity,
- preferredLocales,
- ReturnDiagnostics,
- ct)
- .ConfigureAwait(false);
- }
-
- ///
- public virtual async Task CreateAsync(
- ApplicationConfiguration configuration,
- ReverseConnectManager reverseConnectManager,
- ConfiguredEndpoint endpoint,
- bool updateBeforeConnect,
- bool checkDomain,
- string sessionName,
- uint sessionTimeout,
- IUserIdentity userIdentity,
- IList preferredLocales,
- CancellationToken ct = default)
- {
- if (reverseConnectManager == null)
- {
- return await CreateAsync(
- configuration,
- endpoint,
- updateBeforeConnect,
- checkDomain,
- sessionName,
- sessionTimeout,
- userIdentity,
- preferredLocales,
- ct)
- .ConfigureAwait(false);
- }
-
- ITransportWaitingConnection connection;
- do
- {
- connection = await reverseConnectManager
- .WaitForConnectionAsync(
- endpoint.EndpointUrl,
- endpoint.ReverseConnect?.ServerUri,
- ct)
- .ConfigureAwait(false);
-
- if (updateBeforeConnect)
- {
- await endpoint
- .UpdateFromServerAsync(
- endpoint.EndpointUrl,
- connection,
- endpoint.Description.SecurityMode,
- endpoint.Description.SecurityPolicyUri,
- Telemetry,
- ct)
- .ConfigureAwait(false);
- updateBeforeConnect = false;
- connection = null;
- }
- } while (connection == null);
-
- return await CreateAsync(
- configuration,
- connection,
- endpoint,
- false,
- checkDomain,
- sessionName,
- sessionTimeout,
- userIdentity,
- preferredLocales,
- ct)
- .ConfigureAwait(false);
- }
-
- ///
- public virtual ISession Create(
- ApplicationConfiguration configuration,
- ITransportChannel channel,
- ConfiguredEndpoint endpoint,
- X509Certificate2 clientCertificate,
- EndpointDescriptionCollection availableEndpoints = null,
- StringCollection discoveryProfileUris = null)
- {
- return Session.Create(
- this,
- configuration,
- channel,
- endpoint,
- clientCertificate,
- availableEndpoints,
- discoveryProfileUris);
- }
-
- ///
- public virtual Task CreateChannelAsync(
- ApplicationConfiguration configuration,
- ITransportWaitingConnection connection,
- ConfiguredEndpoint endpoint,
- bool updateBeforeConnect,
- bool checkDomain,
- CancellationToken ct = default)
- {
- return Session.CreateChannelAsync(
- configuration,
- connection,
- endpoint,
- updateBeforeConnect,
- checkDomain,
- ct);
- }
-
- ///
- public virtual async Task RecreateAsync(
- ISession sessionTemplate,
- CancellationToken ct = default)
- {
- if (sessionTemplate is not Session template)
- {
- throw new ArgumentOutOfRangeException(
- nameof(sessionTemplate),
- "The ISession provided is not of a supported type.");
- }
-
- template.ReturnDiagnostics = ReturnDiagnostics;
- return await Session.RecreateAsync(template, ct).ConfigureAwait(false);
- }
-
- ///
- public virtual async Task RecreateAsync(
- ISession sessionTemplate,
- ITransportWaitingConnection connection,
- CancellationToken ct = default)
- {
- if (sessionTemplate is not Session template)
- {
- throw new ArgumentOutOfRangeException(
- nameof(sessionTemplate),
- "The ISession provided is not of a supported type");
- }
-
- template.ReturnDiagnostics = ReturnDiagnostics;
- return await Session.RecreateAsync(template, connection, ct).ConfigureAwait(false);
- }
-
- ///
- public virtual async Task RecreateAsync(
- ISession sessionTemplate,
- ITransportChannel transportChannel,
- CancellationToken ct = default)
- {
- if (sessionTemplate is not Session template)
- {
- throw new ArgumentOutOfRangeException(
- nameof(sessionTemplate),
- "The ISession provided is not of a supported type");
- }
- template.ReturnDiagnostics = ReturnDiagnostics;
- return await Session.RecreateAsync(template, transportChannel, ct)
- .ConfigureAwait(false);
- }
-
- ///
- public virtual Session Create(
- ISessionChannel channel,
- ApplicationConfiguration configuration,
- ConfiguredEndpoint endpoint)
- {
- return new Session(channel, configuration, endpoint)
- {
- ReturnDiagnostics = ReturnDiagnostics
- };
- }
-
- ///
- public virtual Session Create(
- ITransportChannel channel,
- ApplicationConfiguration configuration,
- ConfiguredEndpoint endpoint,
- X509Certificate2 clientCertificate,
- EndpointDescriptionCollection availableEndpoints = null,
- StringCollection discoveryProfileUris = null)
- {
- return new Session(
- channel,
- configuration,
- endpoint,
- clientCertificate,
- availableEndpoints,
- discoveryProfileUris)
- {
- ReturnDiagnostics = ReturnDiagnostics
- };
- }
- }
-}
diff --git a/Libraries/Opc.Ua.Client/Session/Factory/TraceableSessionFactory.cs b/Libraries/Opc.Ua.Client/Session/Factory/TraceableSessionFactory.cs
deleted file mode 100644
index 506feafe8c..0000000000
--- a/Libraries/Opc.Ua.Client/Session/Factory/TraceableSessionFactory.cs
+++ /dev/null
@@ -1,285 +0,0 @@
-/* ========================================================================
- * Copyright (c) 2005-2023 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.Security.Cryptography.X509Certificates;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Opc.Ua.Client
-{
- ///
- /// Object that creates instances of an Opc.Ua.Client.Session object with Activity Source.
- ///
- public class TraceableSessionFactory : DefaultSessionFactory
- {
- ///
- /// The default instance of the factory.
- ///
- [Obsolete("Use new TraceableSessionFactory instead.")]
- public static new readonly TraceableSessionFactory Instance = new(null);
-
- ///
- /// Obsolete default constructor
- ///
- [Obsolete("Use TraceableSessionFactory(ITelemetryContext) instead.")]
- public TraceableSessionFactory()
- : this(null)
- {
- }
-
- ///
- /// Force use of the default instance.
- ///
- public TraceableSessionFactory(ITelemetryContext telemetry)
- : base(telemetry)
- {
- // Set the default Id format to W3C
- // (older .Net versions use ActivityIfFormat.HierarchicalId)
- Activity.DefaultIdFormat = ActivityIdFormat.W3C;
- Activity.ForceDefaultIdFormat = true;
- }
-
- ///
- public override async Task CreateAsync(
- ApplicationConfiguration configuration,
- ConfiguredEndpoint endpoint,
- bool updateBeforeConnect,
- string sessionName,
- uint sessionTimeout,
- IUserIdentity identity,
- IList preferredLocales,
- CancellationToken ct = default)
- {
- using Activity activity = Telemetry.GetActivitySource().StartActivity();
- ISession session = await base.CreateAsync(
- configuration,
- endpoint,
- updateBeforeConnect,
- false,
- sessionName,
- sessionTimeout,
- identity,
- preferredLocales,
- ct)
- .ConfigureAwait(false);
- return new TraceableSession(session, Telemetry);
- }
-
- ///
- public override async Task CreateAsync(
- ApplicationConfiguration configuration,
- ConfiguredEndpoint endpoint,
- bool updateBeforeConnect,
- bool checkDomain,
- string sessionName,
- uint sessionTimeout,
- IUserIdentity identity,
- IList preferredLocales,
- CancellationToken ct = default)
- {
- using Activity activity = Telemetry.GetActivitySource().StartActivity();
- ISession session = await Session
- .CreateAsync(
- this,
- configuration,
- (ITransportWaitingConnection)null,
- endpoint,
- updateBeforeConnect,
- checkDomain,
- sessionName,
- sessionTimeout,
- identity,
- preferredLocales,
- ReturnDiagnostics,
- ct)
- .ConfigureAwait(false);
-
- return new TraceableSession(session, Telemetry);
- }
-
- ///
- public override async Task CreateAsync(
- ApplicationConfiguration configuration,
- ITransportWaitingConnection connection,
- ConfiguredEndpoint endpoint,
- bool updateBeforeConnect,
- bool checkDomain,
- string sessionName,
- uint sessionTimeout,
- IUserIdentity identity,
- IList preferredLocales,
- CancellationToken ct = default)
- {
- using Activity activity = Telemetry.GetActivitySource().StartActivity();
- ISession session = await Session
- .CreateAsync(
- this,
- configuration,
- connection,
- endpoint,
- updateBeforeConnect,
- checkDomain,
- sessionName,
- sessionTimeout,
- identity,
- preferredLocales,
- ReturnDiagnostics,
- ct)
- .ConfigureAwait(false);
-
- return new TraceableSession(session, Telemetry);
- }
-
- ///
- public override ISession Create(
- ApplicationConfiguration configuration,
- ITransportChannel channel,
- ConfiguredEndpoint endpoint,
- X509Certificate2 clientCertificate,
- EndpointDescriptionCollection availableEndpoints = null,
- StringCollection discoveryProfileUris = null)
- {
- using Activity activity = Telemetry.GetActivitySource().StartActivity();
- return new TraceableSession(
- base.Create(
- configuration,
- channel,
- endpoint,
- clientCertificate,
- availableEndpoints,
- discoveryProfileUris), Telemetry);
- }
-
- ///
- public override async Task CreateChannelAsync(
- ApplicationConfiguration configuration,
- ITransportWaitingConnection connection,
- ConfiguredEndpoint endpoint,
- bool updateBeforeConnect,
- bool checkDomain,
- CancellationToken ct = default)
- {
- using Activity activity = Telemetry.GetActivitySource().StartActivity();
- return await base.CreateChannelAsync(
- configuration,
- connection,
- endpoint,
- updateBeforeConnect,
- checkDomain,
- ct)
- .ConfigureAwait(false);
- }
-
- ///
- public override async Task CreateAsync(
- ApplicationConfiguration configuration,
- ReverseConnectManager reverseConnectManager,
- ConfiguredEndpoint endpoint,
- bool updateBeforeConnect,
- bool checkDomain,
- string sessionName,
- uint sessionTimeout,
- IUserIdentity userIdentity,
- IList preferredLocales,
- CancellationToken ct = default)
- {
- using Activity activity = Telemetry.GetActivitySource().StartActivity();
- ISession session = await base.CreateAsync(
- configuration,
- reverseConnectManager,
- endpoint,
- updateBeforeConnect,
- checkDomain,
- sessionName,
- sessionTimeout,
- userIdentity,
- preferredLocales,
- ct)
- .ConfigureAwait(false);
-
- return new TraceableSession(session, Telemetry);
- }
-
- ///
- public override async Task RecreateAsync(
- ISession sessionTemplate,
- CancellationToken ct = default)
- {
- Session session = ValidateISession(sessionTemplate);
- using Activity activity = Telemetry.GetActivitySource().StartActivity();
- return new TraceableSession(
- await Session.RecreateAsync(session, ct).ConfigureAwait(false), Telemetry);
- }
-
- ///
- public override async Task RecreateAsync(
- ISession sessionTemplate,
- ITransportWaitingConnection connection,
- CancellationToken ct = default)
- {
- Session session = ValidateISession(sessionTemplate);
- using Activity activity = Telemetry.GetActivitySource().StartActivity();
- return new TraceableSession(
- await Session.RecreateAsync(session, connection, ct).ConfigureAwait(false), Telemetry);
- }
-
- ///
- public override async Task RecreateAsync(
- ISession sessionTemplate,
- ITransportChannel channel,
- CancellationToken ct = default)
- {
- Session session = ValidateISession(sessionTemplate);
- using Activity activity = Telemetry.GetActivitySource().StartActivity();
- return new TraceableSession(
- await Session.RecreateAsync(session, channel, ct).ConfigureAwait(false), Telemetry);
- }
-
- private static Session ValidateISession(ISession sessionTemplate)
- {
- if (sessionTemplate is not Session session)
- {
- if (sessionTemplate is TraceableSession template)
- {
- session = (Session)template.Session;
- }
- else
- {
- throw new ArgumentOutOfRangeException(
- nameof(sessionTemplate),
- "The ISession provided is not of a supported type.");
- }
- }
- return session;
- }
- }
-}
diff --git a/Libraries/Opc.Ua.Client/Session/ISession.cs b/Libraries/Opc.Ua.Client/Session/ISession.cs
index 68f3dbf77c..fe1a590152 100644
--- a/Libraries/Opc.Ua.Client/Session/ISession.cs
+++ b/Libraries/Opc.Ua.Client/Session/ISession.cs
@@ -153,7 +153,7 @@ public interface ISession : ISessionClient
///
/// Gets the local handle assigned to the session.
///
- object Handle { get; }
+ object? Handle { get; }
///
/// Gets the user identity currently used for the session.
@@ -221,7 +221,8 @@ public interface ISession : ISessionClient
bool DeleteSubscriptionsOnClose { get; set; }
///
- /// Gets or sets the time in milliseconds to wait for outstanding publish requests to complete before canceling them during session close.
+ /// Gets or sets the time in milliseconds to wait for outstanding publish
+ /// requests to complete before canceling them during session close.
///
///
/// A value of 0 means no waiting - outstanding requests are canceled immediately.
@@ -304,6 +305,11 @@ public interface ISession : ISessionClient
///
OperationLimits OperationLimits { get; }
+ ///
+ /// Stores the capabilities of a OPC UA server.
+ ///
+ ServerCapabilities ServerCapabilities { get; }
+
///
/// If the subscriptions are transferred when a session is reconnected.
///
@@ -330,47 +336,31 @@ public interface ISession : ISessionClient
event RenewUserIdentityEventHandler RenewUserIdentity;
///
- /// Reconnects to the server after a network failure.
- ///
- Task ReconnectAsync(CancellationToken ct = default);
-
- ///
- /// Reconnects to the server after a network failure using a waiting connection.
+ /// Reconnects to the server after a network failure using
+ /// a waiting connection or channel which either is provided.
+ /// If none is provided creates a new channel.
///
- Task ReconnectAsync(ITransportWaitingConnection connection, CancellationToken ct = default);
-
- ///
- /// Reconnects to the server using a new channel.
- ///
- Task ReconnectAsync(ITransportChannel channel, CancellationToken ct = default);
+ ///
+ Task ReconnectAsync(
+ ITransportWaitingConnection? connection,
+ ITransportChannel? channel,
+ CancellationToken ct = default);
///
///Reload the own certificate used by the session and the issuer chain when available.
///
Task ReloadInstanceCertificateAsync(CancellationToken ct = default);
- ///
- /// Saves all the subscriptions of the session.
- ///
- /// The file path.
- /// Known types
- void Save(string filePath, IEnumerable knownTypes = null);
-
///
/// Saves a set of subscriptions to a stream.
///
+ ///
+ ///
+ ///
void Save(
Stream stream,
IEnumerable subscriptions,
- IEnumerable knownTypes = null);
-
- ///
- /// Saves a set of subscriptions to a file.
- ///
- void Save(
- string filePath,
- IEnumerable subscriptions,
- IEnumerable knownTypes = null);
+ IEnumerable? knownTypes = null);
///
/// Load the list of subscriptions saved in a stream.
@@ -384,26 +374,12 @@ void Save(
IEnumerable Load(
Stream stream,
bool transferSubscriptions = false,
- IEnumerable knownTypes = null);
-
- ///
- /// Load the list of subscriptions saved in a file.
- ///
- /// The file path.
- /// Load the subscriptions for transfer
- /// after load.
- /// Additional known types that may be needed to
- /// read the saved subscriptions.
- /// The list of loaded subscriptions
- IEnumerable Load(
- string filePath,
- bool transferSubscriptions = false,
- IEnumerable knownTypes = null);
+ IEnumerable? knownTypes = null);
///
/// Returns the active session configuration and writes it to a stream.
///
- SessionConfiguration SaveSessionConfiguration(Stream stream = null);
+ SessionConfiguration SaveSessionConfiguration(Stream? stream = null);
///
/// Applies a session configuration.
@@ -433,73 +409,6 @@ IEnumerable Load(
///
Task FetchTypeTreeAsync(ExpandedNodeIdCollection typeIds, CancellationToken ct = default);
- ///
- /// Reads a byte string which is too large for the (server side) encoder to handle.
- ///
- /// The node id of a byte string variable
- /// Cancelation token to cancel operation with
- Task ReadByteStringInChunksAsync(NodeId nodeId, CancellationToken ct = default);
-
- ///
- /// Fetches all references for the specified node.
- ///
- /// The node id.
- /// Cancelation token to cancel operation with
- Task FetchReferencesAsync(
- NodeId nodeId,
- CancellationToken ct = default);
-
- ///
- /// Fetches all references for the specified nodes.
- ///
- /// The node id collection.
- /// Cancelation token to cancel operation with
- /// A list of reference collections and the errors reported by the server.
- Task<(IList, IList)> FetchReferencesAsync(
- IList nodeIds,
- CancellationToken ct = default);
-
- ///
- /// Establishes a session with the server.
- ///
- /// The name to assign to the session.
- /// The user identity.
- /// The cancellation token.
- Task OpenAsync(string sessionName, IUserIdentity identity, CancellationToken ct = default);
-
- ///
- /// Establishes a session with the server.
- ///
- /// The name to assign to the session.
- /// The session timeout.
- /// The user identity.
- /// The list of preferred locales.
- /// The cancellation token.
- Task OpenAsync(
- string sessionName,
- uint sessionTimeout,
- IUserIdentity identity,
- IList preferredLocales,
- CancellationToken ct = default);
-
- ///
- /// Establishes a session with the server.
- ///
- /// The name to assign to the session.
- /// The session timeout.
- /// The user identity.
- /// The list of preferred locales.
- /// If set to true then the
- /// domain in the certificate must match the endpoint used.
- /// The cancellation token.
- Task OpenAsync(
- string sessionName,
- uint sessionTimeout,
- IUserIdentity identity,
- IList preferredLocales,
- bool checkDomain,
- CancellationToken ct = default);
-
///
/// Establishes a session with the server.
///
@@ -516,7 +425,7 @@ Task OpenAsync(
string sessionName,
uint sessionTimeout,
IUserIdentity identity,
- IList preferredLocales,
+ IList? preferredLocales,
bool checkDomain,
bool closeChannel,
CancellationToken ct = default);
@@ -542,134 +451,14 @@ Task ChangePreferredLocalesAsync(
CancellationToken ct = default);
///