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); /// - /// 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. - /// - /// 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 not be omitted. - /// The cancellation token. - /// The node collection and associated errors. - Task<(IList, IList)> ReadNodesAsync( - IList nodeIds, - NodeClass nodeClass, - bool optionalAttributes = false, - CancellationToken ct = default); - - /// - /// Read display name for a set of nodes - /// - /// node for which to read display name - /// Cancellation token to use to cancel the operation - /// Paired list of displaynames and potential errors per node - Task<(IList, IList)> ReadDisplayNameAsync( - IList nodeIds, - CancellationToken ct = default); - - /// - /// Finds the NodeIds for the components for an instance. - /// - Task<(NodeIdCollection, IList)> FindComponentIdsAsync( - NodeId instanceId, - IList componentPaths, - CancellationToken ct = default); - - /// - /// Returns the available encodings for a node - /// - /// The variable node. - /// Cancellation token to use to cancel the operation - Task ReadAvailableEncodingsAsync( - NodeId variableId, - CancellationToken ct = default); - - /// - /// Returns the data description for the encoding. - /// - /// The encoding Id. - /// Cancellation token to use to cancel the operation - Task FindDataDescriptionAsync(NodeId encodingId, - CancellationToken ct = default); - - /// - /// Reads the value for a node. - /// - /// The node Id. - /// The cancellation token for the request. - Task ReadValueAsync(NodeId nodeId, CancellationToken ct = default); - - /// - /// Reads the value for a node of type T or throws if not matching the type. - /// - /// - /// The node Id. - /// The cancellation token for the request. - Task ReadValueAsync(NodeId nodeId, CancellationToken ct = default); - - /// - /// Reads the values for the node attributes and returns a node object. - /// - /// The nodeId. - /// The cancellation token for the request. - Task ReadNodeAsync(NodeId nodeId, 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. - /// - /// The nodeId. - /// The nodeclass of the node to read. - /// Read optional attributes. - /// The cancellation token for the request. - Task ReadNodeAsync( - NodeId nodeId, - NodeClass nodeClass, - bool optionalAttributes = true, - CancellationToken ct = default); - - /// - /// Reads the values for the node attributes and returns a node object collection. - /// Reads the nodeclass of the nodeIds, then reads - /// the values for the node attributes and returns a node collection. - /// - /// The nodeId collection. - /// If optional attributes to read. - /// The cancellation token. - Task<(IList, IList)> ReadNodesAsync( - IList nodeIds, - bool optionalAttributes = false, - CancellationToken ct = default); - - /// - /// Reads the values for a node collection. Returns diagnostic errors. + /// Disconnects from the server and frees any network resources + /// with the specified timeout. /// - /// The node Id. - /// The cancellation token for the request. - Task<(DataValueCollection, IList)> ReadValuesAsync( - IList nodeIds, + Task CloseAsync( + int timeout, + bool closeChannel, CancellationToken ct = default); - /// - /// Close the session with the server and optionally closes the channel. - /// - Task CloseAsync(bool closeChannel, CancellationToken ct = default); - - /// - /// Disconnects from the server and frees any network resources with the specified timeout. - /// - Task CloseAsync(int timeout, CancellationToken ct = default); - - /// - /// Disconnects from the server and frees any network resources with the specified timeout. - /// - Task CloseAsync(int timeout, bool closeChannel, CancellationToken ct = default); - /// /// Adds a subscription to the session. /// @@ -724,35 +513,6 @@ Task TransferSubscriptionsAsync( bool sendInitialValues, CancellationToken ct = default); - /// - /// Execute BrowseAsync and, if necessary, BrowseNextAsync, in one service call. - /// Takes care of BadNoContinuationPoint and BadInvalidContinuationPoint status codes. - /// - Task<(IList, IList)> ManagedBrowseAsync( - RequestHeader requestHeader, - ViewDescription view, - IList nodesToBrowse, - uint maxResultsToReturn, - BrowseDirection browseDirection, - NodeId referenceTypeId, - bool includeSubtypes, - uint nodeClassMask, - CancellationToken ct = default); - - /// - /// Calls the specified method and returns the output arguments. - /// - /// The NodeId of the object that provides the method. - /// The NodeId of the method to call. - /// The cancellation token for the request. - /// The input arguments. - /// The list of output argument values. - Task> CallAsync( - NodeId objectId, - NodeId methodId, - CancellationToken ct = default, - params object[] args); - /// /// Sends an additional publish request. /// @@ -770,86 +530,5 @@ Task> CallAsync( uint subscriptionId, uint sequenceNumber, CancellationToken ct = default); - - /// - /// Call the ResendData method on the server for all subscriptions. - /// - Task<(bool, IList)> ResendDataAsync( - IEnumerable subscriptions, - CancellationToken ct = default); - - /// - /// Browses the nodes in the server. - /// - /// Request header - /// View to use - /// nodes to browse - /// max results to return - /// Direction of browse - /// Reference type to follow - /// Include subtypes - /// Node classes to match - /// Cancellation token to cancel the operation - /// - Task<( - ResponseHeader responseHeader, - ByteStringCollection continuationPoints, - IList referencesList, - IList errors - )> BrowseAsync( - RequestHeader requestHeader, - ViewDescription view, - IList nodesToBrowse, - uint maxResultsToReturn, - BrowseDirection browseDirection, - NodeId referenceTypeId, - bool includeSubtypes, - uint nodeClassMask, - CancellationToken ct = default); - - /// - /// Browse next - /// - /// - /// - /// - /// - /// - Task<( - ResponseHeader responseHeader, - ByteStringCollection revisedContinuationPoints, - IList referencesList, - IList errors - )> BrowseNextAsync( - RequestHeader requestHeader, - ByteStringCollection continuationPoints, - bool releaseContinuationPoint, - CancellationToken ct = default); - } - - /// - /// controls how the client treats continuation points - /// if the server has restrictions on their number - /// As of now only used for browse/browse next in the - /// ManagedBrowse method. - /// - 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/Session/ISessionFactory.cs b/Libraries/Opc.Ua.Client/Session/ISessionFactory.cs index bd49360832..ee5843370a 100644 --- a/Libraries/Opc.Ua.Client/Session/ISessionFactory.cs +++ b/Libraries/Opc.Ua.Client/Session/ISessionFactory.cs @@ -44,12 +44,39 @@ public interface ISessionFactory /// DiagnosticsMasks ReturnDiagnostics { get; set; } + /// + /// Telemetry configuration to use when creating sessions. + /// + ITelemetryContext Telemetry { get; } + + /// + /// Creates a new unconnected session with the channel to the server. + /// + /// The channel for the server. + /// The configuration for the client application. + /// The endpoint for the server. + /// The certificate to use for the client. + /// The certificate chain for the client cert. + /// The list of available endpoints returned by + /// server in GetEndpoints() response. + /// The value of profileUris used in + /// GetEndpoints() request. + ISession Create( + ITransportChannel channel, + ApplicationConfiguration configuration, + ConfiguredEndpoint endpoint, + X509Certificate2? clientCertificate = null, + X509Certificate2Collection? clientCertificateChain = null, + EndpointDescriptionCollection? availableEndpoints = null, + StringCollection? discoveryProfileUris = null); + /// /// Creates a new communication session with a server by invoking the CreateSession service /// /// The configuration for the client application. /// 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 the discovery endpoint is used + /// to update the endpoint description before connecting. /// The name to assign to the session. /// The timeout period for the session. /// The identity. @@ -62,17 +89,20 @@ Task CreateAsync( bool updateBeforeConnect, string sessionName, uint sessionTimeout, - IUserIdentity identity, - IList preferredLocales, + IUserIdentity? identity, + IList? preferredLocales, CancellationToken ct = default); /// - /// Creates a new communication session with a server by invoking the CreateSession service + /// Creates a new communication session with a server by invoking the CreateSession + /// service /// /// The configuration for the client application. /// 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. + /// 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. @@ -86,35 +116,20 @@ Task CreateAsync( bool checkDomain, string sessionName, uint sessionTimeout, - IUserIdentity identity, - IList preferredLocales, + IUserIdentity? identity, + IList? preferredLocales, CancellationToken ct = default); - /// - /// Creates a new session with a server using the specified channel by invoking the CreateSession service. - /// - /// The configuration for the client application. - /// The channel for the server. - /// The endpoint for the server. - /// The certificate to use for the client. - /// The list of available endpoints returned by server in GetEndpoints() response. - /// The value of profileUris used in GetEndpoints() request. - ISession Create( - ApplicationConfiguration configuration, - ITransportChannel channel, - ConfiguredEndpoint endpoint, - X509Certificate2 clientCertificate, - EndpointDescriptionCollection availableEndpoints = null, - StringCollection discoveryProfileUris = null); - /// /// Creates a secure channel to the specified endpoint. /// /// The application configuration. /// The client endpoint for the reverse connect. /// A configured endpoint to connect to. - /// Update configuration based on server prior connect. - /// Check that the certificate specifies a valid domain (computer) name. + /// Update configuration based on server + /// prior connect. + /// Check that the certificate specifies a valid + /// domain (computer) name. /// The cancellation token. /// A representing the asynchronous operation. Task CreateChannelAsync( @@ -131,8 +146,10 @@ Task CreateChannelAsync( /// 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. + /// 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. @@ -147,18 +164,21 @@ Task CreateAsync( bool checkDomain, string sessionName, uint sessionTimeout, - IUserIdentity identity, - IList preferredLocales, + IUserIdentity? identity, + IList? preferredLocales, CancellationToken ct = default); /// /// Creates a new communication session with a server using a reverse connect manager. /// /// The configuration for the client application. - /// The reverse connect manager for the client connection. + /// The reverse connect manager for the client + /// connection. /// 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. + /// 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. @@ -173,8 +193,8 @@ Task CreateAsync( bool checkDomain, string sessionName, uint sessionTimeout, - IUserIdentity userIdentity, - IList preferredLocales, + IUserIdentity? userIdentity, + IList? preferredLocales, CancellationToken ct = default); /// @@ -183,7 +203,9 @@ Task CreateAsync( /// The ISession object to use as template /// The cancellation token. /// The new session object. - Task RecreateAsync(ISession sessionTemplate, CancellationToken ct = default); + Task RecreateAsync( + ISession sessionTemplate, + CancellationToken ct = default); /// /// Recreates a session based on a specified template. diff --git a/Libraries/Opc.Ua.Client/Session/ServerCapabilities.cs b/Libraries/Opc.Ua.Client/Session/ServerCapabilities.cs new file mode 100644 index 0000000000..791170fef6 --- /dev/null +++ b/Libraries/Opc.Ua.Client/Session/ServerCapabilities.cs @@ -0,0 +1,131 @@ +/* ======================================================================== + * 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; + +namespace Opc.Ua.Client +{ + /// + /// In addition to operation limits this provides additional + /// information about server capabilities to a session user. + /// + [DataContract(Namespace = Namespaces.OpcUaConfig)] + public sealed class ServerCapabilities + { + /// + /// Max browse continuation points + /// + [DataMember(Order = 200)] + public ushort MaxBrowseContinuationPoints { get; set; } + + /// + /// Max query continuation points + /// + [DataMember(Order = 210)] + public ushort MaxQueryContinuationPoints { get; set; } + + /// + /// Max history continuation points + /// + [DataMember(Order = 220)] + public ushort MaxHistoryContinuationPoints { get; set; } + + /// + /// Min supported sampling rate + /// + [DataMember(Order = 240)] + public double MinSupportedSampleRate { get; set; } + + /// + /// Max array length supported + /// + [DataMember(Order = 250)] + public uint MaxArrayLength { get; set; } + + /// + /// Max string length supported + /// + [DataMember(Order = 260)] + public uint MaxStringLength { get; set; } + + /// + /// Max byte buffer length supported + /// + [DataMember(Order = 270)] + public uint MaxByteStringLength { get; set; } + + /// + /// Max sessions the server can handle + /// + [DataMember(Order = 300)] + public uint MaxSessions { get; set; } + + /// + /// Max subscriptions the server can handle + /// + [DataMember(Order = 310)] + public uint MaxSubscriptions { get; set; } + + /// + /// Max monitored items the server can handle + /// + [DataMember(Order = 320)] + public uint MaxMonitoredItems { get; set; } + + /// + /// Max subscriptions per session + /// + [DataMember(Order = 330)] + public uint MaxSubscriptionsPerSession { get; set; } + + /// + /// Max monitored items per subscription + /// + [DataMember(Order = 340)] + public uint MaxMonitoredItemsPerSubscription { get; set; } + + /// + /// Max select clause parameters + /// + [DataMember(Order = 350)] + public uint MaxSelectClauseParameters { get; set; } + + /// + /// Max where clause parameters + /// + [DataMember(Order = 360)] + public uint MaxWhereClauseParameters { get; set; } + + /// + /// Max monitored items queue size + /// + [DataMember(Order = 370)] + public uint MaxMonitoredItemsQueueSize { get; set; } + } +} diff --git a/Libraries/Opc.Ua.Client/Session/Session.cs b/Libraries/Opc.Ua.Client/Session/Session.cs index dc5677d441..b6e1732a21 100644 --- a/Libraries/Opc.Ua.Client/Session/Session.cs +++ b/Libraries/Opc.Ua.Client/Session/Session.cs @@ -33,7 +33,6 @@ using System.Globalization; using System.IO; using System.Linq; -using System.Reflection; using System.Runtime.Serialization; using System.Security.Cryptography.X509Certificates; using System.Threading; @@ -47,7 +46,8 @@ namespace Opc.Ua.Client /// /// Manages a session with a server. /// - public class Session : SessionClientBatched, ISession + public partial class Session : SessionClientBatched, ISession, + ISnapshotRestore, ISnapshotRestore { private const int kReconnectTimeout = 15000; private const int kMinPublishRequestCountMax = 100; @@ -62,15 +62,17 @@ public class Session : SessionClientBatched, ISession /// The channel used to communicate with the server. /// The configuration for the client application. /// The endpoint use to initialize the channel. + [Obsolete("Use constructor with ITransportChannel instead of ISessionChannel.")] public Session( ISessionChannel channel, ApplicationConfiguration configuration, ConfiguredEndpoint endpoint) : this( - channel as ITransportChannel, + channel is ITransportChannel transportChannel ? + transportChannel : + throw new ArgumentException("not a transport channel"), configuration, - endpoint, - clientCertificate: null) + endpoint) { } @@ -81,30 +83,37 @@ public Session( /// The configuration for the client application. /// The endpoint used to initialize the channel. /// The certificate to use for the client. - /// The list of available endpoints returned by server in GetEndpoints() response. - /// The value of profileUris used in GetEndpoints() request. + /// The certificate chain of the client + /// certificate. + /// The list of available endpoints returned + /// by server in GetEndpoints() response. + /// The value of profileUris used in + /// GetEndpoints() request. /// - /// The application configuration is used to look up the certificate if none is provided. - /// The clientCertificate must have the private key. This will require that the certificate - /// be loaded from a certicate store. Converting a DER encoded blob to a X509Certificate2 - /// will not include a private key. - /// The availableEndpoints and discoveryProfileUris parameters are used to validate - /// that the list of EndpointDescriptions returned at GetEndpoints matches the list returned at CreateSession. + /// The application configuration is used to look up the certificate if none + /// is provided. The clientCertificate must have the private key. This will + /// require that the certificate be loaded from a certicate store. Converting + /// a DER encoded blob to a X509Certificate2 will not include a private key. + /// The availableEndpoints and discoveryProfileUris parameters are + /// used to validate that the list of EndpointDescriptions returned at GetEndpoints + /// matches the list returned at CreateSession. /// public Session( ITransportChannel channel, ApplicationConfiguration configuration, ConfiguredEndpoint endpoint, - X509Certificate2 clientCertificate, - EndpointDescriptionCollection availableEndpoints = null, - StringCollection discoveryProfileUris = null) + X509Certificate2? clientCertificate = null, + X509Certificate2Collection? clientCertificateChain = null, + EndpointDescriptionCollection? availableEndpoints = null, + StringCollection? discoveryProfileUris = null) : this( channel, configuration, endpoint, channel.MessageContext ?? configuration.CreateMessageContext(true)) { - LoadInstanceCertificateAsync(clientCertificate).GetAwaiter().GetResult(); + m_instanceCertificate = clientCertificate; + m_instanceCertificateChain = clientCertificateChain; m_discoveryServerEndpoints = availableEndpoints; m_discoveryProfileUris = discoveryProfileUris; } @@ -122,7 +131,9 @@ public Session(ITransportChannel channel, Session template, bool copyEventHandle template.ConfiguredEndpoint, channel.MessageContext ?? template.m_configuration.CreateMessageContext(true)) { - LoadInstanceCertificateAsync(template.m_instanceCertificate).GetAwaiter().GetResult(); + m_instanceCertificate = template.m_instanceCertificate; + m_instanceCertificateChain = template.m_instanceCertificateChain; + m_effectiveEndpoint = template.m_effectiveEndpoint; SessionFactory = template.SessionFactory; m_defaultSubscription = template.m_defaultSubscription; DeleteSubscriptionsOnClose = template.DeleteSubscriptionsOnClose; @@ -186,13 +197,27 @@ private Session( m_telemetry = messageContext.Telemetry; m_logger = m_telemetry.CreateLogger(); - Initialize(); + SessionFactory ??= new DefaultSessionFactory(m_telemetry) + { + ReturnDiagnostics = ReturnDiagnostics + }; + + NamespaceUris = new NamespaceTable(); + ServerUris = new StringTable(); + Factory = EncodeableFactory.Create(); + m_keepAliveInterval = 5000; + m_minPublishRequestCount = kDefaultPublishRequestCount; + m_maxPublishRequestCount = kMaxPublishRequestCountMax; + m_sessionName = string.Empty; + DeleteSubscriptionsOnClose = true; + PublishRequestCancelDelayOnCloseSession = 5000; // 5 seconds default ValidateClientConfiguration(configuration); // save configuration information. m_configuration = configuration; - m_endpoint = endpoint; + m_effectiveEndpoint = m_endpoint = endpoint; + m_identity = new UserIdentity(); // update the default subscription. DefaultSubscription.MinLifetimeInterval = (uint)m_configuration.ClientConfiguration @@ -203,7 +228,7 @@ private Session( Factory = messageContext.Factory; // initialize the NodeCache late, it needs references to the namespaceUris - m_nodeCache = new NodeCache(this, m_telemetry); + m_nodeCache = new NodeCache(new NodeCacheContext(this), m_telemetry); // Create timer for keep alive event triggering but in off state m_keepAliveTimer = new Timer(_ => m_keepAliveEvent.Set(), this, Timeout.Infinite, Timeout.Infinite); @@ -225,44 +250,6 @@ private Session( }; } - /// - /// Sets the object members to default values. - /// - private void Initialize() - { - SessionFactory ??= new DefaultSessionFactory(m_telemetry) - { - ReturnDiagnostics = ReturnDiagnostics - }; - m_sessionTimeout = 0; - NamespaceUris = new NamespaceTable(); - ServerUris = new StringTable(); - Factory = EncodeableFactory.Create(); - m_configuration = null; - m_instanceCertificate = null; - m_endpoint = null; - m_subscriptions = []; - m_acknowledgementsToSend = []; - m_acknowledgementsToSendLock = new object(); -#if DEBUG_SEQUENTIALPUBLISHING - m_latestAcknowledgementsSent = new Dictionary(); -#endif - m_identityHistory = []; - m_outstandingRequests = new LinkedList(); - m_keepAliveInterval = 5000; - m_tooManyPublishRequests = 0; - m_minPublishRequestCount = kDefaultPublishRequestCount; - m_maxPublishRequestCount = kMaxPublishRequestCountMax; - m_sessionName = string.Empty; - DeleteSubscriptionsOnClose = true; - PublishRequestCancelDelayOnCloseSession = 5000; // 5 seconds default - TransferSubscriptionsOnReconnect = false; - Reconnecting = false; - Closing = false; - m_reconnectLock = new SemaphoreSlim(1, 1); - ServerMaxContinuationPointsPerBrowse = 0; - } - /// /// Check if all required configuration fields are populated. /// @@ -303,9 +290,9 @@ private static void ValidateClientConfiguration(ApplicationConfiguration configu /// private void ValidateServerNonce( IUserIdentity identity, - byte[] serverNonce, + byte[]? serverNonce, string securityPolicyUri, - byte[] previousServerNonce, + byte[]? previousServerNonce, MessageSecurityMode channelSecurityMode = MessageSecurityMode.None) { // skip validation if server nonce is not used for encryption. @@ -376,12 +363,9 @@ protected override void Dispose(bool disposing) StopKeepAliveTimerAsync().AsTask().GetAwaiter().GetResult(); Utils.SilentDispose(m_defaultSubscription); - m_defaultSubscription = null; - Utils.SilentDispose(m_nodeCache); - m_nodeCache = null; - List subscriptions = null; + List? subscriptions = null; lock (SyncRoot) { subscriptions = [.. m_subscriptions]; @@ -524,7 +508,7 @@ public event EventHandler SessionConfigurationChanged /// /// Gets the local handle assigned to the session. /// - public object Handle { get; set; } + public object? Handle { get; set; } /// /// Gets the user identity currently used for the session. @@ -636,7 +620,7 @@ public int SubscriptionCount /// public Subscription DefaultSubscription { - get => m_defaultSubscription ??= new Subscription(m_telemetry) + get => m_defaultSubscription ??= CreateSubscription(new SubscriptionOptions { DisplayName = "Subscription", PublishingInterval = 1000, @@ -646,7 +630,7 @@ public Subscription DefaultSubscription PublishingEnabled = true, MinLifetimeInterval = (uint)m_configuration.ClientConfiguration .MinSubscriptionLifetime - }; + }); set { Utils.SilentDispose(m_defaultSubscription); @@ -745,7 +729,7 @@ public int DefunctRequestCount { int count = 0; - for (LinkedListNode ii = m_outstandingRequests.First; + for (LinkedListNode? ii = m_outstandingRequests.First; ii != null; ii = ii.Next) { @@ -771,7 +755,7 @@ public int GoodPublishRequestCount { int count = 0; - for (LinkedListNode ii = m_outstandingRequests.First; + for (LinkedListNode? ii = m_outstandingRequests.First; ii != null; ii = ii.Next) { @@ -836,769 +820,199 @@ public int MaxPublishRequestCount } } - /// - /// Read from the Server capability MaxContinuationPointsPerBrowse when the Operation Limits are fetched - /// - public uint ServerMaxContinuationPointsPerBrowse { get; set; } - - /// - /// Read from the Server capability MaxByteStringLength when the Operation Limits are fetched - /// - public uint ServerMaxByteStringLength { get; set; } + /// + public ServerCapabilities ServerCapabilities { get; } = new(); /// public ContinuationPointPolicy ContinuationPointPolicy { get; set; } = ContinuationPointPolicy.Default; - /// - /// Creates a new communication session with a server by invoking the CreateSession service - /// - /// The configuration for the client application. - /// The endpoint for the server. - /// If set to true the discovery endpoint is - /// used to update the endpoint description before connecting. - /// The name to assign to the session. - /// The timeout period for the session. - /// The identity. - /// The user identity to associate with the session. - /// The cancellation token. - /// The new session object - [Obsolete("Use ISessionFactory.CreateAsync")] - public static Task Create( - ApplicationConfiguration configuration, - ConfiguredEndpoint endpoint, - bool updateBeforeConnect, - string sessionName, - uint sessionTimeout, - IUserIdentity identity, - IList preferredLocales, - CancellationToken ct = default) + /// + public event RenewUserIdentityEventHandler RenewUserIdentity { - return Create( - configuration, - endpoint, - updateBeforeConnect, - false, - sessionName, - sessionTimeout, - identity, - preferredLocales, - ct); + add => m_RenewUserIdentity += value; + remove => m_RenewUserIdentity -= value; } - /// - /// Creates a new communication session with a server by invoking the CreateSession service - /// - /// The configuration for the client application. - /// 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 cancellation token. - /// The new session object. - [Obsolete("Use ISessionFactory.CreateAsync")] - public static Task Create( - ApplicationConfiguration configuration, - ConfiguredEndpoint endpoint, - bool updateBeforeConnect, - bool checkDomain, - string sessionName, - uint sessionTimeout, - IUserIdentity identity, - IList preferredLocales, - CancellationToken ct = default) - { - return Create( - configuration, - (ITransportWaitingConnection)null, - endpoint, - updateBeforeConnect, - checkDomain, - sessionName, - sessionTimeout, - identity, - preferredLocales, - ct); - } + private event RenewUserIdentityEventHandler? m_RenewUserIdentity; - /// - /// Creates a new session with a server using the specified channel by invoking - /// the CreateSession service - /// - /// The configuration for the client application. - /// The channel for the server. - /// The endpoint for the server. - /// The certificate to use for the client. - /// The list of available endpoints returned by server - /// in GetEndpoints() response. - /// The value of profileUris used in GetEndpoints() - /// request. - [Obsolete("Use ISessionFactory.CreateAsync")] - public static Session Create( - ApplicationConfiguration configuration, - ITransportChannel channel, - ConfiguredEndpoint endpoint, - X509Certificate2 clientCertificate, - EndpointDescriptionCollection availableEndpoints = null, - StringCollection discoveryProfileUris = null) + /// + public virtual void Snapshot(out SessionState state) { - return Create( - DefaultSessionFactory.Instance, - configuration, - channel, - endpoint, - clientCertificate, - availableEndpoints, - discoveryProfileUris); - } + using Activity? activity = m_telemetry.StartActivity(); + Snapshot(out SessionConfiguration configuration); - /// - /// Recreates a session based on a specified template. - /// - /// The Session object to use as template - /// The new session object. - [Obsolete("Use ISessionFactory.RecreateAsync")] - public static Session Recreate(Session template) - { - return RecreateAsync(template).GetAwaiter().GetResult(); + // Snapshot subscription state + var subscriptionStateCollection = new SubscriptionStateCollection(SubscriptionCount); + foreach (Subscription subscription in Subscriptions) + { + subscription.Snapshot(out SubscriptionState subscriptionState); + subscriptionStateCollection.Add(subscriptionState); + } + state = new SessionState(configuration) + { + Subscriptions = subscriptionStateCollection + }; } - /// - /// Recreates a session based on a specified template. - /// - /// The Session object to use as template - /// The waiting reverse connection. - /// The new session object. - [Obsolete("Use ISessionFactory.RecreateAsync")] - public static Session Recreate(Session template, ITransportWaitingConnection connection) + /// + public virtual void Restore(SessionState state) { - return RecreateAsync(template, connection).GetAwaiter().GetResult(); + using Activity? activity = m_telemetry.StartActivity(); + ThrowIfDisposed(); + Restore((SessionConfiguration)state); + if (state.Subscriptions == null) + { + return; + } + foreach (SubscriptionState subscriptionState in state.Subscriptions) + { + // Restore subscription from state + Subscription subscription = CreateSubscription(subscriptionState); + subscription.Restore(subscriptionState); + AddSubscription(subscription); + } } - /// - /// Recreates a session based on a specified template using the provided channel. - /// - /// The Session object to use as template - /// The waiting reverse connection. - /// The new session object. - [Obsolete("Use ISessionFactory.RecreateAsync")] - public static Session Recreate(Session template, ITransportChannel transportChannel) + /// + public void Snapshot(out SessionConfiguration sessionConfiguration) { - return RecreateAsync(template, transportChannel).GetAwaiter().GetResult(); + var serverNonce = Nonce.CreateNonce( + m_endpoint.Description?.SecurityPolicyUri, + m_serverNonce); + sessionConfiguration = new SessionConfiguration + { + SessionName = SessionName, + SessionId = SessionId, + AuthenticationToken = AuthenticationToken, + Identity = Identity, + ConfiguredEndpoint = ConfiguredEndpoint, + CheckDomain = CheckDomain, + ServerNonce = serverNonce, + ServerEccEphemeralKey = m_eccServerEphemeralKey, + UserIdentityTokenPolicy = m_userTokenSecurityPolicyUri + }; } - /// - /// 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 cancellation token. - /// The new session object. - [Obsolete("Use ISessionFactory.CreateAsync")] - public static Task Create( - ApplicationConfiguration configuration, - ITransportWaitingConnection connection, - ConfiguredEndpoint endpoint, - bool updateBeforeConnect, - bool checkDomain, - string sessionName, - uint sessionTimeout, - IUserIdentity identity, - IList preferredLocales, - CancellationToken ct = default) + /// + public void Restore(SessionConfiguration sessionConfiguration) { - return CreateAsync( - DefaultSessionFactory.Instance, - configuration, - connection, - endpoint, - updateBeforeConnect, - checkDomain, - sessionName, - sessionTimeout, - identity, - preferredLocales, - DiagnosticsMasks.None, - ct); - } + ThrowIfDisposed(); + byte[]? serverCertificate = m_endpoint.Description?.ServerCertificate; + m_sessionName = sessionConfiguration.SessionName ?? "SessionName"; + m_serverCertificate = + serverCertificate != null + ? CertificateFactory.Create(serverCertificate) + : null; + m_identity = sessionConfiguration.Identity ?? new UserIdentity(); + m_checkDomain = sessionConfiguration.CheckDomain; + m_serverNonce = sessionConfiguration.ServerNonce?.Data; + m_userTokenSecurityPolicyUri = sessionConfiguration.UserIdentityTokenPolicy; + m_eccServerEphemeralKey = sessionConfiguration.ServerEccEphemeralKey; - /// - /// Create a session - /// - [Obsolete("Use ISessionFactory.CreateAsync")] - public static Task Create( - ISessionInstantiator sessionInstantiator, - ApplicationConfiguration configuration, - ITransportWaitingConnection connection, - ConfiguredEndpoint endpoint, - bool updateBeforeConnect, - bool checkDomain, - string sessionName, - uint sessionTimeout, - IUserIdentity identity, - IList preferredLocales, - CancellationToken ct = default) - { - return CreateAsync( - sessionInstantiator, - configuration, - connection, - endpoint, - updateBeforeConnect, - checkDomain, - sessionName, - sessionTimeout, - identity, - preferredLocales, - DiagnosticsMasks.None, - ct); + SessionCreated( + sessionConfiguration.SessionId, + sessionConfiguration.AuthenticationToken); } - /// - /// Create a session - /// - [Obsolete("Use ISessionFactory.CreateAsync")] - public static Task Create( - ISessionInstantiator sessionInstantiator, - ApplicationConfiguration configuration, - ReverseConnectManager reverseConnectManager, - ConfiguredEndpoint endpoint, - bool updateBeforeConnect, - bool checkDomain, - string sessionName, - uint sessionTimeout, - IUserIdentity userIdentity, - IList preferredLocales, - CancellationToken ct = default) + /// + public bool ApplySessionConfiguration(SessionConfiguration sessionConfiguration) { - return CreateAsync( - sessionInstantiator, - configuration, - reverseConnectManager, - endpoint, - updateBeforeConnect, - checkDomain, - sessionName, - sessionTimeout, - userIdentity, - preferredLocales, - DiagnosticsMasks.None, - ct); - } + if (sessionConfiguration == null) + { + throw new ArgumentNullException(nameof(sessionConfiguration)); + } - /// - /// Create a session - /// - [Obsolete("Use ISessionFactory.CreateAsync")] - public static Task Create( - ApplicationConfiguration configuration, - ReverseConnectManager reverseConnectManager, - ConfiguredEndpoint endpoint, - bool updateBeforeConnect, - bool checkDomain, - string sessionName, - uint sessionTimeout, - IUserIdentity userIdentity, - IList preferredLocales, - CancellationToken ct = default) - { - return CreateAsync( - configuration, - reverseConnectManager, - endpoint, - updateBeforeConnect, - checkDomain, - sessionName, - sessionTimeout, - userIdentity, - preferredLocales, - ct); + Restore(sessionConfiguration); + return true; } - /// - /// Creates a new session with a server using the specified channel by invoking the - /// CreateSession service. With the sessionInstantiator subclasses of Sessions can - /// be created. - /// - /// The Session constructor to use to create the session. - /// The configuration for the client application. - /// The channel for the server. - /// The endpoint for the server. - /// The certificate to use for the client. - /// The list of available endpoints returned by - /// server in GetEndpoints() response. - /// The value of profileUris used in GetEndpoints() - /// request. - public static Session Create( - ISessionInstantiator sessionInstantiator, - ApplicationConfiguration configuration, - ITransportChannel channel, - ConfiguredEndpoint endpoint, - X509Certificate2 clientCertificate, - EndpointDescriptionCollection availableEndpoints = null, - StringCollection discoveryProfileUris = null) + /// + public SessionConfiguration SaveSessionConfiguration(Stream? stream = null) { - return sessionInstantiator.Create( - channel, - configuration, - endpoint, - clientCertificate, - availableEndpoints, - discoveryProfileUris); + Snapshot(out SessionConfiguration sessionConfiguration); + if (stream != null) + { + XmlWriterSettings settings = Utils.DefaultXmlWriterSettings(); + using var writer = XmlWriter.Create(stream, settings); + var serializer = new DataContractSerializer(typeof(SessionConfiguration)); + using IDisposable scope = AmbientMessageContext.SetScopedContext(MessageContext); + serializer.WriteObject(writer, sessionConfiguration); + } + return sessionConfiguration; } - /// - /// Creates a secure channel to the specified endpoint. - /// - /// The application configuration. - /// The client endpoint for the reverse connect. - /// A configured endpoint to connect to. - /// Update configuration based on server prior connect. - /// Check that the certificate specifies a valid domain (computer) name. - /// The cancellation token. - /// A representing the asynchronous operation. - public static async Task CreateChannelAsync( - ApplicationConfiguration configuration, - ITransportWaitingConnection connection, - ConfiguredEndpoint endpoint, - bool updateBeforeConnect, - bool checkDomain, - CancellationToken ct = default) + /// + public virtual void Save( + Stream stream, + IEnumerable subscriptions, + IEnumerable? knownTypes = null) { - endpoint.UpdateBeforeConnect = updateBeforeConnect; + using Activity? activity = m_telemetry.StartActivity(); + // Snapshot subscription state + var subscriptionStateCollection = new SubscriptionStateCollection(SubscriptionCount); + foreach (Subscription subscription in Subscriptions) + { + subscription.Snapshot(out SubscriptionState state); + subscriptionStateCollection.Add(state); + } + XmlWriterSettings settings = Utils.DefaultXmlWriterSettings(); - EndpointDescription endpointDescription = endpoint.Description; + using var writer = XmlWriter.Create(stream, settings); + var serializer = new DataContractSerializer(typeof(SubscriptionStateCollection), knownTypes); + using IDisposable scope = AmbientMessageContext.SetScopedContext(MessageContext); + serializer.WriteObject(writer, subscriptionStateCollection); + } - // create the endpoint configuration (use the application configuration to provide default values). - EndpointConfiguration endpointConfiguration = endpoint.Configuration; + /// + public virtual IEnumerable Load( + Stream stream, + bool transferSubscriptions = false, + IEnumerable? knownTypes = null) + { + using Activity? activity = m_telemetry.StartActivity(); + // secure settings + XmlReaderSettings settings = Utils.DefaultXmlReaderSettings(); + settings.CloseInput = true; - if (endpointConfiguration == null) + using var reader = XmlReader.Create(stream, settings); + var serializer = new DataContractSerializer(typeof(SubscriptionStateCollection), knownTypes); + using IDisposable scope = AmbientMessageContext.SetScopedContext(MessageContext); + var stateCollection = (SubscriptionStateCollection?)serializer.ReadObject(reader); + if (stateCollection == null) { - endpoint.Configuration = endpointConfiguration = EndpointConfiguration.Create( - configuration); + return Enumerable.Empty(); } - - // create message context. - ServiceMessageContext messageContext = configuration.CreateMessageContext(true); - - // update endpoint description using the discovery endpoint. - if (endpoint.UpdateBeforeConnect && connection == null) + var subscriptions = new SubscriptionCollection(stateCollection.Count); + foreach (SubscriptionState state in stateCollection) { - await endpoint.UpdateFromServerAsync(messageContext.Telemetry, ct).ConfigureAwait(false); - endpointDescription = endpoint.Description; - endpointConfiguration = endpoint.Configuration; + // Restore subscription from state + Subscription subscription = CreateSubscription(state); + subscription.Restore(state); + if (!transferSubscriptions) + { + // ServerId must be reset if the saved list of subscriptions + // is not used to transfer a subscription + foreach (MonitoredItem monitoredItem in subscription.MonitoredItems) + { + monitoredItem.ServerId = 0; + } + } + AddSubscription(subscription); + subscriptions.Add(subscription); } + return subscriptions; + } - // checks the domains in the certificate. - if (checkDomain && - endpoint.Description.ServerCertificate != null && - endpoint.Description.ServerCertificate.Length > 0) + /// + public override bool Equals(object? obj) + { + if (ReferenceEquals(this, obj)) { - configuration.CertificateValidator?.ValidateDomains( - CertificateFactory.Create(endpoint.Description.ServerCertificate), - endpoint); + return true; } - X509Certificate2 clientCertificate = null; - X509Certificate2Collection clientCertificateChain = null; - if (endpointDescription.SecurityPolicyUri != SecurityPolicies.None) - { - clientCertificate = await LoadCertificateAsync( - configuration, - endpointDescription.SecurityPolicyUri, - messageContext.Telemetry, - ct) - .ConfigureAwait(false); - clientCertificateChain = await 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); - } - - /// - /// Creates a new communication session with a server using a reverse connection. - /// - /// The Session constructor to use to create the session. - /// 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. - public static async Task CreateAsync( - ISessionInstantiator sessionInstantiator, - 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. - Session session = sessionInstantiator.Create(channel, configuration, endpoint, null); - session.ReturnDiagnostics = returnDiagnostics; - - // create the session. - try - { - await session - .OpenAsync( - sessionName, - sessionTimeout, - identity, - preferredLocales, - checkDomain, - ct) - .ConfigureAwait(false); - } - catch (Exception) - { - session.Dispose(); - throw; - } - - return session; - } - - /// - /// Creates a new communication session with a server using a reverse connect manager. - /// - /// The configuration for the client application. - /// The reverse connect manager for the client connection. - /// 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 cancellation token. - /// The new session object. - [Obsolete("Use ISessionFactory.CreateAsync")] - public static Task CreateAsync( - ApplicationConfiguration configuration, - ReverseConnectManager reverseConnectManager, - ConfiguredEndpoint endpoint, - bool updateBeforeConnect, - bool checkDomain, - string sessionName, - uint sessionTimeout, - IUserIdentity userIdentity, - IList preferredLocales, - CancellationToken ct = default) - { - return CreateAsync( - DefaultSessionFactory.Instance, - configuration, - reverseConnectManager, - endpoint, - updateBeforeConnect, - checkDomain, - sessionName, - sessionTimeout, - userIdentity, - preferredLocales, - DiagnosticsMasks.None, - ct); - } - - /// - /// Creates a new communication session with a server using a reverse connect manager. - /// - /// The Session constructor to use to create the session. - /// The configuration for the client application. - /// The reverse connect manager for the client connection. - /// 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. - /// Diagnostics mask to use in the sesion - /// The cancellation token. - /// The new session object. - public static async Task CreateAsync( - ISessionInstantiator sessionInstantiator, - ApplicationConfiguration configuration, - ReverseConnectManager reverseConnectManager, - ConfiguredEndpoint endpoint, - bool updateBeforeConnect, - bool checkDomain, - string sessionName, - uint sessionTimeout, - IUserIdentity userIdentity, - IList preferredLocales, - DiagnosticsMasks returnDiagnostics, - CancellationToken ct = default) - { - if (reverseConnectManager == null) - { - return await CreateAsync( - sessionInstantiator, - configuration, - (ITransportWaitingConnection)null, - endpoint, - updateBeforeConnect, - checkDomain, - sessionName, - sessionTimeout, - userIdentity, - preferredLocales, - returnDiagnostics, - 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, - sessionInstantiator.Telemetry, - ct) - .ConfigureAwait(false); - updateBeforeConnect = false; - connection = null; - } - } while (connection == null); - - return await CreateAsync( - sessionInstantiator, - configuration, - connection, - endpoint, - false, - checkDomain, - sessionName, - sessionTimeout, - userIdentity, - preferredLocales, - returnDiagnostics, - ct) - .ConfigureAwait(false); - } - - /// - public event RenewUserIdentityEventHandler RenewUserIdentity - { - add => m_RenewUserIdentity += value; - remove => m_RenewUserIdentity -= value; - } - - private event RenewUserIdentityEventHandler m_RenewUserIdentity; - - /// - public bool ApplySessionConfiguration(SessionConfiguration sessionConfiguration) - { - ThrowIfDisposed(); - if (sessionConfiguration == null) - { - throw new ArgumentNullException(nameof(sessionConfiguration)); - } - - byte[] serverCertificate = m_endpoint.Description?.ServerCertificate; - m_sessionName = sessionConfiguration.SessionName; - m_serverCertificate = - serverCertificate != null - ? CertificateFactory.Create(serverCertificate) - : null; - m_identity = sessionConfiguration.Identity; - m_checkDomain = sessionConfiguration.CheckDomain; - m_serverNonce = sessionConfiguration.ServerNonce.Data; - m_userTokenSecurityPolicyUri = sessionConfiguration.UserIdentityTokenPolicy; - m_eccServerEphemeralKey = sessionConfiguration.ServerEccEphemeralKey; - SessionCreated( - sessionConfiguration.SessionId, - sessionConfiguration.AuthenticationToken); - - return true; - } - - /// - public SessionConfiguration SaveSessionConfiguration(Stream stream = null) - { - var serverNonce = Nonce.CreateNonce( - m_endpoint.Description?.SecurityPolicyUri, - m_serverNonce); - - var sessionConfiguration = new SessionConfiguration( - this, - serverNonce, - m_userTokenSecurityPolicyUri, - m_eccServerEphemeralKey, - AuthenticationToken); - - if (stream != null) - { - XmlWriterSettings settings = Utils.DefaultXmlWriterSettings(); - using var writer = XmlWriter.Create(stream, settings); - var serializer = new DataContractSerializer(typeof(SessionConfiguration)); - using IDisposable scope = AmbientMessageContext.SetScopedContext(MessageContext); - serializer.WriteObject(writer, sessionConfiguration); - } - return sessionConfiguration; - } - - /// - public void Save(string filePath, IEnumerable knownTypes = null) - { - Save(filePath, Subscriptions, knownTypes); - } - - /// - public void Save( - Stream stream, - IEnumerable subscriptions, - IEnumerable knownTypes = null) - { - var subscriptionList = new SubscriptionCollection(subscriptions); - XmlWriterSettings settings = Utils.DefaultXmlWriterSettings(); - - using var writer = XmlWriter.Create(stream, settings); - var serializer = new DataContractSerializer(typeof(SubscriptionCollection), knownTypes); - using IDisposable scope = AmbientMessageContext.SetScopedContext(MessageContext); - serializer.WriteObject(writer, subscriptionList); - } - - /// - public void Save( - string filePath, - IEnumerable subscriptions, - IEnumerable knownTypes = null) - { - using var stream = new FileStream(filePath, FileMode.Create); - Save(stream, subscriptions, knownTypes); - } - - /// - public IEnumerable Load( - Stream stream, - bool transferSubscriptions = false, - IEnumerable knownTypes = null) - { - // secure settings - XmlReaderSettings settings = Utils.DefaultXmlReaderSettings(); - settings.CloseInput = true; - - using var reader = XmlReader.Create(stream, settings); - var serializer = new DataContractSerializer(typeof(SubscriptionCollection), knownTypes); - using IDisposable scope = AmbientMessageContext.SetScopedContext(MessageContext); - var subscriptions = (SubscriptionCollection)serializer.ReadObject(reader); - foreach (Subscription subscription in subscriptions) - { - if (!transferSubscriptions) - { - // ServerId must be reset if the saved list of subscriptions - // is not used to transfer a subscription - foreach (MonitoredItem monitoredItem in subscription.MonitoredItems) - { - monitoredItem.ServerId = 0; - } - } - AddSubscription(subscription); - } - return subscriptions; - } - - /// - public IEnumerable Load( - string filePath, - bool transferSubscriptions = false, - IEnumerable knownTypes = null) - { - using FileStream stream = File.OpenRead(filePath); - return Load(stream, transferSubscriptions, knownTypes); - } - - /// - public override bool Equals(object obj) - { - if (ReferenceEquals(this, obj)) - { - return true; - } - - if (obj is ISession session) + if (obj is ISession session) { if (!m_endpoint.Equals(session.Endpoint)) { @@ -1636,53 +1050,22 @@ public virtual Session CloneSession(ITransportChannel channel, bool copyEventHan return new Session(channel, this, copyEventHandlers); } - /// - public Task OpenAsync(string sessionName, IUserIdentity identity, CancellationToken ct) - { - return OpenAsync(sessionName, 0, identity, null, ct); - } - - /// - public Task OpenAsync( - string sessionName, - uint sessionTimeout, - IUserIdentity identity, - IList preferredLocales, - CancellationToken ct) - { - return OpenAsync(sessionName, sessionTimeout, identity, preferredLocales, true, ct); - } - - /// - public Task OpenAsync( - string sessionName, - uint sessionTimeout, - IUserIdentity identity, - IList preferredLocales, - bool checkDomain, - CancellationToken ct) - { - return OpenAsync( - sessionName, - sessionTimeout, - identity, - preferredLocales, - checkDomain, - true, - ct); - } - /// public async Task OpenAsync( string sessionName, uint sessionTimeout, IUserIdentity identity, - IList preferredLocales, + IList? preferredLocales, bool checkDomain, bool closeChannel, CancellationToken ct) { ThrowIfDisposed(); + using Activity? activity = m_telemetry.StartActivity(); + + // Load certificate and chain if not already loaded. + await LoadInstanceCertificateAsync(false, ct).ConfigureAwait(false); + OpenValidateIdentity( ref identity, out UserIdentityToken identityToken, @@ -1691,7 +1074,7 @@ public async Task OpenAsync( out bool requireEncryption); // validate the server certificate /certificate chain. - X509Certificate2 serverCertificate = null; + X509Certificate2? serverCertificate = null; byte[] certificateData = m_endpoint.Description.ServerCertificate; if (certificateData != null && certificateData.Length > 0) @@ -1733,8 +1116,8 @@ await m_configuration // send the application instance certificate for the client. BuildCertificateData( - out byte[] clientCertificateData, - out byte[] clientCertificateChainData); + out byte[]? clientCertificateData, + out byte[]? clientCertificateChainData); var clientDescription = new ApplicationDescription { @@ -1755,7 +1138,7 @@ await m_configuration m_endpoint.Description.SecurityPolicyUri); bool successCreateSession = false; - CreateSessionResponse response = null; + CreateSessionResponse? response = null; //if security none, first try to connect without certificate if (m_endpoint.Description.SecurityPolicyUri == SecurityPolicies.None) @@ -1778,7 +1161,7 @@ await m_configuration successCreateSession = true; } - catch (Exception ex) + catch (Exception ex) when (ex is not OperationCanceledException) { m_logger.LogWarning(ex, "Create session failed with client certificate NULL."); successCreateSession = false; @@ -1788,19 +1171,22 @@ await m_configuration if (!successCreateSession) { response = await base.CreateSessionAsync( - requestHeader, - clientDescription, - m_endpoint.Description.Server.ApplicationUri, - m_endpoint.EndpointUrl.ToString(), - sessionName, - clientNonce, - clientCertificateChainData ?? clientCertificateData, - sessionTimeout, - (uint)MessageContext.MaxMessageSize, - ct) - .ConfigureAwait(false); + requestHeader, + clientDescription, + m_endpoint.Description.Server.ApplicationUri, + m_endpoint.EndpointUrl.ToString(), + sessionName, + clientNonce, + clientCertificateChainData ?? clientCertificateData, + sessionTimeout, + (uint)MessageContext.MaxMessageSize, + ct).ConfigureAwait(false); + } + if (NodeId.IsNull(response?.SessionId)) + { + throw ServiceResultException.Unexpected( + "Create response returned null session id"); } - NodeId sessionId = response.SessionId; NodeId sessionCookie = response.AuthenticationToken; byte[] serverNonce = response.ServerNonce; @@ -1864,7 +1250,7 @@ await m_configuration } // save previous nonce - byte[] previousServerNonce = GetCurrentTokenServerNonce(); + byte[]? previousServerNonce = GetCurrentTokenServerNonce(); // validate server nonce and security parameters for user identity. ValidateServerNonce( @@ -1975,10 +1361,6 @@ SignedSoftwareCertificateCollection clientSoftwareCertificates { await base.CloseSessionAsync(null, false, CancellationToken.None) .ConfigureAwait(false); - if (closeChannel) - { - await CloseChannelAsync(CancellationToken.None).ConfigureAwait(false); - } } catch (Exception e) { @@ -1990,7 +1372,10 @@ await base.CloseSessionAsync(null, false, CancellationToken.None) { SessionCreated(null, null); } - + if (closeChannel) + { + await CloseChannelAsync(CancellationToken.None).ConfigureAwait(false); + } throw; } } @@ -2005,12 +1390,13 @@ public Task ChangePreferredLocalesAsync( /// public async Task UpdateSessionAsync( - IUserIdentity identity, + IUserIdentity? identity, StringCollection preferredLocales, CancellationToken ct = default) { ThrowIfDisposed(); - byte[] serverNonce = null; + using Activity? activity = m_telemetry.StartActivity(); + byte[]? serverNonce = null; lock (SyncRoot) { @@ -2048,7 +1434,7 @@ public async Task UpdateSessionAsync( identity.IssuedTokenType, securityPolicyUri) ?? throw ServiceResultException.Create( - StatusCodes.BadUserAccessDenied, + StatusCodes.BadIdentityTokenRejected, "Endpoint does not support the user identity type provided."); // select the security policy for the user token. @@ -2142,6 +1528,7 @@ public async Task RemoveSubscriptionAsync( CancellationToken ct = default) { ThrowIfDisposed(); + using Activity? activity = m_telemetry.StartActivity(); if (subscription == null) { throw new ArgumentNullException(nameof(subscription)); @@ -2162,7 +1549,7 @@ public async Task RemoveSubscriptionAsync( subscription.Session = null; } - m_SubscriptionsChanged?.Invoke(this, null); + m_SubscriptionsChanged?.Invoke(this, EventArgs.Empty); return true; } @@ -2173,6 +1560,7 @@ public async Task RemoveSubscriptionsAsync( CancellationToken ct = default) { ThrowIfDisposed(); + using Activity? activity = m_telemetry.StartActivity(); if (subscriptions == null) { throw new ArgumentNullException(nameof(subscriptions)); @@ -2189,7 +1577,7 @@ public async Task RemoveSubscriptionsAsync( if (removed) { - m_SubscriptionsChanged?.Invoke(this, null); + m_SubscriptionsChanged?.Invoke(this, EventArgs.Empty); } return removed; @@ -2202,6 +1590,7 @@ public async Task ReactivateSubscriptionsAsync( CancellationToken ct = default) { ThrowIfDisposed(); + using Activity? activity = m_telemetry.StartActivity(); UInt32Collection subscriptionIds = CreateSubscriptionIdsForTransfer(subscriptions); int failedSubscriptions = 0; @@ -2229,16 +1618,11 @@ public async Task ReactivateSubscriptionsAsync( if (sendInitialValues) { - (bool success, IList resendResults) = await ResendDataAsync( - subscriptions, - ct) - .ConfigureAwait(false); - if (!success) - { - m_logger.LogError("Failed to call resend data for subscriptions."); - } - else if (resendResults != null) + try { + IReadOnlyList resendResults = await this.ResendDataAsync( + subscriptions.Select(s => s.Id), + ct).ConfigureAwait(false); for (int ii = 0; ii < resendResults.Count; ii++) { // no need to try for subscriptions which do not exist @@ -2250,6 +1634,10 @@ public async Task ReactivateSubscriptionsAsync( } } } + catch (ServiceResultException sre) + { + m_logger.LogError(sre, "Failed to call resend data for subscriptions."); + } } m_logger.LogInformation( @@ -2273,51 +1661,13 @@ public async Task ReactivateSubscriptionsAsync( return failedSubscriptions == 0; } - /// - public async Task<(bool, IList)> ResendDataAsync( - IEnumerable subscriptions, - CancellationToken ct) - { - CallMethodRequestCollection requests = CreateCallRequestsForResendData(subscriptions); - - var errors = new List(requests.Count); - try - { - CallResponse response = await CallAsync(null, requests, ct).ConfigureAwait(false); - CallMethodResultCollection results = response.Results; - DiagnosticInfoCollection diagnosticInfos = response.DiagnosticInfos; - ResponseHeader responseHeader = response.ResponseHeader; - ValidateResponse(results, requests); - ValidateDiagnosticInfos(diagnosticInfos, requests); - - int ii = 0; - foreach (CallMethodResult value in results) - { - ServiceResult result = ServiceResult.Good; - if (StatusCode.IsNotGood(value.StatusCode)) - { - result = GetResult(value.StatusCode, ii, diagnosticInfos, responseHeader); - } - errors.Add(result); - ii++; - } - - return (true, errors); - } - catch (ServiceResultException sre) - { - m_logger.LogError(sre, "Failed to call ResendData on server."); - } - - return (false, errors); - } - /// public async Task TransferSubscriptionsAsync( SubscriptionCollection subscriptions, bool sendInitialValues, CancellationToken ct) { + using Activity? activity = m_telemetry.StartActivity(); UInt32Collection subscriptionIds = CreateSubscriptionIdsForTransfer(subscriptions); int failedSubscriptions = 0; @@ -2426,6 +1776,7 @@ public async Task TransferSubscriptionsAsync( /// public async Task FetchNamespaceTablesAsync(CancellationToken ct = default) { + using Activity? activity = m_telemetry.StartActivity(); ReadValueIdCollection nodesToRead = PrepareNamespaceTableNodesToRead(); // read from server. @@ -2450,6 +1801,7 @@ public async Task FetchNamespaceTablesAsync(CancellationToken ct = default) /// public async Task FetchTypeTreeAsync(ExpandedNodeId typeId, CancellationToken ct = default) { + using Activity? activity = m_telemetry.StartActivity(); if (await NodeCache.FindAsync(typeId, ct).ConfigureAwait(false) is Node node) { var subTypes = new ExpandedNodeIdCollection(); @@ -2469,6 +1821,7 @@ public async Task FetchTypeTreeAsync( ExpandedNodeIdCollection typeIds, CancellationToken ct = default) { + using Activity? activity = m_telemetry.StartActivity(); var referenceTypeIds = new NodeIdCollection { ReferenceTypeIds.HasSubtype }; IList nodes = await NodeCache .FindReferencesAsync(typeIds, referenceTypeIds, false, false, ct) @@ -2493,3389 +1846,1493 @@ public async Task FetchTypeTreeAsync( } } - /// - /// Fetch the operation limits of the server. - /// - public async Task FetchOperationLimitsAsync(CancellationToken ct = default) - { - try - { - var operationLimitsProperties = typeof(OperationLimits).GetProperties() - .Select(p => p.Name) - .ToList(); - - var nodeIds = new NodeIdCollection( - operationLimitsProperties.Select(name => - (NodeId) - typeof(VariableIds) - .GetField( - "Server_ServerCapabilities_OperationLimits_" + name, - BindingFlags.Public | BindingFlags.Static) - .GetValue(null))) - { - // add the server capability MaxContinuationPointPerBrowse and MaxByteStringLength - VariableIds.Server_ServerCapabilities_MaxBrowseContinuationPoints - }; - int maxBrowseContinuationPointIndex = nodeIds.Count - 1; - - nodeIds.Add(VariableIds.Server_ServerCapabilities_MaxByteStringLength); - int maxByteStringLengthIndex = nodeIds.Count - 1; - - (DataValueCollection values, IList errors) = await ReadValuesAsync( - nodeIds, - ct) - .ConfigureAwait(false); - - OperationLimits configOperationLimits = - m_configuration?.ClientConfiguration?.OperationLimits ?? new OperationLimits(); - var operationLimits = new OperationLimits(); - - for (int ii = 0; ii < operationLimitsProperties.Count; ii++) - { - PropertyInfo property = typeof(OperationLimits).GetProperty( - operationLimitsProperties[ii]); - uint value = (uint)property.GetValue(configOperationLimits); - if (values[ii] != null && - ServiceResult.IsNotBad(errors[ii]) && - values[ii].Value is uint serverValue && - serverValue > 0 && - (value == 0 || serverValue < value)) - { - value = serverValue; - } - property.SetValue(operationLimits, value); - } - OperationLimits = operationLimits; - - if (values[maxBrowseContinuationPointIndex] - .Value is ushort serverMaxContinuationPointsPerBrowse && - ServiceResult.IsNotBad(errors[maxBrowseContinuationPointIndex])) - { - ServerMaxContinuationPointsPerBrowse = serverMaxContinuationPointsPerBrowse; - } - - if (values[maxByteStringLengthIndex].Value is uint serverMaxByteStringLength && - ServiceResult.IsNotBad(errors[maxByteStringLengthIndex])) - { - ServerMaxByteStringLength = serverMaxByteStringLength; - } - } - catch (Exception ex) - { - m_logger.LogError( - ex, - "Failed to read operation limits from server. Using configuration defaults."); - OperationLimits operationLimits = m_configuration?.ClientConfiguration? - .OperationLimits; - if (operationLimits != null) - { - OperationLimits = operationLimits; - } - } - } - /// - public async Task<(IList, IList)> ReadNodesAsync( - IList nodeIds, - NodeClass nodeClass, - bool optionalAttributes = false, - CancellationToken ct = default) + public async Task FetchOperationLimitsAsync(CancellationToken ct) { - if (nodeIds.Count == 0) + using Activity? activity = m_telemetry.StartActivity(); + // First we read the node read max to optimize the second read. + var nodeIds = new List { - return (new List(), new List()); - } - - if (nodeClass == NodeClass.Unspecified) - { - return await ReadNodesAsync(nodeIds, optionalAttributes, 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, - optionalAttributes); - - ReadResponse readResponse = await ReadAsync( - null, - 0, - TimestampsToReturn.Neither, - attributesToRead, - ct) - .ConfigureAwait(false); - - DataValueCollection values = readResponse.Results; - DiagnosticInfoCollection diagnosticInfos = readResponse.DiagnosticInfos; - - ValidateResponse(values, attributesToRead); - ValidateDiagnosticInfos(diagnosticInfos, attributesToRead); + VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerRead + }; + (DataValueCollection values, IList errors) = + await this.ReadValuesAsync(nodeIds, ct).ConfigureAwait(false); + int index = 0; + OperationLimits.MaxNodesPerRead = Get(ref index, values, errors); - List serviceResults = new ServiceResult[nodeIds.Count].ToList(); - ProcessAttributesReadNodesResponse( - readResponse.ResponseHeader, - attributesToRead, - attributesPerNodeId, - values, - diagnosticInfos, - nodeCollection, - serviceResults); + nodeIds = + [ + VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerHistoryReadData, + VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerHistoryReadEvents, + VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerWrite, + VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerRead, + VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerHistoryUpdateData, + VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerHistoryUpdateEvents, + VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerMethodCall, + VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerBrowse, + VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerRegisterNodes, + VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerNodeManagement, + VariableIds.Server_ServerCapabilities_OperationLimits_MaxMonitoredItemsPerCall, + VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerTranslateBrowsePathsToNodeIds, + VariableIds.Server_ServerCapabilities_MaxBrowseContinuationPoints, + VariableIds.Server_ServerCapabilities_MaxHistoryContinuationPoints, + VariableIds.Server_ServerCapabilities_MaxQueryContinuationPoints, + VariableIds.Server_ServerCapabilities_MaxStringLength, + VariableIds.Server_ServerCapabilities_MaxArrayLength, + VariableIds.Server_ServerCapabilities_MaxByteStringLength, + VariableIds.Server_ServerCapabilities_MinSupportedSampleRate, + VariableIds.Server_ServerCapabilities_MaxSessions, + VariableIds.Server_ServerCapabilities_MaxSubscriptions, + VariableIds.Server_ServerCapabilities_MaxMonitoredItems, + VariableIds.Server_ServerCapabilities_MaxMonitoredItemsPerSubscription, + VariableIds.Server_ServerCapabilities_MaxMonitoredItemsQueueSize, + VariableIds.Server_ServerCapabilities_MaxSubscriptionsPerSession, + VariableIds.Server_ServerCapabilities_MaxWhereClauseParameters, + VariableIds.Server_ServerCapabilities_MaxSelectClauseParameters + ]; - return (nodeCollection, serviceResults); + (values, errors) = await this.ReadValuesAsync(nodeIds, ct).ConfigureAwait(false); + index = 0; + OperationLimits.MaxNodesPerHistoryReadData = Get(ref index, values, errors); + OperationLimits.MaxNodesPerHistoryReadEvents = Get(ref index, values, errors); + OperationLimits.MaxNodesPerWrite = Get(ref index, values, errors); + OperationLimits.MaxNodesPerRead = Get(ref index, values, errors); + OperationLimits.MaxNodesPerHistoryUpdateData = Get(ref index, values, errors); + OperationLimits.MaxNodesPerHistoryUpdateEvents = Get(ref index, values, errors); + OperationLimits.MaxNodesPerMethodCall = Get(ref index, values, errors); + OperationLimits.MaxNodesPerBrowse = Get(ref index, values, errors); + OperationLimits.MaxNodesPerRegisterNodes = Get(ref index, values, errors); + OperationLimits.MaxNodesPerNodeManagement = Get(ref index, values, errors); + OperationLimits.MaxMonitoredItemsPerCall = Get(ref index, values, errors); + OperationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds = Get(ref index, values, errors); + ServerCapabilities.MaxBrowseContinuationPoints = Get(ref index, values, errors); + ServerCapabilities.MaxHistoryContinuationPoints = Get(ref index, values, errors); + ServerCapabilities.MaxQueryContinuationPoints = Get(ref index, values, errors); + ServerCapabilities.MaxStringLength = Get(ref index, values, errors); + ServerCapabilities.MaxArrayLength = Get(ref index, values, errors); + ServerCapabilities.MaxByteStringLength = Get(ref index, values, errors); + ServerCapabilities.MinSupportedSampleRate = Get(ref index, values, errors); + ServerCapabilities.MaxSessions = Get(ref index, values, errors); + ServerCapabilities.MaxSubscriptions = Get(ref index, values, errors); + ServerCapabilities.MaxMonitoredItems = Get(ref index, values, errors); + ServerCapabilities.MaxMonitoredItemsPerSubscription = Get(ref index, values, errors); + ServerCapabilities.MaxMonitoredItemsQueueSize = Get(ref index, values, errors); + ServerCapabilities.MaxSubscriptionsPerSession = Get(ref index, values, errors); + ServerCapabilities.MaxWhereClauseParameters = Get(ref index, values, errors); + ServerCapabilities.MaxSelectClauseParameters = Get(ref index, values, errors); + + // Helper extraction + static T Get(ref int index, IList values, IList errors) + where T : struct + { + DataValue value = values[index]; + ServiceResult error = errors.Count > 0 ? errors[index] : ServiceResult.Good; + index++; + if (ServiceResult.IsNotBad(error) && value.Value is T retVal) + { + return retVal; + } + return default; + } + + uint maxByteStringLength = (uint?)m_configuration.TransportQuotas?.MaxByteStringLength ?? 0u; + if (maxByteStringLength != 0 && + (ServerCapabilities.MaxByteStringLength == 0 || + ServerCapabilities.MaxByteStringLength > maxByteStringLength)) + { + ServerCapabilities.MaxByteStringLength = maxByteStringLength; + } } - /// - public async Task<(IList, IList)> ReadNodesAsync( - IList nodeIds, - bool optionalAttributes = false, + /// + /// Recreates a session based on a specified template. + /// + /// Cancellation Token to cancel operation with + /// The new session object. + protected internal async Task RecreateAsync( CancellationToken ct = default) { - if (nodeIds.Count == 0) - { - return (new List(), new List()); - } - - 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 ReadAsync( - null, - 0, - TimestampsToReturn.Neither, - itemsToRead, - ct) - .ConfigureAwait(false); - - DataValueCollection nodeClassValues = readResponse.Results; - DiagnosticInfoCollection diagnosticInfos = readResponse.DiagnosticInfos; - - ValidateResponse(nodeClassValues, itemsToRead); - ValidateDiagnosticInfos(diagnosticInfos, itemsToRead); + ServiceMessageContext messageContext = m_configuration + .CreateMessageContext(); + messageContext.Factory = Factory; - // second determine attributes to read per nodeclass - var attributesPerNodeId = new List>(nodeIds.Count); - var serviceResults = new List(nodeIds.Count); - var attributesToRead = new ReadValueIdCollection(); + // create the channel object used to connect to the server. + ITransportChannel channel = await UaChannelBase.CreateUaBinaryChannelAsync( + m_configuration, + ConfiguredEndpoint.Description, + ConfiguredEndpoint.Configuration, + m_instanceCertificate, + m_configuration.SecurityConfiguration.SendCertificateChain + ? m_instanceCertificateChain + : null, + messageContext, + ct).ConfigureAwait(false); - CreateAttributesReadNodesRequest( - readResponse.ResponseHeader, - itemsToRead, - nodeClassValues, - diagnosticInfos, - attributesToRead, - attributesPerNodeId, - nodeCollection, - serviceResults, - optionalAttributes); + // create the session object. + Session session = CloneSession(channel, true); - if (attributesToRead.Count > 0) + try { - readResponse = await ReadAsync( - null, - 0, - TimestampsToReturn.Neither, - attributesToRead, - ct) + session.RecreateRenewUserIdentity(); + // open the session. + await session + .OpenAsync( + SessionName, + (uint)SessionTimeout, + session.Identity ?? new UserIdentity(), + PreferredLocales, + m_checkDomain, + ct) .ConfigureAwait(false); - DataValueCollection values = readResponse.Results; - diagnosticInfos = readResponse.DiagnosticInfos; - - ValidateResponse(values, attributesToRead); - ValidateDiagnosticInfos(diagnosticInfos, attributesToRead); - - ProcessAttributesReadNodesResponse( - readResponse.ResponseHeader, - attributesToRead, - attributesPerNodeId, - values, - diagnosticInfos, - nodeCollection, - serviceResults); + await session.RecreateSubscriptionsAsync( + TransferSubscriptionsOnReconnect, + Subscriptions, + ct).ConfigureAwait(false); } - - return (nodeCollection, serviceResults); - } - - /// - public Task ReadNodeAsync(NodeId nodeId, CancellationToken ct = default) - { - return ReadNodeAsync(nodeId, NodeClass.Unspecified, true, ct); - } - - /// - public async Task ReadNodeAsync( - NodeId nodeId, - NodeClass nodeClass, - bool optionalAttributes = true, - CancellationToken ct = default) - { - // build list of attributes. - IDictionary attributes = CreateAttributes( - nodeClass, - optionalAttributes); - - // build list of values to read. - var itemsToRead = new ReadValueIdCollection(); - foreach (uint attributeId in attributes.Keys) + catch (Exception e) { - var itemToRead = new ReadValueId { NodeId = nodeId, AttributeId = attributeId }; - itemsToRead.Add(itemToRead); + session.Dispose(); + ThrowCouldNotRecreateSessionException(e, SessionName); } - - // read from server. - ReadResponse readResponse = await ReadAsync( - null, - 0, - TimestampsToReturn.Neither, - itemsToRead, - ct) - .ConfigureAwait(false); - - DataValueCollection values = readResponse.Results; - DiagnosticInfoCollection diagnosticInfos = readResponse.DiagnosticInfos; - - ValidateResponse(values, itemsToRead); - ValidateDiagnosticInfos(diagnosticInfos, itemsToRead); - - return ProcessReadResponse( - readResponse.ResponseHeader, - attributes, - itemsToRead, - values, - diagnosticInfos); + return session; } - /// - public async Task<(IList, IList)> ReadDisplayNameAsync( - IList nodeIds, + /// + /// Recreates a session based on a specified template. + /// + /// The waiting reverse connection. + /// Cancellation token to cancel operation with + /// The new session object. + protected internal async Task RecreateAsync( + ITransportWaitingConnection connection, CancellationToken ct = default) { - var displayNames = new List(); - var errors = new List(); - - // build list of values to read. - var valuesToRead = new ReadValueIdCollection(); - - for (int ii = 0; ii < nodeIds.Count; ii++) - { - var valueToRead = new ReadValueId - { - NodeId = nodeIds[ii], - AttributeId = Attributes.DisplayName, - IndexRange = null, - DataEncoding = null - }; - - valuesToRead.Add(valueToRead); - } - - // read the values. + ServiceMessageContext messageContext = m_configuration + .CreateMessageContext(); + messageContext.Factory = Factory; - ReadResponse response = await ReadAsync( - null, - int.MaxValue, - TimestampsToReturn.Neither, - valuesToRead, + // create the channel object used to connect to the server. + ITransportChannel channel = await UaChannelBase.CreateUaBinaryChannelAsync( + m_configuration, + connection, + ConfiguredEndpoint.Description, + ConfiguredEndpoint.Configuration, + m_instanceCertificate, + m_configuration.SecurityConfiguration.SendCertificateChain + ? m_instanceCertificateChain + : null, + messageContext, ct).ConfigureAwait(false); - DataValueCollection results = response.Results; - DiagnosticInfoCollection diagnosticInfos = response.DiagnosticInfos; - ResponseHeader responseHeader = response.ResponseHeader; - - // verify that the server returned the correct number of results. - ValidateResponse(results, valuesToRead); - ValidateDiagnosticInfos(diagnosticInfos, valuesToRead); + // create the session object. + Session session = CloneSession(channel, true); - for (int ii = 0; ii < nodeIds.Count; ii++) + try { - displayNames.Add(string.Empty); - errors.Add(ServiceResult.Good); - - // process any diagnostics associated with bad or uncertain data. - if (StatusCode.IsNotGood(results[ii].StatusCode)) - { - errors[ii] = new ServiceResult( - results[ii].StatusCode, - ii, - diagnosticInfos, - responseHeader.StringTable); - continue; - } - - // extract the name. - LocalizedText displayName = results[ii].GetValue(null); + session.RecreateRenewUserIdentity(); + // open the session. + await session + .OpenAsync( + SessionName, + (uint)SessionTimeout, + session.Identity ?? new UserIdentity(), + PreferredLocales, + CheckDomain, + ct) + .ConfigureAwait(false); - if (!LocalizedText.IsNullOrEmpty(displayName)) - { - displayNames[ii] = displayName.Text; - } + await session.RecreateSubscriptionsAsync( + TransferSubscriptionsOnReconnect, + Subscriptions, + ct).ConfigureAwait(false); + } + catch (Exception e) + { + session.Dispose(); + ThrowCouldNotRecreateSessionException(e, SessionName); } - return (displayNames, errors); + return session; } - /// - public async Task ReadAvailableEncodingsAsync( - NodeId variableId, + /// + /// Recreates a session based on a specified template using the provided channel. + /// + /// The waiting reverse connection. + /// Cancellation token to cancel the operation with + /// The new session object. + protected internal async Task RecreateAsync( + ITransportChannel transportChannel, CancellationToken ct = default) { - if (await NodeCache.FindAsync(variableId, ct).ConfigureAwait(false) - is not VariableNode variable) - { - throw ServiceResultException.Create( - StatusCodes.BadNodeIdInvalid, - "NodeId does not refer to a valid variable node."); - } - - // no encodings available if there was a problem reading the - // data type for the node. - if (NodeId.IsNull(variable.DataType)) - { - return []; - } - - // no encodings for non-structures. - if (!await NodeCache.IsTypeOfAsync( - variable.DataType, - DataTypes.Structure, - ct).ConfigureAwait(false)) + if (transportChannel == null) { - return []; + return await RecreateAsync(ct).ConfigureAwait(false); } - // look for cached values. - IList encodings = await NodeCache.FindAsync( - variableId, - ReferenceTypeIds.HasEncoding, - false, - true, - ct).ConfigureAwait(false); + // create the session object. + Session session = CloneSession(transportChannel, true); - if (encodings.Count > 0) + try { - var references = new ReferenceDescriptionCollection(); - - foreach (INode encoding in encodings) - { - var reference = new ReferenceDescription - { - ReferenceTypeId = ReferenceTypeIds.HasEncoding, - IsForward = true, - NodeId = encoding.NodeId, - NodeClass = encoding.NodeClass, - BrowseName = encoding.BrowseName, - DisplayName = encoding.DisplayName, - TypeDefinition = encoding.TypeDefinitionId - }; - - references.Add(reference); - } + session.RecreateRenewUserIdentity(); + // open the session. + await session + .OpenAsync( + SessionName, + (uint)SessionTimeout, + session.Identity ?? new UserIdentity(), + PreferredLocales, + CheckDomain, + false, + ct) + .ConfigureAwait(false); - return references; + // create the subscriptions. + await session.RecreateSubscriptionsAsync( + false, + Subscriptions, + ct).ConfigureAwait(false); } - - var browser = new Browser(this, m_telemetry) + catch (Exception e) { - BrowseDirection = BrowseDirection.Forward, - ReferenceTypeId = ReferenceTypeIds.HasEncoding, - IncludeSubtypes = false, - NodeClassMask = 0 - }; + session.Dispose(); + ThrowCouldNotRecreateSessionException(e, SessionName); + } - return await browser.BrowseAsync(variable.DataType, ct).ConfigureAwait(false); + return session; } /// - public async Task FindDataDescriptionAsync(NodeId encodingId, - CancellationToken ct = default) + public override Task CloseAsync(CancellationToken ct = default) { - var browser = new Browser(this, m_telemetry) - { - BrowseDirection = BrowseDirection.Forward, - ReferenceTypeId = ReferenceTypeIds.HasDescription, - IncludeSubtypes = false, - NodeClassMask = 0 - }; - - ReferenceDescriptionCollection references = - await browser.BrowseAsync(encodingId, ct).ConfigureAwait(false); - - if (references.Count == 0) - { - throw ServiceResultException.Create( - StatusCodes.BadNodeIdInvalid, - "Encoding does not refer to a valid data description."); - } - - return references[0]; + return CloseAsync(m_keepAliveInterval, true, ct); } /// - public async Task<(NodeIdCollection, IList)> FindComponentIdsAsync( - NodeId instanceId, - IList componentPaths, + public virtual async Task CloseAsync( + int timeout, + bool closeChannel, CancellationToken ct = default) { - var componentIds = new NodeIdCollection(); - var errors = new List(); - - // build list of paths to translate. - var pathsToTranslate = new BrowsePathCollection(); - - for (int ii = 0; ii < componentPaths.Count; ii++) + // check if already called. + if (Disposed) { - var pathToTranslate = new BrowsePath - { - StartingNode = instanceId, - RelativePath = RelativePath.Parse(componentPaths[ii], TypeTree) - }; - - pathsToTranslate.Add(pathToTranslate); + return StatusCodes.Good; } - // translate the paths. - - TranslateBrowsePathsToNodeIdsResponse response = await TranslateBrowsePathsToNodeIdsAsync( - null, - pathsToTranslate, - ct).ConfigureAwait(false); - - BrowsePathResultCollection results = response.Results; - DiagnosticInfoCollection diagnosticInfos = response.DiagnosticInfos; - ResponseHeader responseHeader = response.ResponseHeader; + StatusCode result = StatusCodes.Good; - // verify that the server returned the correct number of results. - ValidateResponse(results, pathsToTranslate); - ValidateDiagnosticInfos(diagnosticInfos, pathsToTranslate); + Closing = true; - for (int ii = 0; ii < componentPaths.Count; ii++) + using Activity? activity = m_telemetry.StartActivity(); + try { - componentIds.Add(NodeId.Null); - errors.Add(ServiceResult.Good); + // stop the keep alive timer. + await StopKeepAliveTimerAsync().ConfigureAwait(false); - // process any diagnostics associated with any error. - if (StatusCode.IsBad(results[ii].StatusCode)) - { - errors[ii] = new ServiceResult( - results[ii].StatusCode, - ii, - diagnosticInfos, - responseHeader.StringTable); - continue; - } - - // Expecting exact one NodeId for a local node. - // Report an error if the server returns anything other than that. - - if (results[ii].Targets.Count == 0) - { - errors[ii] = ServiceResult.Create( - StatusCodes.BadTargetNodeIdInvalid, - "Could not find target for path: {0}.", - componentPaths[ii]); - - continue; - } + // check if correctly connected. + bool connected = Connected; - if (results[ii].Targets.Count != 1) + // halt all background threads. + if (connected && m_SessionClosing != null) { - errors[ii] = ServiceResult.Create( - StatusCodes.BadTooManyMatches, - "Too many matches found for path: {0}.", - componentPaths[ii]); - - continue; + try + { + m_SessionClosing(this, EventArgs.Empty); + } + catch (Exception e) + { + m_logger.LogError(e, "Session: Unexpected error raising SessionClosing event."); + } } - if (results[ii].Targets[0].RemainingPathIndex != uint.MaxValue) + // close the session with the server. + if (connected) { - errors[ii] = ServiceResult.Create( - StatusCodes.BadTargetNodeIdInvalid, - "Cannot follow path to external server: {0}.", - componentPaths[ii]); - - continue; - } + try + { + // Wait for or cancel outstanding publish requests before closing session. + await WaitForOrCancelOutstandingPublishRequestsAsync(ct).ConfigureAwait(false); - if (NodeId.IsNull(results[ii].Targets[0].TargetId)) - { - errors[ii] = ServiceResult.Create( - StatusCodes.BadUnexpectedError, - "Server returned a null NodeId for path: {0}.", - componentPaths[ii]); + // close the session and delete all subscriptions if specified. + var requestHeader = new RequestHeader + { + TimeoutHint = timeout > 0 + ? (uint)timeout + : (uint)(OperationTimeout > 0 ? OperationTimeout : 0) + }; + CloseSessionResponse response = await base.CloseSessionAsync( + requestHeader, + DeleteSubscriptionsOnClose, + ct).ConfigureAwait(false); + } + // don't throw errors on disconnect, but return them + // so the caller can log the error. + catch (ServiceResultException sre) + { + m_logger.LogDebug(sre, "Error closing session during Close."); + result = sre.StatusCode; + } + catch (Exception e1) + { + m_logger.LogDebug(e1, "Error closing session during Close."); + result = StatusCodes.Bad; + } + finally + { + if (closeChannel) + { + try + { + await CloseChannelAsync(ct).ConfigureAwait(false); + } + catch (Exception e2) + { + m_logger.LogDebug(e2, "Error closing channel during Close"); + } + } - continue; + // raised notification indicating the session is closed. + SessionCreated(null, null); + } } - if (results[ii].Targets[0].TargetId.IsAbsolute) + // clean up. + if (closeChannel) { - errors[ii] = ServiceResult.Create( - StatusCodes.BadUnexpectedError, - "Server returned a remote node for path: {0}.", - componentPaths[ii]); - - continue; + Dispose(); } - // suitable target found. - componentIds[ii] = ExpandedNodeId.ToNodeId( - results[ii].Targets[0].TargetId, - NamespaceUris); + return result; + } + finally + { + Closing = false; } - return (componentIds, errors); } /// - public async Task ReadValueAsync(NodeId nodeId, CancellationToken ct = default) + public async Task ReloadInstanceCertificateAsync(CancellationToken ct = default) { - DataValue dataValue = await ReadValueAsync(nodeId, ct).ConfigureAwait(false); - object value = dataValue.Value; - - if (value is ExtensionObject extension) + ThrowIfDisposed(); + using Activity? activity = m_telemetry.StartActivity(); + await m_reconnectLock.WaitAsync(ct).ConfigureAwait(false); + try { - value = extension.Body; + // Force reload + m_instanceCertificate = null; + await LoadInstanceCertificateAsync(false, ct).ConfigureAwait(false); } - - if (!typeof(T).IsInstanceOfType(value)) + finally { - throw ServiceResultException.Create( - StatusCodes.BadTypeMismatch, - "Server returned value unexpected type: {0}", - value != null ? value.GetType().Name : "(null)"); + m_reconnectLock.Release(); } - return (T)value; } /// - public async Task ReadValueAsync(NodeId nodeId, CancellationToken ct = default) + public async Task ReconnectAsync( + ITransportWaitingConnection? connection, + ITransportChannel? transportChannel, + CancellationToken ct) { - var itemToRead = new ReadValueId + ThrowIfDisposed(); + using Activity? activity = m_telemetry.StartActivity(); + bool resetReconnect = false; + await m_reconnectLock.WaitAsync(ct).ConfigureAwait(false); + try { - NodeId = nodeId, - AttributeId = Attributes.Value - }; - var itemsToRead = new ReadValueIdCollection { itemToRead }; + bool reconnecting = Reconnecting; + Reconnecting = true; + resetReconnect = true; + m_reconnectLock.Release(); - // read from server. - ReadResponse readResponse = await ReadAsync( - null, - 0, - TimestampsToReturn.Both, - itemsToRead, - ct) - .ConfigureAwait(false); + // check if already connecting. + if (reconnecting) + { + m_logger.LogWarning("Session is already attempting to reconnect."); - DataValueCollection values = readResponse.Results; - DiagnosticInfoCollection diagnosticInfos = readResponse.DiagnosticInfos; + throw ServiceResultException.Create( + StatusCodes.BadInvalidState, + "Session is already attempting to reconnect."); + } - ValidateResponse(values, itemsToRead); - ValidateDiagnosticInfos(diagnosticInfos, itemsToRead); + m_logger.LogInformation("Session RECONNECT {SessionId} starting...", SessionId); - if (StatusCode.IsBad(values[0].StatusCode)) - { - ServiceResult result = GetResult( - values[0].StatusCode, - 0, - diagnosticInfos, - readResponse.ResponseHeader); - throw new ServiceResultException(result); - } + await StopKeepAliveTimerAsync().ConfigureAwait(false); - return values[0]; - } + // need to refresh the identity (reprompt for password, refresh token). + RecreateRenewUserIdentity(); - /// - public async Task<(DataValueCollection, IList)> ReadValuesAsync( - IList nodeIds, - CancellationToken ct = default) - { - if (nodeIds.Count == 0) - { - return (new DataValueCollection(), new List()); - } + // + // It is possible the session was created and a previous configuration was + // applied, then reconnect can be called even though we are not connected + // But while valid we also want to check that otherwise the endpoint was not + // changed. + // + await LoadInstanceCertificateAsync(true, ct).ConfigureAwait(false); - // read all values from server. - var itemsToRead = new ReadValueIdCollection( - nodeIds.Select( - nodeId => new ReadValueId { NodeId = nodeId, AttributeId = Attributes.Value })); + // create the client signature. + byte[] dataToSign = Utils.Append(m_serverCertificate?.RawData, m_serverNonce); + EndpointDescription endpoint = m_endpoint.Description; + SignatureData clientSignature = SecurityPolicies.Sign( + m_instanceCertificate, + endpoint.SecurityPolicyUri, + dataToSign); - // read from server. - var errors = new List(itemsToRead.Count); + // check that the user identity is supported by the endpoint. + UserTokenPolicy identityPolicy = endpoint.FindUserTokenPolicy( + m_identity.TokenType, + m_identity.IssuedTokenType, + endpoint.SecurityPolicyUri); - ReadResponse readResponse = await ReadAsync( - null, - 0, - TimestampsToReturn.Both, - itemsToRead, - ct) - .ConfigureAwait(false); + if (identityPolicy == null) + { + m_logger.LogError( + "Reconnect: Endpoint does not support the user identity type provided."); - DataValueCollection values = readResponse.Results; - DiagnosticInfoCollection diagnosticInfos = readResponse.DiagnosticInfos; + throw ServiceResultException.Create( + StatusCodes.BadIdentityTokenRejected, + "Endpoint does not support the user identity type provided."); + } - ValidateResponse(values, itemsToRead); - ValidateDiagnosticInfos(diagnosticInfos, itemsToRead); + // select the security policy for the user token. + string tokenSecurityPolicyUri = identityPolicy.SecurityPolicyUri; - foreach (DataValue value in values) - { - ServiceResult result = ServiceResult.Good; - if (StatusCode.IsBad(value.StatusCode)) + if (string.IsNullOrEmpty(tokenSecurityPolicyUri)) { - result = GetResult( - values[0].StatusCode, - 0, - diagnosticInfos, - readResponse.ResponseHeader); + tokenSecurityPolicyUri = endpoint.SecurityPolicyUri; } - errors.Add(result); - } - - return (values, errors); - } - - /// - public async Task ReadByteStringInChunksAsync(NodeId nodeId, CancellationToken ct) - { - int count = (int)ServerMaxByteStringLength; + m_userTokenSecurityPolicyUri = tokenSecurityPolicyUri; - int maxByteStringLength = m_configuration.TransportQuotas.MaxByteStringLength; - if (maxByteStringLength > 0) - { - count = - ServerMaxByteStringLength > maxByteStringLength - ? maxByteStringLength - : (int)ServerMaxByteStringLength; - } + // validate server nonce and security parameters for user identity. + ValidateServerNonce( + m_identity, + m_serverNonce, + tokenSecurityPolicyUri, + m_previousServerNonce, + m_endpoint.Description.SecurityMode); - if (count <= 1) - { - throw ServiceResultException.Create( - StatusCodes.BadIndexRangeNoData, - "The MaxByteStringLength is not known or too small for reading data in chunks."); - } + // sign data with user token. + UserIdentityToken identityToken = m_identity.GetIdentityToken(); + identityToken.PolicyId = identityPolicy.PolicyId; + SignatureData userTokenSignature = identityToken.Sign( + dataToSign, + tokenSecurityPolicyUri, + m_telemetry); - int offset = 0; - using var bytes = new MemoryStream(); - while (true) - { - var valueToRead = new ReadValueId - { - NodeId = nodeId, - AttributeId = Attributes.Value, - IndexRange = new NumericRange(offset, offset + count - 1).ToString(), - DataEncoding = null - }; - var readValueIds = new ReadValueIdCollection { valueToRead }; + // encrypt token. + identityToken.Encrypt( + m_serverCertificate, + m_serverNonce, + m_userTokenSecurityPolicyUri, + MessageContext, + m_eccServerEphemeralKey, + m_instanceCertificate, + m_instanceCertificateChain, + m_endpoint.Description.SecurityMode != MessageSecurityMode.None); - ReadResponse result = await ReadAsync( - null, - 0, - TimestampsToReturn.Neither, - readValueIds, - ct) - .ConfigureAwait(false); + // send the software certificates assigned to the client. + SignedSoftwareCertificateCollection clientSoftwareCertificates + = GetSoftwareCertificates(); - ResponseHeader responseHeader = result.ResponseHeader; - DataValueCollection results = result.Results; - DiagnosticInfoCollection diagnosticInfos = result.DiagnosticInfos; - ValidateResponse(results, readValueIds); - ValidateDiagnosticInfos(diagnosticInfos, readValueIds); + m_logger.LogInformation("Session REPLACING channel for {SessionId}.", SessionId); - if (offset == 0) + if (connection != null) { - Variant wrappedValue = results[0].WrappedValue; - if (wrappedValue.TypeInfo.BuiltInType != BuiltInType.ByteString || - wrappedValue.TypeInfo.ValueRank != ValueRanks.Scalar) - { - throw new ServiceResultException( - StatusCodes.BadTypeMismatch, - "Value is not a ByteString scalar."); - } - } + ITransportChannel channel = NullableTransportChannel; - if (StatusCode.IsBad(results[0].StatusCode)) - { - if (results[0].StatusCode == StatusCodes.BadIndexRangeNoData) + // check if the channel supports reconnect. + if (channel != null && + (channel.SupportedFeatures & TransportChannelFeatures.Reconnect) != 0) { - // this happens when the previous read has fetched all remaining data - break; + await channel.ReconnectAsync(connection, ct).ConfigureAwait(false); } - ServiceResult serviceResult = GetResult( - results[0].StatusCode, - 0, - diagnosticInfos, - responseHeader); - throw new ServiceResultException(serviceResult); - } + else + { + // initialize the channel which will be created with the server. + channel = await UaChannelBase.CreateUaBinaryChannelAsync( + m_configuration, + connection, + m_endpoint.Description, + m_endpoint.Configuration, + m_instanceCertificate, + m_configuration.SecurityConfiguration.SendCertificateChain + ? m_instanceCertificateChain + : null, + MessageContext, + ct).ConfigureAwait(false); - if (results[0].Value is not byte[] chunk || chunk.Length == 0) - { - break; + // disposes the existing channel. + TransportChannel = channel; + } } - - bytes.Write(chunk, 0, chunk.Length); - if (chunk.Length < count) + else if (transportChannel != null) { - break; + TransportChannel = transportChannel; } - offset += count; - } - return bytes.ToArray(); - } - - /// - public async Task<( - ResponseHeader responseHeader, - ByteStringCollection continuationPoints, - IList referencesList, - IList errors - )> BrowseAsync( - RequestHeader requestHeader, - ViewDescription view, - IList nodesToBrowse, - uint maxResultsToReturn, - BrowseDirection browseDirection, - NodeId referenceTypeId, - bool includeSubtypes, - uint nodeClassMask, - CancellationToken ct = default) - { - var browseDescriptions = new BrowseDescriptionCollection(); - foreach (NodeId nodeToBrowse in nodesToBrowse) - { - var description = new BrowseDescription + else { - NodeId = nodeToBrowse, - BrowseDirection = browseDirection, - ReferenceTypeId = referenceTypeId, - IncludeSubtypes = includeSubtypes, - NodeClassMask = nodeClassMask, - ResultMask = (uint)BrowseResultMask.All - }; - - browseDescriptions.Add(description); - } - - BrowseResponse browseResponse = await BrowseAsync( - requestHeader, - view, - maxResultsToReturn, - browseDescriptions, - ct) - .ConfigureAwait(false); + ITransportChannel channel = NullableTransportChannel; - ValidateResponse(browseResponse.ResponseHeader); - BrowseResultCollection results = browseResponse.Results; - DiagnosticInfoCollection diagnosticInfos = browseResponse.DiagnosticInfos; - - ValidateResponse(results, browseDescriptions); - ValidateDiagnosticInfos(diagnosticInfos, browseDescriptions); + // check if the channel supports reconnect. + if (channel != null && + (channel.SupportedFeatures & TransportChannelFeatures.Reconnect) != 0) + { + await channel.ReconnectAsync(ct: ct).ConfigureAwait(false); + } + else + { + // initialize the channel which will be created with the server. + channel = await UaChannelBase.CreateUaBinaryChannelAsync( + m_configuration, + m_endpoint.Description, + m_endpoint.Configuration, + m_instanceCertificate, + m_configuration.SecurityConfiguration.SendCertificateChain + ? m_instanceCertificateChain + : null, + MessageContext, + ct).ConfigureAwait(false); - int ii = 0; - var errors = new List(); - var continuationPoints = new ByteStringCollection(); - var referencesList = new List(); - foreach (BrowseResult result in results) - { - if (StatusCode.IsBad(result.StatusCode)) - { - errors.Add( - new ServiceResult( - result.StatusCode, - ii, - diagnosticInfos, - browseResponse.ResponseHeader.StringTable)); + // disposes the existing channel. + TransportChannel = channel; + } } - else + + m_logger.LogInformation("Session RE-ACTIVATING {SessionId}.", SessionId); + + var header = new RequestHeader { TimeoutHint = kReconnectTimeout }; + + using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct); + timeout.CancelAfter(TimeSpan.FromMilliseconds(kReconnectTimeout / 2)); + try { - errors.Add(ServiceResult.Good); - } - continuationPoints.Add(result.ContinuationPoint); - referencesList.Add(result.References); - ii++; - } + // reactivate session. + ActivateSessionResponse activateResult = await ActivateSessionAsync( + header, + clientSignature, + null, + m_preferredLocales, + new ExtensionObject(identityToken), + userTokenSignature, + timeout.Token).ConfigureAwait(false); - return (browseResponse.ResponseHeader, continuationPoints, referencesList, errors); - } + byte[] serverNonce = activateResult.ServerNonce; + StatusCodeCollection certificateResults = activateResult.Results; + DiagnosticInfoCollection certificateDiagnosticInfos = activateResult.DiagnosticInfos; - /// - public async Task<( - ResponseHeader responseHeader, - ByteStringCollection revisedContinuationPoints, - IList referencesList, - IList errors - )> BrowseNextAsync( - RequestHeader requestHeader, - ByteStringCollection continuationPoints, - bool releaseContinuationPoint, - CancellationToken ct = default) - { - BrowseNextResponse response = await base.BrowseNextAsync( - requestHeader, - releaseContinuationPoint, - continuationPoints, - ct) - .ConfigureAwait(false); + m_logger.LogInformation("Session RECONNECT {SessionId} completed successfully.", SessionId); - ValidateResponse(response.ResponseHeader); + lock (SyncRoot) + { + m_previousServerNonce = m_serverNonce; + m_serverNonce = serverNonce; + } - BrowseResultCollection results = response.Results; - DiagnosticInfoCollection diagnosticInfos = response.DiagnosticInfos; + await m_reconnectLock.WaitAsync(ct).ConfigureAwait(false); + Reconnecting = false; + resetReconnect = false; + m_reconnectLock.Release(); + + StartPublishing(OperationTimeout, true); - ValidateResponse(results, continuationPoints); - ValidateDiagnosticInfos(diagnosticInfos, continuationPoints); + await StartKeepAliveTimerAsync().ConfigureAwait(false); - int ii = 0; - var errors = new List(); - var revisedContinuationPoints = new ByteStringCollection(); - var referencesList = new List(); - foreach (BrowseResult result in results) - { - if (StatusCode.IsBad(result.StatusCode)) + IndicateSessionConfigurationChanged(); + } + catch (OperationCanceledException) + when (timeout.IsCancellationRequested && !ct.IsCancellationRequested) { - errors.Add( - new ServiceResult( - result.StatusCode, - ii, - diagnosticInfos, - response.ResponseHeader.StringTable)); + var error = ServiceResult.Create( + StatusCodes.BadRequestTimeout, + "ACTIVATE SESSION timed out. {0}/{1}", + GoodPublishRequestCount, + OutstandingRequestCount); + + m_logger.LogWarning( + "ACTIVATE SESSION ASYNC timed out. {GoodRequestCount}/{OutstandingRequestCount}", + GoodPublishRequestCount, + OutstandingRequestCount); + throw new ServiceResultException(error); } - else + } + finally + { + if (resetReconnect) { - errors.Add(ServiceResult.Good); + await m_reconnectLock.WaitAsync(ct).ConfigureAwait(false); + Reconnecting = false; + m_reconnectLock.Release(); } - revisedContinuationPoints.Add(result.ContinuationPoint); - referencesList.Add(result.References); - ii++; } - - return (response.ResponseHeader, revisedContinuationPoints, referencesList, errors); } /// - public async Task<(IList, IList)> ManagedBrowseAsync( - RequestHeader requestHeader, - ViewDescription view, - IList nodesToBrowse, - uint maxResultsToReturn, - BrowseDirection browseDirection, - NodeId referenceTypeId, - bool includeSubtypes, - uint nodeClassMask, - CancellationToken ct = default) + public async Task<(bool, ServiceResult)> RepublishAsync( + uint subscriptionId, + uint sequenceNumber, + CancellationToken ct) { - 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++) + using Activity? activity = m_telemetry.StartActivity(); + // send republish request. + var requestHeader = new RequestHeader { - result.Add([]); - errors.Add(new ServiceResult(StatusCodes.Good)); - } + TimeoutHint = (uint)OperationTimeout, + ReturnDiagnostics = (uint)(int)ReturnDiagnostics, + RequestHandle = Utils.IncrementIdentifier(ref m_publishCounter) + }; try { - // 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 = OperationLimits.MaxNodesPerBrowse; - - if (ContinuationPointPolicy == ContinuationPointPolicy.Balanced && - ServerMaxContinuationPointsPerBrowse > 0) - { - maxNodesPerBrowse = - ServerMaxContinuationPointsPerBrowse < maxNodesPerBrowse - ? ServerMaxContinuationPointsPerBrowse - : 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; - - (IList resultForBatch, IList errorsForBatch) = - await BrowseWithBrowseNextAsync( - requestHeader, - view, - nodesToBrowseBatch, - maxResultsToReturn, - browseDirection, - referenceTypeId, - includeSubtypes, - nodeClassMask, - ct) - .ConfigureAwait(false); - - int resultOffset = batchOffset; - for (int ii = 0; ii < nodesToBrowseBatchCount; ii++) - { - StatusCode statusCode = errorsForBatch[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(resultForBatch[ii]); - errorsForPass[resultOffset] = errorsForBatch[ii]; - resultOffset++; - } - - batchOffset += nodesToBrowseBatchCount; - } - - resultForPass = referenceDescriptionsForNextPass; - referenceDescriptionsForNextPass = []; + m_logger.LogInformation( + "Requesting RepublishAsync for {SubscriptionId}-{SequenceNumber}", + subscriptionId, + sequenceNumber); - errorsForPass = errorsForNextPass; - errorsForNextPass = []; + // request republish. + RepublishResponse response = await RepublishAsync( + requestHeader, + subscriptionId, + sequenceNumber, + ct) + .ConfigureAwait(false); + ResponseHeader responseHeader = response.ResponseHeader; + NotificationMessage notificationMessage = response.NotificationMessage; - nodesToBrowseForPass = nodesToBrowseForNextPass; - nodesToBrowseForNextPass = []; + m_logger.LogInformation( + "Received RepublishAsync for {SubscriptionId}-{SequenceNumber}-{ServiceResult}", + subscriptionId, + sequenceNumber, + responseHeader.ServiceResult); - 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."); - } + // process response. + ProcessPublishResponse( + responseHeader, + subscriptionId, + null, + false, + notificationMessage); - passCount++; - } while (nodesToBrowseForPass.Count > 0); + return (true, ServiceResult.Good); } - catch (Exception ex) + catch (Exception e) { - m_logger.LogError(ex, "ManagedBrowse failed"); + return ProcessRepublishResponseError(e, subscriptionId, sequenceNumber); } - - return (result, errors); - } - - /// - /// Used to pass on references to the Service results in the loop in ManagedBrowseAsync. - /// - /// - private class ReferenceWrapper - { - public T Reference { get; set; } } /// - /// Call the browse service asynchronously and call browse next, - /// if applicable, immediately afterwards. Observe proper treatment - /// of specific service results, specifically - /// BadNoContinuationPoint and BadContinuationPointInvalid + /// Recreate the subscriptions in a recreated session. /// - private async Task<(IList, IList)> BrowseWithBrowseNextAsync( - RequestHeader requestHeader, - ViewDescription view, - List nodeIds, - uint maxResultsToReturn, - BrowseDirection browseDirection, - NodeId referenceTypeId, - bool includeSubtypes, - uint nodeClassMask, - CancellationToken ct = default) + /// Uses Transfer service + /// if set to true. + /// The template for the subscriptions. + /// Cancellation token to cancel operation with + private async Task RecreateSubscriptionsAsync( + bool transferSbscriptionTemplates, + IEnumerable subscriptionsTemplate, + CancellationToken ct) { - if (requestHeader != null) - { - requestHeader.RequestHandle = 0; - } - - var result = new List(nodeIds.Count); - - ( - _, - ByteStringCollection continuationPoints, - IList referenceDescriptions, - IList errors - ) = await BrowseAsync( - requestHeader, - view, - nodeIds, - maxResultsToReturn, - browseDirection, - referenceTypeId, - includeSubtypes, - 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++) + using Activity? activity = m_telemetry.StartActivity(); + bool transferred = false; + if (transferSbscriptionTemplates) { - if (continuationPoints[ii] != null && - !StatusCode.IsBad(previousErrors[ii].Reference.StatusCode)) + try { - nextContinuationPoints.Add(continuationPoints[ii]); - nextResults.Add(previousResults[ii]); - nextErrors.Add(previousErrors[ii]); + transferred = await TransferSubscriptionsAsync( + [.. subscriptionsTemplate], + false, + ct) + .ConfigureAwait(false); } - } - while (nextContinuationPoints.Count > 0) - { - if (requestHeader != null) + catch (ServiceResultException sre) { - requestHeader.RequestHandle = 0; + if (sre.StatusCode == StatusCodes.BadServiceUnsupported) + { + TransferSubscriptionsOnReconnect = false; + m_logger.LogWarning( + "Transfer subscription unsupported, TransferSubscriptionsOnReconnect set to false."); + } + else + { + m_logger.LogError(sre, "Transfer subscriptions failed."); + } } - - ( - _, - ByteStringCollection revisedContinuationPoints, - IList browseNextResults, - IList browseNextErrors - ) = await BrowseNextAsync(requestHeader, nextContinuationPoints, false, ct) - .ConfigureAwait(false); - - for (int ii = 0; ii < browseNextResults.Count; ii++) + catch (Exception ex) { - nextResults[ii].AddRange(browseNextResults[ii]); - nextErrors[ii].Reference = browseNextErrors[ii]; + m_logger.LogError(ex, "Unexpected Transfer subscriptions error."); } + } - previousResults = nextResults; - previousErrors = nextErrors; - - nextResults = []; - nextErrors = []; - nextContinuationPoints = []; - - for (int ii = 0; ii < revisedContinuationPoints.Count; ii++) + if (!transferred) + { + // Create the subscriptions which were not transferred. + foreach (Subscription subscription in Subscriptions) { - if (revisedContinuationPoints[ii] != null && - !StatusCode.IsBad(browseNextErrors[ii].StatusCode)) + if (!subscription.Created) { - nextContinuationPoints.Add(revisedContinuationPoints[ii]); - nextResults.Add(previousResults[ii]); - nextErrors.Add(previousErrors[ii]); + await subscription.CreateAsync(ct).ConfigureAwait(false); } } } - var finalErrors = new List(errorAnchors.Count); - foreach (ReferenceWrapper errorReference in errorAnchors) - { - finalErrors.Add(errorReference.Reference); - } - - return (result, finalErrors); } /// - public async Task> CallAsync( - NodeId objectId, - NodeId methodId, - CancellationToken ct = default, - params object[] args) + public bool AddSubscription(Subscription subscription) { - var inputArguments = new VariantCollection(); - - if (args != null) + ThrowIfDisposed(); + if (subscription == null) { - for (int ii = 0; ii < args.Length; ii++) - { - inputArguments.Add(new Variant(args[ii])); - } + throw new ArgumentNullException(nameof(subscription)); } - var request = new CallMethodRequest + lock (SyncRoot) { - ObjectId = objectId, - MethodId = methodId, - InputArguments = inputArguments - }; + if (m_subscriptions.Contains(subscription)) + { + return false; + } - var requests = new CallMethodRequestCollection { request }; + subscription.Session = this; + subscription.Telemetry = m_telemetry; + m_subscriptions.Add(subscription); + } - CallMethodResultCollection results; - DiagnosticInfoCollection diagnosticInfos; + m_SubscriptionsChanged?.Invoke(this, EventArgs.Empty); - CallResponse response = await base.CallAsync(null, requests, ct).ConfigureAwait(false); - - results = response.Results; - diagnosticInfos = response.DiagnosticInfos; - - ValidateResponse(results, requests); - ValidateDiagnosticInfos(diagnosticInfos, requests); - - if (StatusCode.IsBad(results[0].StatusCode)) - { - throw ServiceResultException.Create( - results[0].StatusCode, - 0, - diagnosticInfos, - response.ResponseHeader.StringTable); - } - - var outputArguments = new List(); - - foreach (Variant arg in results[0].OutputArguments) - { - outputArguments.Add(arg.Value); - } - - return outputArguments; - } - - /// - public async Task FetchReferencesAsync( - NodeId nodeId, - CancellationToken ct = default) - { - (IList descriptions, _) = await ManagedBrowseAsync( - null, - null, - [nodeId], - 0, - BrowseDirection.Both, - null, - true, - 0, - ct) - .ConfigureAwait(false); - return descriptions[0]; - } - - /// - public Task<(IList, IList)> FetchReferencesAsync( - IList nodeIds, - CancellationToken ct = default) - { - return ManagedBrowseAsync( - null, - null, - nodeIds, - 0, - BrowseDirection.Both, - null, - true, - 0, - ct); - } - - /// - /// Recreates a session based on a specified template. - /// - /// The Session object to use as template - /// Cancellation Token to cancel operation with - /// The new session object. - public static async Task RecreateAsync( - Session sessionTemplate, - CancellationToken ct = default) - { - ServiceMessageContext messageContext = sessionTemplate.m_configuration - .CreateMessageContext(); - messageContext.Factory = sessionTemplate.Factory; - - // create the channel object used to connect to the server. - ITransportChannel channel = await UaChannelBase.CreateUaBinaryChannelAsync( - sessionTemplate.m_configuration, - sessionTemplate.ConfiguredEndpoint.Description, - sessionTemplate.ConfiguredEndpoint.Configuration, - sessionTemplate.m_instanceCertificate, - sessionTemplate.m_configuration.SecurityConfiguration.SendCertificateChain - ? sessionTemplate.m_instanceCertificateChain - : null, - messageContext, - ct).ConfigureAwait(false); - - // create the session object. - Session session = sessionTemplate.CloneSession(channel, true); - - try - { - session.RecreateRenewUserIdentity(); - // open the session. - await session - .OpenAsync( - sessionTemplate.SessionName, - (uint)sessionTemplate.SessionTimeout, - session.Identity, - sessionTemplate.PreferredLocales, - sessionTemplate.m_checkDomain, - ct) - .ConfigureAwait(false); - - await session.RecreateSubscriptionsAsync(sessionTemplate.Subscriptions, ct) - .ConfigureAwait(false); - } - catch (Exception e) - { - session.Dispose(); - ThrowCouldNotRecreateSessionException(e, sessionTemplate.m_sessionName); - } - - return session; - } - - /// - /// Recreates a session based on a specified template. - /// - /// The Session object to use as template - /// The waiting reverse connection. - /// Cancelation token to cancel operation with - /// The new session object. - public static async Task RecreateAsync( - Session sessionTemplate, - ITransportWaitingConnection connection, - CancellationToken ct = default) - { - ServiceMessageContext messageContext = sessionTemplate.m_configuration - .CreateMessageContext(); - messageContext.Factory = sessionTemplate.Factory; - - // create the channel object used to connect to the server. - ITransportChannel channel = await UaChannelBase.CreateUaBinaryChannelAsync( - sessionTemplate.m_configuration, - connection, - sessionTemplate.m_endpoint.Description, - sessionTemplate.m_endpoint.Configuration, - sessionTemplate.m_instanceCertificate, - sessionTemplate.m_configuration.SecurityConfiguration.SendCertificateChain - ? sessionTemplate.m_instanceCertificateChain - : null, - messageContext, - ct).ConfigureAwait(false); - - // create the session object. - Session session = sessionTemplate.CloneSession(channel, true); - - try - { - session.RecreateRenewUserIdentity(); - // open the session. - await session - .OpenAsync( - sessionTemplate.m_sessionName, - (uint)sessionTemplate.m_sessionTimeout, - session.Identity, - sessionTemplate.m_preferredLocales, - sessionTemplate.m_checkDomain, - ct) - .ConfigureAwait(false); - - await session.RecreateSubscriptionsAsync(sessionTemplate.Subscriptions, ct) - .ConfigureAwait(false); - } - catch (Exception e) - { - session.Dispose(); - ThrowCouldNotRecreateSessionException(e, sessionTemplate.m_sessionName); - } - - return session; - } - - /// - /// Recreates a session based on a specified template using the provided channel. - /// - /// The Session object to use as template - /// The waiting reverse connection. - /// Cancellation token to cancel the operation with - /// The new session object. - public static async Task RecreateAsync( - Session sessionTemplate, - ITransportChannel transportChannel, - CancellationToken ct = default) - { - if (transportChannel == null) - { - return await RecreateAsync(sessionTemplate, ct).ConfigureAwait(false); - } - - ServiceMessageContext messageContext = sessionTemplate.m_configuration - .CreateMessageContext(); - messageContext.Factory = sessionTemplate.Factory; - - // create the session object. - Session session = sessionTemplate.CloneSession(transportChannel, true); - - try - { - session.RecreateRenewUserIdentity(); - // open the session. - await session - .OpenAsync( - sessionTemplate.m_sessionName, - (uint)sessionTemplate.m_sessionTimeout, - session.Identity, - sessionTemplate.m_preferredLocales, - sessionTemplate.m_checkDomain, - false, - ct) - .ConfigureAwait(false); - - // create the subscriptions. - foreach (Subscription subscription in session.Subscriptions) - { - await subscription.CreateAsync(ct).ConfigureAwait(false); - } - } - catch (Exception e) - { - session.Dispose(); - ThrowCouldNotRecreateSessionException(e, sessionTemplate.m_sessionName); - } - - return session; - } - - /// - public override Task CloseAsync(CancellationToken ct = default) - { - return CloseAsync(m_keepAliveInterval, true, ct); - } - - /// - public Task CloseAsync(bool closeChannel, CancellationToken ct = default) - { - return CloseAsync(m_keepAliveInterval, closeChannel, ct); - } - - /// - public Task CloseAsync(int timeout, CancellationToken ct = default) - { - return CloseAsync(timeout, true, ct); - } + return true; + } /// - public virtual async Task CloseAsync( - int timeout, - bool closeChannel, - CancellationToken ct = default) + public bool RemoveTransferredSubscription(Subscription subscription) { - // check if already called. - if (Disposed) - { - return StatusCodes.Good; - } - - StatusCode result = StatusCodes.Good; - - Closing = true; - - try - { - // stop the keep alive timer. - await StopKeepAliveTimerAsync().ConfigureAwait(false); - - // check if correctly connected. - bool connected = Connected; - - // halt all background threads. - if (connected && m_SessionClosing != null) - { - try - { - m_SessionClosing(this, null); - } - catch (Exception e) - { - m_logger.LogError(e, "Session: Unexpected error raising SessionClosing event."); - } - } - - // close the session with the server. - if (connected) - { - try - { - // Wait for or cancel outstanding publish requests before closing session. - await WaitForOrCancelOutstandingPublishRequestsAsync(ct).ConfigureAwait(false); - - // close the session and delete all subscriptions if specified. - var requestHeader = new RequestHeader - { - TimeoutHint = timeout > 0 - ? (uint)timeout - : (uint)(OperationTimeout > 0 ? OperationTimeout : 0) - }; - CloseSessionResponse response = await base.CloseSessionAsync( - requestHeader, - DeleteSubscriptionsOnClose, - ct).ConfigureAwait(false); - } - // don't throw errors on disconnect, but return them - // so the caller can log the error. - catch (ServiceResultException sre) - { - m_logger.LogDebug(sre, "Error closing session during Close."); - result = sre.StatusCode; - } - catch (Exception e1) - { - m_logger.LogDebug(e1, "Error closing session during Close."); - result = StatusCodes.Bad; - } - finally - { - if (closeChannel) - { - try - { - await CloseChannelAsync(ct).ConfigureAwait(false); - } - catch (Exception e2) - { - m_logger.LogDebug(e2, "Error closing channel during Close"); - } - } - - // raised notification indicating the session is closed. - SessionCreated(null, null); - } - } - - // clean up. - if (closeChannel) - { - Dispose(); - } - - return result; - } - finally + ThrowIfDisposed(); + if (subscription == null) { - Closing = false; - } - } - - /// - public Task ReconnectAsync(CancellationToken ct) - { - return ReconnectAsync(null, null, ct); - } - - /// - public Task ReconnectAsync(ITransportWaitingConnection connection, CancellationToken ct) - { - return ReconnectAsync(connection, null, ct); - } - - /// - public Task ReconnectAsync(ITransportChannel channel, CancellationToken ct) - { - return ReconnectAsync(null, channel, ct); - } - - /// - public async Task ReloadInstanceCertificateAsync(CancellationToken ct = default) - { - ThrowIfDisposed(); - await m_reconnectLock.WaitAsync(ct).ConfigureAwait(false); - try - { - await LoadInstanceCertificateAsync(clientCertificate: null, ct).ConfigureAwait(false); - } - finally - { - m_reconnectLock.Release(); - } - } - - /// - /// Reconnects to the server after a network failure using a waiting connection. - /// - /// - private async Task ReconnectAsync( - ITransportWaitingConnection connection, - ITransportChannel transportChannel, - CancellationToken ct) - { - ThrowIfDisposed(); - bool resetReconnect = false; - await m_reconnectLock.WaitAsync(ct).ConfigureAwait(false); - try - { - bool reconnecting = Reconnecting; - Reconnecting = true; - resetReconnect = true; - m_reconnectLock.Release(); - - // check if already connecting. - if (reconnecting) - { - m_logger.LogWarning("Session is already attempting to reconnect."); - - throw ServiceResultException.Create( - StatusCodes.BadInvalidState, - "Session is already attempting to reconnect."); - } - - m_logger.LogInformation("Session RECONNECT {SessionId} starting...", SessionId); - - await StopKeepAliveTimerAsync().ConfigureAwait(false); - - // create the client signature. - byte[] dataToSign = Utils.Append(m_serverCertificate?.RawData, m_serverNonce); - EndpointDescription endpoint = m_endpoint.Description; - SignatureData clientSignature = SecurityPolicies.Sign( - m_instanceCertificate, - endpoint.SecurityPolicyUri, - dataToSign); - - // check that the user identity is supported by the endpoint. - UserTokenPolicy identityPolicy = endpoint.FindUserTokenPolicy( - m_identity.TokenType, - m_identity.IssuedTokenType, - endpoint.SecurityPolicyUri); - - if (identityPolicy == null) - { - m_logger.LogError( - "Reconnect: Endpoint does not support the user identity type provided."); - - throw ServiceResultException.Create( - StatusCodes.BadUserAccessDenied, - "Endpoint does not support the user identity type provided."); - } - - // select the security policy for the user token. - string tokenSecurityPolicyUri = identityPolicy.SecurityPolicyUri; - - if (string.IsNullOrEmpty(tokenSecurityPolicyUri)) - { - tokenSecurityPolicyUri = endpoint.SecurityPolicyUri; - } - m_userTokenSecurityPolicyUri = tokenSecurityPolicyUri; - - // need to refresh the identity (reprompt for password, refresh token). - if (m_RenewUserIdentity != null) - { - m_identity = m_RenewUserIdentity(this, m_identity); - } - - // validate server nonce and security parameters for user identity. - ValidateServerNonce( - m_identity, - m_serverNonce, - tokenSecurityPolicyUri, - m_previousServerNonce, - m_endpoint.Description.SecurityMode); - - // sign data with user token. - UserIdentityToken identityToken = m_identity.GetIdentityToken(); - identityToken.PolicyId = identityPolicy.PolicyId; - SignatureData userTokenSignature = identityToken.Sign( - dataToSign, - tokenSecurityPolicyUri, - m_telemetry); - - // encrypt token. - identityToken.Encrypt( - m_serverCertificate, - m_serverNonce, - m_userTokenSecurityPolicyUri, - MessageContext, - m_eccServerEphemeralKey, - m_instanceCertificate, - m_instanceCertificateChain, - m_endpoint.Description.SecurityMode != MessageSecurityMode.None); - - // send the software certificates assigned to the client. - SignedSoftwareCertificateCollection clientSoftwareCertificates - = GetSoftwareCertificates(); - - m_logger.LogInformation("Session REPLACING channel for {SessionId}.", SessionId); - - if (connection != null) - { - ITransportChannel channel = NullableTransportChannel; - - // check if the channel supports reconnect. - if (channel != null && - (channel.SupportedFeatures & TransportChannelFeatures.Reconnect) != 0) - { - await channel.ReconnectAsync(connection, ct).ConfigureAwait(false); - } - else - { - // initialize the channel which will be created with the server. - channel = await UaChannelBase.CreateUaBinaryChannelAsync( - m_configuration, - connection, - m_endpoint.Description, - m_endpoint.Configuration, - m_instanceCertificate, - m_configuration.SecurityConfiguration.SendCertificateChain - ? m_instanceCertificateChain - : null, - MessageContext, - ct).ConfigureAwait(false); - - // disposes the existing channel. - TransportChannel = channel; - } - } - else if (transportChannel != null) - { - TransportChannel = transportChannel; - } - else - { - ITransportChannel channel = NullableTransportChannel; - - // check if the channel supports reconnect. - if (channel != null && - (channel.SupportedFeatures & TransportChannelFeatures.Reconnect) != 0) - { - await channel.ReconnectAsync(ct: ct).ConfigureAwait(false); - } - else - { - // initialize the channel which will be created with the server. - channel = await UaChannelBase.CreateUaBinaryChannelAsync( - m_configuration, - m_endpoint.Description, - m_endpoint.Configuration, - m_instanceCertificate, - m_configuration.SecurityConfiguration.SendCertificateChain - ? m_instanceCertificateChain - : null, - MessageContext, - ct).ConfigureAwait(false); - - // disposes the existing channel. - TransportChannel = channel; - } - } - - m_logger.LogInformation("Session RE-ACTIVATING {SessionId}.", SessionId); - - var header = new RequestHeader { TimeoutHint = kReconnectTimeout }; - - using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct); - timeout.CancelAfter(TimeSpan.FromMilliseconds(kReconnectTimeout / 2)); - try - { - // reactivate session. - ActivateSessionResponse activateResult = await ActivateSessionAsync( - header, - clientSignature, - null, - m_preferredLocales, - new ExtensionObject(identityToken), - userTokenSignature, - timeout.Token).ConfigureAwait(false); - - byte[] serverNonce = activateResult.ServerNonce; - StatusCodeCollection certificateResults = activateResult.Results; - DiagnosticInfoCollection certificateDiagnosticInfos = activateResult.DiagnosticInfos; - - m_logger.LogInformation("Session RECONNECT {SessionId} completed successfully.", SessionId); - - lock (SyncRoot) - { - m_previousServerNonce = m_serverNonce; - m_serverNonce = serverNonce; - } - - await m_reconnectLock.WaitAsync(ct).ConfigureAwait(false); - Reconnecting = false; - resetReconnect = false; - m_reconnectLock.Release(); - - StartPublishing(OperationTimeout, true); - - await StartKeepAliveTimerAsync().ConfigureAwait(false); - - IndicateSessionConfigurationChanged(); - } - catch (OperationCanceledException) - when (timeout.IsCancellationRequested && !ct.IsCancellationRequested) - { - var error = ServiceResult.Create( - StatusCodes.BadRequestTimeout, - "ACTIVATE SESSION timed out. {0}/{1}", - GoodPublishRequestCount, - OutstandingRequestCount); - - m_logger.LogWarning( - "ACTIVATE SESSION ASYNC timed out. {GoodRequestCount}/{OutstandingRequestCount}", - GoodPublishRequestCount, - OutstandingRequestCount); - throw new ServiceResultException(error); - } - } - finally - { - if (resetReconnect) - { - await m_reconnectLock.WaitAsync(ct).ConfigureAwait(false); - Reconnecting = false; - m_reconnectLock.Release(); - } - } - } - - /// - public async Task<(bool, ServiceResult)> RepublishAsync( - uint subscriptionId, - uint sequenceNumber, - CancellationToken ct) - { - // send republish request. - var requestHeader = new RequestHeader - { - TimeoutHint = (uint)OperationTimeout, - ReturnDiagnostics = (uint)(int)ReturnDiagnostics, - RequestHandle = Utils.IncrementIdentifier(ref m_publishCounter) - }; - - try - { - m_logger.LogInformation( - "Requesting RepublishAsync for {SubscriptionId}-{SequenceNumber}", - subscriptionId, - sequenceNumber); - - // request republish. - RepublishResponse response = await RepublishAsync( - requestHeader, - subscriptionId, - sequenceNumber, - ct) - .ConfigureAwait(false); - ResponseHeader responseHeader = response.ResponseHeader; - NotificationMessage notificationMessage = response.NotificationMessage; - - m_logger.LogInformation( - "Received RepublishAsync for {SubscriptionId}-{SequenceNumber}-{ServiceResult}", - subscriptionId, - sequenceNumber, - responseHeader.ServiceResult); - - // process response. - ProcessPublishResponse( - responseHeader, - subscriptionId, - null, - false, - notificationMessage); - - return (true, ServiceResult.Good); - } - catch (Exception e) - { - return ProcessRepublishResponseError(e, subscriptionId, sequenceNumber); - } - } - - /// - /// Recreate the subscriptions in a reconnected session. - /// Uses Transfer service if is set to true. - /// - /// The template for the subscriptions. - /// Cancelation token to cancel operation with - private async Task RecreateSubscriptionsAsync( - IEnumerable subscriptionsTemplate, - CancellationToken ct) - { - bool transferred = false; - if (TransferSubscriptionsOnReconnect) - { - try - { - transferred = await TransferSubscriptionsAsync( - [.. subscriptionsTemplate], - false, - ct) - .ConfigureAwait(false); - } - catch (ServiceResultException sre) - { - if (sre.StatusCode == StatusCodes.BadServiceUnsupported) - { - TransferSubscriptionsOnReconnect = false; - m_logger.LogWarning( - "Transfer subscription unsupported, TransferSubscriptionsOnReconnect set to false."); - } - else - { - m_logger.LogError(sre, "Transfer subscriptions failed."); - } - } - catch (Exception ex) - { - m_logger.LogError(ex, "Unexpected Transfer subscriptions error."); - } - } - - if (!transferred) - { - // Create the subscriptions which were not transferred. - foreach (Subscription subscription in Subscriptions) - { - if (!subscription.Created) - { - await subscription.CreateAsync(ct).ConfigureAwait(false); - } - } - } - } - - /// - public bool AddSubscription(Subscription subscription) - { - ThrowIfDisposed(); - if (subscription == null) - { - throw new ArgumentNullException(nameof(subscription)); - } - - lock (SyncRoot) - { - if (m_subscriptions.Contains(subscription)) - { - return false; - } - - subscription.Session = this; - subscription.Telemetry = m_telemetry; - m_subscriptions.Add(subscription); - } - - m_SubscriptionsChanged?.Invoke(this, null); - - return true; - } - - /// - public bool RemoveTransferredSubscription(Subscription subscription) - { - ThrowIfDisposed(); - if (subscription == null) - { - throw new ArgumentNullException(nameof(subscription)); - } - - if (subscription.Session != this) - { - return false; - } - - lock (SyncRoot) - { - if (!m_subscriptions.Remove(subscription)) - { - return false; - } - - subscription.Session = null; - } - - m_SubscriptionsChanged?.Invoke(this, null); - - return true; - } - - /// - /// Returns the software certificates assigned to the application. - /// - protected virtual SignedSoftwareCertificateCollection GetSoftwareCertificates() - { - return []; - } - - /// - /// Handles an error when validating the application instance certificate provided by the server. - /// - /// - protected virtual void OnApplicationCertificateError( - byte[] serverCertificate, - ServiceResult result) - { - throw new ServiceResultException(result); - } - - /// - /// Handles an error when validating software certificates provided by the server. - /// - /// - protected virtual void OnSoftwareCertificateError( - SignedSoftwareCertificate signedCertificate, - ServiceResult result) - { - throw new ServiceResultException(result); - } - - /// - /// Inspects the software certificates provided by the server. - /// - protected virtual void ValidateSoftwareCertificates( - List softwareCertificates) - { - // always accept valid certificates. - } - - /// - /// Starts a timer to check that the connection to the server is still available. - /// - private async ValueTask StartKeepAliveTimerAsync() - { - int keepAliveInterval = m_keepAliveInterval; - - m_lastKeepAliveErrorStatusCode = StatusCodes.Good; - Interlocked.Exchange(ref m_lastKeepAliveTime, DateTime.UtcNow.Ticks); - LastKeepAliveTickCount = HiResClock.TickCount; - - m_serverState = ServerState.Unknown; - - var nodesToRead = new ReadValueIdCollection - { - // read the server state. - new ReadValueId - { - NodeId = Variables.Server_ServerStatus_State, - AttributeId = Attributes.Value, - DataEncoding = null, - IndexRange = null - } - }; - - await StopKeepAliveTimerAsync().ConfigureAwait(false); - - lock (SyncRoot) - { - ThrowIfDisposed(); - - if (m_keepAliveWorker == null) - { - m_keepAliveCancellation = new CancellationTokenSource(); - - // start timer - m_keepAliveWorker = Task - .Factory.StartNew( - () => OnSendKeepAliveAsync( - nodesToRead, - m_keepAliveCancellation.Token), - m_keepAliveCancellation.Token, - TaskCreationOptions.LongRunning, - TaskScheduler.Default); - } - - // send initial keep alive. - m_keepAliveTimer.Change(0, m_keepAliveInterval); - } - } - - /// - /// Reset the timer used to send keep alive messages. - /// - private void ResetKeepAliveTimer() - { - lock (SyncRoot) - { - if (m_keepAliveWorker != null) - { - m_keepAliveTimer.Change(m_keepAliveInterval, m_keepAliveInterval); - } - } - } - - /// - /// Stops the keep alive timer. - /// - private async ValueTask StopKeepAliveTimerAsync() - { - Task keepAliveWorker; - CancellationTokenSource keepAliveCancellation; - - lock (SyncRoot) - { - ThrowIfDisposed(); - - keepAliveWorker = m_keepAliveWorker; - keepAliveCancellation = m_keepAliveCancellation; - - m_keepAliveWorker = null; - m_keepAliveCancellation = null; - - m_keepAliveTimer.Change(Timeout.Infinite, Timeout.Infinite); - } - - if (keepAliveWorker == null) - { - Debug.Assert(keepAliveCancellation == null); - return; - } - try - { - keepAliveCancellation.Cancel(); - await keepAliveWorker.ConfigureAwait(false); - } - catch (OperationCanceledException) - { - } - catch (Exception ex) - { - m_logger.LogDebug(ex, "Keep alive task did not stop cleanly."); - } - finally - { - keepAliveCancellation.Dispose(); - } - } - - /// - /// Waits for outstanding publish requests to complete or cancels them. - /// - private async Task WaitForOrCancelOutstandingPublishRequestsAsync(CancellationToken ct) - { - // Get outstanding publish requests - List publishRequestHandles = []; - lock (m_outstandingRequests) - { - foreach (AsyncRequestState state in m_outstandingRequests) - { - if (state.RequestTypeId == DataTypes.PublishRequest && !state.Defunct) - { - publishRequestHandles.Add(state.RequestId); - } - } - } - - if (publishRequestHandles.Count == 0) - { - m_logger.LogDebug("No outstanding publish requests to cancel."); - return; - } - - m_logger.LogInformation( - "Waiting for {Count} outstanding publish requests to complete before closing session.", - publishRequestHandles.Count); - - // Wait for outstanding requests with timeout - if (PublishRequestCancelDelayOnCloseSession != 0) - { - int waitTimeout = PublishRequestCancelDelayOnCloseSession < 0 - ? int.MaxValue - : PublishRequestCancelDelayOnCloseSession; - - int startTime = HiResClock.TickCount; - while (true) - { - // Check if all publish requests completed - int remainingCount = 0; - lock (m_outstandingRequests) - { - foreach (AsyncRequestState state in m_outstandingRequests) - { - if (state.RequestTypeId == DataTypes.PublishRequest && !state.Defunct) - { - remainingCount++; - } - } - } - - if (remainingCount == 0) - { - m_logger.LogDebug("All outstanding publish requests completed."); - return; - } - - // Check timeout - int elapsed = HiResClock.TickCount - startTime; - if (elapsed >= waitTimeout) - { - m_logger.LogWarning( - "Timeout waiting for {Count} publish requests to complete. Cancelling them.", - remainingCount); - break; - } - - // Check cancellation - if (ct.IsCancellationRequested) - { - m_logger.LogWarning("Cancellation requested while waiting for publish requests."); - break; - } - - // Wait a bit before checking again - try - { - await Task.Delay(100, ct).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - m_logger.LogWarning("Cancellation requested while waiting for publish requests."); - break; - } - } - } - - // Cancel remaining outstanding publish requests - List requestsToCancel = []; - lock (m_outstandingRequests) - { - foreach (AsyncRequestState state in m_outstandingRequests) - { - if (state.RequestTypeId == DataTypes.PublishRequest && !state.Defunct) - { - requestsToCancel.Add(state.RequestId); - } - } - } - - if (requestsToCancel.Count > 0) - { - m_logger.LogInformation( - "Cancelling {Count} outstanding publish requests.", - requestsToCancel.Count); - - // Cancel each outstanding publish request - foreach (uint requestHandle in requestsToCancel) - { - try - { - var requestHeader = new RequestHeader - { - TimeoutHint = (uint)OperationTimeout - }; - - await CancelAsync(requestHeader, requestHandle, ct).ConfigureAwait(false); - - m_logger.LogDebug("Cancelled publish request with handle {Handle}.", requestHandle); - } - catch (Exception ex) - { - // Log but don't throw - we're closing anyway - m_logger.LogWarning( - ex, - "Error cancelling publish request with handle {Handle}.", - requestHandle); - } - } - } - } - - /// - /// Removes a completed async request. - /// - private AsyncRequestState RemoveRequest(Task result, uint requestId, uint typeId) - { - lock (m_outstandingRequests) - { - for (LinkedListNode ii = m_outstandingRequests.First; - ii != null; - ii = ii.Next) - { - if (ReferenceEquals(result, ii.Value.Result) || - (requestId == ii.Value.RequestId && typeId == ii.Value.RequestTypeId)) - { - AsyncRequestState state = ii.Value; - m_outstandingRequests.Remove(ii); - return state; - } - } - - return null; - } - } - - /// - /// Adds a new async request. - /// - private void AsyncRequestStarted(Task result, uint requestId, uint typeId) - { - lock (m_outstandingRequests) - { - // check if the request completed asynchronously. - AsyncRequestState state = RemoveRequest(result, requestId, typeId); - - // add a new request. - if (state == null) - { - state = new AsyncRequestState - { - Defunct = false, - RequestId = requestId, - RequestTypeId = typeId, - Result = result, - TickCount = HiResClock.TickCount - }; - - m_outstandingRequests.AddLast(state); - } - } - } - - /// - /// Removes a completed async request. - /// - private void AsyncRequestCompleted(Task result, uint requestId, uint typeId) - { - lock (m_outstandingRequests) - { - // remove the request. - AsyncRequestState state = RemoveRequest(result, requestId, typeId); - - if (state != null) - { - // mark any old requests as default (i.e. the should have returned before this request). - const int maxAge = 1000; - - for (LinkedListNode ii = m_outstandingRequests.First; - ii != null; - ii = ii.Next) - { - if (ii.Value.RequestTypeId == typeId && - (state.TickCount - ii.Value.TickCount) > maxAge) - { - ii.Value.Defunct = true; - } - } - } - - // add a dummy placeholder since the begin request has not completed yet. - if (state == null) - { - state = new AsyncRequestState - { - Defunct = true, - RequestId = requestId, - RequestTypeId = typeId, - Result = result, - TickCount = HiResClock.TickCount - }; - - m_outstandingRequests.AddLast(state); - } - } - } - - /// - /// Sends a keep alive by reading from the server. - /// - private async Task OnSendKeepAliveAsync( - ReadValueIdCollection nodesToRead, - CancellationToken ct) - { - while (!ct.IsCancellationRequested && !Disposed) - { - await m_keepAliveEvent.WaitAsync(ct).ConfigureAwait(false); - try - { - // check if session has been closed. - if (!Connected || Disposed) - { - continue; - } - - // check if session has been closed. - if (Reconnecting) - { - m_logger.LogWarning( - "Session {SessionId}: KeepAlive ignored while reconnecting.", - SessionId); - continue; - } - - // raise error if keep alives are not coming back. - if (KeepAliveStopped && - !OnKeepAliveError( - ServiceResult.Create( - StatusCodes.BadNoCommunication, - "Server not responding to keep alive requests.") - )) - { - continue; - } - - var requestHeader = new RequestHeader - { - RequestHandle = Utils.IncrementIdentifier(ref m_keepAliveCounter), - TimeoutHint = (uint)(KeepAliveInterval * 2), - ReturnDiagnostics = 0 - }; - - ReadResponse result = await ReadAsync( - requestHeader, - 0, - TimestampsToReturn.Neither, - nodesToRead, - ct).ConfigureAwait(false); - - // read the server status. - DataValueCollection values = result.Results; - DiagnosticInfoCollection diagnosticInfos = result.DiagnosticInfos; - ResponseHeader responseHeader = result.ResponseHeader; - - ValidateResponse(values, nodesToRead); - ValidateDiagnosticInfos(diagnosticInfos, nodesToRead); - - // validate value returned. - ServiceResult error = ValidateDataValue( - values[0], - typeof(int), - 0, - diagnosticInfos, - responseHeader); - - if (ServiceResult.IsBad(error)) - { - m_logger.LogError("Keep alive read failed: {ServiceResult}, EndpointUrl={EndpointUrl}, RequestCount={Good}/{Outstanding}", - error, - Endpoint?.EndpointUrl, - GoodPublishRequestCount, - OutstandingRequestCount); - throw new ServiceResultException(error); - } - - // send notification that keep alive completed. - OnKeepAlive((ServerState)(int)values[0].Value, responseHeader.Timestamp); - } - catch (ServiceResultException sre) - { - // recover from error condition when secure channel is still alive - OnKeepAliveError(sre.Result); - } - catch (ObjectDisposedException) when (Disposed) - { - // This should not happen, but we fail gracefully anyway - } - catch (Exception e) - { - m_logger.LogError( - "Could not send keep alive request: {RequestType} {Message}", - e.GetType().FullName, - e.Message); - } - } - } - - /// - /// Called when the server returns a keep alive response. - /// - protected virtual void OnKeepAlive(ServerState currentState, DateTime currentTime) - { - // restart publishing if keep alives recovered. - if (KeepAliveStopped) - { - // ignore if already reconnecting. - if (Reconnecting) - { - return; - } - - m_lastKeepAliveErrorStatusCode = StatusCodes.Good; - Interlocked.Exchange(ref m_lastKeepAliveTime, DateTime.UtcNow.Ticks); - LastKeepAliveTickCount = HiResClock.TickCount; - - lock (m_outstandingRequests) - { - for (LinkedListNode ii = m_outstandingRequests.First; - ii != null; - ii = ii.Next) - { - if (ii.Value.RequestTypeId == DataTypes.PublishRequest) - { - ii.Value.Defunct = true; - } - } - } - - StartPublishing(OperationTimeout, false); - } - else - { - m_lastKeepAliveErrorStatusCode = StatusCodes.Good; - Interlocked.Exchange(ref m_lastKeepAliveTime, DateTime.UtcNow.Ticks); - LastKeepAliveTickCount = HiResClock.TickCount; - } - - // save server state. - m_serverState = currentState; - - KeepAliveEventHandler callback = m_KeepAlive; - - if (callback != null) - { - try - { - callback(this, new KeepAliveEventArgs(null, currentState, currentTime)); - } - catch (Exception e) - { - m_logger.LogError(e, "Session: Unexpected error invoking KeepAliveCallback."); - } - } - } - - /// - /// Called when a error occurs during a keep alive. - /// - protected virtual bool OnKeepAliveError(ServiceResult result) - { - m_lastKeepAliveErrorStatusCode = result.StatusCode; - if (result.StatusCode == StatusCodes.BadNoCommunication) - { - //keep alive read timed out - int delta = HiResClock.TickCount - LastKeepAliveTickCount; - m_logger.LogInformation( - "KEEP ALIVE LATE: {Duration}ms, EndpointUrl={EndpointUrl}, RequestCount={Good}/{Outstanding}", - delta, - Endpoint?.EndpointUrl, - GoodPublishRequestCount, - OutstandingRequestCount); - } - - KeepAliveEventHandler callback = m_KeepAlive; - - if (callback != null) - { - try - { - var args = new KeepAliveEventArgs(result, ServerState.Unknown, DateTime.UtcNow); - callback(this, args); - return !args.CancelKeepAlive; - } - catch (Exception e) - { - m_logger.LogError(e, "Session: Unexpected error invoking KeepAliveCallback."); - } - } - - return true; - } - - /// - /// Prepare a list of subscriptions to delete. - /// - private bool PrepareSubscriptionsToDelete( - IEnumerable subscriptions, - List subscriptionsToDelete) - { - bool removed = false; - lock (SyncRoot) - { - foreach (Subscription subscription in subscriptions) - { - if (m_subscriptions.Remove(subscription)) - { - if (subscription.Created) - { - subscriptionsToDelete.Add(subscription); - } - - removed = true; - } - } + throw new ArgumentNullException(nameof(subscription)); } - return removed; - } - /// - /// Creates a read request with attributes determined by the NodeClass. - /// - private static void CreateNodeClassAttributesReadNodesRequest( - IList nodeIdCollection, - NodeClass nodeClass, - ReadValueIdCollection attributesToRead, - List> attributesPerNodeId, - NodeCollection nodeCollection, - bool optionalAttributes) - { - for (int ii = 0; ii < nodeIdCollection.Count; ii++) + if (subscription.Session != this) { - var node = new Node { NodeId = nodeIdCollection[ii], NodeClass = nodeClass }; + return false; + } - Dictionary attributes = CreateAttributes( - node.NodeClass, - optionalAttributes); - foreach (uint attributeId in attributes.Keys) + lock (SyncRoot) + { + if (!m_subscriptions.Remove(subscription)) { - var itemToRead = new ReadValueId - { - NodeId = node.NodeId, - AttributeId = attributeId - }; - attributesToRead.Add(itemToRead); + return false; } - nodeCollection.Add(node); - attributesPerNodeId.Add(attributes); + subscription.Session = null; } + + m_SubscriptionsChanged?.Invoke(this, EventArgs.Empty); + + return true; } /// - /// Prepares the list of node ids to read to fetch the namespace table. + /// Returns the software certificates assigned to the application. /// - private static ReadValueIdCollection PrepareNamespaceTableNodesToRead() + protected virtual SignedSoftwareCertificateCollection GetSoftwareCertificates() { - var nodesToRead = new ReadValueIdCollection(); - - // request namespace array. - var valueId = new ReadValueId - { - NodeId = Variables.Server_NamespaceArray, - AttributeId = Attributes.Value - }; - - nodesToRead.Add(valueId); - - // request server array. - valueId = new ReadValueId - { - NodeId = Variables.Server_ServerArray, - AttributeId = Attributes.Value - }; - - nodesToRead.Add(valueId); - - return nodesToRead; + return []; } /// - /// Updates the NamespaceTable with the result of the - /// read operation. + /// Handles an error when validating the application instance certificate provided by the server. /// - private void UpdateNamespaceTable( - DataValueCollection values, - DiagnosticInfoCollection diagnosticInfos, - ResponseHeader responseHeader) + /// + protected virtual void OnApplicationCertificateError( + byte[] serverCertificate, + ServiceResult result) { - // validate namespace array. - ServiceResult result = ValidateDataValue( - values[0], - typeof(string[]), - 0, - diagnosticInfos, - responseHeader); - - if (ServiceResult.IsBad(result)) - { - m_logger.LogError( - "FetchNamespaceTables: Cannot read NamespaceArray node: {StatusCOde}", - result.StatusCode); - } - else - { - NamespaceUris.Update((string[])values[0].Value); - } + throw new ServiceResultException(result); + } - // validate server array. - result = ValidateDataValue( - values[1], - typeof(string[]), - 1, - diagnosticInfos, - responseHeader); + /// + /// Handles an error when validating software certificates provided by the server. + /// + /// + protected virtual void OnSoftwareCertificateError( + SignedSoftwareCertificate signedCertificate, + ServiceResult result) + { + throw new ServiceResultException(result); + } - if (ServiceResult.IsBad(result)) - { - m_logger.LogError( - "FetchNamespaceTables: Cannot read ServerArray node: {StatusCode} ", - result.StatusCode); - } - else - { - ServerUris.Update((string[])values[1].Value); - } + /// + /// Inspects the software certificates provided by the server. + /// + protected virtual void ValidateSoftwareCertificates( + List softwareCertificates) + { + // always accept valid certificates. } /// - /// Creates a read request with attributes determined by the NodeClass. + /// Starts a timer to check that the connection to the server is still available. /// - private static void CreateAttributesReadNodesRequest( - ResponseHeader responseHeader, - ReadValueIdCollection itemsToRead, - DataValueCollection nodeClassValues, - DiagnosticInfoCollection diagnosticInfos, - ReadValueIdCollection attributesToRead, - List> attributesPerNodeId, - NodeCollection nodeCollection, - List errors, - bool optionalAttributes) + private async ValueTask StartKeepAliveTimerAsync() { - int? 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; - } + int keepAliveInterval = m_keepAliveInterval; - // check for valid node class. - nodeClass = nodeClassValues[ii].Value as int?; + m_lastKeepAliveErrorStatusCode = StatusCodes.Good; + Interlocked.Exchange(ref m_lastKeepAliveTime, DateTime.UtcNow.Ticks); + LastKeepAliveTickCount = HiResClock.TickCount; + + m_serverState = ServerState.Unknown; - if (nodeClass == null) + var nodesToRead = new ReadValueIdCollection + { + // read the server state. + new ReadValueId { - 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; + NodeId = Variables.Server_ServerStatus_State, + AttributeId = Attributes.Value, + DataEncoding = null, + IndexRange = null } + }; + + await StopKeepAliveTimerAsync().ConfigureAwait(false); - node.NodeClass = (NodeClass)nodeClass; + lock (SyncRoot) + { + ThrowIfDisposed(); - Dictionary attributes = CreateAttributes( - node.NodeClass, - optionalAttributes); - foreach (uint attributeId in attributes.Keys) + if (m_keepAliveWorker == null) { - var itemToRead = new ReadValueId - { - NodeId = node.NodeId, - AttributeId = attributeId - }; - attributesToRead.Add(itemToRead); + m_keepAliveCancellation = new CancellationTokenSource(); + + // start timer + m_keepAliveWorker = Task + .Factory.StartNew( + () => OnSendKeepAliveAsync( + nodesToRead, + m_keepAliveCancellation.Token), + m_keepAliveCancellation.Token, + TaskCreationOptions.LongRunning, + TaskScheduler.Default); } - nodeCollection.Add(node); - errors.Add(ServiceResult.Good); - attributesPerNodeId.Add(attributes); + // send initial keep alive. + m_keepAliveTimer.Change(0, m_keepAliveInterval); } } /// - /// Builds the node collection results based on the attribute values of the read response. + /// Reset the timer used to send keep alive messages. /// - /// The response header 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) + private void ResetKeepAliveTimer() { - int readIndex = 0; - for (int ii = 0; ii < nodeCollection.Count; ii++) + lock (SyncRoot) { - 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) + if (m_keepAliveWorker != null) { - errors[ii] = sre.Result; + m_keepAliveTimer.Change(m_keepAliveInterval, m_keepAliveInterval); } - readIndex += readCount; } } /// - /// Creates a Node based on the read response. + /// Stops the keep alive timer. /// - /// - private static Node ProcessReadResponse( - ResponseHeader responseHeader, - IDictionary attributes, - ReadValueIdCollection itemsToRead, - DataValueCollection values, - DiagnosticInfoCollection diagnosticInfos) + private async ValueTask StopKeepAliveTimerAsync() { - // process results. - int? nodeClass = null; + Task? keepAliveWorker; + CancellationTokenSource? keepAliveCancellation; - for (int ii = 0; ii < itemsToRead.Count; ii++) + lock (SyncRoot) { - uint attributeId = itemsToRead[ii].AttributeId; + ThrowIfDisposed(); - // 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); - } + keepAliveWorker = m_keepAliveWorker; + keepAliveCancellation = m_keepAliveCancellation; - // check for valid node class. - nodeClass = values[ii].Value as int?; + m_keepAliveWorker = null; + m_keepAliveCancellation = null; - if (nodeClass == null) - { - 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; - } + m_keepAliveTimer.Change(Timeout.Infinite, Timeout.Infinite); + } - // 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; - } + if (keepAliveWorker == null) + { + Debug.Assert(keepAliveCancellation == null); + return; + } + Debug.Assert(keepAliveCancellation != null); + try + { + keepAliveCancellation!.Cancel(); + await keepAliveWorker.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + catch (Exception ex) + { + m_logger.LogDebug(ex, "Keep alive task did not stop cleanly."); + } + finally + { + keepAliveCancellation!.Dispose(); + } + } - // all supported attributes must be readable. - if (attributeId != Attributes.Value) + /// + /// Waits for outstanding publish requests to complete or cancels them. + /// + private async Task WaitForOrCancelOutstandingPublishRequestsAsync(CancellationToken ct) + { + // Get outstanding publish requests + List publishRequestHandles = []; + lock (m_outstandingRequests) + { + foreach (AsyncRequestState state in m_outstandingRequests) + { + if (state.RequestTypeId == DataTypes.PublishRequest && !state.Defunct) { - throw ServiceResultException.Create( - values[ii].StatusCode, - ii, - diagnosticInfos, - responseHeader.StringTable); + publishRequestHandles.Add(state.RequestId); } } - - attributes[attributeId] = values[ii]; } - Node node; - DataValue value; - switch ((NodeClass)nodeClass.Value) + if (publishRequestHandles.Count == 0) { - 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."); - } + m_logger.LogDebug("No outstanding publish requests to cancel."); + return; + } - variableNode.ValueRank = value.GetValueOrDefault(); + m_logger.LogInformation( + "Waiting for {Count} outstanding publish requests to complete before closing session.", + publishRequestHandles.Count); - // ArrayDimensions Attribute - value = attributes[Attributes.ArrayDimensions]; + // Wait for outstanding requests with timeout + if (PublishRequestCancelDelayOnCloseSession != 0) + { + int waitTimeout = PublishRequestCancelDelayOnCloseSession < 0 + ? int.MaxValue + : PublishRequestCancelDelayOnCloseSession; - if (value != null) + int startTime = HiResClock.TickCount; + while (true) + { + // Check if all publish requests completed + int remainingCount = 0; + lock (m_outstandingRequests) { - if (value.Value == null) - { - variableNode.ArrayDimensions = Array.Empty(); - } - else + foreach (AsyncRequestState state in m_outstandingRequests) { - variableNode.ArrayDimensions = (uint[])value.GetValue(typeof(uint[])); + if (state.RequestTypeId == DataTypes.PublishRequest && !state.Defunct) + { + remainingCount++; + } } } - // AccessLevel Attribute - value = attributes[Attributes.AccessLevel]; - - if (value == null) + if (remainingCount == 0) { - throw ServiceResultException.Unexpected( - "Variable does not support the AccessLevel attribute."); + m_logger.LogDebug("All outstanding publish requests completed."); + return; } - variableNode.AccessLevel = value.GetValueOrDefault(); - - // UserAccessLevel Attribute - value = attributes[Attributes.UserAccessLevel]; - - if (value == null) + // Check timeout + int elapsed = HiResClock.TickCount - startTime; + if (elapsed >= waitTimeout) { - throw ServiceResultException.Unexpected( - "Variable does not support the UserAccessLevel attribute."); + m_logger.LogWarning( + "Timeout waiting for {Count} publish requests to complete. Cancelling them.", + remainingCount); + break; } - variableNode.UserAccessLevel = value.GetValueOrDefault(); - - // Historizing Attribute - value = attributes[Attributes.Historizing]; - - if (value == null) + // Check cancellation + if (ct.IsCancellationRequested) { - throw ServiceResultException.Unexpected( - "Variable does not support the Historizing attribute."); + m_logger.LogWarning("Cancellation requested while waiting for publish requests."); + break; } - variableNode.Historizing = value.GetValueOrDefault(); - - // MinimumSamplingInterval Attribute - value = attributes[Attributes.MinimumSamplingInterval]; - - if (value != null) + // Wait a bit before checking again + try { - variableNode.MinimumSamplingInterval = Convert.ToDouble( - attributes[Attributes.MinimumSamplingInterval].Value, - CultureInfo.InvariantCulture); + await Task.Delay(100, ct).ConfigureAwait(false); } - - // AccessLevelEx Attribute - value = attributes[Attributes.AccessLevelEx]; - - if (value != null) + catch (OperationCanceledException) { - variableNode.AccessLevelEx = value.GetValueOrDefault(); + m_logger.LogWarning("Cancellation requested while waiting for publish requests."); + break; } + } + } - node = variableNode; - break; - case NodeClass.VariableType: - var variableTypeNode = new VariableTypeNode(); - - // IsAbstract Attribute - value = attributes[Attributes.IsAbstract]; - - if (value == null) + // Cancel remaining outstanding publish requests + List requestsToCancel = []; + lock (m_outstandingRequests) + { + foreach (AsyncRequestState state in m_outstandingRequests) + { + if (state.RequestTypeId == DataTypes.PublishRequest && !state.Defunct) { - throw ServiceResultException.Unexpected( - "VariableType does not support the IsAbstract attribute."); + requestsToCancel.Add(state.RequestId); } + } + } - variableTypeNode.IsAbstract = value.GetValueOrDefault(); - - // DataType Attribute - value = attributes[Attributes.DataType]; + if (requestsToCancel.Count > 0) + { + m_logger.LogInformation( + "Cancelling {Count} outstanding publish requests.", + requestsToCancel.Count); - if (value == null) + // Cancel each outstanding publish request + foreach (uint requestHandle in requestsToCancel) + { + try { - throw ServiceResultException.Unexpected( - "VariableType does not support the DataType attribute."); - } - - variableTypeNode.DataType = (NodeId)value.GetValue(typeof(NodeId)); + var requestHeader = new RequestHeader + { + TimeoutHint = (uint)OperationTimeout + }; - // ValueRank Attribute - value = attributes[Attributes.ValueRank]; + await CancelAsync(requestHeader, requestHandle, ct).ConfigureAwait(false); - if (value == null) - { - throw ServiceResultException.Unexpected( - "VariableType does not support the ValueRank attribute."); + m_logger.LogDebug("Cancelled publish request with handle {Handle}.", requestHandle); } - - variableTypeNode.ValueRank = value.GetValueOrDefault(); - - // ArrayDimensions Attribute - value = attributes[Attributes.ArrayDimensions]; - - if (value != null && value.Value != null) + catch (Exception ex) { - variableTypeNode.ArrayDimensions = (uint[])value.GetValue(typeof(uint[])); + // Log but don't throw - we're closing anyway + m_logger.LogWarning( + ex, + "Error cancelling publish request with handle {Handle}.", + requestHandle); } + } + } + } - node = variableTypeNode; - break; - case NodeClass.Method: - var methodNode = new MethodNode(); - - // Executable Attribute - value = attributes[Attributes.Executable]; - - if (value == null) + /// + /// Removes a completed async request. + /// + private AsyncRequestState? RemoveRequest(Task result, uint requestId, uint typeId) + { + lock (m_outstandingRequests) + { + for (LinkedListNode? ii = m_outstandingRequests.First; + ii != null; + ii = ii.Next) + { + if (ReferenceEquals(result, ii.Value.Result) || + (requestId == ii.Value.RequestId && typeId == ii.Value.RequestTypeId)) { - throw ServiceResultException.Unexpected( - "Method does not support the Executable attribute."); + AsyncRequestState state = ii.Value; + m_outstandingRequests.Remove(ii); + return state; } + } - methodNode.Executable = value.GetValueOrDefault(); + return null; + } + } - // UserExecutable Attribute - value = attributes[Attributes.UserExecutable]; + /// + /// Adds a new async request. + /// + private void AsyncRequestStarted(Task result, Activity? activity, uint requestId, uint typeId) + { + lock (m_outstandingRequests) + { + // check if the request completed asynchronously. + AsyncRequestState? state = RemoveRequest(result, requestId, typeId); - if (value == null) + // add a new request. + if (state == null) + { + state = new AsyncRequestState { - throw ServiceResultException.Unexpected( - "Method does not support the UserExecutable attribute."); - } + Activity = activity, + Defunct = false, + RequestId = requestId, + RequestTypeId = typeId, + Result = result, + TickCount = HiResClock.TickCount + }; - methodNode.UserExecutable = value.GetValueOrDefault(); + m_outstandingRequests.AddLast(state); + } + else + { + state.Dispose(); + } + } + } - node = methodNode; - break; - case NodeClass.DataType: - var dataTypeNode = new DataTypeNode(); + /// + /// Removes a completed async request. + /// + private void AsyncRequestCompleted(Task result, uint requestId, uint typeId) + { + lock (m_outstandingRequests) + { + // remove the request. + AsyncRequestState? state = RemoveRequest(result, requestId, typeId); - // IsAbstract Attribute - value = attributes[Attributes.IsAbstract]; + if (state != null) + { + // mark any old requests as defunct (i.e. the should have returned before this request). + const int maxAge = 1000; - if (value == null) + for (LinkedListNode? ii = m_outstandingRequests.First; + ii != null; + ii = ii.Next) { - throw ServiceResultException.Unexpected( - "DataType does not support the IsAbstract attribute."); + if (ii.Value.RequestTypeId == typeId && + (state.TickCount - ii.Value.TickCount) > maxAge) + { + ii.Value.Defunct = true; + } } - dataTypeNode.IsAbstract = value.GetValueOrDefault(); - - // DataTypeDefinition Attribute - value = attributes[Attributes.DataTypeDefinition]; + state.Dispose(); + } - if (value != null) + // add a dummy placeholder since the begin request has not completed yet. + if (state == null) + { + state = new AsyncRequestState { - dataTypeNode.DataTypeDefinition = value.Value as ExtensionObject; - } - - node = dataTypeNode; - break; - case NodeClass.ReferenceType: - var referenceTypeNode = new ReferenceTypeNode(); + Defunct = true, + RequestId = requestId, + RequestTypeId = typeId, + Result = result, + TickCount = HiResClock.TickCount, + Activity = null + }; - // IsAbstract Attribute - value = attributes[Attributes.IsAbstract]; + m_outstandingRequests.AddLast(state); + } + } + } - if (value == null) + /// + /// Sends a keep alive by reading from the server. + /// + private async Task OnSendKeepAliveAsync( + ReadValueIdCollection nodesToRead, + CancellationToken ct) + { + while (!ct.IsCancellationRequested && !Disposed) + { + await m_keepAliveEvent.WaitAsync(ct).ConfigureAwait(false); + try + { + // check if session has been closed. + if (!Connected || Disposed) { - throw ServiceResultException.Unexpected( - "ReferenceType does not support the IsAbstract attribute."); + continue; } - referenceTypeNode.IsAbstract = value.GetValueOrDefault(); - - // Symmetric Attribute - value = attributes[Attributes.Symmetric]; + // check if session has been closed. + if (Reconnecting) + { + m_logger.LogWarning( + "Session {SessionId}: KeepAlive ignored while reconnecting.", + SessionId); + continue; + } - if (value == null) + // raise error if keep alives are not coming back. + if (KeepAliveStopped && + !OnKeepAliveError( + ServiceResult.Create( + StatusCodes.BadNoCommunication, + "Server not responding to keep alive requests.") + )) { - throw ServiceResultException.Unexpected( - "ReferenceType does not support the Symmetric attribute."); + continue; } - referenceTypeNode.Symmetric = value.GetValueOrDefault(); + var requestHeader = new RequestHeader + { + RequestHandle = Utils.IncrementIdentifier(ref m_keepAliveCounter), + TimeoutHint = (uint)(KeepAliveInterval * 2), + ReturnDiagnostics = 0 + }; - // InverseName Attribute - value = attributes[Attributes.InverseName]; + ReadResponse result = await ReadAsync( + requestHeader, + 0, + TimestampsToReturn.Neither, + nodesToRead, + ct).ConfigureAwait(false); - if (value != null && value.Value != null) - { - referenceTypeNode.InverseName = (LocalizedText)value.GetValue( - typeof(LocalizedText)); - } + // read the server status. + DataValueCollection values = result.Results; + DiagnosticInfoCollection diagnosticInfos = result.DiagnosticInfos; + ResponseHeader responseHeader = result.ResponseHeader; - node = referenceTypeNode; - break; - case NodeClass.View: - var viewNode = new ViewNode(); + ValidateResponse(values, nodesToRead); + ValidateDiagnosticInfos(diagnosticInfos, nodesToRead); - // EventNotifier Attribute - value = attributes[Attributes.EventNotifier]; + // validate value returned. + ServiceResult error = ValidateDataValue( + values[0], + typeof(int), + 0, + diagnosticInfos, + responseHeader); - if (value == null) + if (ServiceResult.IsBad(error)) { - throw ServiceResultException.Unexpected( - "View does not support the EventNotifier attribute."); + m_logger.LogError("Keep alive read failed: {ServiceResult}, EndpointUrl={EndpointUrl}, RequestCount={Good}/{Outstanding}", + error, + Endpoint?.EndpointUrl, + GoodPublishRequestCount, + OutstandingRequestCount); + throw new ServiceResultException(error); } - viewNode.EventNotifier = value.GetValueOrDefault(); + // send notification that keep alive completed. + OnKeepAlive((ServerState)(int)values[0].Value, responseHeader.Timestamp); + } + catch (ServiceResultException sre) + { + // recover from error condition when secure channel is still alive + OnKeepAliveError(sre.Result); + } + catch (ObjectDisposedException) when (Disposed) + { + // This should not happen, but we fail gracefully anyway + } + catch (Exception e) + { + m_logger.LogError( + "Could not send keep alive request: {RequestType} {Message}", + e.GetType().FullName, + e.Message); + } + } + } + + /// + /// Called when the server returns a keep alive response. + /// + protected virtual void OnKeepAlive(ServerState currentState, DateTime currentTime) + { + // restart publishing if keep alives recovered. + if (KeepAliveStopped) + { + // ignore if already reconnecting. + if (Reconnecting) + { + return; + } - // ContainsNoLoops Attribute - value = attributes[Attributes.ContainsNoLoops]; + m_lastKeepAliveErrorStatusCode = StatusCodes.Good; + Interlocked.Exchange(ref m_lastKeepAliveTime, DateTime.UtcNow.Ticks); + LastKeepAliveTickCount = HiResClock.TickCount; - if (value == null) + lock (m_outstandingRequests) + { + for (LinkedListNode? ii = m_outstandingRequests.First; + ii != null; + ii = ii.Next) { - throw ServiceResultException.Unexpected( - "View does not support the ContainsNoLoops attribute."); + if (ii.Value.RequestTypeId == DataTypes.PublishRequest) + { + ii.Value.Defunct = true; + } } + } - 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.Value}."); + StartPublishing(OperationTimeout, false); } - - // NodeId Attribute - value = attributes[Attributes.NodeId]; - - if (value == null) + else { - throw ServiceResultException.Unexpected( - "Node does not support the NodeId attribute."); + m_lastKeepAliveErrorStatusCode = StatusCodes.Good; + Interlocked.Exchange(ref m_lastKeepAliveTime, DateTime.UtcNow.Ticks); + LastKeepAliveTickCount = HiResClock.TickCount; } - node.NodeId = (NodeId)value.GetValue(typeof(NodeId)); - node.NodeClass = (NodeClass)nodeClass.Value; + // save server state. + m_serverState = currentState; - // BrowseName Attribute - value = attributes[Attributes.BrowseName]; + KeepAliveEventHandler? callback = m_KeepAlive; - if (value == null) + if (callback != null) { - throw ServiceResultException.Unexpected( - "Node does not support the BrowseName attribute."); + try + { + callback(this, new KeepAliveEventArgs(null, currentState, currentTime)); + } + catch (Exception e) + { + m_logger.LogError(e, "Session: Unexpected error invoking KeepAliveCallback."); + } } + } - node.BrowseName = (QualifiedName)value.GetValue(typeof(QualifiedName)); - - // DisplayName Attribute - value = attributes[Attributes.DisplayName]; - - if (value == null) + /// + /// Called when a error occurs during a keep alive. + /// + protected virtual bool OnKeepAliveError(ServiceResult result) + { + m_lastKeepAliveErrorStatusCode = result.StatusCode; + if (result.StatusCode == StatusCodes.BadNoCommunication) { - throw ServiceResultException.Unexpected( - "Node does not support the DisplayName attribute."); + //keep alive read timed out + int delta = HiResClock.TickCount - LastKeepAliveTickCount; + m_logger.LogInformation( + "KEEP ALIVE LATE: {Duration}ms, EndpointUrl={EndpointUrl}, RequestCount={Good}/{Outstanding}", + delta, + Endpoint?.EndpointUrl, + GoodPublishRequestCount, + OutstandingRequestCount); } - 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)); - } + KeepAliveEventHandler? callback = m_KeepAlive; - // WriteMask Attribute - if (attributes.TryGetValue(Attributes.WriteMask, out value) && value != null) + if (callback != null) { - node.WriteMask = value.GetValueOrDefault(); + try + { + var args = new KeepAliveEventArgs(result, ServerState.Unknown, DateTime.UtcNow); + callback(this, args); + return !args.CancelKeepAlive; + } + catch (Exception e) + { + m_logger.LogError(e, "Session: Unexpected error invoking KeepAliveCallback."); + } } - // UserWriteMask Attribute - if (attributes.TryGetValue(Attributes.UserWriteMask, out value) && value != null) - { - node.UserWriteMask = value.GetValueOrDefault(); - } + return true; + } - // RolePermissions Attribute - if (attributes.TryGetValue(Attributes.RolePermissions, out value) && value != null) + /// + /// Prepare a list of subscriptions to delete. + /// + private bool PrepareSubscriptionsToDelete( + IEnumerable subscriptions, + List subscriptionsToDelete) + { + bool removed = false; + lock (SyncRoot) { - if (value.Value is ExtensionObject[] rolePermissions) + foreach (Subscription subscription in subscriptions) { - node.RolePermissions = []; - - foreach (ExtensionObject rolePermission in rolePermissions) + if (m_subscriptions.Remove(subscription)) { - node.RolePermissions.Add(rolePermission.Body as RolePermissionType); + if (subscription.Created) + { + subscriptionsToDelete.Add(subscription); + } + + removed = true; } } } + return removed; + } + + /// + /// Prepares the list of node ids to read to fetch the namespace table. + /// + private static ReadValueIdCollection PrepareNamespaceTableNodesToRead() + { + var nodesToRead = new ReadValueIdCollection(); - // UserRolePermissions Attribute - if (attributes.TryGetValue(Attributes.UserRolePermissions, out value) && value != null) + // request namespace array. + var valueId = new ReadValueId { - if (value.Value is ExtensionObject[] userRolePermissions) - { - node.UserRolePermissions = []; + NodeId = Variables.Server_NamespaceArray, + AttributeId = Attributes.Value + }; - foreach (ExtensionObject rolePermission in userRolePermissions) - { - node.UserRolePermissions.Add(rolePermission.Body as RolePermissionType); - } - } - } + nodesToRead.Add(valueId); - // AccessRestrictions Attribute - if (attributes.TryGetValue(Attributes.AccessRestrictions, out value) && value != null) + // request server array. + valueId = new ReadValueId { - node.AccessRestrictions = value.GetValueOrDefault(); - } + NodeId = Variables.Server_ServerArray, + AttributeId = Attributes.Value + }; + + nodesToRead.Add(valueId); - return node; + return nodesToRead; } /// - /// Create a dictionary of attributes to read for a nodeclass. + /// Updates the NamespaceTable with the result of the + /// read operation. + /// Throws in case of types not matching or empty namespace + /// array returned. /// /// - private static Dictionary CreateAttributes( - NodeClass nodeClass = NodeClass.Unspecified, - bool optionalAttributes = true) + private void UpdateNamespaceTable( + DataValueCollection values, + DiagnosticInfoCollection diagnosticInfos, + ResponseHeader responseHeader) { - // Attributes to read for all types of nodes - var attributes = new Dictionary(Attributes.MaxAttributes) + // validate namespace array. + ServiceResult result = ValidateDataValue( + values[0], + typeof(string[]), + 0, + diagnosticInfos, + responseHeader); + + if (ServiceResult.IsBad(result)) { - { Attributes.NodeId, null }, - { Attributes.NodeClass, null }, - { Attributes.BrowseName, null }, - { Attributes.DisplayName, null } - }; + throw ServiceResultException.Create(result.StatusCode.Code, + "Cannot read NamespaceArray node. Validation of returned value failed."); + } - switch (nodeClass) + string[] namespaceArray = (string[])values[0].Value; + if (namespaceArray.Length == 0) { - 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 = new Dictionary(Attributes.MaxAttributes) - { - { Attributes.NodeId, null }, - { Attributes.NodeClass, null }, - { Attributes.BrowseName, null }, - { Attributes.DisplayName, null }, - //{ Attributes.Description, null }, - //{ Attributes.WriteMask, null }, - //{ Attributes.UserWriteMask, null }, - { Attributes.DataType, null }, - { Attributes.ValueRank, null }, - { Attributes.ArrayDimensions, null }, - { Attributes.AccessLevel, null }, - { Attributes.UserAccessLevel, null }, - { Attributes.MinimumSamplingInterval, null }, - { Attributes.Historizing, null }, - { Attributes.EventNotifier, null }, - { Attributes.Executable, null }, - { Attributes.UserExecutable, null }, - { Attributes.IsAbstract, null }, - { Attributes.InverseName, null }, - { Attributes.Symmetric, null }, - { Attributes.ContainsNoLoops, null }, - { Attributes.DataTypeDefinition, null }, - //{ Attributes.RolePermissions, null }, - //{ Attributes.UserRolePermissions, null }, - //{ Attributes.AccessRestrictions, null }, - { Attributes.AccessLevelEx, null } - }; - break; - default: - throw ServiceResultException.Unexpected( - $"Unexpected NodeClass: {nodeClass}."); + throw ServiceResultException.Create( + StatusCodes.BadUnexpectedError, + "Retrieved namespace list contain no entries."); + } + if (namespaceArray[0] != Namespaces.OpcUa) + { + throw ServiceResultException.Create( + StatusCodes.BadUnexpectedError, + "Retrieved namespaces are missing OPC UA namespace at index 0."); + } + + NamespaceUris.Update(namespaceArray); + + if (StatusCode.IsBad(values[1].StatusCode)) + { + // Gracefully handle not loading server array. + m_logger.LogError("Cannot read ServerArray node: {StatusCode} - skipping.", + values[1].StatusCode); + return; } - if (optionalAttributes) + // validate server array. + result = ValidateDataValue( + values[1], + typeof(string[]), + 1, + diagnosticInfos, + responseHeader); + + if (ServiceResult.IsBad(result)) { - 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); + throw ServiceResultException.Create(result.StatusCode.Code, + "Cannot read ServerArray node. Validation of returned value failed."); } - return attributes; + string[] serverArray = (string[])values[1].Value; + ServerUris.Update(serverArray); } /// @@ -5903,11 +3360,11 @@ public bool BeginPublish(int timeout) } // get event handler to modify ack list - PublishSequenceNumbersToAcknowledgeEventHandler callback + PublishSequenceNumbersToAcknowledgeEventHandler? callback = m_PublishSequenceNumbersToAcknowledge; // collect the current set if acknowledgements. - SubscriptionAcknowledgementCollection acknowledgementsToSend = null; + SubscriptionAcknowledgementCollection? acknowledgementsToSend = null; lock (m_acknowledgementsToSendLock) { if (callback != null) @@ -5958,23 +3415,17 @@ var deferredAcknowledgementsToSend RequestHandle = Utils.IncrementIdentifier(ref m_publishCounter) }; - var state = new AsyncRequestState - { - RequestTypeId = DataTypes.PublishRequest, - RequestId = requestHeader.RequestHandle, - TickCount = HiResClock.TickCount - }; - m_logger.LogTrace("PUBLISH #{RequestHandle} SENT", requestHeader.RequestHandle); CoreClientUtils.EventLog.PublishStart((int)requestHeader.RequestHandle); try { + Activity? activity = m_telemetry.StartActivity(); Task task = PublishAsync( requestHeader, acknowledgementsToSend, default); // TODO: Need a session scoped cancellation token. - AsyncRequestStarted(task, requestHeader.RequestHandle, DataTypes.PublishRequest); + AsyncRequestStarted(task, activity, requestHeader.RequestHandle, DataTypes.PublishRequest); task.ConfigureAwait(false) .GetAwaiter() .OnCompleted(() => OnPublishComplete( @@ -6124,7 +3575,7 @@ private void OnPublishComplete( if (error.Code != StatusCodes.BadNoSubscription) { - PublishErrorEventHandler callback = m_PublishError; + PublishErrorEventHandler? callback = m_PublishError; if (callback != null) { @@ -6240,7 +3691,7 @@ public virtual void RecreateRenewUserIdentity() { if (m_RenewUserIdentity != null) { - m_identity = m_RenewUserIdentity(this, m_identity); + m_identity = m_RenewUserIdentity(this, m_identity) ?? new UserIdentity(); } } @@ -6352,8 +3803,8 @@ private void OpenValidateIdentity( } private void BuildCertificateData( - out byte[] clientCertificateData, - out byte[] clientCertificateChainData) + out byte[]? clientCertificateData, + out byte[]? clientCertificateChainData) { // send the application instance certificate for the client. clientCertificateData = (m_instanceCertificate?.RawData); @@ -6414,10 +3865,10 @@ private void ValidateServerCertificateData(byte[] serverCertificateData) /// /// private void ValidateServerSignature( - X509Certificate2 serverCertificate, + X509Certificate2? serverCertificate, SignatureData serverSignature, - byte[] clientCertificateData, - byte[] clientCertificateChainData, + byte[]? clientCertificateData, + byte[]? clientCertificateChainData, byte[] clientNonce) { if (serverSignature == null || serverSignature.Signature == null) @@ -6469,7 +3920,9 @@ private void ValidateServerSignature( /// Ensure the endpoint was matched in /// with the applicationUri of the server description before the validation. /// - private void ValidateServerCertificateApplicationUri(X509Certificate2 serverCertificate, ConfiguredEndpoint endpoint) + private void ValidateServerCertificateApplicationUri( + X509Certificate2? serverCertificate, + ConfiguredEndpoint endpoint) { if (serverCertificate != null) { @@ -6486,7 +3939,7 @@ private void ValidateServerEndpoints(EndpointDescriptionCollection serverEndpoin if (m_discoveryServerEndpoints != null && m_discoveryServerEndpoints.Count > 0) { // Compare EndpointDescriptions returned at GetEndpoints with values returned at CreateSession - EndpointDescriptionCollection expectedServerEndpoints; + EndpointDescriptionCollection? expectedServerEndpoints; if (serverEndpoints != null && m_discoveryProfileUris != null && m_discoveryProfileUris.Count > 0) @@ -6559,7 +4012,7 @@ private void ValidateServerEndpoints(EndpointDescriptionCollection serverEndpoin // find the matching description (TBD - check domains against certificate). bool found = false; - EndpointDescription foundDescription = FindMatchingDescription( + EndpointDescription? foundDescription = FindMatchingDescription( serverEndpoints, m_endpoint.Description, true); @@ -6599,11 +4052,15 @@ private void ValidateServerEndpoints(EndpointDescriptionCollection serverEndpoin /// The description to match /// Match criteria includes port /// Matching description or null if no description is matching - private EndpointDescription FindMatchingDescription( - EndpointDescriptionCollection endpointDescriptions, + private EndpointDescription? FindMatchingDescription( + EndpointDescriptionCollection? endpointDescriptions, EndpointDescription match, bool matchPort) { + if (endpointDescriptions == null) + { + return null; + } Uri expectedUrl = Utils.ParseUri(match.EndpointUrl); for (int ii = 0; ii < endpointDescriptions.Count; ii++) { @@ -6683,7 +4140,7 @@ private static void UpdateDescription( break; } - PublishErrorEventHandler callback = m_PublishError; + PublishErrorEventHandler? callback = m_PublishError; // raise an error event. if (callback != null) @@ -6706,9 +4163,9 @@ private static void UpdateDescription( /// /// If available, returns the current nonce or null. /// - private byte[] GetCurrentTokenServerNonce() + private byte[]? GetCurrentTokenServerNonce() { - ChannelToken currentToken = (NullableTransportChannel as ISecureChannel)?.CurrentToken; + ChannelToken? currentToken = (NullableTransportChannel as ISecureChannel)?.CurrentToken; return currentToken?.ServerNonce; } @@ -6750,11 +4207,11 @@ private void HandleSignedSoftwareCertificates( private void ProcessPublishResponse( ResponseHeader responseHeader, uint subscriptionId, - UInt32Collection availableSequenceNumbers, + UInt32Collection? availableSequenceNumbers, bool moreNotifications, NotificationMessage notificationMessage) { - Subscription subscription = null; + Subscription? subscription = null; // send notification that the server is alive. OnKeepAlive(m_serverState, responseHeader.Timestamp); @@ -6955,7 +4412,7 @@ private void ProcessPublishResponse( subscription.SaveMessageInCache(availableSequenceNumbers, notificationMessage); // raise the notification. - NotificationEventHandler publishEventHandler = m_Publish; + NotificationEventHandler? publishEventHandler = m_Publish; if (publishEventHandler != null) { var args = new NotificationEventArgs( @@ -7054,56 +4511,70 @@ private async ValueTask DeleteSubscriptionAsync( /// /// private async Task LoadInstanceCertificateAsync( - X509Certificate2 clientCertificate, + bool throwIfConfigurationChangedFromLastLoad, CancellationToken ct = default) { - if (m_endpoint.Description.SecurityPolicyUri != SecurityPolicies.None) + if (m_endpoint.Description.SecurityPolicyUri == SecurityPolicies.None) { - if (clientCertificate == null) - { - m_instanceCertificate = await LoadCertificateAsync( - m_configuration, - m_endpoint.Description.SecurityPolicyUri, - m_telemetry, - ct) - .ConfigureAwait(false); - if (m_instanceCertificate == null) - { - throw new ServiceResultException( - StatusCodes.BadConfigurationError, - "The client configuration does not specify an application instance certificate."); - } - } - else - { - // update client certificate. - m_instanceCertificate = clientCertificate; - } + // No need to load instance certificates + return; + } - // check for private key. - if (!m_instanceCertificate.HasPrivateKey) + if (m_instanceCertificate != null && + m_instanceCertificate.HasPrivateKey && + !m_endpoint.Equals(m_effectiveEndpoint)) + { + if (throwIfConfigurationChangedFromLastLoad) { - throw ServiceResultException.Create( - StatusCodes.BadConfigurationError, - "No private key for the application instance certificate. Subject={0}, Thumbprint={1}.", - m_instanceCertificate.Subject, - m_instanceCertificate.Thumbprint); + // Updating a live session must be prevented unless the session was + // closed. Therefore we need to throw here to catch this case during any + // reconnect or other activation operation + throw ServiceResultException.Create(StatusCodes.BadConfigurationError, + "Configuration was changed for an active session."); } + // If the configured endpoint was updated while we are closed we reload. + m_instanceCertificate = null; + } - // load certificate chain. - m_instanceCertificateChain = await LoadCertificateChainAsync( + if (m_instanceCertificate == null || !m_instanceCertificate.HasPrivateKey) + { + m_instanceCertificate = await LoadInstanceCertificateAsync( m_configuration, - m_instanceCertificate, + m_endpoint.Description.SecurityPolicyUri, + m_telemetry, ct) .ConfigureAwait(false); + if (m_instanceCertificate == null) + { + throw new ServiceResultException( + StatusCodes.BadConfigurationError, + "The client configuration does not specify an application instance certificate."); + } + m_effectiveEndpoint = m_endpoint; + m_instanceCertificateChain = null; // Reload the chain too + } + + // check for private key. + if (!m_instanceCertificate.HasPrivateKey) + { + throw ServiceResultException.Create( + StatusCodes.BadConfigurationError, + "Client certificate configured for security policy {0} is missing a private key.", + m_endpoint.Description.SecurityPolicyUri); } + + // load certificate chain. + m_instanceCertificateChain ??= await LoadCertificateChainAsync( + m_configuration, + m_instanceCertificate, + ct).ConfigureAwait(false); } /// /// Load certificate for connection. /// /// - private static async Task LoadCertificateAsync( + internal static async Task LoadInstanceCertificateAsync( ApplicationConfiguration configuration, string securityProfile, ITelemetryContext telemetry, @@ -7123,12 +4594,12 @@ private static async Task LoadCertificateAsync( /// /// Load certificate chain for connection. /// - private static async Task LoadCertificateChainAsync( + internal static async Task LoadCertificateChainAsync( ApplicationConfiguration configuration, X509Certificate2 clientCertificate, CancellationToken ct = default) { - X509Certificate2Collection clientCertificateChain = null; + X509Certificate2Collection? clientCertificateChain = null; // load certificate chain. if (configuration.SecurityConfiguration.SendCertificateChain) { @@ -7245,31 +4716,6 @@ protected virtual int GetDesiredPublishRequestCount(bool createdOnly) } } - /// - /// Creates resend data call requests for the subscriptions. - /// - /// The subscriptions to call resend data. - private static CallMethodRequestCollection CreateCallRequestsForResendData( - IEnumerable subscriptions) - { - var requests = new CallMethodRequestCollection(); - - foreach (Subscription subscription in subscriptions) - { - var inputArguments = new VariantCollection { new Variant(subscription.Id) }; - - var request = new CallMethodRequest - { - ObjectId = ObjectIds.Server, - MethodId = MethodIds.Server_ResendData, - InputArguments = inputArguments - }; - - requests.Add(request); - } - return requests; - } - /// /// Creates and validates the subscription ids for a transfer. /// @@ -7284,7 +4730,7 @@ private UInt32Collection CreateSubscriptionIdsForTransfer( { foreach (Subscription subscription in subscriptions) { - if (subscription.Created && SessionId.Equals(subscription.Session.SessionId)) + if (subscription.Created && SessionId.Equals(subscription.Session?.SessionId)) { throw new ServiceResultException( StatusCodes.BadInvalidState, @@ -7366,13 +4812,21 @@ private RequestHeader CreateRequestHeaderPerUserTokenPolicy( return requestHeader; } + /// + /// Create a subscription the provided item options + /// + protected virtual Subscription CreateSubscription(SubscriptionOptions? options = null) + { + return new Subscription(m_telemetry, options); + } + /// /// Process the AdditionalHeader field of a ResponseHeader /// /// protected virtual void ProcessResponseAdditionalHeader( ResponseHeader responseHeader, - X509Certificate2 serverCertificate) + X509Certificate2? serverCertificate) { if (ExtensionObject.ToEncodeable( responseHeader?.AdditionalHeader) is AdditionalParametersType parameters) @@ -7433,19 +4887,24 @@ protected virtual void ProcessResponseAdditionalHeader( protected ApplicationConfiguration m_configuration; /// - /// The endpoint used to connect to the server. + /// The endpoint configured for the session. /// protected ConfiguredEndpoint m_endpoint; + /// + /// The endpoint used while connected to the server. + /// + protected ConfiguredEndpoint m_effectiveEndpoint; + /// /// The Instance Certificate. /// - protected X509Certificate2 m_instanceCertificate; + protected X509Certificate2? m_instanceCertificate; /// /// The Instance Certificate Chain. /// - protected X509Certificate2Collection m_instanceCertificateChain; + protected X509Certificate2Collection? m_instanceCertificateChain; /// /// The session telemetry context @@ -7472,23 +4931,23 @@ protected virtual void ProcessResponseAdditionalHeader( /// protected int m_keepAliveIntervalFactor = 1; - /// + /// m /// Time in milliseconds added to before is set to true /// protected int m_keepAliveGuardBand = 1000; - private SubscriptionAcknowledgementCollection m_acknowledgementsToSend; - private object m_acknowledgementsToSendLock; + private SubscriptionAcknowledgementCollection m_acknowledgementsToSend = []; + private readonly object m_acknowledgementsToSendLock = new(); #if DEBUG_SEQUENTIALPUBLISHING - private Dictionary m_latestAcknowledgementsSent; + private Dictionary m_latestAcknowledgementsSent = []; #endif - private List m_subscriptions; + private readonly List m_subscriptions = []; private uint m_maxRequestMessageSize; private readonly SystemContext m_systemContext; private NodeCache m_nodeCache; - private List m_identityHistory; - private byte[] m_serverNonce; - private byte[] m_previousServerNonce; - private X509Certificate2 m_serverCertificate; + private readonly List m_identityHistory = []; + private byte[]? m_serverNonce; + private byte[]? m_previousServerNonce; + private X509Certificate2? m_serverCertificate; private uint m_publishCounter; private int m_tooManyPublishRequests; private long m_lastKeepAliveTime; @@ -7498,34 +4957,41 @@ protected virtual void ProcessResponseAdditionalHeader( private readonly Timer m_keepAliveTimer; private readonly AsyncAutoResetEvent m_keepAliveEvent = new(); private uint m_keepAliveCounter; - private Task m_keepAliveWorker; - private CancellationTokenSource m_keepAliveCancellation; - private SemaphoreSlim m_reconnectLock; + private Task? m_keepAliveWorker; + private CancellationTokenSource? m_keepAliveCancellation; + private readonly SemaphoreSlim m_reconnectLock = new(1, 1); private int m_minPublishRequestCount; private int m_maxPublishRequestCount; - private LinkedList m_outstandingRequests; - private string m_userTokenSecurityPolicyUri; - private Nonce m_eccServerEphemeralKey; - private Subscription m_defaultSubscription; - private readonly EndpointDescriptionCollection m_discoveryServerEndpoints; - private readonly StringCollection m_discoveryProfileUris; - - private class AsyncRequestState + private readonly LinkedList m_outstandingRequests = []; + private string? m_userTokenSecurityPolicyUri; + private Nonce? m_eccServerEphemeralKey; + private Subscription? m_defaultSubscription; + private readonly EndpointDescriptionCollection? m_discoveryServerEndpoints; + private readonly StringCollection? m_discoveryProfileUris; + + private sealed class AsyncRequestState : IDisposable { - public uint RequestTypeId; - public uint RequestId; - public int TickCount; - public Task Result; - public bool Defunct; + public uint RequestTypeId { get; init; } + public uint RequestId { get; init; } + public int TickCount { get; init; } + public required Task Result { get; set; } + public bool Defunct { get; set; } + public required Activity? Activity { get; init; } + + public void Dispose() + { + Activity?.Dispose(); + Debug.Assert(Result.IsCompleted); + } } - private event KeepAliveEventHandler m_KeepAlive; - private event NotificationEventHandler m_Publish; - private event PublishErrorEventHandler m_PublishError; - private event PublishSequenceNumbersToAcknowledgeEventHandler m_PublishSequenceNumbersToAcknowledge; - private event EventHandler m_SubscriptionsChanged; - private event EventHandler m_SessionClosing; - private event EventHandler m_SessionConfigurationChanged; + private event KeepAliveEventHandler? m_KeepAlive; + private event NotificationEventHandler? m_Publish; + private event PublishErrorEventHandler? m_PublishError; + private event PublishSequenceNumbersToAcknowledgeEventHandler? m_PublishSequenceNumbersToAcknowledge; + private event EventHandler? m_SubscriptionsChanged; + private event EventHandler? m_SessionClosing; + private event EventHandler? m_SessionConfigurationChanged; } /// @@ -7537,7 +5003,7 @@ public class KeepAliveEventArgs : EventArgs /// Creates a new instance. /// public KeepAliveEventArgs( - ServiceResult status, + ServiceResult? status, ServerState currentState, DateTime currentTime) { @@ -7549,7 +5015,7 @@ public KeepAliveEventArgs( /// /// Gets the status associated with the keep alive operation. /// - public ServiceResult Status { get; } + public ServiceResult? Status { get; } /// /// Gets the current server state. diff --git a/Libraries/Opc.Ua.Client/Session/SessionClientBatched.cs b/Libraries/Opc.Ua.Client/Session/SessionClientBatched.cs index 48eebb5c7d..61eaac7e14 100644 --- a/Libraries/Opc.Ua.Client/Session/SessionClientBatched.cs +++ b/Libraries/Opc.Ua.Client/Session/SessionClientBatched.cs @@ -27,6 +27,8 @@ * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ +#nullable disable + using System.Collections.Generic; using System.Linq; using System.Threading; diff --git a/Libraries/Opc.Ua.Client/Session/SessionClientExtensions.cs b/Libraries/Opc.Ua.Client/Session/SessionClientExtensions.cs new file mode 100644 index 0000000000..dd095cd76c --- /dev/null +++ b/Libraries/Opc.Ua.Client/Session/SessionClientExtensions.cs @@ -0,0 +1,941 @@ +/* ======================================================================== + * 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.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Opc.Ua.Client +{ + /// + /// Extensions to ISessionClient that are not dependent on anything internal + /// to the client but layer over ISessionClient. + /// + public static class SessionClientExtensions + { + /// + /// Reads the values for a set of variables. + /// + public static async ValueTask<( + IList, + IList + )> ReadValuesAsync( + this ISessionClient session, + IList variableIds, + IList expectedTypes, + CancellationToken ct = default) + { + (DataValueCollection dataValues, IList errors) = + await session.ReadValuesAsync( + variableIds, + ct).ConfigureAwait(false); + + object[] values = new object[dataValues.Count]; + for (int ii = 0; ii < variableIds.Count; ii++) + { + object value = dataValues[ii].Value; + + // extract the body from extension objects. + if (value is ExtensionObject extension && + extension.Body is IEncodeable) + { + value = extension.Body; + } + + // check expected type. + if (expectedTypes[ii] != null && + !expectedTypes[ii].IsInstanceOfType(value)) + { + errors[ii] = ServiceResult.Create( + StatusCodes.BadTypeMismatch, + "Value {0} does not have expected type: {1}.", + value, + expectedTypes[ii].Name); + continue; + } + + // suitable value found. + values[ii] = value; + } + return (values, errors); + } + + /// + /// Invokes the Browse service. + /// + /// + public static async ValueTask<( + ResponseHeader, + byte[], + ReferenceDescriptionCollection + )> BrowseAsync( + this ISessionClient session, + RequestHeader? requestHeader, + ViewDescription? view, + NodeId nodeToBrowse, + uint maxResultsToReturn, + BrowseDirection browseDirection, + NodeId referenceTypeId, + bool includeSubtypes, + uint nodeClassMask, + CancellationToken ct = default) + { + ResponseHeader responseHeader; + IList errors; + IList referencesList; + ByteStringCollection continuationPoints; + (responseHeader, continuationPoints, referencesList, errors) = + await session.BrowseAsync( + requestHeader, + view, + [nodeToBrowse], + maxResultsToReturn, + browseDirection, + referenceTypeId, + includeSubtypes, + nodeClassMask, + ct).ConfigureAwait(false); + + Debug.Assert(errors.Count <= 1); + if (errors.Count > 0 && StatusCode.IsBad(errors[0].StatusCode)) + { + throw new ServiceResultException(errors[0]); + } + + Debug.Assert(referencesList.Count == 1); + Debug.Assert(continuationPoints.Count == 1); + return (responseHeader, continuationPoints[0], referencesList[0]); + } + + /// + /// Invokes the BrowseNext service. + /// + /// + public static async ValueTask<( + ResponseHeader, + byte[], + ReferenceDescriptionCollection + )> BrowseNextAsync( + this ISessionClient session, + RequestHeader? requestHeader, + bool releaseContinuationPoint, + byte[]? continuationPoint, + CancellationToken ct = default) + { + ResponseHeader responseHeader; + IList errors; + IList referencesList; + + ByteStringCollection revisedContinuationPoints; + (responseHeader, revisedContinuationPoints, referencesList, errors) = + await session.BrowseNextAsync( + requestHeader, + [continuationPoint], + releaseContinuationPoint, + ct).ConfigureAwait(false); + Debug.Assert(errors.Count <= 1); + if (errors.Count > 0 && StatusCode.IsBad(errors[0].StatusCode)) + { + throw new ServiceResultException(errors[0]); + } + + Debug.Assert(referencesList.Count == 1); + Debug.Assert(revisedContinuationPoints.Count == 1); + return (responseHeader, revisedContinuationPoints[0], referencesList[0]); + } + + /// + /// Managed browsing using browser + /// + public static async Task<( + IList, + IList + )> ManagedBrowseAsync( + this ISessionClient session, + RequestHeader? requestHeader, + ViewDescription? view, + IList nodesToBrowse, + uint maxResultsToReturn, + BrowseDirection browseDirection, + NodeId? referenceTypeId, + bool includeSubtypes, + uint nodeClassMask, + CancellationToken ct = default) + { + var browser = new Browser(session, new BrowserOptions + { + RequestHeader = requestHeader, + View = view, + MaxReferencesReturned = maxResultsToReturn, + BrowseDirection = browseDirection, + ReferenceTypeId = referenceTypeId ?? NodeId.Null, + IncludeSubtypes = includeSubtypes, + NodeClassMask = (int)nodeClassMask + }); + ResultSet result = + await browser.BrowseAsync([.. nodesToBrowse], ct).ConfigureAwait(false); + return (result.Results.ToList(), result.Errors.ToList()); + } + + /// + /// Fetches all references for the specified node. + /// + /// session to use + /// The node id. + /// Cancellation token to cancel operation with + public static async Task FetchReferencesAsync( + this ISessionClient session, + NodeId nodeId, + CancellationToken ct = default) + { + (IList descriptions, _) = + await session.ManagedBrowseAsync( + null, + null, + [nodeId], + 0, + BrowseDirection.Both, + null, + true, + 0, + ct) + .ConfigureAwait(false); + return descriptions[0]; + } + + /// + /// Fetches all references for the specified nodes. + /// + /// session to use + /// The node id collection. + /// Cancellation token to cancel operation with + /// A list of reference collections and the errors reported + /// by the server. + public static Task<( + IList, + IList + )> FetchReferencesAsync( + this ISessionClient session, + IList nodeIds, + CancellationToken ct = default) + { + return session.ManagedBrowseAsync( + null, + null, + nodeIds, + 0, + BrowseDirection.Both, + null, + true, + 0, + ct); + } + + /// + /// 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. + /// + /// The session 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 not be omitted. + /// The cancellation token. + /// The node collection and associated errors. + public static async Task<(IList, IList)> ReadNodesAsync( + this ISessionClient session, + IList nodeIds, + NodeClass nodeClass, + bool optionalAttributes = false, + CancellationToken ct = default) + { + var nodeCacheContext = new NodeCacheContext(session); + ResultSet result = await nodeCacheContext.FetchNodesAsync( + null, + [.. nodeIds], + nodeClass, + !optionalAttributes, + ct).ConfigureAwait(false); + return (result.Results.ToList(), result.Errors.ToList()); + } + + /// + /// Reads the values for the node attributes and returns a node object collection. + /// Reads the nodeclass of the nodeIds, then reads + /// the values for the node attributes and returns a node collection. + /// + /// The session to use + /// The nodeId collection. + /// If optional attributes to read. + /// The cancellation token. + public static async Task<(IList, IList)> ReadNodesAsync( + this ISessionClient session, + IList nodeIds, + bool optionalAttributes = false, + CancellationToken ct = default) + { + var nodeCacheContext = new NodeCacheContext(session); + ResultSet result = await nodeCacheContext.FetchNodesAsync( + null, + [.. nodeIds], + !optionalAttributes, + ct).ConfigureAwait(false); + return (result.Results.ToList(), result.Errors.ToList()); + } + + /// + /// Reads the values for the node attributes and returns a node object. + /// + /// The session to use + /// The nodeId. + /// The cancellation token for the request. + public static Task ReadNodeAsync( + this ISessionClient session, + NodeId nodeId, + CancellationToken ct = default) + { + return session.ReadNodeAsync(nodeId, NodeClass.Unspecified, true, ct); + } + + /// + /// Reads the values for the node attributes and returns a node object. + /// + /// + /// If the nodeclass is known, only the supported attribute values are read. + /// + /// The session to use + /// The nodeId. + /// The nodeclass of the node to read. + /// Read optional attributes. + /// The cancellation token for the request. + public static Task ReadNodeAsync( + this ISessionClient session, + NodeId nodeId, + NodeClass nodeClass, + bool optionalAttributes = true, + CancellationToken ct = default) + { + var nodeCacheContext = new NodeCacheContext(session); + return nodeCacheContext.FetchNodeAsync( + null, + nodeId, + nodeClass, + !optionalAttributes, + ct).AsTask(); + } + + /// + /// Read display name for a set of nodes + /// + /// The session to use + /// node for which to read display name + /// Cancellation token to use to cancel the operation + /// Paired list of displaynames and potential errors per node + public static async Task<(IList, IList)> ReadDisplayNameAsync( + this ISessionClient session, + IList nodeIds, + CancellationToken ct = default) + { + var displayNames = new List(); + var errors = new List(); + + // build list of values to read. + var valuesToRead = new ReadValueIdCollection(); + + for (int ii = 0; ii < nodeIds.Count; ii++) + { + var valueToRead = new ReadValueId + { + NodeId = nodeIds[ii], + AttributeId = Attributes.DisplayName, + IndexRange = null, + DataEncoding = null + }; + + valuesToRead.Add(valueToRead); + } + + // read the values. + + ReadResponse response = await session.ReadAsync( + null, + int.MaxValue, + TimestampsToReturn.Neither, + valuesToRead, + ct).ConfigureAwait(false); + + DataValueCollection results = response.Results; + DiagnosticInfoCollection diagnosticInfos = response.DiagnosticInfos; + ResponseHeader responseHeader = response.ResponseHeader; + + // verify that the server returned the correct number of results. + ClientBase.ValidateResponse(results, valuesToRead); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, valuesToRead); + + for (int ii = 0; ii < nodeIds.Count; ii++) + { + displayNames.Add(string.Empty); + errors.Add(ServiceResult.Good); + + // process any diagnostics associated with bad or uncertain data. + if (StatusCode.IsNotGood(results[ii].StatusCode)) + { + errors[ii] = new ServiceResult( + results[ii].StatusCode, + ii, + diagnosticInfos, + responseHeader.StringTable); + continue; + } + + // extract the name. + LocalizedText displayName = results[ii].GetValue(LocalizedText.Null); + + if (!LocalizedText.IsNullOrEmpty(displayName)) + { + displayNames[ii] = displayName.Text; + } + } + + return (displayNames, errors); + } + + /// + /// Returns the data description for the encoding. + /// + /// The session to use + /// The encoding Id. + /// Cancellation token to use to cancel the operation + /// + public static async Task FindDataDescriptionAsync( + this ISessionClient session, + NodeId encodingId, + CancellationToken ct = default) + { + var browser = new Browser(session, new BrowserOptions + { + BrowseDirection = BrowseDirection.Forward, + ReferenceTypeId = ReferenceTypeIds.HasDescription, + IncludeSubtypes = false, + NodeClassMask = 0 + }); + + ReferenceDescriptionCollection references = + await browser.BrowseAsync(encodingId, ct).ConfigureAwait(false); + + if (references.Count == 0) + { + throw ServiceResultException.Create( + StatusCodes.BadNodeIdInvalid, + "Encoding does not refer to a valid data description."); + } + + return references[0]; + } + + /// + /// Reads the value for a node of type T or throws if not matching the type. + /// + /// + /// The session to use + /// The node Id. + /// The cancellation token for the request. + /// + public static async Task ReadValueAsync( + this ISessionClient session, + NodeId nodeId, + CancellationToken ct = default) + { + DataValue dataValue = await session.ReadValueAsync(nodeId, ct).ConfigureAwait(false); + object value = dataValue.Value; + + if (value is ExtensionObject extension) + { + value = extension.Body; + } + + if (!typeof(T).IsInstanceOfType(value)) + { + throw ServiceResultException.Create( + StatusCodes.BadTypeMismatch, + "Server returned value unexpected type: {0}", + value != null ? value.GetType().Name : "(null)"); + } + return (T)value; + } + + /// + /// Reads the value for a node. + /// + /// The session to use + /// The node Id. + /// The cancellation token for the request. + /// + public static Task ReadValueAsync( + this ISessionClient session, + NodeId nodeId, + CancellationToken ct = default) + { + var nodeCacheContext = new NodeCacheContext(session); + return nodeCacheContext.FetchValueAsync( + null, + nodeId, + ct).AsTask(); + } + + /// + /// Reads the values for a node collection. Returns diagnostic errors. + /// + /// The session to use + /// The node Id. + /// The cancellation token for the request. + public static async Task<(DataValueCollection, IList)> ReadValuesAsync( + this ISessionClient session, + IList nodeIds, + CancellationToken ct = default) + { + var nodeCacheContext = new NodeCacheContext(session); + ResultSet result = await nodeCacheContext.FetchValuesAsync( + null, + [.. nodeIds], + ct).ConfigureAwait(false); + return (new DataValueCollection(result.Results), result.Errors.ToList()); + } + + /// + /// Browses the nodes in the server. + /// + /// The session to use + /// Request header + /// View to use + /// nodes to browse + /// max results to return + /// Direction of browse + /// Reference type to follow + /// Include subtypes + /// Node classes to match + /// Cancellation token to cancel the operation + /// + public static async Task<( + ResponseHeader responseHeader, + ByteStringCollection continuationPoints, + IList referencesList, + IList errors + )> BrowseAsync( + this ISessionClient session, + RequestHeader? requestHeader, + ViewDescription? view, + IList nodesToBrowse, + uint maxResultsToReturn, + BrowseDirection browseDirection, + NodeId referenceTypeId, + bool includeSubtypes, + uint nodeClassMask, + CancellationToken ct = default) + { + var browseDescriptions = new BrowseDescriptionCollection(); + foreach (NodeId nodeToBrowse in nodesToBrowse) + { + var description = new BrowseDescription + { + NodeId = nodeToBrowse, + BrowseDirection = browseDirection, + ReferenceTypeId = referenceTypeId, + IncludeSubtypes = includeSubtypes, + NodeClassMask = nodeClassMask, + ResultMask = (uint)BrowseResultMask.All + }; + + browseDescriptions.Add(description); + } + + BrowseResponse browseResponse = await session.BrowseAsync( + requestHeader, + view, + maxResultsToReturn, + browseDescriptions, + ct).ConfigureAwait(false); + + BrowseResultCollection results = browseResponse.Results; + DiagnosticInfoCollection diagnosticInfos = browseResponse.DiagnosticInfos; + + ClientBase.ValidateResponse(results, browseDescriptions); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, browseDescriptions); + + int ii = 0; + var errors = new List(); + var continuationPoints = new ByteStringCollection(); + var referencesList = new List(); + foreach (BrowseResult result in results) + { + if (StatusCode.IsBad(result.StatusCode)) + { + errors.Add( + new ServiceResult( + result.StatusCode, + ii, + diagnosticInfos, + browseResponse.ResponseHeader.StringTable)); + } + else + { + errors.Add(ServiceResult.Good); + } + continuationPoints.Add(result.ContinuationPoint); + referencesList.Add(result.References); + ii++; + } + + return (browseResponse.ResponseHeader, continuationPoints, referencesList, errors); + } + + /// + /// Browse next + /// + /// The session to use + /// + /// + /// + /// + /// + public static async Task<( + ResponseHeader responseHeader, + ByteStringCollection revisedContinuationPoints, + IList referencesList, + IList errors + )> BrowseNextAsync( + this ISessionClient session, + RequestHeader? requestHeader, + ByteStringCollection? continuationPoints, + bool releaseContinuationPoint, + CancellationToken ct = default) + { + BrowseNextResponse response = await session.BrowseNextAsync( + requestHeader, + releaseContinuationPoint, + continuationPoints, + ct) + .ConfigureAwait(false); + + BrowseResultCollection results = response.Results; + DiagnosticInfoCollection diagnosticInfos = response.DiagnosticInfos; + + ClientBase.ValidateResponse(results, continuationPoints); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, continuationPoints); + + int ii = 0; + var errors = new List(); + var revisedContinuationPoints = new ByteStringCollection(); + var referencesList = new List(); + foreach (BrowseResult result in results) + { + if (StatusCode.IsBad(result.StatusCode)) + { + errors.Add( + new ServiceResult( + result.StatusCode, + ii, + diagnosticInfos, + response.ResponseHeader.StringTable)); + } + else + { + errors.Add(ServiceResult.Good); + } + revisedContinuationPoints.Add(result.ContinuationPoint); + referencesList.Add(result.References); + ii++; + } + + return (response.ResponseHeader, revisedContinuationPoints, referencesList, errors); + } + + /// + /// Reads a byte string value safely in fragments if needed. Uses the byte + /// string size limits to chunk the reads if needed. The first read happens + /// as usual and no stream is allocated, if the result is below the limits + /// the buffer that is read into is returned, otherwise buffers are added + /// to a memory stream whose content is finally returned. + /// + /// session to use + /// The node id of a byte string variable + /// A chunk size to enforce + /// Cancellation token to cancel operation with + /// + /// + public static async ValueTask> ReadBytesAsync( + this ISessionClient session, + NodeId nodeId, + int maxByteStringLength, + CancellationToken ct = default) + { + if (maxByteStringLength == 0 || + maxByteStringLength > session.MessageContext.MaxByteStringLength) + { + maxByteStringLength = session.MessageContext.MaxByteStringLength; + } + if (maxByteStringLength <= 0) + { + throw new ArgumentOutOfRangeException(nameof(maxByteStringLength), + "maxByteStringLength must be a positive integer."); + } + + int offset = 0; + MemoryStream? stream = null; + try + { + while (true) + { + var valueToRead = new ReadValueId + { + NodeId = nodeId, + AttributeId = Attributes.Value, + IndexRange = new NumericRange( + offset, + // Range is inclusive and starts at 0. Therefore + // to read 5 bytes you need to specify 0-4. + offset + maxByteStringLength - 1).ToString(), + DataEncoding = null + }; + var readValueIds = new ReadValueIdCollection { valueToRead }; + + ReadResponse result = await session.ReadAsync( + null, + 0, + TimestampsToReturn.Neither, + readValueIds, + ct) + .ConfigureAwait(false); + + ResponseHeader responseHeader = result.ResponseHeader; + DataValueCollection results = result.Results; + DiagnosticInfoCollection diagnosticInfos = result.DiagnosticInfos; + ClientBase.ValidateResponse(results, readValueIds); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, readValueIds); + + Variant wrappedValue = results[0].WrappedValue; + + // First call returned bytes, next a string, that we cannot tolerate + // But we allow null byte string which signals the end of the stream. + if (!wrappedValue.IsNull && + (wrappedValue.TypeInfo.BuiltInType != BuiltInType.ByteString || + wrappedValue.TypeInfo.ValueRank != ValueRanks.Scalar)) + { + throw new ServiceResultException( + StatusCodes.BadTypeMismatch, + "Value is not a scalar ByteString."); + } + + if (StatusCode.IsBad(results[0].StatusCode)) + { + if (results[0].StatusCode == StatusCodes.BadIndexRangeNoData) + { + // this happens when the previous read has fetched all remaining data + break; + } + ServiceResult serviceResult = ClientBase.GetResult( + results[0].StatusCode, + 0, + diagnosticInfos, + responseHeader); + throw new ServiceResultException(serviceResult); + } + + if (results[0].Value is not byte[] chunk || chunk.Length == 0) + { + // End of stream - fast path (no stream allocated yet) + // will return empty array constant. + break; + } + if (chunk.Length < maxByteStringLength && offset == 0) + { + // Fast path for small values, just return the chunk + return chunk; + } + stream ??= new MemoryStream(); +#if NET8_0_OR_GREATER + await stream.WriteAsync(chunk, ct).ConfigureAwait(false); +#else + stream.Write(chunk, 0, chunk.Length); +#endif + if (chunk.Length < maxByteStringLength) + { + break; + } + offset += maxByteStringLength; + } + return stream?.ToArray() ?? []; + } + finally + { +#if NET8_0_OR_GREATER + if (stream != null) + { + await stream.DisposeAsync().ConfigureAwait(false); + } +#else + stream?.Dispose(); +#endif + } + } + + /// + /// Calls the specified method and returns the output arguments. + /// + /// The session to use + /// The NodeId of the object that provides the method. + /// The NodeId of the method to call. + /// The cancellation token for the request. + /// The input arguments. + /// The list of output argument values. + /// + public static async Task> CallAsync( + this ISessionClient session, + NodeId objectId, + NodeId methodId, + CancellationToken ct = default, + params object[] args) + { + var inputArguments = new VariantCollection(); + + if (args != null) + { + for (int ii = 0; ii < args.Length; ii++) + { + inputArguments.Add(new Variant(args[ii])); + } + } + + var request = new CallMethodRequest + { + ObjectId = objectId, + MethodId = methodId, + InputArguments = inputArguments + }; + + var requests = new CallMethodRequestCollection { request }; + + CallMethodResultCollection results; + DiagnosticInfoCollection diagnosticInfos; + + CallResponse response = await session.CallAsync(null, requests, ct).ConfigureAwait(false); + + results = response.Results; + diagnosticInfos = response.DiagnosticInfos; + + ClientBase.ValidateResponse(results, requests); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, requests); + + if (StatusCode.IsBad(results[0].StatusCode)) + { + throw ServiceResultException.Create( + results[0].StatusCode, + 0, + diagnosticInfos, + response.ResponseHeader.StringTable); + } + + var outputArguments = new List(); + + foreach (Variant arg in results[0].OutputArguments) + { + outputArguments.Add(arg.Value); + } + + return outputArguments; + } + + /// + /// Call the ResendData method on the server for all subscriptions. + /// + public static async ValueTask> ResendDataAsync( + this ISessionClient session, + IEnumerable subscriptionIds, + CancellationToken ct = default) + { + CallMethodRequestCollection requests = CreateCallRequestsForResendData(subscriptionIds); + + var errors = new List(requests.Count); + CallResponse response = await session.CallAsync(null, requests, ct).ConfigureAwait(false); + CallMethodResultCollection results = response.Results; + DiagnosticInfoCollection diagnosticInfos = response.DiagnosticInfos; + ResponseHeader responseHeader = response.ResponseHeader; + ClientBase.ValidateResponse(results, requests); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, requests); + + int ii = 0; + foreach (CallMethodResult value in results) + { + ServiceResult result = ServiceResult.Good; + if (StatusCode.IsNotGood(value.StatusCode)) + { + result = ClientBase.GetResult(value.StatusCode, ii, diagnosticInfos, responseHeader); + } + errors.Add(result); + ii++; + } + + return errors; + } + + /// + /// Creates resend data call requests for the subscriptions. + /// + /// The subscriptions to call resend data. + private static CallMethodRequestCollection CreateCallRequestsForResendData( + IEnumerable subscriptionIds) + { + var requests = new CallMethodRequestCollection(); + + foreach (uint subscriptionId in subscriptionIds) + { + var inputArguments = new VariantCollection { new Variant(subscriptionId) }; + + var request = new CallMethodRequest + { + ObjectId = ObjectIds.Server, + MethodId = MethodIds.Server_ResendData, + InputArguments = inputArguments + }; + + requests.Add(request); + } + return requests; + } + } +} diff --git a/Libraries/Opc.Ua.Client/Session/SessionConfiguration.cs b/Libraries/Opc.Ua.Client/Session/SessionConfiguration.cs index 5c95c6fb33..bff0116920 100644 --- a/Libraries/Opc.Ua.Client/Session/SessionConfiguration.cs +++ b/Libraries/Opc.Ua.Client/Session/SessionConfiguration.cs @@ -30,10 +30,16 @@ using System; using System.IO; using System.Runtime.Serialization; +using System.Text.Json.Serialization; using System.Xml; namespace Opc.Ua.Client { + [JsonSerializable(typeof(SessionOptions))] + [JsonSerializable(typeof(SessionState))] + [JsonSerializable(typeof(SessionConfiguration))] + internal partial class SessionConfigurationContext : JsonSerializerContext; + /// /// A session configuration stores all the information /// needed to reconnect a session with a new secure channel. @@ -44,101 +50,142 @@ namespace Opc.Ua.Client [KnownType(typeof(X509IdentityToken))] [KnownType(typeof(IssuedIdentityToken))] [KnownType(typeof(UserIdentity))] - public class SessionConfiguration + public record class SessionOptions { /// - /// Creates a session configuration + /// The session name used by the client. + /// + [DataMember(IsRequired = true, Order = 20)] + public string? SessionName { get; init; } + + /// + /// The identity used to create the session. /// - public SessionConfiguration( - ISession session, - Nonce serverNonce, - string userIdentityTokenPolicy, - Nonce eccServerEphemeralKey, - NodeId authenthicationToken) + [DataMember(IsRequired = true, Order = 50)] + public IUserIdentity? Identity { get; init; } + + /// + /// The configured endpoint for the secure channel. + /// + [DataMember(IsRequired = true, Order = 60)] + public ConfiguredEndpoint? ConfiguredEndpoint { get; init; } + + /// + /// If the client is configured to check the certificate domain. + /// + [DataMember(IsRequired = false, Order = 70)] + public bool CheckDomain { get; init; } + } + + /// + /// A session state stores not just configuration but + /// also the subscription states + /// + [DataContract(Namespace = Namespaces.OpcUaXsd)] + [KnownType(typeof(UserIdentityToken))] + [KnownType(typeof(AnonymousIdentityToken))] + [KnownType(typeof(X509IdentityToken))] + [KnownType(typeof(IssuedIdentityToken))] + [KnownType(typeof(UserIdentity))] + public record class SessionState : SessionOptions + { + /// + /// Default constructor + /// + public SessionState() { - Timestamp = DateTime.UtcNow; - SessionName = session.SessionName; - SessionId = session.SessionId; - AuthenticationToken = authenthicationToken; - Identity = session.Identity; - ConfiguredEndpoint = session.ConfiguredEndpoint; - CheckDomain = session.CheckDomain; - ServerNonce = serverNonce; - ServerEccEphemeralKey = eccServerEphemeralKey; - UserIdentityTokenPolicy = userIdentityTokenPolicy; } /// - /// Creates the session configuration from a stream. + /// Creates a session state /// - public static SessionConfiguration Create(Stream stream, ITelemetryContext telemetry) + public SessionState(SessionOptions options) + : base(options) { - // secure settings - XmlReaderSettings settings = Utils.DefaultXmlReaderSettings(); - using var reader = XmlReader.Create(stream, settings); - var serializer = new DataContractSerializer(typeof(SessionConfiguration)); - using IDisposable scope = AmbientMessageContext.SetScopedContext(telemetry); - return (SessionConfiguration)serializer.ReadObject(reader); } /// /// When the session configuration was created. /// [DataMember(IsRequired = true, Order = 10)] - public DateTime Timestamp { get; set; } - - /// - /// The session name used by the client. - /// - [DataMember(IsRequired = true, Order = 20)] - public string SessionName { get; set; } + public DateTime Timestamp { get; init; } = DateTime.UtcNow; /// /// The session id assigned by the server. /// [DataMember(IsRequired = true, Order = 30)] - public NodeId SessionId { get; set; } + public NodeId SessionId { get; init; } = NodeId.Null; /// /// The authentication token used by the server to identify the session. /// [DataMember(IsRequired = true, Order = 40)] - public NodeId AuthenticationToken { get; set; } + public NodeId AuthenticationToken { get; init; } = NodeId.Null; /// - /// The identity used to create the session. + /// The last server nonce received. /// - [DataMember(IsRequired = true, Order = 50)] - public IUserIdentity Identity { get; set; } + [DataMember(IsRequired = true, Order = 80)] + public Nonce? ServerNonce { get; init; } /// - /// The configured endpoint for the secure channel. + /// The user identity token policy which was used to create the session. /// - [DataMember(IsRequired = true, Order = 60)] - public ConfiguredEndpoint ConfiguredEndpoint { get; set; } + [DataMember(IsRequired = true, Order = 90)] + public string? UserIdentityTokenPolicy { get; init; } /// - /// If the client is configured to check the certificate domain. + /// The last server ecc ephemeral key received. /// - [DataMember(IsRequired = false, Order = 70)] - public bool CheckDomain { get; set; } + [DataMember(IsRequired = false, Order = 100)] + public Nonce? ServerEccEphemeralKey { get; init; } /// - /// The last server nonce received. + /// Allows the list of subscriptions to be saved/restored + /// when the object is serialized. /// - [DataMember(IsRequired = true, Order = 80)] - public Nonce ServerNonce { get; set; } + [DataMember(Order = 200)] + public SubscriptionStateCollection? Subscriptions { get; init; } + } + + /// + /// A session configuration stores all the information + /// needed to reconnect a session with a new secure channel. + /// + [DataContract(Namespace = Namespaces.OpcUaXsd)] + [KnownType(typeof(UserIdentityToken))] + [KnownType(typeof(AnonymousIdentityToken))] + [KnownType(typeof(X509IdentityToken))] + [KnownType(typeof(IssuedIdentityToken))] + [KnownType(typeof(UserIdentity))] + public record class SessionConfiguration : SessionState + { + /// + /// Default constructor + /// + public SessionConfiguration() + { + } /// - /// The user identity token policy which was used to create the session. + /// Creates a session configuration /// - [DataMember(IsRequired = true, Order = 90)] - public string UserIdentityTokenPolicy { get; set; } + public SessionConfiguration(SessionState state) + : base(state) + { + } /// - /// The last server ecc ephemeral key received. + /// Creates the session configuration from a stream. /// - [DataMember(IsRequired = false, Order = 100)] - public Nonce ServerEccEphemeralKey { get; set; } + public static SessionConfiguration? Create(Stream stream, ITelemetryContext telemetry) + { + // secure settings + XmlReaderSettings settings = Utils.DefaultXmlReaderSettings(); + using var reader = XmlReader.Create(stream, settings); + var serializer = new DataContractSerializer(typeof(SessionConfiguration)); + using IDisposable scope = AmbientMessageContext.SetScopedContext(telemetry); + return (SessionConfiguration?)serializer.ReadObject(reader); + } } } diff --git a/Libraries/Opc.Ua.Client/Session/SessionExtensions.cs b/Libraries/Opc.Ua.Client/Session/SessionExtensions.cs index 13110ce19a..c729116d2c 100644 --- a/Libraries/Opc.Ua.Client/Session/SessionExtensions.cs +++ b/Libraries/Opc.Ua.Client/Session/SessionExtensions.cs @@ -29,143 +29,417 @@ using System; using System.Collections.Generic; -using System.Diagnostics; +using System.IO; using System.Threading; using System.Threading.Tasks; namespace Opc.Ua.Client { /// - /// Manages a session with a server. + /// Extensions to ISession that are not dependent on anything internal + /// to the Session but layer over ISession /// public static class SessionExtensions { /// - /// Reads the values for a set of variables. + /// Establishes a session with the server. /// - public static async ValueTask<( - IList, - IList - )> ReadValuesAsync( - this ISession session, - IList variableIds, - IList expectedTypes, - CancellationToken ct = default) + /// session to use + /// The name to assign to the session. + /// The user identity. + /// The cancellation token. + public static Task OpenAsync( + this ISession session, + string sessionName, + IUserIdentity identity, + CancellationToken ct = default) { - (DataValueCollection dataValues, IList errors) = - await session.ReadValuesAsync( - variableIds, - ct).ConfigureAwait(false); + return session.OpenAsync(sessionName, 0, identity, null, ct); + } + + /// + /// Establishes a session with the server. + /// + /// session to use + /// The name to assign to the session. + /// The session timeout. + /// The user identity. + /// The list of preferred locales. + /// The cancellation token. + public static Task OpenAsync( + this ISession session, + string sessionName, + uint sessionTimeout, + IUserIdentity identity, + IList? preferredLocales, + CancellationToken ct = default) + { + return session.OpenAsync( + sessionName, + sessionTimeout, + identity, + preferredLocales, + true, + ct); + } + + /// + /// Establishes a session with the server. + /// + /// session to use + /// 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. + public static Task OpenAsync( + this ISession session, + string sessionName, + uint sessionTimeout, + IUserIdentity identity, + IList? preferredLocales, + bool checkDomain, + CancellationToken ct = default) + { + return session.OpenAsync( + sessionName, + sessionTimeout, + identity, + preferredLocales, + checkDomain, + true, + ct); + } + + /// + /// Reconnects to the server after a network failure. + /// Uses the current channel if possible or creates + /// a new one. + /// + public static Task ReconnectAsync( + this ISession session, + CancellationToken ct = default) + { + return session.ReconnectAsync(null, null, ct); + } + + /// + /// Reconnects to the server on a waiting connection + /// + public static Task ReconnectAsync( + this ISession session, + ITransportWaitingConnection connection, + CancellationToken ct = default) + { + return session.ReconnectAsync(connection, null, ct); + } + + /// + /// Reconnects to the server after a network failure + /// using a new channel. + /// + public static Task ReconnectAsync( + this ISession session, + ITransportChannel channel, + CancellationToken ct = default) + { + return session.ReconnectAsync(null, channel, ct); + } + + /// + /// Saves all the subscriptions of the session. + /// + /// session to use + /// The file path. + /// Known types + public static void Save( + this ISession session, + string filePath, + IEnumerable? knownTypes = null) + { + session.Save(filePath, session.Subscriptions, knownTypes); + } + + /// + /// Load the list of subscriptions saved in a file. + /// + /// session to use + /// 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 + public static IEnumerable Load( + this ISession session, + string filePath, + bool transferSubscriptions = false, + IEnumerable? knownTypes = null) + { + using FileStream stream = File.OpenRead(filePath); + return session.Load(stream, transferSubscriptions, knownTypes); + } + + /// + /// Saves a set of subscriptions to a file. + /// + public static void Save( + this ISession session, + string filePath, + IEnumerable subscriptions, + IEnumerable? knownTypes = null) + { + using var stream = new FileStream(filePath, FileMode.Create); + session.Save(stream, subscriptions, knownTypes); + } + + /// + /// Close the session with the server and optionally closes the channel. + /// + public static Task CloseAsync( + this ISession session, + bool closeChannel, + CancellationToken ct = default) + { + return session.CloseAsync(session.KeepAliveInterval, closeChannel, ct); + } + + /// + /// Disconnects from the server and frees any network resources (closes + /// the channel) with the specified timeout. + /// + public static Task CloseAsync( + this ISession session, + int timeout, + CancellationToken ct = default) + { + return session.CloseAsync(timeout, true, ct); + } + + /// + /// Reads a byte string which is too large for the (server side) encoder to handle. + /// + /// session to use + /// The node id of a byte string variable + /// Cancellation token to cancel operation with + /// + public static async Task ReadByteStringInChunksAsync( + this ISession session, + NodeId nodeId, + CancellationToken ct = default) + { + int maxByteStringLength = (int)session.ServerCapabilities.MaxByteStringLength; + if (maxByteStringLength <= 1) + { + throw ServiceResultException.Create( + StatusCodes.BadIndexRangeNoData, + "The MaxByteStringLength is not known or too small for reading data in chunks."); + } + + ReadOnlyMemory buffer = await session.ReadBytesAsync( + nodeId, + maxByteStringLength, + ct).ConfigureAwait(false); + + return buffer.ToArray(); + } + + /// + /// Finds the NodeIds for the components for an instance. + /// + public static async Task<(NodeIdCollection, IList)> FindComponentIdsAsync( + this ISession session, + NodeId instanceId, + IList componentPaths, + CancellationToken ct = default) + { + var componentIds = new NodeIdCollection(); + var errors = new List(); + + // build list of paths to translate. + var pathsToTranslate = new BrowsePathCollection(); + + for (int ii = 0; ii < componentPaths.Count; ii++) + { + var pathToTranslate = new BrowsePath + { + StartingNode = instanceId, + RelativePath = RelativePath.Parse(componentPaths[ii], session.TypeTree) + }; + + pathsToTranslate.Add(pathToTranslate); + } + + // translate the paths. + + TranslateBrowsePathsToNodeIdsResponse response = await session.TranslateBrowsePathsToNodeIdsAsync( + null, + pathsToTranslate, + ct).ConfigureAwait(false); + + BrowsePathResultCollection results = response.Results; + DiagnosticInfoCollection diagnosticInfos = response.DiagnosticInfos; + ResponseHeader responseHeader = response.ResponseHeader; + + // verify that the server returned the correct number of results. + ClientBase.ValidateResponse(results, pathsToTranslate); + ClientBase.ValidateDiagnosticInfos(diagnosticInfos, pathsToTranslate); - object[] values = new object[dataValues.Count]; - for (int ii = 0; ii < variableIds.Count; ii++) + for (int ii = 0; ii < componentPaths.Count; ii++) { - object value = dataValues[ii].Value; + componentIds.Add(NodeId.Null); + errors.Add(ServiceResult.Good); + + // process any diagnostics associated with any error. + if (StatusCode.IsBad(results[ii].StatusCode)) + { + errors[ii] = new ServiceResult( + results[ii].StatusCode, + ii, + diagnosticInfos, + responseHeader.StringTable); + continue; + } + + // Expecting exact one NodeId for a local node. + // Report an error if the server returns anything other than that. + + if (results[ii].Targets.Count == 0) + { + errors[ii] = ServiceResult.Create( + StatusCodes.BadTargetNodeIdInvalid, + "Could not find target for path: {0}.", + componentPaths[ii]); + + continue; + } + + if (results[ii].Targets.Count != 1) + { + errors[ii] = ServiceResult.Create( + StatusCodes.BadTooManyMatches, + "Too many matches found for path: {0}.", + componentPaths[ii]); + + continue; + } - // extract the body from extension objects. - if (value is ExtensionObject extension && - extension.Body is IEncodeable) + if (results[ii].Targets[0].RemainingPathIndex != uint.MaxValue) { - value = extension.Body; + errors[ii] = ServiceResult.Create( + StatusCodes.BadTargetNodeIdInvalid, + "Cannot follow path to external server: {0}.", + componentPaths[ii]); + + continue; } - // check expected type. - if (expectedTypes[ii] != null && - !expectedTypes[ii].IsInstanceOfType(value)) + if (NodeId.IsNull(results[ii].Targets[0].TargetId)) { errors[ii] = ServiceResult.Create( - StatusCodes.BadTypeMismatch, - "Value {0} does not have expected type: {1}.", - value, - expectedTypes[ii].Name); + StatusCodes.BadUnexpectedError, + "Server returned a null NodeId for path: {0}.", + componentPaths[ii]); + continue; } - // suitable value found. - values[ii] = value; + if (results[ii].Targets[0].TargetId.IsAbsolute) + { + errors[ii] = ServiceResult.Create( + StatusCodes.BadUnexpectedError, + "Server returned a remote node for path: {0}.", + componentPaths[ii]); + + continue; + } + + // suitable target found. + componentIds[ii] = ExpandedNodeId.ToNodeId( + results[ii].Targets[0].TargetId, + session.NamespaceUris); } - return (values, errors); + return (componentIds, errors); } /// - /// Invokes the Browse service. + /// Returns the available encodings for a node /// + /// The session to use + /// The variable node. + /// Cancellation token to use to cancel the operation /// - public static async ValueTask<( - ResponseHeader, - byte[], - ReferenceDescriptionCollection - )> BrowseAsync( - this ISession session, - RequestHeader requestHeader, - ViewDescription view, - NodeId nodeToBrowse, - uint maxResultsToReturn, - BrowseDirection browseDirection, - NodeId referenceTypeId, - bool includeSubtypes, - uint nodeClassMask, - CancellationToken ct = default) + public static async Task ReadAvailableEncodingsAsync( + this ISession session, + NodeId variableId, + CancellationToken ct = default) { - ResponseHeader responseHeader; - IList errors; - IList referencesList; - ByteStringCollection continuationPoints; - (responseHeader, continuationPoints, referencesList, errors) = - await session.BrowseAsync( - requestHeader, - view, - [nodeToBrowse], - maxResultsToReturn, - browseDirection, - referenceTypeId, - includeSubtypes, - nodeClassMask, - ct).ConfigureAwait(false); - - Debug.Assert(errors.Count <= 1); - if (errors.Count > 0 && StatusCode.IsBad(errors[0].StatusCode)) + if (await session.NodeCache.FindAsync(variableId, ct).ConfigureAwait(false) + is not VariableNode variable) { - throw new ServiceResultException(errors[0]); + throw ServiceResultException.Create( + StatusCodes.BadNodeIdInvalid, + "NodeId does not refer to a valid variable node."); } - Debug.Assert(referencesList.Count == 1); - Debug.Assert(continuationPoints.Count == 1); - return (responseHeader, continuationPoints[0], referencesList[0]); - } + // no encodings available if there was a problem reading the + // data type for the node. + if (NodeId.IsNull(variable.DataType)) + { + return []; + } - /// - /// Invokes the BrowseNext service. - /// - /// - public static async ValueTask<( - ResponseHeader, - byte[], - ReferenceDescriptionCollection - )> BrowseNextAsync( - this ISession session, - RequestHeader requestHeader, - bool releaseContinuationPoint, - byte[] continuationPoint, - CancellationToken ct = default) - { - ResponseHeader responseHeader; - IList errors; - IList referencesList; - - ByteStringCollection revisedContinuationPoints; - (responseHeader, revisedContinuationPoints, referencesList, errors) = - await session.BrowseNextAsync(requestHeader, [continuationPoint], releaseContinuationPoint, ct) - .ConfigureAwait(false); - Debug.Assert(errors.Count <= 1); - if (errors.Count > 0 && StatusCode.IsBad(errors[0].StatusCode)) + // no encodings for non-structures. + if (!await session.NodeCache.IsTypeOfAsync( + variable.DataType, + DataTypes.Structure, + ct).ConfigureAwait(false)) { - throw new ServiceResultException(errors[0]); + return []; } - Debug.Assert(referencesList.Count == 1); - Debug.Assert(revisedContinuationPoints.Count == 1); - return (responseHeader, revisedContinuationPoints[0], referencesList[0]); + // look for cached values. + IList encodings = await session.NodeCache.FindAsync( + variableId, + ReferenceTypeIds.HasEncoding, + false, + true, + ct).ConfigureAwait(false); + + if (encodings.Count > 0) + { + var references = new ReferenceDescriptionCollection(); + + foreach (INode encoding in encodings) + { + var reference = new ReferenceDescription + { + ReferenceTypeId = ReferenceTypeIds.HasEncoding, + IsForward = true, + NodeId = encoding.NodeId, + NodeClass = encoding.NodeClass, + BrowseName = encoding.BrowseName, + DisplayName = encoding.DisplayName, + TypeDefinition = encoding.TypeDefinitionId + }; + + references.Add(reference); + } + + return references; + } + + var browser = new Browser(session, new BrowserOptions + { + BrowseDirection = BrowseDirection.Forward, + ReferenceTypeId = ReferenceTypeIds.HasEncoding, + IncludeSubtypes = false, + NodeClassMask = 0 + }); + + return await browser.BrowseAsync(variable.DataType, ct).ConfigureAwait(false); } } } diff --git a/Libraries/Opc.Ua.Client/Session/SessionObsolete.cs b/Libraries/Opc.Ua.Client/Session/SessionObsolete.cs index 21c3e9636b..135b16553f 100644 --- a/Libraries/Opc.Ua.Client/Session/SessionObsolete.cs +++ b/Libraries/Opc.Ua.Client/Session/SessionObsolete.cs @@ -27,9 +27,15 @@ * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ +#nullable disable + using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; namespace Opc.Ua.Client { @@ -706,10 +712,580 @@ public static bool ResendData( IEnumerable subscriptions, out IList errors) { - (bool result, errors) = session.ResendDataAsync(subscriptions) + (bool success, errors) = session.ResendDataAsync(subscriptions) .GetAwaiter() .GetResult(); - return result; + return success; + } + + /// + /// Call the ResendData method on the server for all subscriptions. + /// + [Obsolete("Use ResendDataAsync using subscription ids instead.")] + public static async Task<(bool, IList)> ResendDataAsync( + this ISession session, + IEnumerable subscriptions, + CancellationToken ct = default) + { + try + { + IReadOnlyList errorsRo = await session.ResendDataAsync( + subscriptions.Select(s => s.Id), ct).ConfigureAwait(false); + return (true, errorsRo.ToList()); + } + catch + { + return (false, Array.Empty()); + } + } + } + + public partial class Session + { + /// + /// Creates a new communication session with a server by invoking the CreateSession service + /// + [Obsolete("Use ISessionFactory.CreateAsync")] + public static Task Create( + ApplicationConfiguration configuration, + ConfiguredEndpoint endpoint, + bool updateBeforeConnect, + string sessionName, + uint sessionTimeout, + IUserIdentity identity, + IList preferredLocales, + CancellationToken ct = default) + { + return Create( + configuration, + endpoint, + updateBeforeConnect, + false, + sessionName, + sessionTimeout, + identity, + preferredLocales, + ct); + } + + /// + /// Creates a new communication session with a server by invoking the CreateSession service + /// + [Obsolete("Use ISessionFactory.CreateAsync")] + public static Task Create( + ApplicationConfiguration configuration, + ConfiguredEndpoint endpoint, + bool updateBeforeConnect, + bool checkDomain, + string sessionName, + uint sessionTimeout, + IUserIdentity identity, + IList preferredLocales, + CancellationToken ct = default) + { + return Create( + configuration, + (ITransportWaitingConnection)null, + endpoint, + updateBeforeConnect, + checkDomain, + sessionName, + sessionTimeout, + identity, + preferredLocales, + ct); + } + + /// + /// Creates a new session with a server using the specified channel + /// + [Obsolete("Use ISessionFactory.CreateAsync")] + public static Session Create( + ApplicationConfiguration configuration, + ITransportChannel channel, + ConfiguredEndpoint endpoint, + X509Certificate2 clientCertificate, + EndpointDescriptionCollection availableEndpoints = null, + StringCollection discoveryProfileUris = null) + { + return Create( + null, + configuration, + channel, + endpoint, + clientCertificate, + availableEndpoints, + discoveryProfileUris); + } + + /// + /// Recreates a session based on a specified template. + /// + /// The Session object to use as template + /// The new session object. + [Obsolete("Use ISessionFactory.RecreateAsync")] + public static Session Recreate(Session template) + { + return RecreateAsync(template).GetAwaiter().GetResult(); + } + + /// + /// Recreates a session based on a specified template. + /// + /// The Session object to use as template + /// The waiting reverse connection. + /// The new session object. + [Obsolete("Use ISessionFactory.RecreateAsync")] + public static Session Recreate(Session template, ITransportWaitingConnection connection) + { + return RecreateAsync(template, connection).GetAwaiter().GetResult(); + } + + /// + /// Recreates a session based on a specified template using the provided channel. + /// + /// The Session object to use as template + /// The waiting reverse connection. + /// The new session object. + [Obsolete("Use ISessionFactory.RecreateAsync")] + public static Session Recreate(Session template, ITransportChannel transportChannel) + { + return RecreateAsync(template, transportChannel).GetAwaiter().GetResult(); + } + + /// + /// Recreates a session based on a specified template. + /// + [Obsolete("Use ISessionFactory.RecreateAsync")] + public static async Task RecreateAsync( + Session sessionTemplate, + CancellationToken ct = default) + { + var factory = new DefaultSessionFactory(sessionTemplate.MessageContext.Telemetry) + { + ReturnDiagnostics = sessionTemplate.ReturnDiagnostics + }; + return (Session)await factory.RecreateAsync( + sessionTemplate, + ct).ConfigureAwait(false); + } + + /// + /// Recreates a session based on a specified template. + /// + [Obsolete("Use ISessionFactory.RecreateAsync")] + public static async Task RecreateAsync( + Session sessionTemplate, + ITransportWaitingConnection connection, + CancellationToken ct = default) + { + var factory = new DefaultSessionFactory(sessionTemplate.MessageContext.Telemetry) + { + ReturnDiagnostics = sessionTemplate.ReturnDiagnostics + }; + return (Session)await factory.RecreateAsync( + sessionTemplate, + connection, + ct).ConfigureAwait(false); + } + + /// + /// Recreates a session based on a specified template using the provided channel. + /// + [Obsolete("Use ISessionFactory.RecreateAsync")] + public static async Task RecreateAsync( + Session sessionTemplate, + ITransportChannel transportChannel, + CancellationToken ct = default) + { + var factory = new DefaultSessionFactory(sessionTemplate.MessageContext.Telemetry) + { + ReturnDiagnostics = sessionTemplate.ReturnDiagnostics + }; + return (Session)await factory.RecreateAsync( + sessionTemplate, + transportChannel, + ct).ConfigureAwait(false); + } + + /// + /// Creates a new communication session with a server using a reverse connection. + /// + [Obsolete("Use ISessionFactory.CreateAsync")] + public static Task Create( + ApplicationConfiguration configuration, + ITransportWaitingConnection connection, + ConfiguredEndpoint endpoint, + bool updateBeforeConnect, + bool checkDomain, + string sessionName, + uint sessionTimeout, + IUserIdentity identity, + IList preferredLocales, + CancellationToken ct = default) + { + return CreateAsync( + null, + configuration, + connection, + endpoint, + updateBeforeConnect, + checkDomain, + sessionName, + sessionTimeout, + identity, + preferredLocales, + DiagnosticsMasks.None, + ct); + } + + /// + /// Create a session + /// + [Obsolete("Use ISessionFactory.CreateAsync")] + public static Task Create( + ISessionInstantiator sessionInstantiator, + ApplicationConfiguration configuration, + ITransportWaitingConnection connection, + ConfiguredEndpoint endpoint, + bool updateBeforeConnect, + bool checkDomain, + string sessionName, + uint sessionTimeout, + IUserIdentity identity, + IList preferredLocales, + CancellationToken ct = default) + { + return CreateAsync( + sessionInstantiator, + configuration, + connection, + endpoint, + updateBeforeConnect, + checkDomain, + sessionName, + sessionTimeout, + identity, + preferredLocales, + DiagnosticsMasks.None, + ct); + } + + /// + /// Create a session + /// + [Obsolete("Use ISessionFactory.CreateAsync")] + public static Task Create( + ISessionInstantiator sessionInstantiator, + ApplicationConfiguration configuration, + ReverseConnectManager reverseConnectManager, + ConfiguredEndpoint endpoint, + bool updateBeforeConnect, + bool checkDomain, + string sessionName, + uint sessionTimeout, + IUserIdentity userIdentity, + IList preferredLocales, + CancellationToken ct = default) + { + return CreateAsync( + sessionInstantiator, + configuration, + reverseConnectManager, + endpoint, + updateBeforeConnect, + checkDomain, + sessionName, + sessionTimeout, + userIdentity, + preferredLocales, + DiagnosticsMasks.None, + ct); + } + + /// + /// Create a session + /// + [Obsolete("Use ISessionFactory.CreateAsync")] + public static Task Create( + ApplicationConfiguration configuration, + ReverseConnectManager reverseConnectManager, + ConfiguredEndpoint endpoint, + bool updateBeforeConnect, + bool checkDomain, + string sessionName, + uint sessionTimeout, + IUserIdentity userIdentity, + IList preferredLocales, + CancellationToken ct = default) + { + return CreateAsync( + configuration, + reverseConnectManager, + endpoint, + updateBeforeConnect, + checkDomain, + sessionName, + sessionTimeout, + userIdentity, + preferredLocales, + ct); + } + + /// + /// Creates a new session. + /// + [Obsolete("Use ISessionFactory.Create")] + public static Session Create( + ISessionInstantiator sessionInstantiator, + ApplicationConfiguration configuration, + ITransportChannel channel, + ConfiguredEndpoint endpoint, + X509Certificate2 clientCertificate, + EndpointDescriptionCollection availableEndpoints = null, + StringCollection discoveryProfileUris = null) + { + ServiceMessageContext context = configuration.CreateMessageContext(false); + var factory = new DefaultSessionFactory(context.Telemetry); + return (Session)factory.Create( + channel, + configuration, + endpoint, + clientCertificate, + null, + availableEndpoints, + discoveryProfileUris); + } + + /// + /// Creates a secure channel to the specified endpoint. + /// + [Obsolete("Use ISessionFactory.CreateChannelAsync")] + public static Task CreateChannelAsync( + ApplicationConfiguration configuration, + ITransportWaitingConnection connection, + ConfiguredEndpoint endpoint, + bool updateBeforeConnect, + bool checkDomain, + CancellationToken ct = default) + { + ServiceMessageContext context = configuration.CreateMessageContext(false); + var factory = new DefaultSessionFactory(context.Telemetry); + return factory.CreateChannelAsync( + configuration, + connection, + endpoint, + updateBeforeConnect, + checkDomain, + ct); + } + + /// + /// Creates a new communication session with a server using a reverse connection. + /// + [Obsolete("Use ISessionFactory.CreateAsync")] + public static async Task CreateAsync( + ISessionInstantiator sessionInstantiator, + ApplicationConfiguration configuration, + ITransportWaitingConnection connection, + ConfiguredEndpoint endpoint, + bool updateBeforeConnect, + bool checkDomain, + string sessionName, + uint sessionTimeout, + IUserIdentity identity, + IList preferredLocales, + DiagnosticsMasks returnDiagnostics, + CancellationToken ct = default) + { + ServiceMessageContext context = configuration.CreateMessageContext(false); + var factory = new DefaultSessionFactory(context.Telemetry) + { + ReturnDiagnostics = returnDiagnostics + }; + return (Session)await factory.CreateAsync( + configuration, + connection, + endpoint, + updateBeforeConnect, + checkDomain, + sessionName, + sessionTimeout, + identity, + preferredLocales, + ct).ConfigureAwait(false); + } + + /// + /// Creates a new communication session with a server using a reverse connect manager. + /// + [Obsolete("Use ISessionFactory.CreateAsync")] + public static async Task CreateAsync( + ApplicationConfiguration configuration, + ReverseConnectManager reverseConnectManager, + ConfiguredEndpoint endpoint, + bool updateBeforeConnect, + bool checkDomain, + string sessionName, + uint sessionTimeout, + IUserIdentity userIdentity, + IList preferredLocales, + CancellationToken ct = default) + { + ServiceMessageContext context = configuration.CreateMessageContext(false); + var factory = new DefaultSessionFactory(context.Telemetry); + return (Session)await factory.CreateAsync( + configuration, + reverseConnectManager, + endpoint, + updateBeforeConnect, + checkDomain, + sessionName, + sessionTimeout, + userIdentity, + preferredLocales, + ct).ConfigureAwait(false); + } + + /// + /// Creates a new communication session with a server using a reverse connect manager. + /// + [Obsolete("Use ISessionFactory.CreateAsync")] + public static async Task CreateAsync( + ISessionInstantiator sessionInstantiator, + ApplicationConfiguration configuration, + ReverseConnectManager reverseConnectManager, + ConfiguredEndpoint endpoint, + bool updateBeforeConnect, + bool checkDomain, + string sessionName, + uint sessionTimeout, + IUserIdentity userIdentity, + IList preferredLocales, + DiagnosticsMasks returnDiagnostics, + CancellationToken ct = default) + { + ServiceMessageContext context = configuration.CreateMessageContext(false); + var factory = new DefaultSessionFactory(context.Telemetry) + { + ReturnDiagnostics = returnDiagnostics + }; + return (Session)await factory.CreateAsync( + configuration, + reverseConnectManager, + endpoint, + updateBeforeConnect, + checkDomain, + sessionName, + sessionTimeout, + userIdentity, + preferredLocales, + ct).ConfigureAwait(false); + } + } + + /// + /// Obsolete traceable session, which now is supported by session + /// + [Obsolete("Use Session which also provides tracing")] + public class TraceableSession : Session + { + /// + /// Create session + /// + public TraceableSession( + ISessionChannel channel, + ApplicationConfiguration configuration, + ConfiguredEndpoint endpoint) + : base(channel, configuration, endpoint) + { + } + + /// + /// Create session + /// + public TraceableSession( + ITransportChannel channel, + Session template, + bool copyEventHandlers) + : base(channel, template, copyEventHandlers) + { + } + + /// + /// Create session + /// + public TraceableSession( + ITransportChannel channel, + ApplicationConfiguration configuration, + ConfiguredEndpoint endpoint, + X509Certificate2 clientCertificate, + EndpointDescriptionCollection availableEndpoints = null, + StringCollection discoveryProfileUris = null) + : base( + channel, + configuration, + endpoint, + clientCertificate, + null, + availableEndpoints, + discoveryProfileUris) + { + } + + /// + /// Object that creates instances of a session + /// + [Obsolete("Use DefaultSessionFactory which also provides tracing capabilities.")] + public class TraceableSessionFactory : DefaultSessionFactory + { + /// + /// The default instance of the factory. + /// + public static new readonly TraceableSessionFactory Instance = new(); + + /// + /// Obsolete default constructor + /// + public TraceableSessionFactory() + : base(null) + { + } + } + } + + /// + /// Object that creates an instance of a Session object. + /// + [Obsolete("Use ISessionFactory instead. This interface will be removed in a future release.")] + public interface ISessionInstantiator : ISessionFactory; + + /// + /// Obsolete session factory api + /// + public static class SessionFactoryObsolete + { + /// + /// Create session + /// + [Obsolete("Use Create with channel and configuration reversed")] + public static ISession Create( + this ISessionFactory factory, + ApplicationConfiguration configuration, + ITransportChannel channel, + ConfiguredEndpoint endpoint, + X509Certificate2 clientCertificate, + EndpointDescriptionCollection availableEndpoints = null, + StringCollection discoveryProfileUris = null) + { + return factory.Create( + channel, + configuration, + endpoint, + clientCertificate, + null, + availableEndpoints, + discoveryProfileUris); } } } diff --git a/Libraries/Opc.Ua.Client/Session/SessionReconnectHandler.cs b/Libraries/Opc.Ua.Client/Session/SessionReconnectHandler.cs index 550cf25175..fd795a399e 100644 --- a/Libraries/Opc.Ua.Client/Session/SessionReconnectHandler.cs +++ b/Libraries/Opc.Ua.Client/Session/SessionReconnectHandler.cs @@ -27,6 +27,8 @@ * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ +#nullable disable + using System; using System.Threading; using System.Threading.Tasks; diff --git a/Libraries/Opc.Ua.Client/Session/TraceableSession.cs b/Libraries/Opc.Ua.Client/Session/TraceableSession.cs deleted file mode 100644 index fc6d9e8931..0000000000 --- a/Libraries/Opc.Ua.Client/Session/TraceableSession.cs +++ /dev/null @@ -1,2947 +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.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Opc.Ua.Client -{ - /// - /// Decorator class for traceable session with Activity Source. - /// - public class TraceableSession : ISession - { - /// - /// Obsolete default constructor - /// - [Obsolete("Use TraceableSession(ITelemetryContext) instead.")] - public TraceableSession(ISession session) - : this(session, null) - { - } - - /// - /// Initializes a new instance of the class. - /// - public TraceableSession(ISession session, ITelemetryContext telemetry) - { - Session = session; - m_telemetry = telemetry; - SessionFactory = new TraceableSessionFactory(telemetry) - { - ReturnDiagnostics = ReturnDiagnostics - }; - } - - /// - public ISession Session { get; } - - /// - public event KeepAliveEventHandler KeepAlive - { - add => Session.KeepAlive += value; - remove => Session.KeepAlive -= value; - } - - /// - public event NotificationEventHandler Notification - { - add => Session.Notification += value; - remove => Session.Notification -= value; - } - - /// - public event PublishErrorEventHandler PublishError - { - add => Session.PublishError += value; - remove => Session.PublishError -= value; - } - - /// - public event PublishSequenceNumbersToAcknowledgeEventHandler PublishSequenceNumbersToAcknowledge - { - add => Session.PublishSequenceNumbersToAcknowledge += value; - remove => Session.PublishSequenceNumbersToAcknowledge -= value; - } - - /// - public event EventHandler SubscriptionsChanged - { - add => Session.SubscriptionsChanged += value; - remove => Session.SubscriptionsChanged -= value; - } - - /// - public event EventHandler SessionClosing - { - add => Session.SessionClosing += value; - remove => Session.SessionClosing -= value; - } - - /// - public event EventHandler SessionConfigurationChanged - { - add => Session.SessionConfigurationChanged += value; - remove => Session.SessionConfigurationChanged -= value; - } - - /// - public event RenewUserIdentityEventHandler RenewUserIdentity - { - add => Session.RenewUserIdentity += value; - remove => Session.RenewUserIdentity -= value; - } - - /// - public ISessionFactory SessionFactory { get; } - - /// - public ConfiguredEndpoint ConfiguredEndpoint => Session.ConfiguredEndpoint; - - /// - public string SessionName => Session.SessionName; - - /// - public double SessionTimeout => Session.SessionTimeout; - - /// - public object Handle => Session.Handle; - - /// - public IUserIdentity Identity => Session.Identity; - - /// - public IEnumerable IdentityHistory => Session.IdentityHistory; - - /// - public NamespaceTable NamespaceUris => Session.NamespaceUris; - - /// - public StringTable ServerUris => Session.ServerUris; - - /// - public ISystemContext SystemContext => Session.SystemContext; - - /// - public IEncodeableFactory Factory => Session.Factory; - - /// - public ITypeTable TypeTree => Session.TypeTree; - - /// - public INodeCache NodeCache => Session.NodeCache; - - /// - public FilterContext FilterContext => Session.FilterContext; - - /// - public StringCollection PreferredLocales => Session.PreferredLocales; - - /// - public IEnumerable Subscriptions => Session.Subscriptions; - - /// - public int SubscriptionCount => Session.SubscriptionCount; - - /// - public bool DeleteSubscriptionsOnClose - { - get => Session.DeleteSubscriptionsOnClose; - set => Session.DeleteSubscriptionsOnClose = value; - } - - /// - public int PublishRequestCancelDelayOnCloseSession - { - get => Session.PublishRequestCancelDelayOnCloseSession; - set => Session.PublishRequestCancelDelayOnCloseSession = value; - } - - /// - public Subscription DefaultSubscription - { - get => Session.DefaultSubscription; - set => Session.DefaultSubscription = value; - } - - /// - public int KeepAliveInterval - { - get => Session.KeepAliveInterval; - set => Session.KeepAliveInterval = value; - } - - /// - public bool KeepAliveStopped => Session.KeepAliveStopped; - - /// - public DateTime LastKeepAliveTime => Session.LastKeepAliveTime; - - /// - public int LastKeepAliveTickCount => Session.LastKeepAliveTickCount; - - /// - public int OutstandingRequestCount => Session.OutstandingRequestCount; - - /// - public int DefunctRequestCount => Session.DefunctRequestCount; - - /// - public int GoodPublishRequestCount => Session.GoodPublishRequestCount; - - /// - public int MinPublishRequestCount - { - get => Session.MinPublishRequestCount; - set => Session.MinPublishRequestCount = value; - } - - /// - public int MaxPublishRequestCount - { - get => Session.MaxPublishRequestCount; - set => Session.MaxPublishRequestCount = value; - } - - /// - public OperationLimits OperationLimits => Session.OperationLimits; - - /// - public bool TransferSubscriptionsOnReconnect - { - get => Session.TransferSubscriptionsOnReconnect; - set => Session.TransferSubscriptionsOnReconnect = value; - } - - /// - public NodeId SessionId => Session.SessionId; - - /// - public bool Connected => Session.Connected; - - /// - public bool Reconnecting => Session.Reconnecting; - - /// - public EndpointDescription Endpoint => Session.Endpoint; - - /// - public EndpointConfiguration EndpointConfiguration => Session.EndpointConfiguration; - - /// - public IServiceMessageContext MessageContext => Session.MessageContext; - - /// - public ITransportChannel NullableTransportChannel => Session.NullableTransportChannel; - - /// - public ITransportChannel TransportChannel => Session.TransportChannel; - - /// - public DiagnosticsMasks ReturnDiagnostics - { - get => Session.ReturnDiagnostics; - set => Session.ReturnDiagnostics = value; - } - - /// - public int OperationTimeout - { - get => Session.OperationTimeout; - set => Session.OperationTimeout = value; - } - - /// - public int DefaultTimeoutHint - { - get => Session.DefaultTimeoutHint; - set => Session.DefaultTimeoutHint = value; - } - - /// - public bool Disposed => Session.Disposed; - - /// - public bool CheckDomain => Session.CheckDomain; - - /// - public ContinuationPointPolicy ContinuationPointPolicy - { - get => Session.ContinuationPointPolicy; - set => Session.ContinuationPointPolicy = value; - } - - /// - public override bool Equals(object obj) - { - if (ReferenceEquals(this, obj)) - { - return true; - } - // Presume that the wrapper is being compared to the - // wrapped object, e.g. in a keep alive callback. - if (ReferenceEquals(Session, obj)) - { - return true; - } - - return Session?.Equals(obj) ?? false; - } - - /// - public override int GetHashCode() - { - return Session?.GetHashCode() ?? base.GetHashCode(); - } - - /// - public async Task ReconnectAsync(CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - await Session.ReconnectAsync(ct).ConfigureAwait(false); - } - - /// - public async Task ReconnectAsync( - ITransportWaitingConnection connection, - CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - await Session.ReconnectAsync(connection, ct).ConfigureAwait(false); - } - - /// - public async Task ReconnectAsync(ITransportChannel channel, CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - await Session.ReconnectAsync(channel, ct).ConfigureAwait(false); - } - - /// - public async Task ReloadInstanceCertificateAsync(CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - await Session.ReloadInstanceCertificateAsync(ct).ConfigureAwait(false); - } - - /// - public void Save(string filePath, IEnumerable knownTypes = null) - { - using Activity activity = m_telemetry.StartActivity(); - Session.Save(filePath, knownTypes); - } - - /// - public void Save( - Stream stream, - IEnumerable subscriptions, - IEnumerable knownTypes = null) - { - using Activity activity = m_telemetry.StartActivity(); - Session.Save(stream, subscriptions, knownTypes); - } - - /// - public void Save( - string filePath, - IEnumerable subscriptions, - IEnumerable knownTypes = null) - { - using Activity activity = m_telemetry.StartActivity(); - Session.Save(filePath, subscriptions, knownTypes); - } - - /// - public IEnumerable Load( - Stream stream, - bool transferSubscriptions = false, - IEnumerable knownTypes = null) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.Load(stream, transferSubscriptions, knownTypes); - } - - /// - public IEnumerable Load( - string filePath, - bool transferSubscriptions = false, - IEnumerable knownTypes = null) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.Load(filePath, transferSubscriptions, knownTypes); - } - - /// - public async Task FetchTypeTreeAsync(ExpandedNodeId typeId, CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - await Session.FetchTypeTreeAsync(typeId, ct).ConfigureAwait(false); - } - - /// - public async Task FetchTypeTreeAsync( - ExpandedNodeIdCollection typeIds, - CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - await Session.FetchTypeTreeAsync(typeIds, ct).ConfigureAwait(false); - } - - /// - public async Task FetchReferencesAsync( - NodeId nodeId, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.FetchReferencesAsync(nodeId, ct).ConfigureAwait(false); - } - - /// - public async Task<(IList, IList)> FetchReferencesAsync( - IList nodeIds, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.FetchReferencesAsync(nodeIds, ct).ConfigureAwait(false); - } - - /// - public async Task ChangePreferredLocalesAsync( - StringCollection preferredLocales, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - await Session.ChangePreferredLocalesAsync(preferredLocales, ct) - .ConfigureAwait(false); - } - - /// - public async Task UpdateSessionAsync( - IUserIdentity identity, - StringCollection preferredLocales, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - await Session.UpdateSessionAsync(identity, preferredLocales, ct) - .ConfigureAwait(false); - } - - /// - public void FindComponentIds( - NodeId instanceId, - IList componentPaths, - out NodeIdCollection componentIds, - out IList errors) - { - using Activity activity = m_telemetry.StartActivity(); - Session.FindComponentIds(instanceId, componentPaths, out componentIds, out errors); - } - - /// - public async Task ReadByteStringInChunksAsync(NodeId nodeId, CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.ReadByteStringInChunksAsync(nodeId, ct).ConfigureAwait(false); - } - - /// - public async Task OpenAsync( - string sessionName, - IUserIdentity identity, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - await Session.OpenAsync(sessionName, identity, ct).ConfigureAwait(false); - } - - /// - public async Task OpenAsync( - string sessionName, - uint sessionTimeout, - IUserIdentity identity, - IList preferredLocales, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - await Session.OpenAsync(sessionName, sessionTimeout, identity, preferredLocales, ct) - .ConfigureAwait(false); - } - - /// - public async Task OpenAsync( - string sessionName, - uint sessionTimeout, - IUserIdentity identity, - IList preferredLocales, - bool checkDomain, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - await Session - .OpenAsync(sessionName, sessionTimeout, identity, preferredLocales, checkDomain, ct) - .ConfigureAwait(false); - } - - /// - public async Task OpenAsync( - string sessionName, - uint sessionTimeout, - IUserIdentity identity, - IList preferredLocales, - bool checkDomain, - bool closeChannel, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - await Session - .OpenAsync( - sessionName, - sessionTimeout, - identity, - preferredLocales, - checkDomain, - closeChannel, - ct) - .ConfigureAwait(false); - } - - /// - public async Task FetchNamespaceTablesAsync(CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - await Session.FetchNamespaceTablesAsync(ct).ConfigureAwait(false); - } - - /// - public async Task<(IList, IList)> ReadNodesAsync( - IList nodeIds, - NodeClass nodeClass, - bool optionalAttributes = false, - CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.ReadNodesAsync(nodeIds, nodeClass, optionalAttributes, ct) - .ConfigureAwait(false); - } - - /// - public async Task ReadValueAsync(NodeId nodeId, CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.ReadValueAsync(nodeId, ct).ConfigureAwait(false); - } - - /// - public async Task ReadNodeAsync(NodeId nodeId, CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.ReadNodeAsync(nodeId, ct).ConfigureAwait(false); - } - - /// - public async Task ReadNodeAsync( - NodeId nodeId, - NodeClass nodeClass, - bool optionalAttributes = true, - CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.ReadNodeAsync(nodeId, nodeClass, optionalAttributes, ct) - .ConfigureAwait(false); - } - - /// - public async Task<(IList, IList)> ReadNodesAsync( - IList nodeIds, - bool optionalAttributes = false, - CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.ReadNodesAsync(nodeIds, optionalAttributes, ct) - .ConfigureAwait(false); - } - - /// - public async Task<(DataValueCollection, IList)> ReadValuesAsync( - IList nodeIds, - CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.ReadValuesAsync(nodeIds, ct).ConfigureAwait(false); - } - - /// - public async Task CloseAsync(CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.CloseAsync(ct).ConfigureAwait(false); - } - - /// - public async Task CloseAsync(bool closeChannel, CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.CloseAsync(closeChannel, ct).ConfigureAwait(false); - } - - /// - public async Task CloseAsync(int timeout, CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.CloseAsync(timeout, ct).ConfigureAwait(false); - } - - /// - public async Task CloseAsync( - int timeout, - bool closeChannel, - CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.CloseAsync(timeout, closeChannel, ct).ConfigureAwait(false); - } - - /// - public bool AddSubscription(Subscription subscription) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.AddSubscription(subscription); - } - - /// - public bool RemoveTransferredSubscription(Subscription subscription) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.RemoveTransferredSubscription(subscription); - } - - /// - public async Task RemoveSubscriptionAsync(Subscription subscription) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.RemoveSubscriptionAsync(subscription).ConfigureAwait(false); - } - - /// - public async Task RemoveSubscriptionsAsync(IEnumerable subscriptions) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.RemoveSubscriptionsAsync(subscriptions).ConfigureAwait(false); - } - - /// - public bool BeginPublish(int timeout) - { - return Session.BeginPublish(timeout); - } - - /// - public void StartPublishing(int timeout, bool fullQueue) - { - using Activity activity = m_telemetry.StartActivity(); - Session.StartPublishing(timeout, fullQueue); - } - - /// - public async Task<(bool, ServiceResult)> RepublishAsync( - uint subscriptionId, - uint sequenceNumber, - CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.RepublishAsync(subscriptionId, sequenceNumber, ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use CreateSessionAsync() instead.")] - public ResponseHeader CreateSession( - RequestHeader requestHeader, - ApplicationDescription clientDescription, - string serverUri, - string endpointUrl, - string sessionName, - byte[] clientNonce, - byte[] clientCertificate, - double requestedSessionTimeout, - uint maxResponseMessageSize, - out NodeId sessionId, - out NodeId authenticationToken, - out double revisedSessionTimeout, - out byte[] serverNonce, - out byte[] serverCertificate, - out EndpointDescriptionCollection serverEndpoints, - out SignedSoftwareCertificateCollection serverSoftwareCertificates, - out SignatureData serverSignature, - out uint maxRequestMessageSize) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.CreateSession( - requestHeader, - clientDescription, - serverUri, - endpointUrl, - sessionName, - clientNonce, - clientCertificate, - requestedSessionTimeout, - maxResponseMessageSize, - out sessionId, - out authenticationToken, - out revisedSessionTimeout, - out serverNonce, - out serverCertificate, - out serverEndpoints, - out serverSoftwareCertificates, - out serverSignature, - out maxRequestMessageSize); - } - - /// - [Obsolete("Use CreateSessionAsync() instead.")] - public IAsyncResult BeginCreateSession( - RequestHeader requestHeader, - ApplicationDescription clientDescription, - string serverUri, - string endpointUrl, - string sessionName, - byte[] clientNonce, - byte[] clientCertificate, - double requestedSessionTimeout, - uint maxResponseMessageSize, - AsyncCallback callback, - object asyncState) - { - return Session.BeginCreateSession( - requestHeader, - clientDescription, - serverUri, - endpointUrl, - sessionName, - clientNonce, - clientCertificate, - requestedSessionTimeout, - maxResponseMessageSize, - callback, - asyncState); - } - - /// - [Obsolete("Use CreateSessionAsync() instead.")] - public ResponseHeader EndCreateSession( - IAsyncResult result, - out NodeId sessionId, - out NodeId authenticationToken, - out double revisedSessionTimeout, - out byte[] serverNonce, - out byte[] serverCertificate, - out EndpointDescriptionCollection serverEndpoints, - out SignedSoftwareCertificateCollection serverSoftwareCertificates, - out SignatureData serverSignature, - out uint maxRequestMessageSize) - { - return Session.EndCreateSession( - result, - out sessionId, - out authenticationToken, - out revisedSessionTimeout, - out serverNonce, - out serverCertificate, - out serverEndpoints, - out serverSoftwareCertificates, - out serverSignature, - out maxRequestMessageSize); - } - - /// - public async Task CreateSessionAsync( - RequestHeader requestHeader, - ApplicationDescription clientDescription, - string serverUri, - string endpointUrl, - string sessionName, - byte[] clientNonce, - byte[] clientCertificate, - double requestedSessionTimeout, - uint maxResponseMessageSize, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session - .CreateSessionAsync( - requestHeader, - clientDescription, - serverUri, - endpointUrl, - sessionName, - clientNonce, - clientCertificate, - requestedSessionTimeout, - maxResponseMessageSize, - ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use ActivateSessionAsync() instead.")] - public ResponseHeader ActivateSession( - RequestHeader requestHeader, - SignatureData clientSignature, - SignedSoftwareCertificateCollection clientSoftwareCertificates, - StringCollection localeIds, - ExtensionObject userIdentityToken, - SignatureData userTokenSignature, - out byte[] serverNonce, - out StatusCodeCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.ActivateSession( - requestHeader, - clientSignature, - clientSoftwareCertificates, - localeIds, - userIdentityToken, - userTokenSignature, - out serverNonce, - out results, - out diagnosticInfos); - } - - /// - [Obsolete("Use ActivateSessionAsync() instead.")] - public IAsyncResult BeginActivateSession( - RequestHeader requestHeader, - SignatureData clientSignature, - SignedSoftwareCertificateCollection clientSoftwareCertificates, - StringCollection localeIds, - ExtensionObject userIdentityToken, - SignatureData userTokenSignature, - AsyncCallback callback, - object asyncState) - { - return Session.BeginActivateSession( - requestHeader, - clientSignature, - clientSoftwareCertificates, - localeIds, - userIdentityToken, - userTokenSignature, - callback, - asyncState); - } - - /// - [Obsolete("Use ActivateSessionAsync() instead.")] - public ResponseHeader EndActivateSession( - IAsyncResult result, - out byte[] serverNonce, - out StatusCodeCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - return Session.EndActivateSession( - result, - out serverNonce, - out results, - out diagnosticInfos); - } - - /// - public async Task ActivateSessionAsync( - RequestHeader requestHeader, - SignatureData clientSignature, - SignedSoftwareCertificateCollection clientSoftwareCertificates, - StringCollection localeIds, - ExtensionObject userIdentityToken, - SignatureData userTokenSignature, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session - .ActivateSessionAsync( - requestHeader, - clientSignature, - clientSoftwareCertificates, - localeIds, - userIdentityToken, - userTokenSignature, - ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use CloseSessionAsync() instead.")] - public ResponseHeader CloseSession(RequestHeader requestHeader, bool deleteSubscriptions) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.CloseSession(requestHeader, deleteSubscriptions); - } - - /// - [Obsolete("Use CloseSessionAsync() instead.")] - public IAsyncResult BeginCloseSession( - RequestHeader requestHeader, - bool deleteSubscriptions, - AsyncCallback callback, - object asyncState) - { - return Session.BeginCloseSession( - requestHeader, - deleteSubscriptions, - callback, - asyncState); - } - - /// - [Obsolete("Use CloseSessionAsync() instead.")] - public ResponseHeader EndCloseSession(IAsyncResult result) - { - return Session.EndCloseSession(result); - } - - /// - public async Task CloseSessionAsync( - RequestHeader requestHeader, - bool deleteSubscriptions, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.CloseSessionAsync(requestHeader, deleteSubscriptions, ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use CancelAsync() instead.")] - public ResponseHeader Cancel( - RequestHeader requestHeader, - uint requestHandle, - out uint cancelCount) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.Cancel(requestHeader, requestHandle, out cancelCount); - } - - /// - [Obsolete("Use CancelAsync() instead.")] - public IAsyncResult BeginCancel( - RequestHeader requestHeader, - uint requestHandle, - AsyncCallback callback, - object asyncState) - { - return Session.BeginCancel(requestHeader, requestHandle, callback, asyncState); - } - - /// - [Obsolete("Use CancelAsync() instead.")] - public ResponseHeader EndCancel(IAsyncResult result, out uint cancelCount) - { - return Session.EndCancel(result, out cancelCount); - } - - /// - public async Task CancelAsync( - RequestHeader requestHeader, - uint requestHandle, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.CancelAsync(requestHeader, requestHandle, ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use AddNodesAsync() instead.")] - public ResponseHeader AddNodes( - RequestHeader requestHeader, - AddNodesItemCollection nodesToAdd, - out AddNodesResultCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.AddNodes(requestHeader, nodesToAdd, out results, out diagnosticInfos); - } - - /// - [Obsolete("Use AddNodesAsync() instead.")] - public IAsyncResult BeginAddNodes( - RequestHeader requestHeader, - AddNodesItemCollection nodesToAdd, - AsyncCallback callback, - object asyncState) - { - return Session.BeginAddNodes(requestHeader, nodesToAdd, callback, asyncState); - } - - /// - [Obsolete("Use AddNodesAsync() instead.")] - public ResponseHeader EndAddNodes( - IAsyncResult result, - out AddNodesResultCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - return Session.EndAddNodes(result, out results, out diagnosticInfos); - } - - /// - public async Task AddNodesAsync( - RequestHeader requestHeader, - AddNodesItemCollection nodesToAdd, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.AddNodesAsync(requestHeader, nodesToAdd, ct).ConfigureAwait(false); - } - - /// - [Obsolete("Use AddReferencesAsync() instead.")] - public ResponseHeader AddReferences( - RequestHeader requestHeader, - AddReferencesItemCollection referencesToAdd, - out StatusCodeCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.AddReferences( - requestHeader, - referencesToAdd, - out results, - out diagnosticInfos); - } - - /// - [Obsolete("Use AddReferencesAsync() instead.")] - public IAsyncResult BeginAddReferences( - RequestHeader requestHeader, - AddReferencesItemCollection referencesToAdd, - AsyncCallback callback, - object asyncState) - { - return Session.BeginAddReferences(requestHeader, referencesToAdd, callback, asyncState); - } - - /// - [Obsolete("Use AddReferencesAsync() instead.")] - public ResponseHeader EndAddReferences( - IAsyncResult result, - out StatusCodeCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - return Session.EndAddReferences(result, out results, out diagnosticInfos); - } - - /// - public async Task AddReferencesAsync( - RequestHeader requestHeader, - AddReferencesItemCollection referencesToAdd, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.AddReferencesAsync(requestHeader, referencesToAdd, ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use DeleteNodesAsync() instead.")] - public ResponseHeader DeleteNodes( - RequestHeader requestHeader, - DeleteNodesItemCollection nodesToDelete, - out StatusCodeCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.DeleteNodes( - requestHeader, - nodesToDelete, - out results, - out diagnosticInfos); - } - - /// - [Obsolete("Use DeleteNodesAsync() instead.")] - public IAsyncResult BeginDeleteNodes( - RequestHeader requestHeader, - DeleteNodesItemCollection nodesToDelete, - AsyncCallback callback, - object asyncState) - { - return Session.BeginDeleteNodes(requestHeader, nodesToDelete, callback, asyncState); - } - - /// - [Obsolete("Use DeleteNodesAsync() instead.")] - public ResponseHeader EndDeleteNodes( - IAsyncResult result, - out StatusCodeCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - return Session.EndDeleteNodes(result, out results, out diagnosticInfos); - } - - /// - public async Task DeleteNodesAsync( - RequestHeader requestHeader, - DeleteNodesItemCollection nodesToDelete, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.DeleteNodesAsync(requestHeader, nodesToDelete, ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use DeleteReferencesAsync() instead.")] - public ResponseHeader DeleteReferences( - RequestHeader requestHeader, - DeleteReferencesItemCollection referencesToDelete, - out StatusCodeCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.DeleteReferences( - requestHeader, - referencesToDelete, - out results, - out diagnosticInfos); - } - - /// - [Obsolete("Use DeleteReferencesAsync() instead.")] - public IAsyncResult BeginDeleteReferences( - RequestHeader requestHeader, - DeleteReferencesItemCollection referencesToDelete, - AsyncCallback callback, - object asyncState) - { - return Session.BeginDeleteReferences( - requestHeader, - referencesToDelete, - callback, - asyncState); - } - - /// - [Obsolete("Use DeleteReferencesAsync() instead.")] - public ResponseHeader EndDeleteReferences( - IAsyncResult result, - out StatusCodeCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - return Session.EndDeleteReferences(result, out results, out diagnosticInfos); - } - - /// - public async Task DeleteReferencesAsync( - RequestHeader requestHeader, - DeleteReferencesItemCollection referencesToDelete, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.DeleteReferencesAsync(requestHeader, referencesToDelete, ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use BrowseAsync() instead.")] - public ResponseHeader Browse( - RequestHeader requestHeader, - ViewDescription view, - uint requestedMaxReferencesPerNode, - BrowseDescriptionCollection nodesToBrowse, - out BrowseResultCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.Browse( - requestHeader, - view, - requestedMaxReferencesPerNode, - nodesToBrowse, - out results, - out diagnosticInfos); - } - - /// - [Obsolete("Use BrowseAsync() instead.")] - public IAsyncResult BeginBrowse( - RequestHeader requestHeader, - ViewDescription view, - uint requestedMaxReferencesPerNode, - BrowseDescriptionCollection nodesToBrowse, - AsyncCallback callback, - object asyncState) - { - return Session.BeginBrowse( - requestHeader, - view, - requestedMaxReferencesPerNode, - nodesToBrowse, - callback, - asyncState); - } - - /// - [Obsolete("Use BrowseAsync() instead.")] - public ResponseHeader EndBrowse( - IAsyncResult result, - out BrowseResultCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - return Session.EndBrowse(result, out results, out diagnosticInfos); - } - - /// - public async Task BrowseAsync( - RequestHeader requestHeader, - ViewDescription view, - uint requestedMaxReferencesPerNode, - BrowseDescriptionCollection nodesToBrowse, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session - .BrowseAsync(requestHeader, view, requestedMaxReferencesPerNode, nodesToBrowse, ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use BrowseNextAsync() instead.")] - public ResponseHeader BrowseNext( - RequestHeader requestHeader, - bool releaseContinuationPoints, - ByteStringCollection continuationPoints, - out BrowseResultCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.BrowseNext( - requestHeader, - releaseContinuationPoints, - continuationPoints, - out results, - out diagnosticInfos); - } - - /// - [Obsolete("Use BrowseNextAsync() instead.")] - public IAsyncResult BeginBrowseNext( - RequestHeader requestHeader, - bool releaseContinuationPoints, - ByteStringCollection continuationPoints, - AsyncCallback callback, - object asyncState) - { - return Session.BeginBrowseNext( - requestHeader, - releaseContinuationPoints, - continuationPoints, - callback, - asyncState); - } - - /// - [Obsolete("Use BrowseNextAsync() instead.")] - public ResponseHeader EndBrowseNext( - IAsyncResult result, - out BrowseResultCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - return Session.EndBrowseNext(result, out results, out diagnosticInfos); - } - - /// - public async Task BrowseNextAsync( - RequestHeader requestHeader, - bool releaseContinuationPoints, - ByteStringCollection continuationPoints, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session - .BrowseNextAsync(requestHeader, releaseContinuationPoints, continuationPoints, ct) - .ConfigureAwait(false); - } - - /// - public async Task<(IList, IList)> ManagedBrowseAsync( - RequestHeader requestHeader, - ViewDescription view, - IList nodesToBrowse, - uint maxResultsToReturn, - BrowseDirection browseDirection, - NodeId referenceTypeId, - bool includeSubtypes, - uint nodeClassMask, - CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session - .ManagedBrowseAsync( - requestHeader, - view, - nodesToBrowse, - maxResultsToReturn, - browseDirection, - referenceTypeId, - includeSubtypes, - nodeClassMask, - ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use TranslateBrowsePathsToNodeIdsAsync() instead.")] - public ResponseHeader TranslateBrowsePathsToNodeIds( - RequestHeader requestHeader, - BrowsePathCollection browsePaths, - out BrowsePathResultCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.TranslateBrowsePathsToNodeIds( - requestHeader, - browsePaths, - out results, - out diagnosticInfos); - } - - /// - [Obsolete("Use TranslateBrowsePathsToNodeIdsAsync() instead.")] - public IAsyncResult BeginTranslateBrowsePathsToNodeIds( - RequestHeader requestHeader, - BrowsePathCollection browsePaths, - AsyncCallback callback, - object asyncState) - { - return Session.BeginTranslateBrowsePathsToNodeIds( - requestHeader, - browsePaths, - callback, - asyncState); - } - - /// - [Obsolete("Use TranslateBrowsePathsToNodeIdsAsync() instead.")] - public ResponseHeader EndTranslateBrowsePathsToNodeIds( - IAsyncResult result, - out BrowsePathResultCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - return Session.EndTranslateBrowsePathsToNodeIds( - result, - out results, - out diagnosticInfos); - } - - /// - public async Task TranslateBrowsePathsToNodeIdsAsync( - RequestHeader requestHeader, - BrowsePathCollection browsePaths, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session - .TranslateBrowsePathsToNodeIdsAsync(requestHeader, browsePaths, ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use RegisterNodesAsync() instead.")] - public ResponseHeader RegisterNodes( - RequestHeader requestHeader, - NodeIdCollection nodesToRegister, - out NodeIdCollection registeredNodeIds) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.RegisterNodes(requestHeader, nodesToRegister, out registeredNodeIds); - } - - /// - [Obsolete("Use RegisterNodesAsync() instead.")] - public IAsyncResult BeginRegisterNodes( - RequestHeader requestHeader, - NodeIdCollection nodesToRegister, - AsyncCallback callback, - object asyncState) - { - return Session.BeginRegisterNodes(requestHeader, nodesToRegister, callback, asyncState); - } - - /// - [Obsolete("Use RegisterNodesAsync() instead.")] - public ResponseHeader EndRegisterNodes( - IAsyncResult result, - out NodeIdCollection registeredNodeIds) - { - return Session.EndRegisterNodes(result, out registeredNodeIds); - } - - /// - public async Task RegisterNodesAsync( - RequestHeader requestHeader, - NodeIdCollection nodesToRegister, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.RegisterNodesAsync(requestHeader, nodesToRegister, ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use UnregisterNodesAsync() instead.")] - public ResponseHeader UnregisterNodes( - RequestHeader requestHeader, - NodeIdCollection nodesToUnregister) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.UnregisterNodes(requestHeader, nodesToUnregister); - } - - /// - [Obsolete("Use UnregisterNodesAsync() instead.")] - public IAsyncResult BeginUnregisterNodes( - RequestHeader requestHeader, - NodeIdCollection nodesToUnregister, - AsyncCallback callback, - object asyncState) - { - return Session.BeginUnregisterNodes( - requestHeader, - nodesToUnregister, - callback, - asyncState); - } - - /// - [Obsolete("Use UnregisterNodesAsync() instead.")] - public ResponseHeader EndUnregisterNodes(IAsyncResult result) - { - return Session.EndUnregisterNodes(result); - } - - /// - public async Task UnregisterNodesAsync( - RequestHeader requestHeader, - NodeIdCollection nodesToUnregister, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.UnregisterNodesAsync(requestHeader, nodesToUnregister, ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use QueryFirstAsync() instead.")] - public ResponseHeader QueryFirst( - RequestHeader requestHeader, - ViewDescription view, - NodeTypeDescriptionCollection nodeTypes, - ContentFilter filter, - uint maxDataSetsToReturn, - uint maxReferencesToReturn, - out QueryDataSetCollection queryDataSets, - out byte[] continuationPoint, - out ParsingResultCollection parsingResults, - out DiagnosticInfoCollection diagnosticInfos, - out ContentFilterResult filterResult) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.QueryFirst( - requestHeader, - view, - nodeTypes, - filter, - maxDataSetsToReturn, - maxReferencesToReturn, - out queryDataSets, - out continuationPoint, - out parsingResults, - out diagnosticInfos, - out filterResult); - } - - /// - [Obsolete("Use QueryFirstAsync() instead.")] - public IAsyncResult BeginQueryFirst( - RequestHeader requestHeader, - ViewDescription view, - NodeTypeDescriptionCollection nodeTypes, - ContentFilter filter, - uint maxDataSetsToReturn, - uint maxReferencesToReturn, - AsyncCallback callback, - object asyncState) - { - return Session.BeginQueryFirst( - requestHeader, - view, - nodeTypes, - filter, - maxDataSetsToReturn, - maxReferencesToReturn, - callback, - asyncState); - } - - /// - [Obsolete("Use QueryFirstAsync() instead.")] - public ResponseHeader EndQueryFirst( - IAsyncResult result, - out QueryDataSetCollection queryDataSets, - out byte[] continuationPoint, - out ParsingResultCollection parsingResults, - out DiagnosticInfoCollection diagnosticInfos, - out ContentFilterResult filterResult) - { - return Session.EndQueryFirst( - result, - out queryDataSets, - out continuationPoint, - out parsingResults, - out diagnosticInfos, - out filterResult); - } - - /// - public async Task QueryFirstAsync( - RequestHeader requestHeader, - ViewDescription view, - NodeTypeDescriptionCollection nodeTypes, - ContentFilter filter, - uint maxDataSetsToReturn, - uint maxReferencesToReturn, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session - .QueryFirstAsync( - requestHeader, - view, - nodeTypes, - filter, - maxDataSetsToReturn, - maxReferencesToReturn, - ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use QueryNextAsync() instead.")] - public ResponseHeader QueryNext( - RequestHeader requestHeader, - bool releaseContinuationPoint, - byte[] continuationPoint, - out QueryDataSetCollection queryDataSets, - out byte[] revisedContinuationPoint) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.QueryNext( - requestHeader, - releaseContinuationPoint, - continuationPoint, - out queryDataSets, - out revisedContinuationPoint); - } - - /// - [Obsolete("Use QueryNextAsync() instead.")] - public IAsyncResult BeginQueryNext( - RequestHeader requestHeader, - bool releaseContinuationPoint, - byte[] continuationPoint, - AsyncCallback callback, - object asyncState) - { - return Session.BeginQueryNext( - requestHeader, - releaseContinuationPoint, - continuationPoint, - callback, - asyncState); - } - - /// - [Obsolete("Use QueryNextAsync() instead.")] - public ResponseHeader EndQueryNext( - IAsyncResult result, - out QueryDataSetCollection queryDataSets, - out byte[] revisedContinuationPoint) - { - return Session.EndQueryNext(result, out queryDataSets, out revisedContinuationPoint); - } - - /// - public async Task QueryNextAsync( - RequestHeader requestHeader, - bool releaseContinuationPoint, - byte[] continuationPoint, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session - .QueryNextAsync(requestHeader, releaseContinuationPoint, continuationPoint, ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use ReadAsync() instead.")] - public ResponseHeader Read( - RequestHeader requestHeader, - double maxAge, - TimestampsToReturn timestampsToReturn, - ReadValueIdCollection nodesToRead, - out DataValueCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.Read( - requestHeader, - maxAge, - timestampsToReturn, - nodesToRead, - out results, - out diagnosticInfos); - } - - /// - [Obsolete("Use ReadAsync() instead.")] - public IAsyncResult BeginRead( - RequestHeader requestHeader, - double maxAge, - TimestampsToReturn timestampsToReturn, - ReadValueIdCollection nodesToRead, - AsyncCallback callback, - object asyncState) - { - return Session.BeginRead( - requestHeader, - maxAge, - timestampsToReturn, - nodesToRead, - callback, - asyncState); - } - - /// - [Obsolete("Use ReadAsync() instead.")] - public ResponseHeader EndRead( - IAsyncResult result, - out DataValueCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - return Session.EndRead(result, out results, out diagnosticInfos); - } - - /// - public async Task ReadAsync( - RequestHeader requestHeader, - double maxAge, - TimestampsToReturn timestampsToReturn, - ReadValueIdCollection nodesToRead, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session - .ReadAsync(requestHeader, maxAge, timestampsToReturn, nodesToRead, ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use HistoryReadAsync() instead.")] - public ResponseHeader HistoryRead( - RequestHeader requestHeader, - ExtensionObject historyReadDetails, - TimestampsToReturn timestampsToReturn, - bool releaseContinuationPoints, - HistoryReadValueIdCollection nodesToRead, - out HistoryReadResultCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.HistoryRead( - requestHeader, - historyReadDetails, - timestampsToReturn, - releaseContinuationPoints, - nodesToRead, - out results, - out diagnosticInfos); - } - - /// - [Obsolete("Use HistoryReadAsync() instead.")] - public IAsyncResult BeginHistoryRead( - RequestHeader requestHeader, - ExtensionObject historyReadDetails, - TimestampsToReturn timestampsToReturn, - bool releaseContinuationPoints, - HistoryReadValueIdCollection nodesToRead, - AsyncCallback callback, - object asyncState) - { - return Session.BeginHistoryRead( - requestHeader, - historyReadDetails, - timestampsToReturn, - releaseContinuationPoints, - nodesToRead, - callback, - asyncState); - } - - /// - [Obsolete("Use HistoryReadAsync() instead.")] - public ResponseHeader EndHistoryRead( - IAsyncResult result, - out HistoryReadResultCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - return Session.EndHistoryRead(result, out results, out diagnosticInfos); - } - - /// - public async Task HistoryReadAsync( - RequestHeader requestHeader, - ExtensionObject historyReadDetails, - TimestampsToReturn timestampsToReturn, - bool releaseContinuationPoints, - HistoryReadValueIdCollection nodesToRead, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session - .HistoryReadAsync( - requestHeader, - historyReadDetails, - timestampsToReturn, - releaseContinuationPoints, - nodesToRead, - ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use WriteAsync() instead.")] - public ResponseHeader Write( - RequestHeader requestHeader, - WriteValueCollection nodesToWrite, - out StatusCodeCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.Write(requestHeader, nodesToWrite, out results, out diagnosticInfos); - } - - /// - [Obsolete("Use WriteAsync() instead.")] - public IAsyncResult BeginWrite( - RequestHeader requestHeader, - WriteValueCollection nodesToWrite, - AsyncCallback callback, - object asyncState) - { - return Session.BeginWrite(requestHeader, nodesToWrite, callback, asyncState); - } - - /// - [Obsolete("Use WriteAsync() instead.")] - public ResponseHeader EndWrite( - IAsyncResult result, - out StatusCodeCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - return Session.EndWrite(result, out results, out diagnosticInfos); - } - - /// - public async Task WriteAsync( - RequestHeader requestHeader, - WriteValueCollection nodesToWrite, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.WriteAsync(requestHeader, nodesToWrite, ct).ConfigureAwait(false); - } - - /// - [Obsolete("Use HistoryUpdateAsync() instead.")] - public ResponseHeader HistoryUpdate( - RequestHeader requestHeader, - ExtensionObjectCollection historyUpdateDetails, - out HistoryUpdateResultCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.HistoryUpdate( - requestHeader, - historyUpdateDetails, - out results, - out diagnosticInfos); - } - - /// - [Obsolete("Use HistoryUpdateAsync() instead.")] - public IAsyncResult BeginHistoryUpdate( - RequestHeader requestHeader, - ExtensionObjectCollection historyUpdateDetails, - AsyncCallback callback, - object asyncState) - { - return Session.BeginHistoryUpdate( - requestHeader, - historyUpdateDetails, - callback, - asyncState); - } - - /// - [Obsolete("Use HistoryUpdateAsync() instead.")] - public ResponseHeader EndHistoryUpdate( - IAsyncResult result, - out HistoryUpdateResultCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - return Session.EndHistoryUpdate(result, out results, out diagnosticInfos); - } - - /// - public async Task HistoryUpdateAsync( - RequestHeader requestHeader, - ExtensionObjectCollection historyUpdateDetails, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.HistoryUpdateAsync(requestHeader, historyUpdateDetails, ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use CallAsync() instead.")] - public ResponseHeader Call( - RequestHeader requestHeader, - CallMethodRequestCollection methodsToCall, - out CallMethodResultCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.Call(requestHeader, methodsToCall, out results, out diagnosticInfos); - } - - /// - [Obsolete("Use CallAsync() instead.")] - public IAsyncResult BeginCall( - RequestHeader requestHeader, - CallMethodRequestCollection methodsToCall, - AsyncCallback callback, - object asyncState) - { - return Session.BeginCall(requestHeader, methodsToCall, callback, asyncState); - } - - /// - [Obsolete("Use CallAsync() instead.")] - public ResponseHeader EndCall( - IAsyncResult result, - out CallMethodResultCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - return Session.EndCall(result, out results, out diagnosticInfos); - } - - /// - public async Task CallAsync( - RequestHeader requestHeader, - CallMethodRequestCollection methodsToCall, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.CallAsync(requestHeader, methodsToCall, ct).ConfigureAwait(false); - } - - /// - [Obsolete("Use CreateMonitoredItemsAsync() instead.")] - public ResponseHeader CreateMonitoredItems( - RequestHeader requestHeader, - uint subscriptionId, - TimestampsToReturn timestampsToReturn, - MonitoredItemCreateRequestCollection itemsToCreate, - out MonitoredItemCreateResultCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.CreateMonitoredItems( - requestHeader, - subscriptionId, - timestampsToReturn, - itemsToCreate, - out results, - out diagnosticInfos); - } - - /// - [Obsolete("Use CreateMonitoredItemsAsync() instead.")] - public IAsyncResult BeginCreateMonitoredItems( - RequestHeader requestHeader, - uint subscriptionId, - TimestampsToReturn timestampsToReturn, - MonitoredItemCreateRequestCollection itemsToCreate, - AsyncCallback callback, - object asyncState) - { - return Session.BeginCreateMonitoredItems( - requestHeader, - subscriptionId, - timestampsToReturn, - itemsToCreate, - callback, - asyncState); - } - - /// - [Obsolete("Use CreateMonitoredItemsAsync() instead.")] - public ResponseHeader EndCreateMonitoredItems( - IAsyncResult result, - out MonitoredItemCreateResultCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - return Session.EndCreateMonitoredItems(result, out results, out diagnosticInfos); - } - - /// - public async Task CreateMonitoredItemsAsync( - RequestHeader requestHeader, - uint subscriptionId, - TimestampsToReturn timestampsToReturn, - MonitoredItemCreateRequestCollection itemsToCreate, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session - .CreateMonitoredItemsAsync( - requestHeader, - subscriptionId, - timestampsToReturn, - itemsToCreate, - ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use ModifyMonitoredItemsAsync() instead.")] - public ResponseHeader ModifyMonitoredItems( - RequestHeader requestHeader, - uint subscriptionId, - TimestampsToReturn timestampsToReturn, - MonitoredItemModifyRequestCollection itemsToModify, - out MonitoredItemModifyResultCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.ModifyMonitoredItems( - requestHeader, - subscriptionId, - timestampsToReturn, - itemsToModify, - out results, - out diagnosticInfos); - } - - /// - [Obsolete("Use ModifyMonitoredItemsAsync() instead.")] - public IAsyncResult BeginModifyMonitoredItems( - RequestHeader requestHeader, - uint subscriptionId, - TimestampsToReturn timestampsToReturn, - MonitoredItemModifyRequestCollection itemsToModify, - AsyncCallback callback, - object asyncState) - { - return Session.BeginModifyMonitoredItems( - requestHeader, - subscriptionId, - timestampsToReturn, - itemsToModify, - callback, - asyncState); - } - - /// - [Obsolete("Use ModifyMonitoredItemsAsync() instead.")] - public ResponseHeader EndModifyMonitoredItems( - IAsyncResult result, - out MonitoredItemModifyResultCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - return Session.EndModifyMonitoredItems(result, out results, out diagnosticInfos); - } - - /// - public async Task ModifyMonitoredItemsAsync( - RequestHeader requestHeader, - uint subscriptionId, - TimestampsToReturn timestampsToReturn, - MonitoredItemModifyRequestCollection itemsToModify, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session - .ModifyMonitoredItemsAsync( - requestHeader, - subscriptionId, - timestampsToReturn, - itemsToModify, - ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use SetMonitoringModeAsync() instead.")] - public ResponseHeader SetMonitoringMode( - RequestHeader requestHeader, - uint subscriptionId, - MonitoringMode monitoringMode, - UInt32Collection monitoredItemIds, - out StatusCodeCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.SetMonitoringMode( - requestHeader, - subscriptionId, - monitoringMode, - monitoredItemIds, - out results, - out diagnosticInfos); - } - - /// - [Obsolete("Use SetMonitoringModeAsync() instead.")] - public IAsyncResult BeginSetMonitoringMode( - RequestHeader requestHeader, - uint subscriptionId, - MonitoringMode monitoringMode, - UInt32Collection monitoredItemIds, - AsyncCallback callback, - object asyncState) - { - return Session.BeginSetMonitoringMode( - requestHeader, - subscriptionId, - monitoringMode, - monitoredItemIds, - callback, - asyncState); - } - - /// - [Obsolete("Use SetMonitoringModeAsync() instead.")] - public ResponseHeader EndSetMonitoringMode( - IAsyncResult result, - out StatusCodeCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - return Session.EndSetMonitoringMode(result, out results, out diagnosticInfos); - } - - /// - public async Task SetMonitoringModeAsync( - RequestHeader requestHeader, - uint subscriptionId, - MonitoringMode monitoringMode, - UInt32Collection monitoredItemIds, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session - .SetMonitoringModeAsync( - requestHeader, - subscriptionId, - monitoringMode, - monitoredItemIds, - ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use SetTriggeringAsync() instead.")] - public ResponseHeader SetTriggering( - RequestHeader requestHeader, - uint subscriptionId, - uint triggeringItemId, - UInt32Collection linksToAdd, - UInt32Collection linksToRemove, - out StatusCodeCollection addResults, - out DiagnosticInfoCollection addDiagnosticInfos, - out StatusCodeCollection removeResults, - out DiagnosticInfoCollection removeDiagnosticInfos) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.SetTriggering( - requestHeader, - subscriptionId, - triggeringItemId, - linksToAdd, - linksToRemove, - out addResults, - out addDiagnosticInfos, - out removeResults, - out removeDiagnosticInfos); - } - - /// - [Obsolete("Use SetTriggeringAsync() instead.")] - public IAsyncResult BeginSetTriggering( - RequestHeader requestHeader, - uint subscriptionId, - uint triggeringItemId, - UInt32Collection linksToAdd, - UInt32Collection linksToRemove, - AsyncCallback callback, - object asyncState) - { - return Session.BeginSetTriggering( - requestHeader, - subscriptionId, - triggeringItemId, - linksToAdd, - linksToRemove, - callback, - asyncState); - } - - /// - [Obsolete("Use SetTriggeringAsync() instead.")] - public ResponseHeader EndSetTriggering( - IAsyncResult result, - out StatusCodeCollection addResults, - out DiagnosticInfoCollection addDiagnosticInfos, - out StatusCodeCollection removeResults, - out DiagnosticInfoCollection removeDiagnosticInfos) - { - return Session.EndSetTriggering( - result, - out addResults, - out addDiagnosticInfos, - out removeResults, - out removeDiagnosticInfos); - } - - /// - public async Task SetTriggeringAsync( - RequestHeader requestHeader, - uint subscriptionId, - uint triggeringItemId, - UInt32Collection linksToAdd, - UInt32Collection linksToRemove, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session - .SetTriggeringAsync( - requestHeader, - subscriptionId, - triggeringItemId, - linksToAdd, - linksToRemove, - ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use DeleteMonitoredItemsAsync() instead.")] - public ResponseHeader DeleteMonitoredItems( - RequestHeader requestHeader, - uint subscriptionId, - UInt32Collection monitoredItemIds, - out StatusCodeCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.DeleteMonitoredItems( - requestHeader, - subscriptionId, - monitoredItemIds, - out results, - out diagnosticInfos); - } - - /// - [Obsolete("Use DeleteMonitoredItemsAsync() instead.")] - public IAsyncResult BeginDeleteMonitoredItems( - RequestHeader requestHeader, - uint subscriptionId, - UInt32Collection monitoredItemIds, - AsyncCallback callback, - object asyncState) - { - return Session.BeginDeleteMonitoredItems( - requestHeader, - subscriptionId, - monitoredItemIds, - callback, - asyncState); - } - - /// - [Obsolete("Use DeleteMonitoredItemsAsync() instead.")] - public ResponseHeader EndDeleteMonitoredItems( - IAsyncResult result, - out StatusCodeCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - return Session.EndDeleteMonitoredItems(result, out results, out diagnosticInfos); - } - - /// - public async Task DeleteMonitoredItemsAsync( - RequestHeader requestHeader, - uint subscriptionId, - UInt32Collection monitoredItemIds, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session - .DeleteMonitoredItemsAsync(requestHeader, subscriptionId, monitoredItemIds, ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use CreateSubscriptionAsync() instead.")] - public ResponseHeader CreateSubscription( - RequestHeader requestHeader, - double requestedPublishingInterval, - uint requestedLifetimeCount, - uint requestedMaxKeepAliveCount, - uint maxNotificationsPerPublish, - bool publishingEnabled, - byte priority, - out uint subscriptionId, - out double revisedPublishingInterval, - out uint revisedLifetimeCount, - out uint revisedMaxKeepAliveCount) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.CreateSubscription( - requestHeader, - requestedPublishingInterval, - requestedLifetimeCount, - requestedMaxKeepAliveCount, - maxNotificationsPerPublish, - publishingEnabled, - priority, - out subscriptionId, - out revisedPublishingInterval, - out revisedLifetimeCount, - out revisedMaxKeepAliveCount); - } - - /// - [Obsolete("Use CreateSubscriptionAsync() instead.")] - public IAsyncResult BeginCreateSubscription( - RequestHeader requestHeader, - double requestedPublishingInterval, - uint requestedLifetimeCount, - uint requestedMaxKeepAliveCount, - uint maxNotificationsPerPublish, - bool publishingEnabled, - byte priority, - AsyncCallback callback, - object asyncState) - { - return Session.BeginCreateSubscription( - requestHeader, - requestedPublishingInterval, - requestedLifetimeCount, - requestedMaxKeepAliveCount, - maxNotificationsPerPublish, - publishingEnabled, - priority, - callback, - asyncState); - } - - /// - [Obsolete("Use CreateSubscriptionAsync() instead.")] - public ResponseHeader EndCreateSubscription( - IAsyncResult result, - out uint subscriptionId, - out double revisedPublishingInterval, - out uint revisedLifetimeCount, - out uint revisedMaxKeepAliveCount) - { - return Session.EndCreateSubscription( - result, - out subscriptionId, - out revisedPublishingInterval, - out revisedLifetimeCount, - out revisedMaxKeepAliveCount); - } - - /// - public async Task CreateSubscriptionAsync( - RequestHeader requestHeader, - double requestedPublishingInterval, - uint requestedLifetimeCount, - uint requestedMaxKeepAliveCount, - uint maxNotificationsPerPublish, - bool publishingEnabled, - byte priority, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session - .CreateSubscriptionAsync( - requestHeader, - requestedPublishingInterval, - requestedLifetimeCount, - requestedMaxKeepAliveCount, - maxNotificationsPerPublish, - publishingEnabled, - priority, - ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use ModifySubscriptionAsync() instead.")] - public ResponseHeader ModifySubscription( - RequestHeader requestHeader, - uint subscriptionId, - double requestedPublishingInterval, - uint requestedLifetimeCount, - uint requestedMaxKeepAliveCount, - uint maxNotificationsPerPublish, - byte priority, - out double revisedPublishingInterval, - out uint revisedLifetimeCount, - out uint revisedMaxKeepAliveCount) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.ModifySubscription( - requestHeader, - subscriptionId, - requestedPublishingInterval, - requestedLifetimeCount, - requestedMaxKeepAliveCount, - maxNotificationsPerPublish, - priority, - out revisedPublishingInterval, - out revisedLifetimeCount, - out revisedMaxKeepAliveCount); - } - - /// - [Obsolete("Use ModifySubscriptionAsync() instead.")] - public IAsyncResult BeginModifySubscription( - RequestHeader requestHeader, - uint subscriptionId, - double requestedPublishingInterval, - uint requestedLifetimeCount, - uint requestedMaxKeepAliveCount, - uint maxNotificationsPerPublish, - byte priority, - AsyncCallback callback, - object asyncState) - { - return Session.BeginModifySubscription( - requestHeader, - subscriptionId, - requestedPublishingInterval, - requestedLifetimeCount, - requestedMaxKeepAliveCount, - maxNotificationsPerPublish, - priority, - callback, - asyncState); - } - - /// - [Obsolete("Use ModifySubscriptionAsync() instead.")] - public ResponseHeader EndModifySubscription( - IAsyncResult result, - out double revisedPublishingInterval, - out uint revisedLifetimeCount, - out uint revisedMaxKeepAliveCount) - { - return Session.EndModifySubscription( - result, - out revisedPublishingInterval, - out revisedLifetimeCount, - out revisedMaxKeepAliveCount); - } - - /// - public async Task ModifySubscriptionAsync( - RequestHeader requestHeader, - uint subscriptionId, - double requestedPublishingInterval, - uint requestedLifetimeCount, - uint requestedMaxKeepAliveCount, - uint maxNotificationsPerPublish, - byte priority, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session - .ModifySubscriptionAsync( - requestHeader, - subscriptionId, - requestedPublishingInterval, - requestedLifetimeCount, - requestedMaxKeepAliveCount, - maxNotificationsPerPublish, - priority, - ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use SetPublishingModeAsync() instead.")] - public ResponseHeader SetPublishingMode( - RequestHeader requestHeader, - bool publishingEnabled, - UInt32Collection subscriptionIds, - out StatusCodeCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.SetPublishingMode( - requestHeader, - publishingEnabled, - subscriptionIds, - out results, - out diagnosticInfos); - } - - /// - [Obsolete("Use SetPublishingModeAsync() instead.")] - public IAsyncResult BeginSetPublishingMode( - RequestHeader requestHeader, - bool publishingEnabled, - UInt32Collection subscriptionIds, - AsyncCallback callback, - object asyncState) - { - return Session.BeginSetPublishingMode( - requestHeader, - publishingEnabled, - subscriptionIds, - callback, - asyncState); - } - - /// - [Obsolete("Use SetPublishingModeAsync() instead.")] - public ResponseHeader EndSetPublishingMode( - IAsyncResult result, - out StatusCodeCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - return Session.EndSetPublishingMode(result, out results, out diagnosticInfos); - } - - /// - public async Task SetPublishingModeAsync( - RequestHeader requestHeader, - bool publishingEnabled, - UInt32Collection subscriptionIds, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session - .SetPublishingModeAsync(requestHeader, publishingEnabled, subscriptionIds, ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use PublishAsync() instead.")] - public ResponseHeader Publish( - RequestHeader requestHeader, - SubscriptionAcknowledgementCollection subscriptionAcknowledgements, - out uint subscriptionId, - out UInt32Collection availableSequenceNumbers, - out bool moreNotifications, - out NotificationMessage notificationMessage, - out StatusCodeCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.Publish( - requestHeader, - subscriptionAcknowledgements, - out subscriptionId, - out availableSequenceNumbers, - out moreNotifications, - out notificationMessage, - out results, - out diagnosticInfos); - } - - /// - [Obsolete("Use PublishAsync() instead.")] - public IAsyncResult BeginPublish( - RequestHeader requestHeader, - SubscriptionAcknowledgementCollection subscriptionAcknowledgements, - AsyncCallback callback, - object asyncState) - { - return Session.BeginPublish( - requestHeader, - subscriptionAcknowledgements, - callback, - asyncState); - } - - /// - [Obsolete("Use PublishAsync() instead.")] - public ResponseHeader EndPublish( - IAsyncResult result, - out uint subscriptionId, - out UInt32Collection availableSequenceNumbers, - out bool moreNotifications, - out NotificationMessage notificationMessage, - out StatusCodeCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - return Session.EndPublish( - result, - out subscriptionId, - out availableSequenceNumbers, - out moreNotifications, - out notificationMessage, - out results, - out diagnosticInfos); - } - - /// - public async Task PublishAsync( - RequestHeader requestHeader, - SubscriptionAcknowledgementCollection subscriptionAcknowledgements, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.PublishAsync(requestHeader, subscriptionAcknowledgements, ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use RepublishAsync() instead.")] - public ResponseHeader Republish( - RequestHeader requestHeader, - uint subscriptionId, - uint retransmitSequenceNumber, - out NotificationMessage notificationMessage) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.Republish( - requestHeader, - subscriptionId, - retransmitSequenceNumber, - out notificationMessage); - } - - /// - [Obsolete("Use RepublishAsync() instead.")] - public IAsyncResult BeginRepublish( - RequestHeader requestHeader, - uint subscriptionId, - uint retransmitSequenceNumber, - AsyncCallback callback, - object asyncState) - { - return Session.BeginRepublish( - requestHeader, - subscriptionId, - retransmitSequenceNumber, - callback, - asyncState); - } - - /// - [Obsolete("Use RepublishAsync() instead.")] - public ResponseHeader EndRepublish( - IAsyncResult result, - out NotificationMessage notificationMessage) - { - return Session.EndRepublish(result, out notificationMessage); - } - - /// - public async Task RepublishAsync( - RequestHeader requestHeader, - uint subscriptionId, - uint retransmitSequenceNumber, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session - .RepublishAsync(requestHeader, subscriptionId, retransmitSequenceNumber, ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use TransferSubscriptionsAsync() instead.")] - public ResponseHeader TransferSubscriptions( - RequestHeader requestHeader, - UInt32Collection subscriptionIds, - bool sendInitialValues, - out TransferResultCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.TransferSubscriptions( - requestHeader, - subscriptionIds, - sendInitialValues, - out results, - out diagnosticInfos); - } - - /// - [Obsolete("Use TransferSubscriptionsAsync() instead.")] - public IAsyncResult BeginTransferSubscriptions( - RequestHeader requestHeader, - UInt32Collection subscriptionIds, - bool sendInitialValues, - AsyncCallback callback, - object asyncState) - { - return Session.BeginTransferSubscriptions( - requestHeader, - subscriptionIds, - sendInitialValues, - callback, - asyncState); - } - - /// - [Obsolete("Use TransferSubscriptionsAsync() instead.")] - public ResponseHeader EndTransferSubscriptions( - IAsyncResult result, - out TransferResultCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - return Session.EndTransferSubscriptions(result, out results, out diagnosticInfos); - } - - /// - public async Task TransferSubscriptionsAsync( - RequestHeader requestHeader, - UInt32Collection subscriptionIds, - bool sendInitialValues, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session - .TransferSubscriptionsAsync(requestHeader, subscriptionIds, sendInitialValues, ct) - .ConfigureAwait(false); - } - - /// - [Obsolete("Use DeleteSubscriptionsAsync() instead.")] - public ResponseHeader DeleteSubscriptions( - RequestHeader requestHeader, - UInt32Collection subscriptionIds, - out StatusCodeCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.DeleteSubscriptions( - requestHeader, - subscriptionIds, - out results, - out diagnosticInfos); - } - - /// - [Obsolete("Use DeleteSubscriptionsAsync() instead.")] - public IAsyncResult BeginDeleteSubscriptions( - RequestHeader requestHeader, - UInt32Collection subscriptionIds, - AsyncCallback callback, - object asyncState) - { - return Session.BeginDeleteSubscriptions( - requestHeader, - subscriptionIds, - callback, - asyncState); - } - - /// - [Obsolete("Use DeleteSubscriptionsAsync() instead.")] - public ResponseHeader EndDeleteSubscriptions( - IAsyncResult result, - out StatusCodeCollection results, - out DiagnosticInfoCollection diagnosticInfos) - { - return Session.EndDeleteSubscriptions(result, out results, out diagnosticInfos); - } - - /// - public async Task DeleteSubscriptionsAsync( - RequestHeader requestHeader, - UInt32Collection subscriptionIds, - CancellationToken ct) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.DeleteSubscriptionsAsync(requestHeader, subscriptionIds, ct) - .ConfigureAwait(false); - } - - /// - public void AttachChannel(ITransportChannel channel) - { - using Activity activity = m_telemetry.StartActivity(); - Session.AttachChannel(channel); - } - - /// - public void DetachChannel() - { - using Activity activity = m_telemetry.StartActivity(); - Session.DetachChannel(); - } - - /// - public uint NewRequestHandle() - { - return Session.NewRequestHandle(); - } - - /// - /// Disposes the session. - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - // note: do not null the session here, - // properties may still be accessed after dispose. - Utils.SilentDispose(Session); - } - } - - /// - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - - /// - public SessionConfiguration SaveSessionConfiguration(Stream stream = null) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.SaveSessionConfiguration(stream); - } - - /// - public bool ApplySessionConfiguration(SessionConfiguration sessionConfiguration) - { - using Activity activity = m_telemetry.StartActivity(); - return Session.ApplySessionConfiguration(sessionConfiguration); - } - - /// - public async Task RemoveSubscriptionAsync( - Subscription subscription, - CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.RemoveSubscriptionAsync(subscription, ct).ConfigureAwait(false); - } - - /// - public async Task RemoveSubscriptionsAsync( - IEnumerable subscriptions, - CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.RemoveSubscriptionsAsync(subscriptions, ct).ConfigureAwait(false); - } - - /// - public async Task ReactivateSubscriptionsAsync( - SubscriptionCollection subscriptions, - bool sendInitialValues, - CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session - .ReactivateSubscriptionsAsync(subscriptions, sendInitialValues, ct) - .ConfigureAwait(false); - } - - /// - public async Task TransferSubscriptionsAsync( - SubscriptionCollection subscriptions, - bool sendInitialValues, - CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.TransferSubscriptionsAsync(subscriptions, sendInitialValues, ct) - .ConfigureAwait(false); - } - - /// - public async Task> CallAsync( - NodeId objectId, - NodeId methodId, - CancellationToken ct = default, - params object[] args) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.CallAsync(objectId, methodId, ct, args).ConfigureAwait(false); - } - - /// - public async Task<(bool, IList)> ResendDataAsync( - IEnumerable subscriptions, - CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.ResendDataAsync(subscriptions, ct).ConfigureAwait(false); - } - - /// - public async Task<(IList, IList)> ReadDisplayNameAsync( - IList nodeIds, - CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.ReadDisplayNameAsync( - nodeIds, - ct).ConfigureAwait(false); - } - - /// - public async Task<(NodeIdCollection, IList)> FindComponentIdsAsync( - NodeId instanceId, - IList componentPaths, - CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.FindComponentIdsAsync( - instanceId, - componentPaths, - ct).ConfigureAwait(false); - } - - /// - public async Task ReadAvailableEncodingsAsync( - NodeId variableId, - CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.ReadAvailableEncodingsAsync( - variableId, - ct).ConfigureAwait(false); - } - - /// - public async Task FindDataDescriptionAsync( - NodeId encodingId, - CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.FindDataDescriptionAsync( - encodingId, - ct).ConfigureAwait(false); - } - - /// - public async Task ReadValueAsync( - NodeId nodeId, - CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.ReadValueAsync(nodeId, ct).ConfigureAwait(false); - } - - /// - public async Task<( - ResponseHeader responseHeader, - ByteStringCollection continuationPoints, - IList referencesList, - IList errors - )> BrowseAsync( - RequestHeader requestHeader, - ViewDescription view, - IList nodesToBrowse, - uint maxResultsToReturn, - BrowseDirection browseDirection, - NodeId referenceTypeId, - bool includeSubtypes, - uint nodeClassMask, - CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.BrowseAsync( - requestHeader, - view, - nodesToBrowse, - maxResultsToReturn, - browseDirection, - referenceTypeId, - includeSubtypes, - nodeClassMask, - ct) - .ConfigureAwait(false); - } - - /// - public async Task<( - ResponseHeader responseHeader, - ByteStringCollection revisedContinuationPoints, - IList referencesList, - IList errors)> - BrowseNextAsync( - RequestHeader requestHeader, - ByteStringCollection continuationPoints, - bool releaseContinuationPoint, - CancellationToken ct = default) - { - using Activity activity = m_telemetry.StartActivity(); - return await Session.BrowseNextAsync( - requestHeader, - continuationPoints, - releaseContinuationPoint, - ct) - .ConfigureAwait(false); - } - - private readonly ITelemetryContext m_telemetry; - } -} diff --git a/Libraries/Opc.Ua.Client/Subscription/MonitoredItem.cs b/Libraries/Opc.Ua.Client/Subscription/MonitoredItem.cs index 3582bf5d22..047c8b9ff7 100644 --- a/Libraries/Opc.Ua.Client/Subscription/MonitoredItem.cs +++ b/Libraries/Opc.Ua.Client/Subscription/MonitoredItem.cs @@ -30,7 +30,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Runtime.Serialization; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -40,19 +39,17 @@ namespace Opc.Ua.Client /// /// A monitored item. /// - [DataContract(Namespace = Namespaces.OpcUaXsd)] - [KnownType(typeof(DataChangeFilter))] - [KnownType(typeof(EventFilter))] - [KnownType(typeof(AggregateFilter))] - public class MonitoredItem : ICloneable + public class MonitoredItem : ISnapshotRestore, ICloneable { private static readonly TimeSpan s_time_epsilon = TimeSpan.FromMilliseconds(500); /// /// Initializes a new instance of the class. /// - public MonitoredItem() - : this(Utils.IncrementIdentifier(ref s_globalClientHandle)) + public MonitoredItem( + ITelemetryContext telemetry, + MonitoredItemOptions? options = null) + : this(Utils.IncrementIdentifier(ref s_globalClientHandle), telemetry, options) { } @@ -61,316 +58,278 @@ public MonitoredItem() /// /// The client handle. The caller must ensure it /// uniquely identifies the monitored item. - public MonitoredItem(uint clientHandle) + /// + /// + public MonitoredItem( + uint clientHandle, + ITelemetryContext telemetry, + MonitoredItemOptions? options = null) { - Initialize(); + State = options ?? new MonitoredItemOptions(); + Status = new MonitoredItemStatus(); + m_logger = telemetry.CreateLogger(); ClientHandle = clientHandle; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class from a template. /// - /// The template used to specify the monitoring parameters. - /// if set to true the event handlers are copied. - /// if set to true the clientHandle is of the template copied. public MonitoredItem( MonitoredItem template, bool copyEventHandlers = false, bool copyClientHandle = false) { - if (template != null) + Status = new MonitoredItemStatus(); + if (template == null) { - m_logger = template.m_logger; + throw new ArgumentNullException(nameof(template)); } + m_logger = template.m_logger; - Initialize(); + State = template.State; + ClientHandle = 0; + AttributesModified = true; + m_logger ??= Utils.Null.Logger; - if (template != null) + string displayName = template.DisplayName; + if (displayName != null) { - string displayName = template.DisplayName; - - if (displayName != null) - { - // remove any existing numeric suffix. - int index = displayName.LastIndexOf(' '); - - if (index != -1) - { - try - { - displayName = displayName[..index]; - } - catch - { - // not a numeric suffix. - } - } - } - - Handle = template.Handle; - DisplayName = Utils.Format("{0} {1}", displayName, ClientHandle); - StartNodeId = template.StartNodeId; - m_relativePath = template.m_relativePath; - AttributeId = template.AttributeId; - IndexRange = template.IndexRange; - Encoding = template.Encoding; - MonitoringMode = template.MonitoringMode; - m_samplingInterval = template.m_samplingInterval; - m_filter = Utils.Clone(template.m_filter); - m_queueSize = template.m_queueSize; - m_discardOldest = template.m_discardOldest; - AttributesModified = true; - - if (copyEventHandlers) - { - m_Notification = template.m_Notification; - } - - if (copyClientHandle) - { - ClientHandle = template.ClientHandle; - } - else + int index = displayName.LastIndexOf(' '); + if (index != -1) { - ClientHandle = Utils.IncrementIdentifier(ref s_globalClientHandle); + displayName = displayName[..index]; } + } - // this ensures the state is consistent with the node class. - NodeClass = template.m_nodeClass; + Handle = template.Handle; + DisplayName = Utils.Format("{0} {1}", displayName, ClientHandle); + // copy state (except client handle logic handled below) + State = template.State with { DisplayName = DisplayName }; + if (copyEventHandlers) + { + m_Notification = template.m_Notification; + } + if (copyClientHandle) + { + ClientHandle = template.ClientHandle; } else { - // assign a unique handle. ClientHandle = Utils.IncrementIdentifier(ref s_globalClientHandle); } + // ensure state consistency with node class transitions + NodeClass = State.NodeClass; } /// - /// Called by the .NET framework during deserialization. + /// Public parameterless ctor for serialization/deserialization scenarios. /// - [OnDeserializing] - protected void Initialize(StreamingContext context) + [Obsolete("Use constructor with ITelemetryContext argument")] + public MonitoredItem() + : this(null!, null) { - // object initializers are not called during deserialization. - m_cache = new Lock(); + } - Initialize(); + /// + public virtual void Restore(MonitoredItemState state) + { + State = state; + ClientHandle = state.ClientId; + ServerId = state.ServerId; + } - // assign a unique handle. - ClientHandle = Utils.IncrementIdentifier(ref s_globalClientHandle); + /// + public virtual void Snapshot(out MonitoredItemState state) + { + state = new MonitoredItemState(State) + { + ServerId = Status.Id, + ClientId = ClientHandle + }; } /// - /// Sets the private members to default values. + /// Monitored item state/options. /// - private void Initialize() - { - StartNodeId = null; - m_relativePath = null; - ClientHandle = 0; - AttributeId = Attributes.Value; - IndexRange = null; - Encoding = null; - MonitoringMode = MonitoringMode.Reporting; - m_samplingInterval = -1; - m_filter = null; - m_queueSize = 0; - m_discardOldest = true; - AttributesModified = true; - Status = new MonitoredItemStatus(); - - // this ensures the state is consistent with the node class. - NodeClass = NodeClass.Variable; - - // Creates a default logger even if telemetry is null - m_logger ??= Utils.Null.Logger; - } + public MonitoredItemOptions State { get; internal set; } /// - /// A display name for the monitored item. + /// A display name for the monitored item /// - [DataMember(Order = 1)] - public string DisplayName { get; set; } + public string DisplayName + { + get => State.DisplayName ?? "MonitoredItem"; + set => State = State with { DisplayName = value }; + } /// - /// The start node for the browse path that identifies the node to monitor. + /// The start node id /// - [DataMember(Order = 2)] - public NodeId StartNodeId { get; set; } + public NodeId StartNodeId + { + get => State.StartNodeId; + set => State = State with { StartNodeId = value ?? NodeId.Null }; + } /// - /// The relative path from the browse path to the node to monitor. + /// The relative path /// - /// - /// A null or empty string specifies that the start node id should be monitored. - /// - [DataMember(Order = 3)] - public string RelativePath + public string? RelativePath { - get => m_relativePath; + get => State.RelativePath; set { - // clear resolved path if relative path has changed. - if (m_relativePath != value) + if (value != State.RelativePath) { - m_resolvedNodeId = null; + m_resolvedNodeId = NodeId.Null; } - - m_relativePath = value; + State = State with { RelativePath = value }; } } /// - /// The node class of the node being monitored (affects the type of filter available). + /// The node class /// - [DataMember(Order = 4)] public NodeClass NodeClass { - get => m_nodeClass; + get => State.NodeClass; set { - if (m_nodeClass != value) + if (State.NodeClass == value) { - if (((int)value & ((int)NodeClass.Object | (int)NodeClass.View)) != 0) + return; + } + if (((int)value & ((int)NodeClass.Object | (int)NodeClass.View)) != 0) + { + State = State with { - // ensure a valid event filter. - if (m_filter is not EventFilter) - { - UseDefaultEventFilter(); - } - - // set the queue size to the default for events. - if (QueueSize <= 1) - { - QueueSize = int.MaxValue; - } - - AttributeId = Attributes.EventNotifier; - } - else + NodeClass = value, + AttributeId = Attributes.EventNotifier, + QueueSize = State.QueueSize <= 1 ? int.MaxValue : State.QueueSize, + Filter = State.Filter is not EventFilter ? + GetDefaultEventFilter() : State.Filter + }; + } + else + { + State = State with { - // clear the filter if it is only valid for events. - if (m_filter is EventFilter) - { - m_filter = null; - } - - // set the queue size to the default for data changes. - if (QueueSize == int.MaxValue) - { - QueueSize = 1; - } - - AttributeId = Attributes.Value; - } - - m_dataCache = null; - m_eventCache = null; + NodeClass = value, + AttributeId = Attributes.Value, + QueueSize = State.QueueSize == int.MaxValue ? 1 : State.QueueSize, + Filter = State.Filter is EventFilter ? + null : State.Filter, + }; } - - m_nodeClass = value; + m_dataCache = null; + m_eventCache = null; } } /// - /// The attribute to monitor. + /// The attribute id /// - [DataMember(Order = 5)] - public uint AttributeId { get; set; } + public uint AttributeId + { + get => State.AttributeId; + set => State = State with { AttributeId = value }; + } /// - /// The range of array indexes to monitor. + /// The index range /// - [DataMember(Order = 6)] - public string IndexRange { get; set; } + public string? IndexRange + { + get => State.IndexRange; + set => State = State with { IndexRange = value }; + } /// - /// The encoding to use when returning notifications. + /// The data encoding /// - [DataMember(Order = 7)] - public QualifiedName Encoding { get; set; } + public QualifiedName Encoding + { + get => State.Encoding; + set => State = State with { Encoding = value ?? QualifiedName.Null }; + } /// - /// The monitoring mode. + /// The monitoring mode /// - [DataMember(Order = 8)] - public MonitoringMode MonitoringMode { get; set; } + public MonitoringMode MonitoringMode + { + get => State.MonitoringMode; + set => State = State with { MonitoringMode = value }; + } /// - /// The sampling interval. + /// The sampling interval /// - [DataMember(Order = 9)] public int SamplingInterval { - get => m_samplingInterval; + get => State.SamplingInterval; set { - if (m_samplingInterval != value) + if (State.SamplingInterval != value) { AttributesModified = true; } - - m_samplingInterval = value; + State = State with { SamplingInterval = value }; } } /// - /// The filter to use to select values to return. + /// The monitoring filter /// - [DataMember(Order = 10)] - public MonitoringFilter Filter + public MonitoringFilter? Filter { - get => m_filter; + get => State.Filter; set { - // validate filter against node class. - ValidateFilter(m_nodeClass, value); - - AttributesModified = true; - m_filter = value; + if (!Equals(State.Filter, value)) + { + ValidateFilter(NodeClass, value); + AttributesModified = true; + } + State = State with { Filter = value }; } } /// - /// The length of the queue used to buffer values. + /// The queue size /// - [DataMember(Order = 11)] public uint QueueSize { - get => m_queueSize; + get => State.QueueSize; set { - if (m_queueSize != value) + if (State.QueueSize != value) { AttributesModified = true; } - - m_queueSize = value; + State = State with { QueueSize = value }; } } /// - /// Whether to discard the oldest entries in the queue when it is full. + /// Discard oldest when full /// - [DataMember(Order = 12)] public bool DiscardOldest { - get => m_discardOldest; + get => State.DiscardOldest; set { - if (m_discardOldest != value) + if (State.DiscardOldest != value) { AttributesModified = true; } - - m_discardOldest = value; + State = State with { DiscardOldest = value }; } } /// /// Server-assigned id for the MonitoredItem. /// - [DataMember(Order = 13)] public uint ServerId { get => Status.Id; @@ -380,12 +339,12 @@ public uint ServerId /// /// The subscription that owns the monitored item. /// - public Subscription Subscription + public Subscription? Subscription { get => m_subscription; internal set { - if (m_subscription == null && value.Telemetry != null) + if (m_subscription == null && value?.Telemetry != null) { m_logger = value.Telemetry.CreateLogger(); } @@ -396,7 +355,7 @@ internal set /// /// A local handle assigned to the monitored item. /// - public object Handle { get; set; } + public object? Handle { get; set; } /// /// Whether the item has been created on the server. @@ -416,11 +375,10 @@ public NodeId ResolvedNodeId get { // just return the start id if relative path is empty. - if (string.IsNullOrEmpty(m_relativePath)) + if (string.IsNullOrEmpty(State.RelativePath)) { return StartNodeId; } - return m_resolvedNodeId; } internal set => m_resolvedNodeId = value; @@ -429,12 +387,12 @@ public NodeId ResolvedNodeId /// /// Whether the monitoring attributes have been modified since the item was created. /// - public bool AttributesModified { get; private set; } + public bool AttributesModified { get; private set; } = true; /// /// The status associated with the monitored item. /// - public MonitoredItemStatus Status { get; private set; } + public MonitoredItemStatus Status { get; } /// /// Returns the queue size used by the cache. @@ -474,7 +432,7 @@ public int CacheQueueSize /// /// The last value or event received from the server. /// - public IEncodeable LastValue + public IEncodeable? LastValue { get { @@ -520,7 +478,7 @@ public IList DequeueEvents() /// /// The last message containing a notification for the item. /// - public NotificationMessage LastMessage + public NotificationMessage? LastMessage { get { @@ -528,12 +486,12 @@ public NotificationMessage LastMessage { if (m_dataCache != null) { - return ((MonitoredItemNotification)m_lastNotification).Message; + return ((MonitoredItemNotification?)m_lastNotification)?.Message; } if (m_eventCache != null) { - return ((EventFieldList)m_lastNotification).Message; + return ((EventFieldList?)m_lastNotification)?.Message; } return null; @@ -617,7 +575,8 @@ public void SaveValueInCache(IEncodeable newValue) if (datachange.Value.StatusCode.Overflow) { m_logger.LogWarning( - "Overflow bit set for data change with ServerTimestamp {ServerTimestamp} and value {Value} for MonitoredItemId {MonitoredItemId}", + "Overflow bit set for data change with ServerTimestamp {ServerTimestamp} " + + "and value {Value} for MonitoredItemId {MonitoredItemId}", datachange.Value.ServerTimestamp.ToLocalTime(), datachange.Value.Value, ClientHandle); @@ -672,13 +631,13 @@ public void SetError(ServiceResult error) /// /// Updates the object with the results of a translate browse path request. /// - public void SetResolvePathResult( + protected internal void SetResolvePathResult( BrowsePathResult result, int index, DiagnosticInfoCollection diagnosticInfos, ResponseHeader responseHeader) { - ServiceResult error = null; + ServiceResult? error = null; if (StatusCode.IsBad(result.StatusCode)) { @@ -693,7 +652,7 @@ public void SetResolvePathResult( ResolvedNodeId = NodeId.Null; // update the node id. - if (result.Targets.Count > 0) + if (result.Targets.Count > 0 && Subscription?.Session != null) { ResolvedNodeId = ExpandedNodeId.ToNodeId( result.Targets[0].TargetId, @@ -707,14 +666,14 @@ public void SetResolvePathResult( /// /// Updates the object with the results of a create monitored item request. /// - public void SetCreateResult( + protected internal void SetCreateResult( MonitoredItemCreateRequest request, MonitoredItemCreateResult result, int index, DiagnosticInfoCollection diagnosticInfos, ResponseHeader responseHeader) { - ServiceResult error = null; + ServiceResult? error = null; if (StatusCode.IsBad(result.StatusCode)) { @@ -732,14 +691,14 @@ public void SetCreateResult( /// /// Updates the object with the results of a modify monitored item request. /// - public void SetModifyResult( + protected internal void SetModifyResult( MonitoredItemModifyRequest request, MonitoredItemModifyResult result, int index, DiagnosticInfoCollection diagnosticInfos, ResponseHeader responseHeader) { - ServiceResult error = null; + ServiceResult? error = null; if (StatusCode.IsBad(result.StatusCode)) { @@ -757,7 +716,7 @@ public void SetModifyResult( /// /// Updates the object with the results of a transfer subscription request. /// - public void SetTransferResult(uint clientHandle) + protected internal void SetTransferResult(uint clientHandle) { // ensure the global counter is not duplicating future handle ids Utils.SetIdentifierToAtLeast(ref s_globalClientHandle, clientHandle); @@ -769,13 +728,13 @@ public void SetTransferResult(uint clientHandle) /// /// Updates the object with the results of a delete monitored item request. /// - public void SetDeleteResult( + protected internal void SetDeleteResult( StatusCode result, int index, - DiagnosticInfoCollection diagnosticInfos, - ResponseHeader responseHeader) + DiagnosticInfoCollection? diagnosticInfos, + ResponseHeader? responseHeader) { - ServiceResult error = null; + ServiceResult? error = null; if (StatusCode.IsBad(result)) { @@ -788,9 +747,9 @@ public void SetDeleteResult( /// /// Returns the field name the specified SelectClause in the EventFilter. /// - public string GetFieldName(int index) + public string? GetFieldName(int index) { - if (m_filter is not EventFilter filter) + if (State.Filter is not EventFilter filter) { return null; } @@ -808,7 +767,7 @@ public string GetFieldName(int index) /// /// Returns value of the field name containing the event type. /// - public object GetFieldValue( + public object? GetFieldValue( EventFieldList eventFields, NodeId eventTypeId, string browsePath, @@ -821,7 +780,7 @@ public object GetFieldValue( /// /// Returns value of the field name containing the event type. /// - public object GetFieldValue( + public object? GetFieldValue( EventFieldList eventFields, NodeId eventTypeId, QualifiedName browseName) @@ -833,7 +792,7 @@ public object GetFieldValue( /// /// Returns value of the field name containing the event type. /// - public object GetFieldValue( + public object? GetFieldValue( EventFieldList eventFields, NodeId eventTypeId, IList browsePath, @@ -844,7 +803,7 @@ public object GetFieldValue( return null; } - if (m_filter is not EventFilter filter) + if (State.Filter is not EventFilter filter) { return null; } @@ -920,7 +879,7 @@ public object GetFieldValue( /// /// Returns value of the field name containing the event type. /// - public async ValueTask GetEventTypeAsync( + public async ValueTask GetEventTypeAsync( EventFieldList eventFields, CancellationToken ct = default) { @@ -966,7 +925,7 @@ public DateTime GetEventTime(EventFieldList eventFields) /// /// The service result for a data change notification. /// - public static ServiceResult GetServiceResult(IEncodeable notification) + public static ServiceResult? GetServiceResult(IEncodeable notification) { if (notification is not MonitoredItemNotification datachange) { @@ -987,9 +946,10 @@ public static ServiceResult GetServiceResult(IEncodeable notification) } /// - /// The service result for a field in an notification (the field must contain a Status object). + /// The service result for a field in an notification + /// (the field must contain a Status object). /// - public static ServiceResult GetServiceResult(IEncodeable notification, int index) + public static ServiceResult? GetServiceResult(IEncodeable notification, int index) { if (notification is not EventFieldList eventFields) { @@ -1026,7 +986,7 @@ private void EnsureCacheIsInitialized() { if (m_dataCache == null && m_eventCache == null) { - if (((int)m_nodeClass & ((int)NodeClass.Object | (int)NodeClass.View)) != 0) + if (((int)State.NodeClass & ((int)NodeClass.Object | (int)NodeClass.View)) != 0) { m_eventCache = new MonitoredItemEventCache(100); } @@ -1041,7 +1001,7 @@ private void EnsureCacheIsInitialized() /// Throws an exception if the flter cannot be used with the node class. /// /// - private void ValidateFilter(NodeClass nodeClass, MonitoringFilter filter) + private void ValidateFilter(NodeClass nodeClass, MonitoringFilter? filter) { if (filter == null) { @@ -1054,7 +1014,7 @@ private void ValidateFilter(NodeClass nodeClass, MonitoringFilter filter) case NodeClass.VariableType: if (!typeof(DataChangeFilter).IsInstanceOfType(filter)) { - m_nodeClass = NodeClass.Variable; + State = State with { NodeClass = NodeClass.Variable }; } break; @@ -1062,7 +1022,7 @@ private void ValidateFilter(NodeClass nodeClass, MonitoringFilter filter) case NodeClass.View: if (!typeof(EventFilter).IsInstanceOfType(filter)) { - m_nodeClass = NodeClass.Object; + State = State with { NodeClass = NodeClass.Object }; } break; @@ -1084,9 +1044,9 @@ private void ValidateFilter(NodeClass nodeClass, MonitoringFilter filter) /// /// Sets the default event filter. /// - private void UseDefaultEventFilter() + private static EventFilter GetDefaultEventFilter() { - EventFilter filter = _ = new EventFilter(); + var filter = new EventFilter(); filter.AddSelectClause(ObjectTypes.BaseEventType, BrowseNames.EventId); filter.AddSelectClause(ObjectTypes.BaseEventType, BrowseNames.EventType); @@ -1098,24 +1058,18 @@ private void UseDefaultEventFilter() filter.AddSelectClause(ObjectTypes.BaseEventType, BrowseNames.Message); filter.AddSelectClause(ObjectTypes.BaseEventType, BrowseNames.Severity); - m_filter = filter; + return filter; } - private string m_relativePath; - private NodeId m_resolvedNodeId; - private NodeClass m_nodeClass; - private int m_samplingInterval; - private MonitoringFilter m_filter; - private uint m_queueSize; - private bool m_discardOldest; private static uint s_globalClientHandle; - private Subscription m_subscription; + private NodeId m_resolvedNodeId = NodeId.Null; + private Subscription? m_subscription; private ILogger m_logger = Utils.Null.Logger; - private Lock m_cache = new(); - private MonitoredItemDataCache m_dataCache; - private MonitoredItemEventCache m_eventCache; - private IEncodeable m_lastNotification; - private event MonitoredItemNotificationEventHandler m_Notification; + private readonly Lock m_cache = new(); + private MonitoredItemDataCache? m_dataCache; + private MonitoredItemEventCache? m_eventCache; + private IEncodeable? m_lastNotification; + private event MonitoredItemNotificationEventHandler? m_Notification; } /// @@ -1153,7 +1107,7 @@ public class MonitoredItemDataCache /// /// Constructs a cache for a monitored item. /// - public MonitoredItemDataCache(ITelemetryContext telemetry, int queueSize = 1) + public MonitoredItemDataCache(ITelemetryContext? telemetry, int queueSize = 1) { QueueSize = queueSize; m_logger = telemetry.CreateLogger(); @@ -1175,7 +1129,7 @@ public MonitoredItemDataCache(ITelemetryContext telemetry, int queueSize = 1) /// /// The last value received from the server. /// - public DataValue LastValue { get; private set; } + public DataValue? LastValue { get; private set; } /// /// Returns all values in the queue. @@ -1188,7 +1142,7 @@ public IList Publish() values = new List(m_values.Count); for (int ii = 0; ii < values.Count; ii++) { - if (!m_values.TryDequeue(out DataValue dequeued)) + if (!m_values.TryDequeue(out DataValue? dequeued)) { break; } @@ -1223,7 +1177,7 @@ public void OnNotification(MonitoredItemNotification notification) m_values.Enqueue(notification.Value); while (m_values.Count > QueueSize) { - if (!m_values.TryDequeue(out DataValue dropped)) + if (!m_values.TryDequeue(out DataValue? dropped)) { break; } @@ -1258,9 +1212,16 @@ public void SetQueueSize(int queueSize) QueueSize = queueSize; + if (m_values == null) + { + return; + } while (m_values.Count > QueueSize) { - m_values.TryDequeue(out DataValue dropped); + if (!m_values.TryDequeue(out DataValue? dropped)) + { + break; + } m_logger.LogDebug( "Setting queue size dropped value: Value={Value}, SourceTime={SourceTime}", dropped.WrappedValue, @@ -1268,7 +1229,7 @@ public void SetQueueSize(int queueSize) } } - private ConcurrentQueue m_values; + private ConcurrentQueue? m_values; private readonly ILogger m_logger; } @@ -1294,7 +1255,7 @@ public MonitoredItemEventCache(int queueSize) /// /// The last event received. /// - public EventFieldList LastEvent { get; private set; } + public EventFieldList? LastEvent { get; private set; } /// /// Returns all events in the queue. diff --git a/Libraries/Opc.Ua.Client/Subscription/MonitoredItemOptions.cs b/Libraries/Opc.Ua.Client/Subscription/MonitoredItemOptions.cs new file mode 100644 index 0000000000..8ae8b1a3b8 --- /dev/null +++ b/Libraries/Opc.Ua.Client/Subscription/MonitoredItemOptions.cs @@ -0,0 +1,179 @@ +/* ======================================================================== + * 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(MonitoredItemOptions))] + [JsonSerializable(typeof(MonitoredItemState))] + internal partial class MonitoredItemOptionsContext : JsonSerializerContext; + + /// + /// Serializable options for a client monitored item. + /// + /// These options map to parameters used when creating and modifying monitored + /// items within a subscription. See OPC UA Part4 v1.05 Sections 5.13 and 5.14 + /// (Subscription / MonitoredItem Service Sets): + /// https://reference.opcfoundation.org/Core/Part4/v105/docs/5.13 and + /// https://reference.opcfoundation.org/Core/Part4/v105/docs/5.14. + /// + /// + [DataContract(Namespace = Namespaces.OpcUaXsd)] + [KnownType(typeof(DataChangeFilter))] + [KnownType(typeof(EventFilter))] + [KnownType(typeof(AggregateFilter))] + public record class MonitoredItemOptions + { + /// + /// Local human readable display name used by the client for logging and + /// diagnostics of the monitored item. This is not the DisplayName attribute + /// of the target node and is not sent in the CreateMonitoredItems request. + /// Choose a name that reflects the business purpose (e.g. "Tank1.Level"). + /// Spec Context: Client side metadata. + /// Reference: Part4 Section5.13. + /// + [DataMember(Order = 1)] + public string DisplayName { get; init; } = "MonitoredItem"; + + /// + /// Starting NodeId used with RelativePath to resolve the + /// target node. If RelativePath is null the StartNodeId is the node + /// directly monitored. This corresponds to the nodeId when constructed + /// after path resolution. NodeId.Null indicates it is not set yet. + /// Spec: ReadValueId.nodeId. + /// Reference: Part4 Section5.13. + /// + [DataMember(Order = 2)] + public NodeId StartNodeId { get; init; } = NodeId.Null; + + /// + /// A relative browse path (client side string form) from StartNodeId + /// to the final target node or attribute to monitor. When supplied the client + /// resolves it to a NodeId using Browse / TranslateBrowsePaths services before + /// creating the monitored item. Null means no path; monitor the StartNodeId + /// directly. + /// + [DataMember(Order = 3)] + public string? RelativePath { get; init; } + + /// + /// The expected NodeClass of the target node (Variable, Object, etc.). + /// Primarily used for client validation after resolving the NodeId. + /// Monitoring Variables yields DataChange notifications; monitoring Events + /// uses Object / View and an EventFilter. Ensuring the correct NodeClass + /// helps avoid invalid monitored item creation requests. + /// + [DataMember(Order = 4)] + public NodeClass NodeClass { get; init; } = NodeClass.Variable; + + /// + /// The AttributeId to monitor on the target node. For data changes this + /// is typically Attributes.Value; for Events the attribute may be + /// Attributes.EventNotifier; for other use cases StatusCode or Timestamp + /// attributes may be monitored. Maps to ReadValueId.AttributeId in + /// the service request. + /// + [DataMember(Order = 5)] + public uint AttributeId { get; init; } = Attributes.Value; + + /// + /// IndexRange selecting a subset of an array or matrix value (e.g. "0:9" + /// for first10 elements). If null the entire value is monitored. This + /// corresponds to ReadValueId.indexRange and is applied by the + /// server when generating notifications, reducing bandwidth for large arrays. + /// + [DataMember(Order = 6)] + public string? IndexRange { get; init; } + + /// + /// Requested data encoding (QualifiedName) for complex values (e.g. + /// specific DataTypeEncoding). This maps to ReadValueId.dataEncoding. + /// Use QualifiedName.Null for default encoding. Ensures notifications + /// are serialized in a form understood by the client. + /// + [DataMember(Order = 7)] + public QualifiedName Encoding { get; init; } = QualifiedName.Null; + + /// + /// Requested MonitoringMode for the item: Disabled (no sampling), + /// Sampling (server samples but does not queue notifications), Reporting + /// (samples and queues notifications for Publish). This value may be changed + /// later via SetMonitoringMode. Default is Reporting for typical data + /// collection. + /// + [DataMember(Order = 8)] + public MonitoringMode MonitoringMode { get; init; } = MonitoringMode.Reporting; + + /// + /// Requested samplingInterval (ms) for the server's data sampling + /// of the underlying value. A value of -1 requests the server default. + /// The actual revised interval may differ; use the created MonitoredItem's + /// server-revised value for final timing. Very small intervals increase + /// load; very large intervals reduce data freshness. + /// + [DataMember(Order = 9)] + public int SamplingInterval { get; init; } = -1; + + /// + /// Optional server side filter controlling which data changes or events + /// generate notifications. For Variables use DataChangeFilter or + /// AggregateFilter; for Events use EventFilter. Null means + /// no additional filtering beyond sampling and queueing. Proper filters + /// reduce bandwidth and client processing. + /// Spec: MonitoringFilter types (DataChangeFilter, EventFilter, + /// AggregateFilter). + /// Reference: Part4 Section5.13. + /// + [DataMember(Order = 10)] + public MonitoringFilter? Filter { get; init; } + + /// + /// Requested queueSize specifying the maximum number of notifications + /// the server retains for this item between Publish responses.0 (or 1 + /// depending on server) means minimal queue. + /// Larger queues reduce risk of data loss during short client delays but + /// increase memory usage. Maps to requestedParameters.queueSize. + /// The server may revise. + /// + [DataMember(Order = 11)] + public uint QueueSize { get; init; } = 0; + + /// + /// discardOldest policy: if true the server discards the oldest + /// entry when the queue is full and a new notification arrives; if false + /// it discards the newest notification. The usual recommendation is true + /// for streaming latest-value scenarios; false preserves the earliest + /// samples for batch integrity. + /// + [DataMember(Order = 12)] + public bool DiscardOldest { get; init; } = true; + } +} diff --git a/Libraries/Opc.Ua.Client/Subscription/MonitoredItemState.cs b/Libraries/Opc.Ua.Client/Subscription/MonitoredItemState.cs new file mode 100644 index 0000000000..181ca0bd14 --- /dev/null +++ b/Libraries/Opc.Ua.Client/Subscription/MonitoredItemState.cs @@ -0,0 +1,127 @@ +/* ======================================================================== + * 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; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; + +namespace Opc.Ua.Client +{ + /// + /// State object that is used for snapshotting the monitored item + /// + [DataContract(Namespace = Namespaces.OpcUaXsd)] + public record class MonitoredItemState : MonitoredItemOptions + { + /// + /// Create monitored item state + /// + public MonitoredItemState() + { + } + + /// + /// Create state from options + /// + /// + public MonitoredItemState(MonitoredItemOptions options) + : base(options) + { + } + + /// + /// Server-side identifier assigned to this monitored item (the + /// monitoredItemId). Stored so the client can correlate notifications + /// and perform Modify/SetMonitoringMode/Delete operations across reconnects. + /// 0 indicates not yet created or invalidated. + /// + [DataMember(Order = 13)] + public uint ServerId { get; init; } + + /// + /// Client-assigned handle used in Publish notifications (clientHandle) + /// to quickly map incoming data changes or events to local application + /// structures without lookups on serverId. Should be unique per subscription. + /// Typically used as an index/key into client data structures. + /// + [DataMember(Order = 14)] + public uint ClientId { get; init; } + + /// + /// When the state was created. + /// + [DataMember(Order = 15)] + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + } + + /// + /// A collection of monitored item states. + /// + [CollectionDataContract( + Name = "ListOfMonitoredItems", + Namespace = Namespaces.OpcUaXsd, + ItemName = "MonitoredItems")] + public class MonitoredItemStateCollection : List, ICloneable + { + /// + /// Initializes an empty collection. + /// + public MonitoredItemStateCollection() + { + } + + /// + /// Initializes the collection from another collection. + /// + /// The existing collection to use as + /// the basis of creating this collection + public MonitoredItemStateCollection(IEnumerable collection) + : base(collection) + { + } + + /// + /// Initializes the collection with the specified capacity. + /// + /// The max. capacity of the collection + public MonitoredItemStateCollection(int capacity) + : base(capacity) + { + } + + /// + public virtual object Clone() + { + var clone = new MonitoredItemStateCollection(); + clone.AddRange(this.Select(item => item with { })); + return clone; + } + } +} diff --git a/Libraries/Opc.Ua.Client/Subscription/MonitoredItemStatus.cs b/Libraries/Opc.Ua.Client/Subscription/MonitoredItemStatus.cs index cb3886722a..9096898d04 100644 --- a/Libraries/Opc.Ua.Client/Subscription/MonitoredItemStatus.cs +++ b/Libraries/Opc.Ua.Client/Subscription/MonitoredItemStatus.cs @@ -34,32 +34,8 @@ namespace Opc.Ua.Client /// /// The current status of monitored item. /// - public class MonitoredItemStatus + public sealed record class MonitoredItemStatus { - /// - /// Creates a empty object. - /// - internal MonitoredItemStatus() - { - Initialize(); - } - - private void Initialize() - { - Id = 0; - NodeId = null; - AttributeId = Attributes.Value; - IndexRange = null; - DataEncoding = null; - MonitoringMode = MonitoringMode.Disabled; - ClientHandle = 0; - SamplingInterval = 0; - Filter = null; - FilterResult = null; - QueueSize = 0; - DiscardOldest = true; - } - /// /// The identifier assigned by the server. /// @@ -73,32 +49,32 @@ private void Initialize() /// /// Any error condition associated with the monitored item. /// - public ServiceResult Error { get; private set; } + public ServiceResult? Error { get; private set; } /// /// The node id being monitored. /// - public NodeId NodeId { get; private set; } + public NodeId NodeId { get; private set; } = NodeId.Null; /// /// The attribute being monitored. /// - public uint AttributeId { get; private set; } + public uint AttributeId { get; private set; } = Attributes.Value; /// /// The range of array indexes to being monitored. /// - public string IndexRange { get; private set; } + public string? IndexRange { get; private set; } /// /// The encoding to use when returning notifications. /// - public QualifiedName DataEncoding { get; private set; } + public QualifiedName DataEncoding { get; private set; } = QualifiedName.Null; /// /// The monitoring mode. /// - public MonitoringMode MonitoringMode { get; private set; } + public MonitoringMode MonitoringMode { get; private set; } = MonitoringMode.Disabled; /// /// The identifier assigned by the client. @@ -113,12 +89,12 @@ private void Initialize() /// /// The filter to use to select values to return. /// - public MonitoringFilter Filter { get; private set; } + public MonitoringFilter? Filter { get; private set; } /// /// The result of applying the filter /// - public MonitoringFilterResult FilterResult { get; private set; } + public MonitoringFilterResult? FilterResult { get; private set; } /// /// The length of the queue used to buffer values. @@ -128,7 +104,7 @@ private void Initialize() /// /// Whether to discard the oldest entries in the queue when it is full. /// - public bool DiscardOldest { get; private set; } + public bool DiscardOldest { get; private set; } = true; /// /// Updates the monitoring mode. @@ -141,7 +117,7 @@ public void SetMonitoringMode(MonitoringMode monitoringMode) /// /// Updates the object with the results of a translate browse paths request. /// - internal void SetResolvePathResult(ServiceResult error) + internal void SetResolvePathResult(ServiceResult? error) { Error = error; } @@ -153,7 +129,7 @@ internal void SetResolvePathResult(ServiceResult error) internal void SetCreateResult( MonitoredItemCreateRequest request, MonitoredItemCreateResult result, - ServiceResult error) + ServiceResult? error) { if (request == null) { @@ -232,7 +208,7 @@ internal void SetTransferResult(MonitoredItem monitoredItem) internal void SetModifyResult( MonitoredItemModifyRequest request, MonitoredItemModifyResult result, - ServiceResult error) + ServiceResult? error) { if (request == null) { @@ -274,7 +250,7 @@ internal void SetModifyResult( /// /// Updates the object with the results of a delete item request. /// - internal void SetDeleteResult(ServiceResult error) + internal void SetDeleteResult(ServiceResult? error) { Id = 0; Error = error; diff --git a/Libraries/Opc.Ua.Client/Subscription/Subscription.cs b/Libraries/Opc.Ua.Client/Subscription/Subscription.cs index 89c7336faf..d4c407a89c 100644 --- a/Libraries/Opc.Ua.Client/Subscription/Subscription.cs +++ b/Libraries/Opc.Ua.Client/Subscription/Subscription.cs @@ -31,18 +31,17 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Runtime.Serialization; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using System.Diagnostics.CodeAnalysis; namespace Opc.Ua.Client { /// /// A subscription. /// - [DataContract(Namespace = Namespaces.OpcUaXsd)] - public class Subscription : IDisposable, ICloneable + public class Subscription : ISnapshotRestore, IDisposable, ICloneable { private const int kMinKeepAliveTimerInterval = 1000; private const int kKeepAliveTimerMargin = 1000; @@ -54,18 +53,19 @@ public class Subscription : IDisposable, ICloneable /// [Obsolete("Use Subscription(TelemetryContext) instead")] public Subscription() - : this(null) + : this(null!, null) { } /// /// Creates a empty object. /// - public Subscription(ITelemetryContext telemetry) + public Subscription(ITelemetryContext telemetry, SubscriptionOptions? options = null) { - Telemetry = telemetry; - - Initialize(); + Telemetry = telemetry ?? AmbientMessageContext.Telemetry; + m_logger = Telemetry.CreateLogger(); + State = options ?? new SubscriptionOptions(); + DefaultItem = CreateMonitoredItem(); } /// @@ -75,54 +75,78 @@ public Subscription(ITelemetryContext telemetry) /// if set to true the event handlers are copied. public Subscription(Subscription template, bool copyEventHandlers = false) { - if (template != null) + if (template == null) { - m_logger = template.m_logger; - m_telemetry = template.Telemetry; + throw new ArgumentNullException(nameof(template)); } - Initialize(); + m_telemetry = template.m_telemetry; + m_logger = template.m_logger; + State = template.State; + Handle = template.Handle; + DefaultItem = CreateMonitoredItem(template.DefaultItem.State); + m_lastSequenceNumberProcessed = template.m_lastSequenceNumberProcessed; - if (template != null) + if (copyEventHandlers) { - DisplayName = template.DisplayName; - PublishingInterval = template.PublishingInterval; - KeepAliveCount = template.KeepAliveCount; - LifetimeCount = template.LifetimeCount; - MinLifetimeInterval = template.MinLifetimeInterval; - MaxNotificationsPerPublish = template.MaxNotificationsPerPublish; - PublishingEnabled = template.PublishingEnabled; - Priority = template.Priority; - TimestampsToReturn = template.TimestampsToReturn; - m_maxMessageCount = template.m_maxMessageCount; - m_sequentialPublishing = template.m_sequentialPublishing; - RepublishAfterTransfer = template.RepublishAfterTransfer; - DefaultItem = (MonitoredItem)template.DefaultItem.Clone(); - Handle = template.Handle; - DisableMonitoredItemCache = template.DisableMonitoredItemCache; - TransferId = template.TransferId; + m_StateChanged = template.m_StateChanged; + m_PublishStatusChanged = template.m_PublishStatusChanged; + FastDataChangeCallback = template.FastDataChangeCallback; + FastEventCallback = template.FastEventCallback; + FastKeepAliveCallback = template.FastKeepAliveCallback; + } - if (copyEventHandlers) - { - m_StateChanged = template.m_StateChanged; - m_PublishStatusChanged = template.m_PublishStatusChanged; - FastDataChangeCallback = template.FastDataChangeCallback; - FastEventCallback = template.FastEventCallback; - FastKeepAliveCallback = template.FastKeepAliveCallback; - } + // copy the list of monitored items. + var clonedMonitoredItems = new List(); + foreach (MonitoredItem monitoredItem in template.MonitoredItems) + { + MonitoredItem clone = monitoredItem.CloneMonitoredItem(copyEventHandlers, true); + clone.DisplayName = monitoredItem.DisplayName; + clonedMonitoredItems.Add(clone); + } + if (clonedMonitoredItems.Count > 0) + { + AddItems(clonedMonitoredItems); + } + } + + /// + public virtual void Restore(SubscriptionState state) + { + CurrentPublishingInterval = state.CurrentPublishingInterval; + CurrentKeepAliveCount = state.CurrentKeepAliveCount; + CurrentLifetimeCount = state.CurrentLifetimeCount; + + var monitoredItems = new List(state.MonitoredItems.Count); + foreach (MonitoredItemState monitoredItemState in state.MonitoredItems) + { + MonitoredItem monitoredItem = CreateMonitoredItem(monitoredItemState); + monitoredItem.Restore(monitoredItemState); + monitoredItems.Add(monitoredItem); + } + + AddItems(monitoredItems); + } - // copy the list of monitored items. - var clonedMonitoredItems = new List(); - foreach (MonitoredItem monitoredItem in template.MonitoredItems) + /// + public virtual void Snapshot(out SubscriptionState state) + { + lock (m_cache) + { + var monitoredItemStateCollection = new MonitoredItemStateCollection( + m_monitoredItems.Count); + foreach (MonitoredItem monitoredItem in m_monitoredItems.Values) { - MonitoredItem clone = monitoredItem.CloneMonitoredItem(copyEventHandlers, true); - clone.DisplayName = monitoredItem.DisplayName; - clonedMonitoredItems.Add(clone); + monitoredItem.Snapshot(out MonitoredItemState monitoredItemState); + monitoredItemStateCollection.Add(monitoredItemState); } - if (clonedMonitoredItems.Count > 0) + state = new SubscriptionState(State) { - AddItems(clonedMonitoredItems); - } + MonitoredItems = monitoredItemStateCollection, + CurrentKeepAliveCount = CurrentKeepAliveCount, + CurrentLifetimeCount = CurrentLifetimeCount, + CurrentPublishingInterval = CurrentPublishingInterval + }; } } @@ -139,8 +163,8 @@ private void ResetPublishTimerAndWorkerState() /// private async Task ResetPublishTimerAndWorkerStateAsync() { - Task workerTask; - CancellationTokenSource workerCts; + Task? workerTask; + CancellationTokenSource? workerCts; lock (m_cache) { // Called under the m_cache lock @@ -172,7 +196,7 @@ private async Task ResetPublishTimerAndWorkerStateAsync() try { m_messageWorkerEvent.Set(); - workerCts.Cancel(); + workerCts?.Cancel(); await workerTask.ConfigureAwait(false); } catch (Exception e) @@ -185,56 +209,6 @@ private async Task ResetPublishTimerAndWorkerStateAsync() } } - /// - /// Called by the .NET framework during deserialization. - /// - [OnDeserializing] - protected void Initialize(StreamingContext context) - { - m_cache = new Lock(); - m_telemetry = AmbientMessageContext.Telemetry; - Initialize(); - } - - /// - /// Sets the private members to default values. - /// - private void Initialize() - { - TransferId = Id = 0; - DisplayName = "Subscription"; - PublishingInterval = 0; - KeepAliveCount = 0; - m_keepAliveInterval = 0; - LifetimeCount = 0; - MaxNotificationsPerPublish = 0; - PublishingEnabled = false; - TimestampsToReturn = TimestampsToReturn.Both; - m_maxMessageCount = 10; - RepublishAfterTransfer = false; - m_outstandingMessageWorkers = 0; - m_sequentialPublishing = false; - m_lastSequenceNumberProcessed = 0; - m_messageCache = new LinkedList(); - m_monitoredItems = []; - m_deletedItems = []; - m_messageWorkerEvent = new AsyncAutoResetEvent(); - m_messageWorkerCts = null; - m_resyncLastSequenceNumberProcessed = false; - - // Creates a default logger even if telemetry is null - m_logger ??= Telemetry.CreateLogger(); - - DefaultItem = new MonitoredItem - { - DisplayName = "MonitoredItem", - SamplingInterval = -1, - MonitoringMode = MonitoringMode.Reporting, - QueueSize = 0, - DiscardOldest = true - }; - } - /// /// Frees any unmanaged resources. /// @@ -277,6 +251,19 @@ public virtual Subscription CloneSubscription(bool copyEventHandlers) return new Subscription(this, copyEventHandlers); } + /// + /// Create a monitored item with the provided item state + /// + protected virtual MonitoredItem CreateMonitoredItem(MonitoredItemOptions? options = null) + { + return new MonitoredItem(Telemetry!, options); + } + + /// + /// Subscription state/options + /// + public SubscriptionOptions State { get; private set; } + /// /// Raised to indicate that the state of the subscription has changed. /// @@ -298,66 +285,89 @@ public event PublishStateChangedEventHandler PublishStatusChanged /// /// A display name for the subscription. /// - [DataMember(Order = 1)] - public string DisplayName { get; set; } + public string DisplayName + { + get => State.DisplayName; + set => State = State with { DisplayName = value }; + } /// /// The publishing interval. /// - [DataMember(Order = 2)] - public int PublishingInterval { get; set; } + public int PublishingInterval + { + get => State.PublishingInterval; + set => State = State with { PublishingInterval = value }; + } /// /// The keep alive count. /// - [DataMember(Order = 3)] - public uint KeepAliveCount { get; set; } + public uint KeepAliveCount + { + get => State.KeepAliveCount; + set => State = State with { KeepAliveCount = value }; + } /// /// The life time of the subscription in counts of /// publish interval. /// LifetimeCount shall be at least 3*KeepAliveCount. /// - [DataMember(Order = 4)] - public uint LifetimeCount { get; set; } + public uint LifetimeCount + { + get => State.LifetimeCount; + set => State = State with { LifetimeCount = value }; + } /// /// The maximum number of notifications per publish request. /// - [DataMember(Order = 5)] - public uint MaxNotificationsPerPublish { get; set; } + public uint MaxNotificationsPerPublish + { + get => State.MaxNotificationsPerPublish; + set => State = State with { MaxNotificationsPerPublish = value }; + } /// /// Whether publishing is enabled. /// - [DataMember(Order = 6)] - public bool PublishingEnabled { get; set; } + public bool PublishingEnabled + { + get => State.PublishingEnabled; + set => State = State with { PublishingEnabled = value }; + } /// - /// The priority assigned to subscription. + /// The priority assigned to the subscription. /// - [DataMember(Order = 7)] - public byte Priority { get; set; } + public byte Priority + { + get => State.Priority; + set => State = State with { Priority = value }; + } /// /// The timestamps to return with the notification messages. /// - [DataMember(Order = 8)] - public TimestampsToReturn TimestampsToReturn { get; set; } + public TimestampsToReturn TimestampsToReturn + { + get => State.TimestampsToReturn; + set => State = State with { TimestampsToReturn = value }; + } /// /// The maximum number of messages to keep in the internal cache. /// - [DataMember(Order = 9)] public int MaxMessageCount { - get => m_maxMessageCount; + get => State.MaxMessageCount; set { // lock needed to synchronize with message list processing lock (m_cache) { - m_maxMessageCount = value; + State = State with { MaxMessageCount = value }; } } } @@ -365,14 +375,16 @@ public int MaxMessageCount /// /// The default monitored item. /// - [DataMember(Order = 10)] public MonitoredItem DefaultItem { get; set; } /// /// The minimum lifetime for subscriptions in milliseconds. /// - [DataMember(Order = 12)] - public uint MinLifetimeInterval { get; set; } + public uint MinLifetimeInterval + { + get => State.MinLifetimeInterval; + set => State = State with { MinLifetimeInterval = value }; + } /// /// Gets or sets a value indicating whether the notifications are cached within the monitored items. @@ -384,8 +396,11 @@ public int MaxMessageCount /// Applications must process the Session.Notication event if this is set to true. /// This flag improves performance by eliminating the processing involved in updating the cache. /// - [DataMember(Order = 13)] - public bool DisableMonitoredItemCache { get; set; } + public bool DisableMonitoredItemCache + { + get => State.DisableMonitoredItemCache; + set => State = State with { DisableMonitoredItemCache = value }; + } /// /// Gets or sets the behavior of waiting for sequential order in handling incoming messages. @@ -397,16 +412,15 @@ public int MaxMessageCount /// Setting to true means incoming messages are processed in /// a "single-threaded" manner and callbacks will not be invoked in parallel. /// - [DataMember(Order = 14)] public bool SequentialPublishing { - get => m_sequentialPublishing; + get => State.SequentialPublishing; set { // synchronize with message list processing lock (m_cache) { - m_sequentialPublishing = value; + State = State with { SequentialPublishing = value }; } } } @@ -420,14 +434,20 @@ public bool SequentialPublishing /// and available publish requests (sequence numbers) that were never acknowledged should be /// recovered with a republish. The setting is used after a subscription transfer. /// - [DataMember(Name = "RepublishAfterTransfer", Order = 15)] - public bool RepublishAfterTransfer { get; set; } + public bool RepublishAfterTransfer + { + get => State.RepublishAfterTransfer; + set => State = State with { RepublishAfterTransfer = value }; + } /// /// The unique identifier assigned by the server which can be used to transfer a session. /// - [DataMember(Name = "TransferId", Order = 16)] - public uint TransferId { get; set; } + public uint TransferId + { + get => State.TransferId; + set => State = State with { TransferId = value }; + } /// /// Gets or sets the fast data change callback. @@ -436,7 +456,7 @@ public bool SequentialPublishing /// /// Only one callback is allowed at a time but it is more efficient to call than an event. /// - public FastDataChangeNotificationEventHandler FastDataChangeCallback { get; set; } + public FastDataChangeNotificationEventHandler? FastDataChangeCallback { get; set; } /// /// Gets or sets the fast event callback. @@ -445,7 +465,7 @@ public bool SequentialPublishing /// /// Only one callback is allowed at a time but it is more efficient to call than an event. /// - public FastEventNotificationEventHandler FastEventCallback { get; set; } + public FastEventNotificationEventHandler? FastEventCallback { get; set; } /// /// Gets or sets the fast keep alive callback. @@ -454,7 +474,7 @@ public bool SequentialPublishing /// /// Only one callback is allowed at a time but it is more efficient to call than an event. /// - public FastKeepAliveNotificationEventHandler FastKeepAliveCallback { get; set; } + public FastKeepAliveNotificationEventHandler? FastKeepAliveCallback { get; set; } /// /// The items to monitor. @@ -470,36 +490,6 @@ public IEnumerable MonitoredItems } } - /// - /// Allows the list of monitored items to be saved/restored when the object is serialized. - /// - /// - [DataMember(Name = "MonitoredItems", Order = 11)] - internal List SavedMonitoredItems - { - get - { - lock (m_cache) - { - return [.. m_monitoredItems.Values]; - } - } - set - { - if (Created) - { - throw new InvalidOperationException( - "Cannot update a subscription that has been created on the server."); - } - - lock (m_cache) - { - m_monitoredItems.Clear(); - AddItems(value); - } - } - } - /// /// Returns true if the subscription has changes that need to be applied. /// @@ -549,12 +539,12 @@ public uint MonitoredItemCount /// /// The session that owns the subscription item. /// - public ISession Session { get; protected internal set; } + public ISession? Session { get; protected internal set; } /// /// Enables owners to set the telemetry context /// - protected internal ITelemetryContext Telemetry + protected internal ITelemetryContext? Telemetry { get => m_telemetry; // Accessible from monitored item internal set @@ -567,7 +557,7 @@ internal set /// /// A local handle assigned to the subscription /// - public object Handle { get; set; } + public object? Handle { get; set; } /// /// The unique identifier assigned by the server. @@ -582,19 +572,16 @@ internal set /// /// The current publishing interval. /// - [DataMember(Name = "CurrentPublishInterval", Order = 20)] public double CurrentPublishingInterval { get; set; } /// /// The current keep alive count. /// - [DataMember(Name = "CurrentKeepAliveCount", Order = 21)] public uint CurrentKeepAliveCount { get; set; } /// /// The current lifetime count. /// - [DataMember(Name = "CurrentLifetimeCount", Order = 22)] public uint CurrentLifetimeCount { get; set; } /// @@ -618,7 +605,7 @@ public DateTime PublishTime { if (m_messageCache.Count > 0) { - return m_messageCache.Last.Value.PublishTime; + return m_messageCache.Last!.Value.PublishTime; } } @@ -649,7 +636,7 @@ public uint SequenceNumber { if (m_messageCache.Count > 0) { - return m_messageCache.Last.Value.SequenceNumber; + return m_messageCache.Last!.Value.SequenceNumber; } } @@ -668,7 +655,7 @@ public uint NotificationCount { if (m_messageCache.Count > 0) { - return (uint)m_messageCache.Last.Value.NotificationData.Count; + return (uint)m_messageCache.Last!.Value.NotificationData.Count; } } @@ -679,7 +666,7 @@ public uint NotificationCount /// /// The last notification received from the server. /// - public NotificationMessage LastNotification + public NotificationMessage? LastNotification { get { @@ -687,7 +674,7 @@ public NotificationMessage LastNotification { if (m_messageCache.Count > 0) { - return m_messageCache.Last.Value; + return m_messageCache.Last!.Value; } return null; @@ -762,13 +749,14 @@ public bool PublishingStopped /// public async Task CreateAsync(CancellationToken ct = default) { - VerifySubscriptionState(false); + using Activity? activity = m_telemetry.StartActivity(); + VerifySessionAndSubscriptionState(false); // create the subscription. uint revisedMaxKeepAliveCount = KeepAliveCount; uint revisedLifetimeCount = LifetimeCount; - AdjustCounts(ref revisedMaxKeepAliveCount, ref revisedLifetimeCount); + AdjustCounts(Session.SessionTimeout, ref revisedMaxKeepAliveCount, ref revisedLifetimeCount); CreateSubscriptionResponse response = await Session .CreateSubscriptionAsync( @@ -806,13 +794,14 @@ public async Task CreateAsync(CancellationToken ct = default) /// public async Task DeleteAsync(bool silent, CancellationToken ct = default) { + using Activity? activity = m_telemetry.StartActivity(); if (!silent) { - VerifySubscriptionState(true); + VerifySessionAndSubscriptionState(true); } // nothing to do if not created. - if (!Created) + if (!Created || Session == null) { return; } @@ -868,13 +857,14 @@ public async Task DeleteAsync(bool silent, CancellationToken ct = default) /// public async Task ModifyAsync(CancellationToken ct = default) { - VerifySubscriptionState(true); + using Activity? activity = m_telemetry.StartActivity(); + VerifySessionAndSubscriptionState(true); // modify the subscription. uint revisedKeepAliveCount = KeepAliveCount; uint revisedLifetimeCounter = LifetimeCount; - AdjustCounts(ref revisedKeepAliveCount, ref revisedLifetimeCounter); + AdjustCounts(Session.SessionTimeout, ref revisedKeepAliveCount, ref revisedLifetimeCounter); ModifySubscriptionResponse response = await Session .ModifySubscriptionAsync( @@ -903,7 +893,8 @@ public async Task ModifyAsync(CancellationToken ct = default) /// public async Task SetPublishingModeAsync(bool enabled, CancellationToken ct = default) { - VerifySubscriptionState(true); + using Activity? activity = m_telemetry.StartActivity(); + VerifySessionAndSubscriptionState(true); // modify the subscription. UInt32Collection subscriptionIds = new uint[] { Id }; @@ -940,7 +931,8 @@ public async Task RepublishAsync( uint sequenceNumber, CancellationToken ct = default) { - VerifySubscriptionState(true); + using Activity? activity = m_telemetry.StartActivity(); + VerifySessionAndSubscriptionState(true); RepublishResponse response = await Session .RepublishAsync(null, Id, sequenceNumber, ct) @@ -954,6 +946,7 @@ public async Task RepublishAsync( /// public async Task ApplyChangesAsync(CancellationToken ct = default) { + using Activity? activity = m_telemetry.StartActivity(); await DeleteItemsAsync(ct).ConfigureAwait(false); await ModifyItemsAsync(ct).ConfigureAwait(false); await CreateItemsAsync(ct).ConfigureAwait(false); @@ -964,7 +957,8 @@ public async Task ApplyChangesAsync(CancellationToken ct = default) /// public async Task ResolveItemNodeIdsAsync(CancellationToken ct = default) { - VerifySubscriptionState(true); + using Activity? activity = m_telemetry.StartActivity(); + VerifySessionAndSubscriptionState(true); // collect list of browse paths. var browsePaths = new BrowsePathCollection(); @@ -1008,6 +1002,7 @@ public async Task> CreateItemsAsync(CancellationToken ct = { MonitoredItemCreateRequestCollection requestItems; List itemsToCreate; + VerifySession(); (requestItems, itemsToCreate) = await PrepareItemsToCreateAsync(ct) .ConfigureAwait(false); @@ -1017,6 +1012,7 @@ public async Task> CreateItemsAsync(CancellationToken ct = return itemsToCreate; } + using Activity? activity = m_telemetry.StartActivity(); // create monitored items. CreateMonitoredItemsResponse response = await Session .CreateMonitoredItemsAsync(null, Id, TimestampsToReturn, requestItems, ct) @@ -1050,7 +1046,7 @@ public async Task> CreateItemsAsync(CancellationToken ct = /// public async Task> ModifyItemsAsync(CancellationToken ct = default) { - VerifySubscriptionState(true); + VerifySessionAndSubscriptionState(true); var requestItems = new MonitoredItemModifyRequestCollection(); var itemsToModify = new List(); @@ -1062,6 +1058,7 @@ public async Task> ModifyItemsAsync(CancellationToken ct = return itemsToModify; } + using Activity? activity = m_telemetry.StartActivity(); // modify the subscription. ModifyMonitoredItemsResponse response = await Session .ModifyMonitoredItemsAsync(null, Id, TimestampsToReturn, requestItems, ct) @@ -1096,13 +1093,14 @@ public async Task> ModifyItemsAsync(CancellationToken ct = public async Task> DeleteItemsAsync( CancellationToken ct = default) { - VerifySubscriptionState(true); + VerifySessionAndSubscriptionState(true); if (m_deletedItems.Count == 0) { return []; } + using Activity? activity = m_telemetry.StartActivity(); List itemsToDelete = m_deletedItems; m_deletedItems = []; @@ -1143,7 +1141,7 @@ public async Task> DeleteItemsAsync( /// /// /// is null. - public async Task> SetMonitoringModeAsync( + public async Task?> SetMonitoringModeAsync( MonitoringMode monitoringMode, IList monitoredItems, CancellationToken ct = default) @@ -1153,7 +1151,8 @@ public async Task> SetMonitoringModeAsync( throw new ArgumentNullException(nameof(monitoredItems)); } - VerifySubscriptionState(true); + using Activity? activity = m_telemetry.StartActivity(); + VerifySessionAndSubscriptionState(true); if (monitoredItems.Count == 0) { @@ -1176,7 +1175,7 @@ public async Task> SetMonitoringModeAsync( ClientBase.ValidateDiagnosticInfos(response.DiagnosticInfos, monitoredItemIds); // update results. - var errors = new List(); + var errors = new List(); bool noErrors = UpdateMonitoringMode( monitoredItems, errors, @@ -1203,7 +1202,8 @@ public async Task> SetMonitoringModeAsync( /// public async Task ConditionRefreshAsync(CancellationToken ct = default) { - VerifySubscriptionState(true); + using Activity? activity = m_telemetry.StartActivity(); + VerifySessionAndSubscriptionState(true); var methodsToCall = new CallMethodRequestCollection { @@ -1234,7 +1234,8 @@ public async Task ConditionRefresh2Async( uint monitoredItemId, CancellationToken ct = default) { - VerifySubscriptionState(true); + using Activity? activity = m_telemetry.StartActivity(); + VerifySessionAndSubscriptionState(true); var methodsToCall = new CallMethodRequestCollection { @@ -1270,6 +1271,7 @@ public async Task TransferAsync( UInt32Collection availableSequenceNumbers, CancellationToken ct = default) { + using Activity? activity = m_telemetry.StartActivity(); if (Created) { // handle the case when the client has the subscription template and reconnects @@ -1358,10 +1360,10 @@ await session.RemoveSubscriptionsAsync(subscriptionsToRemove, ct) /// Adds the notification message to internal cache. /// public void SaveMessageInCache( - IList availableSequenceNumbers, + IList? availableSequenceNumbers, NotificationMessage message) { - PublishStateChangedEventHandler callback = null; + PublishStateChangedEventHandler? callback = null; lock (m_cache) { @@ -1402,12 +1404,12 @@ public void SaveMessageInCache( } // fill in any gaps in the queue - LinkedListNode node = m_incomingMessages.First; + LinkedListNode? node = m_incomingMessages.First; while (node != null) { entry = node.Value; - LinkedListNode next = node.Next; + LinkedListNode? next = node.Next; if (next != null && next.Value.SequenceNumber > entry.SequenceNumber + 1) { @@ -1430,7 +1432,7 @@ public void SaveMessageInCache( while (node != null) { entry = node.Value; - LinkedListNode next = node.Next; + LinkedListNode? next = node.Next; // can only pull off processed or expired or missing messages. if (!entry.Processed && @@ -1605,11 +1607,11 @@ public void RemoveItems(IEnumerable monitoredItems) /// /// Returns the monitored item identified by the client handle. /// - public MonitoredItem FindItemByClientHandle(uint clientHandle) + public MonitoredItem? FindItemByClientHandle(uint clientHandle) { lock (m_cache) { - if (m_monitoredItems.TryGetValue(clientHandle, out MonitoredItem monitoredItem)) + if (m_monitoredItems.TryGetValue(clientHandle, out MonitoredItem? monitoredItem)) { return monitoredItem; } @@ -1623,7 +1625,8 @@ public MonitoredItem FindItemByClientHandle(uint clientHandle) /// public async Task ResendDataAsync(CancellationToken ct = default) { - VerifySubscriptionState(true); + using Activity? activity = m_telemetry.StartActivity(); + VerifySessionAndSubscriptionState(true); try { await Session.CallAsync(ObjectIds.Server, MethodIds.Server_ResendData, ct, Id) @@ -1646,6 +1649,8 @@ await Session.CallAsync(ObjectIds.Server, MethodIds.Server_ResendData, ct, Id) UInt32Collection )> GetMonitoredItemsAsync(CancellationToken ct = default) { + using Activity? activity = m_telemetry.StartActivity(); + VerifySession(); var serverHandles = new UInt32Collection(); var clientHandles = new UInt32Collection(); try @@ -1679,7 +1684,9 @@ await Session.CallAsync(ObjectIds.Server, MethodIds.Server_ResendData, ct, Id) uint lifetimeInHours, CancellationToken ct = default) { + using Activity? activity = m_telemetry.StartActivity(); uint revisedLifetimeInHours = lifetimeInHours; + VerifySession(); try { @@ -1848,13 +1855,13 @@ private void StartKeepAliveTimer() } // start publishing. Fill the queue. - Session.StartPublishing(BeginPublishTimeout(), false); + Session?.StartPublishing(BeginPublishTimeout(), false); } /// /// Checks if a notification has arrived. Sends a publish if it has not. /// - private void OnKeepAlive(object state) + private void OnKeepAlive(object? state) { if (!PublishingStopped) { @@ -1870,17 +1877,14 @@ private void OnKeepAlive(object state) private void HandleOnKeepAliveStopped() { // check if a publish has arrived. - PublishStateChangedEventHandler callback = m_PublishStatusChanged; - ISession session = Session; + PublishStateChangedEventHandler? callback = m_PublishStatusChanged; + ISession? session = Session; Interlocked.Increment(ref m_publishLateCount); - bool connected = - session != null && + if (session != null && session.Connected && - !session.Reconnecting; - - if (connected) + !session.Reconnecting) { TraceState("PUBLISHING STOPPED"); @@ -2127,7 +2131,7 @@ private void DeleteSubscription() /// /// Ensures sensible values for the counts. /// - private void AdjustCounts(ref uint keepAliveCount, ref uint lifetimeCount) + private void AdjustCounts(double sessionTimeout, ref uint keepAliveCount, ref uint lifetimeCount) { const uint kDefaultKeepAlive = 10; const uint kDefaultLifeTime = 1000; @@ -2146,12 +2150,12 @@ private void AdjustCounts(ref uint keepAliveCount, ref uint lifetimeCount) // ensure the lifetime is sensible given the sampling interval. if (PublishingInterval > 0) { - if (MinLifetimeInterval > 0 && MinLifetimeInterval < Session.SessionTimeout) + if (MinLifetimeInterval > 0 && MinLifetimeInterval < sessionTimeout) { m_logger.LogWarning( "A smaller minLifetimeInterval {LifetimeInterval}ms than session timeout {SessionTimeout}ms configured for subscription {SubscriptionId}.", MinLifetimeInterval, - Session.SessionTimeout, + sessionTimeout, Id); } @@ -2172,13 +2176,13 @@ private void AdjustCounts(ref uint keepAliveCount, ref uint lifetimeCount) Id); } - if (lifetimeCount * PublishingInterval < Session.SessionTimeout) + if (lifetimeCount * PublishingInterval < sessionTimeout) { m_logger.LogWarning( "Lifetime {LifetimeCount}ms configured for subscription {SubscriptionId} is less than session timeout {SessionTimeout}ms.", lifetimeCount * PublishingInterval, Id, - Session.SessionTimeout); + sessionTimeout); } } else if (lifetimeCount == 0) @@ -2215,18 +2219,18 @@ private async Task OnMessageReceivedAsync(CancellationToken ct) { Interlocked.Increment(ref m_outstandingMessageWorkers); - ISession session = null; + ISession? session = null; uint subscriptionId = 0; - PublishStateChangedEventHandler callback = null; + PublishStateChangedEventHandler? callback = null; // list of new messages to process. - List messagesToProcess = null; + List? messagesToProcess = null; // list of keep alive messages to process. - List keepAliveToProcess = null; + List? keepAliveToProcess = null; // list of new messages to republish. - List messagesToRepublish = null; + List? messagesToRepublish = null; PublishStateChangedMask publishStateChangedMask = PublishStateChangedMask.None; @@ -2237,19 +2241,19 @@ private async Task OnMessageReceivedAsync(CancellationToken ct) return; } - for (LinkedListNode ii = m_incomingMessages.First; + for (LinkedListNode? ii = m_incomingMessages.First; ii != null; ii = ii.Next) { // update monitored items with unprocessed messages. if (ii.Value.Message != null && !ii.Value.Processed && - (!m_sequentialPublishing || ValidSequentialPublishMessage(ii.Value))) + (!State.SequentialPublishing || ValidSequentialPublishMessage(ii.Value))) { (messagesToProcess ??= []).Add(ii.Value.Message); // remove the oldest items. - while (m_messageCache.Count > m_maxMessageCount) + while (m_messageCache.Count > MaxMessageCount) { m_messageCache.RemoveFirst(); } @@ -2329,7 +2333,7 @@ private async Task OnMessageReceivedAsync(CancellationToken ct) } // process new keep alive messages. - FastKeepAliveNotificationEventHandler keepAliveCallback = FastKeepAliveCallback; + FastKeepAliveNotificationEventHandler? keepAliveCallback = FastKeepAliveCallback; if (keepAliveToProcess != null && keepAliveCallback != null) { foreach (IncomingMessage message in keepAliveToProcess) @@ -2347,9 +2351,9 @@ private async Task OnMessageReceivedAsync(CancellationToken ct) if (messagesToProcess != null) { int noNotificationsReceived; - FastDataChangeNotificationEventHandler datachangeCallback + FastDataChangeNotificationEventHandler? datachangeCallback = FastDataChangeCallback; - FastEventNotificationEventHandler eventCallback = FastEventCallback; + FastEventNotificationEventHandler? eventCallback = FastEventCallback; foreach (NotificationMessage message in messagesToProcess) { @@ -2488,23 +2492,34 @@ FastDataChangeNotificationEventHandler datachangeCallback /// Throws an exception if the subscription is not in the correct state. /// /// - private void VerifySubscriptionState(bool created) + [MemberNotNull(nameof(Session))] + private void VerifySessionAndSubscriptionState(bool verifyCreated) { - if (created && Id == 0) + if (verifyCreated && Id == 0) { throw new ServiceResultException( StatusCodes.BadInvalidState, "Subscription has not been created."); } - if (!created && Id != 0) + if (!verifyCreated && Id != 0) { throw new ServiceResultException( StatusCodes.BadInvalidState, "Subscription has already been created."); } - if (!created && Session is null) // Occurs only on Create() and CreateAsync() + VerifySession(); + } + + /// + /// Verify session is assigned. + /// + /// + [MemberNotNull(nameof(Session))] + private void VerifySession() + { + if (Session is null) // Occurs only on Create() and CreateAsync() { throw new ServiceResultException( StatusCodes.BadInvalidState, @@ -2531,7 +2546,7 @@ private bool ValidSequentialPublishMessage(IncomingMessage message) /// private static bool UpdateMonitoringMode( IList monitoredItems, - List errors, + List errors, StatusCodeCollection results, DiagnosticInfoCollection diagnosticInfos, ResponseHeader responseHeader, @@ -2542,7 +2557,7 @@ private static bool UpdateMonitoringMode( for (int ii = 0; ii < results.Count; ii++) { - ServiceResult error = null; + ServiceResult? error = null; if (StatusCode.IsBad(results[ii])) { @@ -2569,7 +2584,7 @@ private static bool UpdateMonitoringMode( List )> PrepareItemsToCreateAsync(CancellationToken ct = default) { - VerifySubscriptionState(true); + VerifySessionAndSubscriptionState(true); await ResolveItemNodeIdsAsync(ct).ConfigureAwait(false); @@ -2721,7 +2736,7 @@ private void PrepareResolveItemNodeIds( { browsePath.RelativePath = RelativePath.Parse( monitoredItem.RelativePath, - Session.TypeTree); + Session!.TypeTree); } catch (Exception e) { @@ -2758,14 +2773,15 @@ private void SaveDataChange( MonitoredItemNotification notification = notifications.MonitoredItems[ii]; // lookup monitored item, - MonitoredItem monitoredItem = null; + MonitoredItem? monitoredItem = null; lock (m_cache) { if (!m_monitoredItems.TryGetValue(notification.ClientHandle, out monitoredItem)) { m_logger.LogWarning( - "Publish response contains invalid MonitoredItem. SubscriptionId={SubscriptionId}, ClientHandle = {ClientHandle}", + "Publish response contains invalid MonitoredItem. " + + "SubscriptionId={SubscriptionId}, ClientHandle = {ClientHandle}", Id, notification.ClientHandle); continue; @@ -2798,14 +2814,15 @@ private void SaveEvents( { EventFieldList eventFields = notifications.Events[ii]; - MonitoredItem monitoredItem = null; + MonitoredItem? monitoredItem = null; lock (m_cache) { if (!m_monitoredItems.TryGetValue(eventFields.ClientHandle, out monitoredItem)) { m_logger.LogWarning( - "Publish response contains invalid MonitoredItem.SubscriptionId={SubscriptionId}, ClientHandle = {ClientHandle}", + "Publish response contains invalid MonitoredItem." + + "SubscriptionId={SubscriptionId}, ClientHandle = {ClientHandle}", Id, eventFields.ClientHandle); continue; @@ -2831,14 +2848,15 @@ private IncomingMessage FindOrCreateEntry( int tickCount, uint sequenceNumber) { - IncomingMessage entry = null; - LinkedListNode node = m_incomingMessages.Last; + IncomingMessage? entry = null; + m_incomingMessages ??= new LinkedList(); + LinkedListNode? node = m_incomingMessages.Last; Debug.Assert(m_cache.IsHeldByCurrentThread); while (node != null) { entry = node.Value; - LinkedListNode previous = node.Previous; + LinkedListNode? previous = node.Previous; if (entry.SequenceNumber == sequenceNumber) { @@ -2881,7 +2899,7 @@ private IncomingMessage FindOrCreateEntry( /// Helper to callback event handlers and to catch exceptions. /// private void PublishingStateChanged( - PublishStateChangedEventHandler callback, + PublishStateChangedEventHandler? callback, PublishStateChangedMask newState) { try @@ -2897,30 +2915,31 @@ private void PublishingStateChanged( } } - private List m_deletedItems; - private event SubscriptionStateChangedEventHandler m_StateChanged; + private List m_deletedItems = []; + private event SubscriptionStateChangedEventHandler? m_StateChanged; private SubscriptionChangeMask m_changeMask; - private Timer m_publishTimer; + private Timer? m_publishTimer; private long m_lastNotificationTime; private int m_lastNotificationTickCount; private int m_keepAliveInterval; private int m_publishLateCount; - private event PublishStateChangedEventHandler m_PublishStatusChanged; + private event PublishStateChangedEventHandler? m_PublishStatusChanged; private bool m_disposed; - private Lock m_cache = new(); - private LinkedList m_messageCache; - private IList m_availableSequenceNumbers; - private int m_maxMessageCount; - private Dictionary m_monitoredItems; - private AsyncAutoResetEvent m_messageWorkerEvent; - private CancellationTokenSource m_messageWorkerCts; - private Task m_messageWorkerTask; + private readonly Lock m_cache = new(); + private readonly LinkedList m_messageCache = new(); + private IList? m_availableSequenceNumbers; + private Dictionary m_monitoredItems = []; + private readonly AsyncAutoResetEvent m_messageWorkerEvent = new(); + private CancellationTokenSource? m_messageWorkerCts; + private Task? m_messageWorkerTask; private int m_outstandingMessageWorkers; - private bool m_sequentialPublishing; private uint m_lastSequenceNumberProcessed; private bool m_resyncLastSequenceNumberProcessed; + private LinkedList? m_incomingMessages; + private ITelemetryContext? m_telemetry; + private ILogger m_logger; /// /// A message received from the server cached until is processed or discarded. @@ -2930,15 +2949,11 @@ private class IncomingMessage public uint SequenceNumber; public DateTime Timestamp; public int TickCount; - public NotificationMessage Message; + public NotificationMessage? Message; public bool Processed; public bool Republished; public StatusCode RepublishStatus; } - - private LinkedList m_incomingMessages; - private ITelemetryContext m_telemetry; - private ILogger m_logger; } /// @@ -3123,10 +3138,6 @@ public delegate void PublishStateChangedEventHandler( /// /// A collection of subscriptions. /// - [CollectionDataContract( - Name = "ListOfSubscription", - Namespace = Namespaces.OpcUaXsd, - ItemName = "Subscription")] public class SubscriptionCollection : List, ICloneable { /// diff --git a/Libraries/Opc.Ua.Client/Subscription/SubscriptionObsolete.cs b/Libraries/Opc.Ua.Client/Subscription/SubscriptionObsolete.cs index d98c769599..0d599e275d 100644 --- a/Libraries/Opc.Ua.Client/Subscription/SubscriptionObsolete.cs +++ b/Libraries/Opc.Ua.Client/Subscription/SubscriptionObsolete.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/Subscription/SubscriptionOptions.cs b/Libraries/Opc.Ua.Client/Subscription/SubscriptionOptions.cs new file mode 100644 index 0000000000..feda6ae35c --- /dev/null +++ b/Libraries/Opc.Ua.Client/Subscription/SubscriptionOptions.cs @@ -0,0 +1,188 @@ +/* ======================================================================== + * Copyright (c) 2005-2021 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(SubscriptionOptions))] + [JsonSerializable(typeof(SubscriptionState))] + internal partial class SubscriptionOptionsContext : JsonSerializerContext; + + /// + /// Serializable options for a subscription. + /// + /// These client side options correspond to parameters used with the + /// CreateSubscription / ModifySubscription and related services in the + /// OPC UA Subscription Service Set. See OPC UA Part4 v1.05 Sections 5.13 + /// and 5.14: https://reference.opcfoundation.org/Core/Part4/v105/docs/5.13 + /// and https://reference.opcfoundation.org/Core/Part4/v105/docs/5.14. + /// + /// + [DataContract(Namespace = Namespaces.OpcUaXsd)] + public record class SubscriptionOptions + { + /// + /// A human readable display name for the subscription instance used + /// locally by the client for logging, diagnostics and UI. This value + /// is not sent to the server as part of the service parameters; + /// it is purely client side metadata. Choose a name that helps + /// operators identify the purpose of the subscription (e.g. + /// "ProcessValues", "Alarms"). + /// + [DataMember(Order = 1)] + public string DisplayName { get; init; } = "Subscription"; + + /// + /// Requested publishingInterval (ms) sent in CreateSubscription. + /// It defines the cyclic interval at which the server evaluates the + /// subscription for notifications and prepares the next Publish response. + /// The server may revise this value; the effective revised value is + /// available via the Subscription object after creation. A value <=0 + /// falls back to the server default. Selecting too small an interval + /// can increase CPU and network load. + /// + [DataMember(Order = 2)] + public int PublishingInterval { get; init; } + + /// + /// Requested keepAliveCount (number of publishing intervals) + /// determining how many consecutive publishing cycles may pass with no + /// notifications before the server sends a KeepAlive Publish response. + /// Lower values mean the client gets more frequent confirmation that the + /// subscription is alive even in quiet periods. The server may revise this. + /// + [DataMember(Order = 3)] + public uint KeepAliveCount { get; init; } + + /// + /// Requested lifetimeCount (number of publishing intervals) + /// used by the server to detect client inactivity. If the server does + /// not receive Publish requests for lifetimeCount consecutive intervals + /// the subscription may be terminated. Must be larger than keepAliveCount + /// (spec recommends at least3x). The server may revise this value. + /// + [DataMember(Order = 4)] + public uint LifetimeCount { get; init; } + + /// + /// Requested maxNotificationsPerPublish limiting the number + /// of notifications returned in a single Publish response to prevent + /// oversized messages.0 means server default (usually unlimited or some + /// internal cap). The server may revise. Tune this to balance latency versus + /// network packet size and memory consumption. Large bursts get split + /// across multiple Publish responses if necessary. + /// + [DataMember(Order = 5)] + public uint MaxNotificationsPerPublish { get; init; } + + /// + /// Requested publishingEnabled state. If false the server creates + /// the subscription but does not send notifications until it is later + /// enabled with ModifySubscription or SetPublishingMode. Useful for + /// staging monitored items. + /// + [DataMember(Order = 6)] + public bool PublishingEnabled { get; init; } + + /// + /// Requested priority hint allowing the server to schedule higher + /// priority subscriptions first when resources are constrained. OPC UA + /// servers may use this value as a relative ranking (higher = more important) + /// but are not required to strictly enforce ordering. + /// + [DataMember(Order = 7)] + public byte Priority { get; init; } + + /// + /// Which timestamps (Source / Server / Both / Neither) the client + /// requests in notifications. This maps to the TimestampsToReturn enum + /// used in the MonitoredItem and Read services. The default of Both provides + /// maximum context at the cost of a few extra bytes. + /// + [DataMember(Order = 8)] + public TimestampsToReturn TimestampsToReturn { get; init; } = TimestampsToReturn.Both; + + /// + /// Maximum number of Publish responses cached locally by the client for late + /// processing or sequence gap recovery. This is a client side buffering control + /// (not part of wire services). Larger values allow more tolerance to temporary + /// processing delays; smaller values reduce memory usage. + /// + [DataMember(Order = 9)] + public int MaxMessageCount { get; init; } = 10; + + /// + /// A client side min interval (ms) used to derive a safe lifetimeCount + /// when constructing subscription requests. Helps enforce policy such that + /// lifetimeCount * publishingInterval is not below an application defined + /// minimum lifetime.0 means disabled. + /// + [DataMember(Order = 12)] + public uint MinLifetimeInterval { get; init; } + + /// + /// When true the client disables its per-monitored-item value cache, + /// potentially improving throughput for high frequency data changes at the + /// cost of losing last-value lookups without application tracking. Use for + /// streaming scenarios where each notification is processed once then discarded. + /// + [DataMember(Order = 13)] + public bool DisableMonitoredItemCache { get; init; } + + /// + /// When true incoming Publish responses are processed strictly sequentially + /// in the order of their sequence numbers, even if multiple arrive concurrently. + /// This can simplify application logic for ordering dependent processing + /// (e.g. aggregate calculations) at the cost of reduced parallelism. + /// + [DataMember(Order = 14)] + public bool SequentialPublishing { get; init; } + + /// + /// When true the client will automatically issue Republish requests + /// after a TransferSubscription or other recovery scenario to obtain any + /// available lost sequence numbers and minimize data loss. This automates + /// the gap recovery behavior defined for the Republish service. + /// + [DataMember(Name = "RepublishAfterTransfer", Order = 15)] + public bool RepublishAfterTransfer { get; init; } + + /// + /// The transferable subscription identifier (server assigned) used to + /// reattach a subscription to a new session via TransferSubscriptions. + /// 0 indicates no server transfer id known or the subscription is not + /// currently transferable. Persisting this value allows restoring behavior + /// after reconnect. + /// + [DataMember(Name = "TransferId", Order = 16)] + public uint TransferId { get; init; } + } +} diff --git a/Libraries/Opc.Ua.Client/Subscription/SubscriptionState.cs b/Libraries/Opc.Ua.Client/Subscription/SubscriptionState.cs new file mode 100644 index 0000000000..876b98ce39 --- /dev/null +++ b/Libraries/Opc.Ua.Client/Subscription/SubscriptionState.cs @@ -0,0 +1,134 @@ +/* ======================================================================== + * Copyright (c) 2005-2021 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.Linq; +using System.Runtime.Serialization; + +namespace Opc.Ua.Client +{ + /// + /// State object that is used for snapshotting the subscription state + /// + [DataContract(Namespace = Namespaces.OpcUaXsd)] + public record class SubscriptionState : SubscriptionOptions + { + /// + /// Create subscription state + /// + public SubscriptionState() + { + } + + /// + /// Create subscription state with current options + /// + /// + public SubscriptionState(SubscriptionOptions options) + : base(options) + { + } + + /// + /// Allows the list of monitored items to be saved/restored + /// when the object is serialized. + /// + [DataMember(Order = 11)] + public required MonitoredItemStateCollection MonitoredItems { get; init; } + + /// + /// The current publishing interval. + /// + [DataMember(Order = 20)] + public double CurrentPublishingInterval { get; init; } + + /// + /// The current keep alive count. + /// + [DataMember(Order = 21)] + public uint CurrentKeepAliveCount { get; init; } + + /// + /// The current lifetime count. + /// + [DataMember(Order = 22)] + public uint CurrentLifetimeCount { get; init; } + + /// + /// When the state was created. + /// + [DataMember(Order = 23)] + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + } + + /// + /// A collection of subscription states. + /// + [CollectionDataContract( + Name = "ListOfSubscription", + Namespace = Namespaces.OpcUaXsd, + ItemName = "Subscription")] + public class SubscriptionStateCollection : List, ICloneable + { + /// + /// Initializes an empty collection. + /// + public SubscriptionStateCollection() + { + } + + /// + /// Initializes the collection from another collection. + /// + /// The existing collection to use as + /// the basis of creating this collection + public SubscriptionStateCollection(IEnumerable collection) + : base(collection) + { + } + + /// + /// Initializes the collection with the specified capacity. + /// + /// The max. capacity of the collection + public SubscriptionStateCollection(int capacity) + : base(capacity) + { + } + + /// + public virtual object Clone() + { + var clone = new SubscriptionStateCollection(); + clone.AddRange(this.Select(item => item with { })); + return clone; + } + } +} diff --git a/Libraries/Opc.Ua.Client/Utils/AsyncAutoResetEvent.cs b/Libraries/Opc.Ua.Client/Utils/AsyncAutoResetEvent.cs index 38366f2df6..c89ae5e10f 100644 --- a/Libraries/Opc.Ua.Client/Utils/AsyncAutoResetEvent.cs +++ b/Libraries/Opc.Ua.Client/Utils/AsyncAutoResetEvent.cs @@ -112,11 +112,11 @@ private bool TryCancel(Task task, CancellationToken cancellationToken) { lock (m_mutex) { - for (LinkedListNode> i = m_queue.First; + for (LinkedListNode>? i = m_queue.First; i != null; i = i.Next) { - TaskCompletionSource cur = i.Value; + TaskCompletionSource cur = i.Value; if (cur.Task == task) { cur.TrySetCanceled(cancellationToken); @@ -128,13 +128,13 @@ private bool TryCancel(Task task, CancellationToken cancellationToken) } } - private Task Enqueue(CancellationToken token) + private Task Enqueue(CancellationToken token) { if (token.IsCancellationRequested) { - return Task.FromCanceled(token); + return Task.FromCanceled(token); } - var tcs = new TaskCompletionSource( + var tcs = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously); m_queue.AddLast(tcs); if (!token.CanBeCanceled) @@ -154,12 +154,15 @@ private Task Enqueue(CancellationToken token) private void Dequeue() { - TaskCompletionSource head = m_queue.First.Value; - m_queue.RemoveFirst(); - head.TrySetResult(null); + TaskCompletionSource? head = m_queue.First?.Value; + if (head != null) + { + m_queue.RemoveFirst(); + head.TrySetResult(null); + } } - private readonly LinkedList> m_queue = new(); + private readonly LinkedList> m_queue = new(); private readonly Lock m_mutex = new(); private bool m_set; } diff --git a/Libraries/Opc.Ua.Client/Utils/AsyncManualResetEvent.cs b/Libraries/Opc.Ua.Client/Utils/AsyncManualResetEvent.cs index 29dceddfe0..c005644f2d 100644 --- a/Libraries/Opc.Ua.Client/Utils/AsyncManualResetEvent.cs +++ b/Libraries/Opc.Ua.Client/Utils/AsyncManualResetEvent.cs @@ -48,7 +48,7 @@ public sealed class AsyncManualResetEvent /// initially set or unset. public AsyncManualResetEvent(bool set) { - m_tcs = new TaskCompletionSource( + m_tcs = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously); if (set) { @@ -132,13 +132,13 @@ public void Reset() { if (m_tcs.Task.IsCompleted) { - m_tcs = new TaskCompletionSource( + m_tcs = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously); } } } private readonly Lock m_lock = new(); - private TaskCompletionSource m_tcs; + private TaskCompletionSource m_tcs; } } diff --git a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs index 3247d42dd5..7a0fee2d6f 100644 --- a/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs +++ b/Libraries/Opc.Ua.Configuration/ApplicationInstance.cs @@ -332,7 +332,7 @@ await DeleteApplicationInstanceCertificateAsync(ApplicationConfiguration, id, ct /// /// if set to true no dialogs will be displayed. /// The lifetime in months. - /// Cancelation token to cancel operation with + /// Cancellation token to cancel operation with /// public async ValueTask CheckApplicationInstanceCertificatesAsync( bool silent, @@ -456,8 +456,8 @@ await id.LoadPrivateKeyExAsync(passwordProvider, configuration.ApplicationUri, m { throw ServiceResultException.Create( StatusCodes.BadConfigurationError, - "Cannot access certificate private key. Subject={0}", - certificate.Subject); + "Cannot access private key for certificate with thumbprint={0}", + certificate.Thumbprint); } // check for missing thumbprint. @@ -882,7 +882,7 @@ private async Task CheckDomainsInCertificateAsync( /// The configuration. /// The certificate identifier. /// The lifetime in months. - /// Cancelation token to cancel operation with + /// Cancellation token to cancel operation with /// The new certificate /// private async Task CreateApplicationInstanceCertificateAsync( @@ -997,7 +997,7 @@ await configuration /// /// The configuration instance that stores the configurable information for a UA application. /// The certificate identifier. - /// Cancelation token to cancel operation with + /// Cancellation token to cancel operation with private async Task DeleteApplicationInstanceCertificateAsync( ApplicationConfiguration configuration, CertificateIdentifier id, diff --git a/Libraries/Opc.Ua.Client/Session/ISessionInstantiator.cs b/Libraries/Opc.Ua.Configuration/ISnapshotRestore.cs similarity index 57% rename from Libraries/Opc.Ua.Client/Session/ISessionInstantiator.cs rename to Libraries/Opc.Ua.Configuration/ISnapshotRestore.cs index ec4c614239..676997c01f 100644 --- a/Libraries/Opc.Ua.Client/Session/ISessionInstantiator.cs +++ b/Libraries/Opc.Ua.Configuration/ISnapshotRestore.cs @@ -1,5 +1,5 @@ /* ======================================================================== - * Copyright (c) 2005-2023 The OPC Foundation, Inc. All rights reserved. + * Copyright (c) 2005-2021 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * @@ -27,39 +27,24 @@ * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ -using System.Security.Cryptography.X509Certificates; - -namespace Opc.Ua.Client +namespace Opc.Ua.Configuration { /// - /// Object that creates an instance of a Session object. - /// It can be used to subclass enhanced Session - /// classes which survive reconnect handling etc. + /// Snapshot and restore interface /// - public interface ISessionInstantiator + /// + public interface ISnapshotRestore { /// - /// Telemetry configuration to use when creating sessions. - /// - ITelemetryContext Telemetry { get; } - - /// - /// Constructs a new instance of the class. + /// Restore /// - Session Create( - ISessionChannel channel, - ApplicationConfiguration configuration, - ConfiguredEndpoint endpoint); + /// + void Restore(T state); /// - /// Constructs a new instance of the class. + /// Get state to serialize /// - Session Create( - ITransportChannel channel, - ApplicationConfiguration configuration, - ConfiguredEndpoint endpoint, - X509Certificate2 clientCertificate, - EndpointDescriptionCollection availableEndpoints = null, - StringCollection discoveryProfileUris = null); + /// + T Snapshot(); } } diff --git a/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs b/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs index 3793c5e820..5b275b7265 100644 --- a/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs +++ b/Stack/Opc.Ua.Core/Security/Certificates/CertificateIdentifier.cs @@ -1094,8 +1094,7 @@ public async Task AddAsync( { throw ServiceResultException.Create( StatusCodes.BadEntryExists, - "A certificate with the specified thumbprint already exists. Subject={0}, Thumbprint={1}", - certificate.SubjectName, + "A certificate with the specified thumbprint {0} already exists.", certificate.Thumbprint); } } diff --git a/Stack/Opc.Ua.Core/Stack/Client/ClientBase.cs b/Stack/Opc.Ua.Core/Stack/Client/ClientBase.cs index 3fe3bd0d15..c723c9a6e8 100644 --- a/Stack/Opc.Ua.Core/Stack/Client/ClientBase.cs +++ b/Stack/Opc.Ua.Core/Stack/Client/ClientBase.cs @@ -550,7 +550,7 @@ public static ServiceResult ValidateDataValue( if (expectedType != null && !expectedType.IsInstanceOfType(value.Value)) { return ServiceResult.Create( - StatusCodes.BadUnexpectedError, + StatusCodes.BadTypeMismatch, "The server returned data value of type {0} when a value of type {1} was expected.", value.Value != null ? value.Value.GetType().Name : "(null)", expectedType.Name); diff --git a/Stack/Opc.Ua.Core/Stack/Diagnostics/DefaultTelemetry.cs b/Stack/Opc.Ua.Core/Stack/Diagnostics/DefaultTelemetry.cs index e539490575..73d4c02778 100644 --- a/Stack/Opc.Ua.Core/Stack/Diagnostics/DefaultTelemetry.cs +++ b/Stack/Opc.Ua.Core/Stack/Diagnostics/DefaultTelemetry.cs @@ -61,6 +61,10 @@ private DefaultTelemetry(Action configure) { LoggerFactory = Microsoft.Extensions.Logging.LoggerFactory .Create(configure); + + // Set the default Id format to W3C + Activity.DefaultIdFormat = ActivityIdFormat.W3C; + Activity.ForceDefaultIdFormat = true; } /// diff --git a/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs b/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs index 699be520a8..74484888df 100644 --- a/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Https/HttpsTransportChannel.cs @@ -202,6 +202,7 @@ public async ValueTask SendRequestAsync( } IServiceMessageContext context = m_quotas?.MessageContext ?? throw BadNotConnected(); HttpClient client = m_client ?? throw BadNotConnected(); + using Activity? activity = m_telemetry.StartActivity(); try { var content = new ByteArrayContent( @@ -301,6 +302,7 @@ protected virtual void Dispose(bool disposing) /// /// The server url. /// The settings for the transport channel. + /// private void SaveSettings(Uri url, TransportChannelSettings settings) { if (m_disposed) diff --git a/Stack/Opc.Ua.Core/Stack/Nodes/IAsyncNodeTable.cs b/Stack/Opc.Ua.Core/Stack/Nodes/IAsyncNodeTable.cs index c0e8ac1757..64698ec54a 100644 --- a/Stack/Opc.Ua.Core/Stack/Nodes/IAsyncNodeTable.cs +++ b/Stack/Opc.Ua.Core/Stack/Nodes/IAsyncNodeTable.cs @@ -10,6 +10,8 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ +#nullable enable + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -54,7 +56,7 @@ ValueTask ExistsAsync( /// The node identifier. /// Token to use to cancel the operation. /// Returns null if the node does not exist. - ValueTask FindAsync( + ValueTask FindAsync( ExpandedNodeId nodeId, CancellationToken ct = default); @@ -71,7 +73,7 @@ ValueTask FindAsync( /// /// Returns null if the source does not exist or if there is no matching target. /// - ValueTask FindAsync( + ValueTask FindAsync( ExpandedNodeId sourceId, NodeId referenceTypeId, bool isInverse, @@ -142,7 +144,7 @@ public bool Exists(ExpandedNodeId nodeId) } /// - public INode Find(ExpandedNodeId nodeId) + public INode? Find(ExpandedNodeId nodeId) { return m_table.FindAsync(nodeId) .AsTask() @@ -151,7 +153,7 @@ public INode Find(ExpandedNodeId nodeId) } /// - public INode Find( + public INode? Find( ExpandedNodeId sourceId, NodeId referenceTypeId, bool isInverse, diff --git a/Stack/Opc.Ua.Core/Stack/Nodes/IAsyncTypeTable.cs b/Stack/Opc.Ua.Core/Stack/Nodes/IAsyncTypeTable.cs index 74a210b66d..5c22fd4851 100644 --- a/Stack/Opc.Ua.Core/Stack/Nodes/IAsyncTypeTable.cs +++ b/Stack/Opc.Ua.Core/Stack/Nodes/IAsyncTypeTable.cs @@ -10,6 +10,8 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ +#nullable enable + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -116,7 +118,7 @@ ValueTask IsTypeOfAsync( /// Cancellation token to cancel operation with /// A name qualified with a namespace for the reference /// . - ValueTask FindReferenceTypeNameAsync( + ValueTask FindReferenceTypeNameAsync( NodeId referenceTypeId, CancellationToken ct = default); @@ -248,7 +250,7 @@ public bool IsKnown(NodeId typeId) } /// - public NodeId FindSuperType(ExpandedNodeId typeId) + public NodeId? FindSuperType(ExpandedNodeId typeId) { return m_table.FindSuperTypeAsync(typeId) .AsTask() @@ -257,7 +259,7 @@ public NodeId FindSuperType(ExpandedNodeId typeId) } /// - public NodeId FindSuperType(NodeId typeId) + public NodeId? FindSuperType(NodeId typeId) { return m_table.FindSuperTypeAsync(typeId) .AsTask() @@ -311,7 +313,7 @@ public bool IsTypeOf(NodeId subTypeId, NodeId superTypeId) } /// - public QualifiedName FindReferenceTypeName(NodeId referenceTypeId) + public QualifiedName? FindReferenceTypeName(NodeId referenceTypeId) { return m_table.FindReferenceTypeNameAsync(referenceTypeId) .AsTask() diff --git a/Stack/Opc.Ua.Core/Stack/State/NodeState.cs b/Stack/Opc.Ua.Core/Stack/State/NodeState.cs index fdf6a04cb0..49cff0b82c 100644 --- a/Stack/Opc.Ua.Core/Stack/State/NodeState.cs +++ b/Stack/Opc.Ua.Core/Stack/State/NodeState.cs @@ -4289,7 +4289,7 @@ protected virtual void RemoveExplicitlyDefinedChild(BaseInstanceState child) { // no explicitly defined children on base type. } - + /// /// Removes a child from the node. /// diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs index 0a0f414d16..e6d359bc5a 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryClientChannel.cs @@ -15,6 +15,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Security.Cryptography.X509Certificates; @@ -54,7 +55,8 @@ public UaSCUaBinaryClientChannel( endpoint != null ? endpoint.SecurityPolicyUri : SecurityPolicies.None, telemetry) { - m_logger = telemetry.CreateLogger(); + m_telemetry = telemetry; + m_logger = m_telemetry.CreateLogger(); if (endpoint == null) { @@ -125,6 +127,7 @@ protected override void Dispose(bool disposing) /// public async ValueTask ConnectAsync(Uri url, int timeout, CancellationToken ct) { + using Activity? activity = m_telemetry.StartActivity(); if (url == null) { throw new ArgumentNullException(nameof(url)); @@ -211,6 +214,7 @@ public async ValueTask ConnectAsync(Uri url, int timeout, CancellationToken ct) /// public async Task CloseAsync(int timeout, CancellationToken ct = default) { + using Activity? activity = m_telemetry.StartActivity(); WriteOperation? operation = InternalClose(timeout); // wait for the close to succeed. @@ -255,6 +259,7 @@ public async ValueTask SendRequestAsync( throw new ArgumentException("Timeout must be greater than zero.", nameof(timeout)); } + using Activity? activity = m_telemetry.StartActivity(); WriteOperation? operation = null; lock (DataLock) { @@ -1683,6 +1688,7 @@ private bool ProcessResponseMessage(uint messageType, ArraySegment message private List? m_queuedOperations; private readonly Random m_random; private readonly ILogger m_logger; + private readonly ITelemetryContext m_telemetry; private static readonly string s_implementationString = "UA.NETStandard ClientChannel {0} " + Utils.GetAssemblyBuildNumber(); diff --git a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs index 05558bbcca..23bb95f924 100644 --- a/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs +++ b/Stack/Opc.Ua.Core/Stack/Tcp/UaSCBinaryTransportChannel.cs @@ -289,6 +289,7 @@ private async ValueTask SendRequestInternalAsync( /// of failure /// /// + /// private async ValueTask OpenInternalAsync( Uri endpointUrl, ITransportWaitingConnection? connection, diff --git a/Stack/Opc.Ua.Core/Stack/Types/ResultSet.cs b/Stack/Opc.Ua.Core/Stack/Types/ResultSet.cs new file mode 100644 index 0000000000..3081cbf92b --- /dev/null +++ b/Stack/Opc.Ua.Core/Stack/Types/ResultSet.cs @@ -0,0 +1,95 @@ +/* Copyright (c) 1996-2022 The OPC Foundation. All rights reserved. + The source code in this file is covered under a dual-license scenario: + - RCL: for OPC Foundation Corporate Members in good-standing + - GPL V2: everybody else + RCL license terms accompanied with this source code. See http://opcfoundation.org/License/RCL/1.00/ + GNU General Public License as published by the Free Software Foundation; + version 2 of the License are accompanied with this source code. See http://opcfoundation.org/License/GPLv2 + This source code is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#nullable enable + +using System.Collections.Generic; +using System.Linq; + +namespace Opc.Ua +{ + /// + /// Result set + /// + /// + /// + /// + public readonly record struct ResultSet( + IReadOnlyList Results, + IReadOnlyList Errors) + { + /// + /// Empty result set + /// + public static ResultSet Empty { get; } = new([], []); + } + + /// + /// Result set helpers + /// + public static class ResultSet + { + /// + /// Create result set + /// + /// + /// + /// + public static ResultSet From(IReadOnlyList results) + { + return results.Count == 0 ? + ResultSet.Empty : + new(results, [.. Enumerable.Repeat(ServiceResult.Good, results.Count)]); + } + + /// + /// Create result set + /// + /// + /// + /// + public static ResultSet From(IEnumerable results) + { + return From(results.ToArray()); + } + + /// + /// Create result set + /// + /// + /// + /// + /// + public static ResultSet From( + IReadOnlyList results, + IReadOnlyList errors) + { + return results.Count == 0 ? + ResultSet.Empty : + new(results, errors); + } + + /// + /// Create result set + /// + /// + /// + /// + /// + public static ResultSet From( + IEnumerable results, + IEnumerable errors) + { + return new([.. results], [.. errors]); + } + } +} diff --git a/Stack/Opc.Ua.Core/Types/BuiltIn/ExtensionObject.cs b/Stack/Opc.Ua.Core/Types/BuiltIn/ExtensionObject.cs index f213708b27..025c545add 100644 --- a/Stack/Opc.Ua.Core/Types/BuiltIn/ExtensionObject.cs +++ b/Stack/Opc.Ua.Core/Types/BuiltIn/ExtensionObject.cs @@ -17,6 +17,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System.Text; using System.Xml; using Newtonsoft.Json.Linq; +using System.Diagnostics.CodeAnalysis; namespace Opc.Ua { @@ -583,7 +584,7 @@ public virtual object Clone() /// true if the specified is null /// of the embedded object is null; otherwise, false. /// - public static bool IsNull(ExtensionObject extension) + public static bool IsNull([NotNullWhen(false)] ExtensionObject extension) { return extension == null || extension.m_body == null; } diff --git a/Stack/Opc.Ua.Core/Types/BuiltIn/NodeId.cs b/Stack/Opc.Ua.Core/Types/BuiltIn/NodeId.cs index c506721978..a8f6960562 100644 --- a/Stack/Opc.Ua.Core/Types/BuiltIn/NodeId.cs +++ b/Stack/Opc.Ua.Core/Types/BuiltIn/NodeId.cs @@ -20,6 +20,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System.Globalization; using System.Runtime.Serialization; using System.Text; +using System.Diagnostics.CodeAnalysis; namespace Opc.Ua { @@ -769,7 +770,7 @@ public static implicit operator NodeId(string text) /// Returns a true/false value to indicate if the specified NodeId is null. /// /// The NodeId to validate - public static bool IsNull(NodeId nodeId) + public static bool IsNull([NotNullWhen(false)] NodeId nodeId) { if (nodeId == null) { @@ -786,7 +787,7 @@ public static bool IsNull(NodeId nodeId) /// Returns a true/false to indicate if the specified is null. /// /// The ExpandedNodeId to validate - public static bool IsNull(ExpandedNodeId nodeId) + public static bool IsNull([NotNullWhen(false)]ExpandedNodeId nodeId) { if (nodeId == null) { diff --git a/Stack/Opc.Ua.Core/Types/BuiltIn/QualifiedName.cs b/Stack/Opc.Ua.Core/Types/BuiltIn/QualifiedName.cs index ff917f1e3a..1ffa9c91b8 100644 --- a/Stack/Opc.Ua.Core/Types/BuiltIn/QualifiedName.cs +++ b/Stack/Opc.Ua.Core/Types/BuiltIn/QualifiedName.cs @@ -15,6 +15,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System.Reflection; using System.Runtime.Serialization; using System.Text; +using System.Diagnostics.CodeAnalysis; namespace Opc.Ua { @@ -548,7 +549,7 @@ public string Format(IServiceMessageContext context, bool useNamespaceUri = fals /// Returns true if the value is null. /// /// The qualified name to check - public static bool IsNull(QualifiedName value) + public static bool IsNull([NotNullWhen(false)] QualifiedName value) { if (value != null) { diff --git a/Stack/Opc.Ua.Core/Types/Encoders/ISnapshotRestore.cs b/Stack/Opc.Ua.Core/Types/Encoders/ISnapshotRestore.cs new file mode 100644 index 0000000000..56420c62eb --- /dev/null +++ b/Stack/Opc.Ua.Core/Types/Encoders/ISnapshotRestore.cs @@ -0,0 +1,35 @@ +/* Copyright (c) 1996-2024 The OPC Foundation. All rights reserved. + The source code in this file is covered under a dual-license scenario: + - RCL: for OPC Foundation Corporate Members in good-standing + - GPL V2: everybody else + RCL license terms accompanied with this source code. See http://opcfoundation.org/License/RCL/1.00/ + GNU General Public License as published by the Free Software Foundation; + version 2 of the License are accompanied with this source code. See http://opcfoundation.org/License/GPLv2 + This source code is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +*/ + +#nullable enable + +namespace Opc.Ua +{ + /// + /// Snapshot and restore interface + /// + /// + public interface ISnapshotRestore + { + /// + /// Restore + /// + /// + void Restore(T state); + + /// + /// Get state to serialize + /// + /// + void Snapshot(out T state); + } +} diff --git a/Stack/Opc.Ua.Core/Types/Schemas/BinarySchemaValidator.cs b/Stack/Opc.Ua.Core/Types/Schemas/BinarySchemaValidator.cs index 0874edf05c..f7819c8c7d 100644 --- a/Stack/Opc.Ua.Core/Types/Schemas/BinarySchemaValidator.cs +++ b/Stack/Opc.Ua.Core/Types/Schemas/BinarySchemaValidator.cs @@ -18,6 +18,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System.Text; using System.Xml; using System.Xml.Serialization; +using System.Diagnostics.CodeAnalysis; namespace Opc.Ua.Schema.Binary { @@ -234,7 +235,7 @@ private void Import(ImportDirective directive) /// /// Returns true if the documentation element is empty. /// - private static bool IsNull(Documentation documentation) + private static bool IsNull([NotNullWhen(false)] Documentation documentation) { if (documentation == null) { diff --git a/Stack/Opc.Ua.Core/Types/Schemas/SchemaValidator.cs b/Stack/Opc.Ua.Core/Types/Schemas/SchemaValidator.cs index 27a624baa4..ac1966e92b 100644 --- a/Stack/Opc.Ua.Core/Types/Schemas/SchemaValidator.cs +++ b/Stack/Opc.Ua.Core/Types/Schemas/SchemaValidator.cs @@ -16,6 +16,7 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System.Reflection; using System.Xml; using System.Xml.Serialization; +using System.Diagnostics.CodeAnalysis; namespace Opc.Ua.Schema { @@ -77,7 +78,7 @@ public SchemaValidator(IDictionary importFiles) /// /// Returns true if the QName is null. /// - protected static bool IsNull(XmlQualifiedName name) + protected static bool IsNull([NotNullWhen(false)] XmlQualifiedName name) { return name == null || string.IsNullOrEmpty(name.Name); } diff --git a/Stack/Opc.Ua.Core/Types/Utils/ServiceResultException.cs b/Stack/Opc.Ua.Core/Types/Utils/ServiceResultException.cs index 3bb4213707..3a69026749 100644 --- a/Stack/Opc.Ua.Core/Types/Utils/ServiceResultException.cs +++ b/Stack/Opc.Ua.Core/Types/Utils/ServiceResultException.cs @@ -12,7 +12,6 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. using System; using System.Collections.Generic; -using System.Runtime.Serialization; using System.Text; namespace Opc.Ua @@ -20,7 +19,6 @@ namespace Opc.Ua /// /// An exception thrown when a UA defined error occurs. /// - [DataContract] [Serializable] public class ServiceResultException : Exception { diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/DataDictionaryTests.cs b/Tests/Opc.Ua.Client.ComplexTypes.Tests/DataDictionaryTests.cs index 8728567d8a..55b75cbb9e 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/DataDictionaryTests.cs +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/DataDictionaryTests.cs @@ -158,7 +158,7 @@ public async Task ReadDictionaryByteStringAsync() VariableIds.OpcUa_BinarySchema, await GetTestDataDictionaryNodeIdAsync().ConfigureAwait(false) }; - var theSession = (Session)((TraceableSession)Session).Session; + ISession theSession = Session; foreach (NodeId dataDictionaryId in dictionaryIds) { diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/NodeCacheResolverTests.cs b/Tests/Opc.Ua.Client.ComplexTypes.Tests/NodeCacheResolverTests.cs index fcc53aebd2..5fe0b72c7b 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/NodeCacheResolverTests.cs +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/NodeCacheResolverTests.cs @@ -136,7 +136,7 @@ System.Collections.Generic.IReadOnlyDictionary typeSyste public async Task LoadAllServerDataTypeSystemsAsync(NodeId dataTypeSystem) { // find the dictionary for the description. - var browser = new Browser(Session, Telemetry) + var browser = new Browser(Session) { BrowseDirection = BrowseDirection.Forward, ReferenceTypeId = ReferenceTypeIds.HasComponent, diff --git a/Tests/Opc.Ua.Client.ComplexTypes.Tests/TypeSystemClientTest.cs b/Tests/Opc.Ua.Client.ComplexTypes.Tests/TypeSystemClientTest.cs index 89372de121..4f06de1ea2 100644 --- a/Tests/Opc.Ua.Client.ComplexTypes.Tests/TypeSystemClientTest.cs +++ b/Tests/Opc.Ua.Client.ComplexTypes.Tests/TypeSystemClientTest.cs @@ -223,9 +223,8 @@ public async Task BrowseComplexTypesServerAsync() TestContext.Out.WriteLine("VariableIds: {0}", variableIds.Count); - (DataValueCollection values, IList serviceResults) = await samples - .ReadAllValuesAsync(this, variableIds) - .ConfigureAwait(false); + (IReadOnlyList values, IReadOnlyList serviceResults) = + await samples.ReadAllValuesAsync(this, variableIds).ConfigureAwait(false); int ii = 0; foreach (ServiceResult serviceResult in serviceResults) @@ -263,9 +262,8 @@ r is VariableNode v && TestContext.Out.WriteLine("VariableIds: {0}", variableIds.Count); - (DataValueCollection values, IList serviceResults) = await samples - .ReadAllValuesAsync(this, variableIds) - .ConfigureAwait(false); + (IReadOnlyList values, IReadOnlyList serviceResults) = + await samples.ReadAllValuesAsync(this, variableIds).ConfigureAwait(false); foreach (ServiceResult serviceResult in serviceResults) { diff --git a/Tests/Opc.Ua.Client.Tests/ClientFixture.cs b/Tests/Opc.Ua.Client.Tests/ClientFixture.cs index 33df9741ab..0155a78687 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientFixture.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientFixture.cs @@ -75,7 +75,7 @@ public ClientFixture(bool useTracing, bool disableActivityLogging, ITelemetryCon } else { - SessionFactory = new TraceableSessionFactory(telemetry) + SessionFactory = new DefaultSessionFactory(telemetry) { ReturnDiagnostics = DiagnosticsMasks.SymbolicIdAndText }; @@ -360,7 +360,7 @@ public Task CreateChannelAsync( /// The configured endpoint public ISession CreateSession(ITransportChannel channel, ConfiguredEndpoint endpoint) { - return SessionFactory.Create(Config, channel, endpoint, null); + return SessionFactory.Create(channel, Config, endpoint, null); } /// diff --git a/Tests/Opc.Ua.Client.Tests/ClientTest.cs b/Tests/Opc.Ua.Client.Tests/ClientTest.cs index 48158efa27..2d8dc42139 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTest.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTest.cs @@ -808,7 +808,7 @@ await session1.ReconnectAsync(channel2, CancellationToken.None) NUnit.Framework.Assert.ThrowsAsync( () => channel1.CloseAsync(default).AsTask()); // Calling dispose twice will not throw. - NUnit.Framework.Assert.DoesNotThrow(() => channel1.Dispose()); + NUnit.Framework.Assert.DoesNotThrow(channel1.Dispose); } // test by reading a value @@ -1014,8 +1014,8 @@ public async Task RecreateSessionWithRenewUserIdentityAsync() // hook callback to renew the user identity session1.RenewUserIdentity += (_, _) => userIdentityPW; - Session session2 = await Client - .Session.RecreateAsync((Session)((TraceableSession)session1).Session) + ISession session2 = await session1.SessionFactory + .RecreateAsync(session1) .ConfigureAwait(false); // create new channel @@ -1024,8 +1024,8 @@ public async Task RecreateSessionWithRenewUserIdentityAsync() .ConfigureAwait(false); Assert.NotNull(channel2); - Session session3 = await Client - .Session.RecreateAsync((Session)((TraceableSession)session1).Session, channel2) + ISession session3 = await session1.SessionFactory + .RecreateAsync(session1, channel2) .ConfigureAwait(false); // validate new Session Ids are used and also UserName PW identity token is @@ -1668,11 +1668,10 @@ .. CommonTestWorkers.NodeIdTestSetStatic public class TestableTraceableRequestHeaderClientSession : TraceableRequestHeaderClientSession { public TestableTraceableRequestHeaderClientSession( - ISessionChannel channel, + ITransportChannel channel, ApplicationConfiguration configuration, - ConfiguredEndpoint endpoint, - ITelemetryContext telemetry) - : base(channel, configuration, endpoint) + ConfiguredEndpoint endpoint) + : base(channel, configuration, endpoint, null) { } @@ -1758,14 +1757,14 @@ public async Task ClientTestRequestHeaderUpdateAsync() // Mock the channel and session var channelMock = new Mock(); - Mock sessionChannelMock = channelMock.As(); + var messageContext = new ServiceMessageContext(telemetry); + channelMock.Setup(mock => mock.MessageContext).Returns(messageContext); var testableTraceableRequestHeaderClientSession = new TestableTraceableRequestHeaderClientSession( - sessionChannelMock.Object, - ClientFixture.Config, - endpoint, - telemetry); + channelMock.Object, + ClientFixture.Config, + endpoint); var request = new CreateSessionRequest { RequestHeader = new RequestHeader() }; // Mock call TestableUpdateRequestHeader() to simulate the header update @@ -2049,12 +2048,10 @@ public async Task SetSubscriptionDurableSuccessAsync() sessionMock .Setup(mock => mock.CallAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(outputParameters); + It.IsAny(), + It.Is(c => c.HasArgsOfType(typeof(uint), typeof(uint))), + It.IsAny())) + .ReturnsAsync(outputParameters.ToResponse()); var subscription = new Subscription(telemetry) { Session = sessionMock.Object }; @@ -2082,12 +2079,10 @@ public async Task SetSubscriptionDurableExceptionAsync() var sessionMock = new Mock(); sessionMock - .Setup(mock => mock.CallAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) + .Setup(mock => mock.CallAsync( + It.IsAny(), + It.Is(c => c.HasArgsOfType(typeof(uint), typeof(uint))), + It.IsAny())) .ThrowsAsync(new ServiceResultException(StatusCodes.BadSubscriptionIdInvalid)); var subscription = new Subscription(telemetry) { Session = sessionMock.Object }; @@ -2112,13 +2107,11 @@ public async Task SetSubscriptionDurableNoOutputParametersAsync() var sessionMock = new Mock(); sessionMock - .Setup(mock => mock.CallAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(outputParameters); + .Setup(mock => mock.CallAsync( + It.IsAny(), + It.Is(c => c.HasArgsOfType(typeof(uint), typeof(uint))), + It.IsAny())) + .ReturnsAsync(outputParameters.ToResponse()); var subscription = new Subscription(telemetry) { Session = sessionMock.Object }; @@ -2143,12 +2136,10 @@ public async Task SetSubscriptionDurableNullOutputParametersAsync() sessionMock .Setup(mock => mock.CallAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(outputParameters); + It.IsAny(), + It.Is(c => c.HasArgsOfType(typeof(uint), typeof(uint))), + It.IsAny())) + .ReturnsAsync(outputParameters.ToResponse()); var subscription = new Subscription(telemetry) { Session = sessionMock.Object }; @@ -2175,12 +2166,10 @@ public async Task SetSubscriptionDurableTooManyOutputParametersAsync() sessionMock .Setup(mock => mock.CallAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(outputParameters); + It.IsAny(), + It.Is(c => c.HasArgsOfType(typeof(uint), typeof(uint))), + It.IsAny())) + .ReturnsAsync(outputParameters.ToResponse()); var subscription = new Subscription(telemetry) { Session = sessionMock.Object }; @@ -2198,19 +2187,20 @@ public async Task GetMonitoredItemsSuccessAsync() { ITelemetryContext telemetry = NUnitTelemetryContext.Create(); - var outputParameters = new List { + var outputParameters = new List + { new uint[] { 1, 2, 3, 4, 5 }, - new uint[] { 6, 7, 8, 9, 10 } }; + new uint[] { 6, 7, 8, 9, 10 } + }; var sessionMock = new Mock(); sessionMock .Setup(mock => mock.CallAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(outputParameters); + It.IsAny(), + It.Is(c => c.HasArgsOfType(typeof(uint))), + It.IsAny())) + .ReturnsAsync(outputParameters.ToResponse()); var subscription = new Subscription(telemetry) { Session = sessionMock.Object }; @@ -2236,10 +2226,9 @@ public async Task GetMonitoredItemsExceptionAsync() sessionMock .Setup(mock => mock.CallAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) + It.IsAny(), + It.Is(c => c.HasArgsOfType(typeof(uint))), + It.IsAny())) .ThrowsAsync(new ServiceResultException(StatusCodes.BadSubscriptionIdInvalid)); var subscription = new Subscription(telemetry) { Session = sessionMock.Object }; @@ -2267,11 +2256,10 @@ public async Task GetMonitoredItemsNoOutputParametersAsync() sessionMock .Setup(mock => mock.CallAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(outputParameters); + It.IsAny(), + It.Is(c => c.HasArgsOfType(typeof(uint))), + It.IsAny())) + .ReturnsAsync(outputParameters.ToResponse()); var subscription = new Subscription(telemetry) { Session = sessionMock.Object }; @@ -2298,11 +2286,10 @@ public async Task GetMonitoredItemsNullOutputParametersAsync() sessionMock .Setup(mock => mock.CallAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(outputParameters); + It.IsAny(), + It.Is(c => c.HasArgsOfType(typeof(uint))), + It.IsAny())) + .ReturnsAsync(outputParameters.ToResponse()); var subscription = new Subscription(telemetry) { Session = sessionMock.Object }; @@ -2333,12 +2320,11 @@ public async Task GetMonitoredItemsTooManyOutputParametersAsync() var sessionMock = new Mock(); sessionMock - .Setup(mock => mock.CallAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(outputParameters); + .Setup(mock => mock.CallAsync( + It.IsAny(), + It.Is(c => c.HasArgsOfType(typeof(uint))), + It.IsAny())) + .ReturnsAsync(outputParameters.ToResponse()); var subscription = new Subscription(telemetry) { Session = sessionMock.Object }; diff --git a/Tests/Opc.Ua.Client.Tests/ClientTestServerQuotas.cs b/Tests/Opc.Ua.Client.Tests/ClientTestServerQuotas.cs index c9e977032b..3eee9254e4 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTestServerQuotas.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTestServerQuotas.cs @@ -147,7 +147,7 @@ public override void GlobalCleanup() [Order(200)] public async Task TestBoundaryCaseForReadingChunksAsync() { - var theSession = (Session)((TraceableSession)Session).Session; + ISession theSession = Session; int namespaceIndex = theSession.NamespaceUris.GetIndex( "http://opcfoundation.org/Quickstarts/ReferenceServer"); diff --git a/Tests/Opc.Ua.Client.Tests/ContinuationPointInBatchTest.cs b/Tests/Opc.Ua.Client.Tests/ContinuationPointInBatchTest.cs index 14e4fbe693..16fbb0be49 100644 --- a/Tests/Opc.Ua.Client.Tests/ContinuationPointInBatchTest.cs +++ b/Tests/Opc.Ua.Client.Tests/ContinuationPointInBatchTest.cs @@ -240,7 +240,7 @@ public override void GlobalCleanup() [Order(100)] public async Task MBNodeCacheBrowseAllVariablesAsync(ManagedBrowseTestDataProvider testData) { - var theSession = (Session)((TraceableSession)Session).Session; + ISession theSession = Session; theSession.NodeCache.Clear(); theSession.ContinuationPointPolicy = ContinuationPointPolicy.Default; @@ -327,8 +327,8 @@ public async Task MBNodeCacheBrowseAllVariablesAsync(ManagedBrowseTestDataProvid public async Task ManagedBrowseWithManyContinuationPointsAsync( ManagedBrowseTestDataProvider testData) { - var theSession = (Session)((TraceableSession)Session).Session; - await theSession.FetchOperationLimitsAsync().ConfigureAwait(false); + var theSession = (Session)Session; + await theSession.FetchOperationLimitsAsync(default).ConfigureAwait(false); theSession.ContinuationPointPolicy = ContinuationPointPolicy.Default; @@ -353,7 +353,7 @@ public async Task ManagedBrowseWithManyContinuationPointsAsync( ReferenceServerWithLimits.SetMaxNumberOfContinuationPoints( pass1ExpectedResults.InputMaxNumberOfContinuationPoints); - theSession.ServerMaxContinuationPointsPerBrowse = pass1ExpectedResults + theSession.ServerCapabilities.MaxBrowseContinuationPoints = (ushort)pass1ExpectedResults .InputMaxNumberOfContinuationPoints; List nodeIds = GetMassFolderNodesToBrowse(); @@ -379,7 +379,7 @@ public async Task ManagedBrowseWithManyContinuationPointsAsync( ReferenceServerWithLimits.SetMaxNumberOfContinuationPoints( pass2ExpectedResults.InputMaxNumberOfContinuationPoints); - theSession.ServerMaxContinuationPointsPerBrowse = pass2ExpectedResults + theSession.ServerCapabilities.MaxBrowseContinuationPoints = (ushort)pass2ExpectedResults .InputMaxNumberOfContinuationPoints; IList referenceDescriptionsPass2; @@ -470,7 +470,7 @@ await theSession.BrowseAsync( public async Task BalancedManagedBrowseWithManyContinuationPointsAsync( ManagedBrowseTestDataProvider testData) { - var theSession = (Session)((TraceableSession)Session).Session; + var theSession = (Session)Session; theSession.ContinuationPointPolicy = ContinuationPointPolicy.Balanced; @@ -495,7 +495,7 @@ public async Task BalancedManagedBrowseWithManyContinuationPointsAsync( ReferenceServerWithLimits.SetMaxNumberOfContinuationPoints( pass1ExpectedResults.InputMaxNumberOfContinuationPoints); - theSession.ServerMaxContinuationPointsPerBrowse = pass1ExpectedResults + theSession.ServerCapabilities.MaxBrowseContinuationPoints = (ushort)pass1ExpectedResults .InputMaxNumberOfContinuationPoints; List nodeIds = GetMassFolderNodesToBrowse(); @@ -521,7 +521,7 @@ public async Task BalancedManagedBrowseWithManyContinuationPointsAsync( ReferenceServerWithLimits.SetMaxNumberOfContinuationPoints( pass2ExpectedResults.InputMaxNumberOfContinuationPoints); - theSession.ServerMaxContinuationPointsPerBrowse = pass2ExpectedResults + theSession.ServerCapabilities.MaxBrowseContinuationPoints = (ushort)pass2ExpectedResults .InputMaxNumberOfContinuationPoints; theSession.ContinuationPointPolicy = ContinuationPointPolicy.Balanced; @@ -611,7 +611,7 @@ public async Task ParallelManagedBrowseWithManyContinuationPointsAsync( ManagedBrowseTestDataProvider testData, ContinuationPointPolicy policy) { - var theSession = (Session)((TraceableSession)Session).Session; + var theSession = (Session)Session; theSession.ContinuationPointPolicy = policy; @@ -636,7 +636,7 @@ public async Task ParallelManagedBrowseWithManyContinuationPointsAsync( ReferenceServerWithLimits.SetMaxNumberOfContinuationPoints( pass1ExpectedResults.InputMaxNumberOfContinuationPoints); - theSession.ServerMaxContinuationPointsPerBrowse = pass1ExpectedResults + theSession.ServerCapabilities.MaxBrowseContinuationPoints = (ushort)pass1ExpectedResults .InputMaxNumberOfContinuationPoints; List nodeIds = GetMassFolderNodesToBrowse(); @@ -689,7 +689,7 @@ public async Task ParallelManagedBrowseWithManyContinuationPointsAsync( ReferenceServerWithLimits.SetMaxNumberOfContinuationPoints( pass2ExpectedResults.InputMaxNumberOfContinuationPoints); - theSession.ServerMaxContinuationPointsPerBrowse = pass2ExpectedResults + theSession.ServerCapabilities.MaxBrowseContinuationPoints = (ushort)pass2ExpectedResults .InputMaxNumberOfContinuationPoints; ByteStringCollection continuationPoints2ndBrowse; @@ -751,7 +751,7 @@ public async Task MBNodeCacheBrowseAllVariablesMultipleNodesAsync( ManagedBrowseTestDataProvider testData, ContinuationPointPolicy policy) { - var theSession = (Session)((TraceableSession)Session).Session; + var theSession = (Session)Session; theSession.NodeCache.Clear(); theSession.ContinuationPointPolicy = policy; @@ -770,7 +770,7 @@ public async Task MBNodeCacheBrowseAllVariablesMultipleNodesAsync( ReferenceServerWithLimits.SetMaxNumberOfContinuationPoints( pass1ExpectedResults.InputMaxNumberOfContinuationPoints); - theSession.ServerMaxContinuationPointsPerBrowse = pass1ExpectedResults + theSession.ServerCapabilities.MaxBrowseContinuationPoints = (ushort)pass1ExpectedResults .InputMaxNumberOfContinuationPoints; var result = new List(); diff --git a/Tests/Opc.Ua.Client.Tests/DurableSubscriptionTest.cs b/Tests/Opc.Ua.Client.Tests/DurableSubscriptionTest.cs index 2db4e71572..a3fb033b15 100644 --- a/Tests/Opc.Ua.Client.Tests/DurableSubscriptionTest.cs +++ b/Tests/Opc.Ua.Client.Tests/DurableSubscriptionTest.cs @@ -247,7 +247,7 @@ public async Task TestRevisedQueueSizeAsync( } else { - mi = new MonitoredItem + mi = new MonitoredItem(Session.MessageContext.Telemetry) { AttributeId = Attributes.Value, StartNodeId = VariableIds.Server_ServerStatus_CurrentTime, @@ -298,7 +298,7 @@ public async Task SetSubscriptionDurableFailsWhenMIExistsAsync() uint id = subscription.Id; - var mi = new MonitoredItem + var mi = new MonitoredItem(Session.MessageContext.Telemetry) { AttributeId = Attributes.Value, StartNodeId = VariableIds.Server_ServerStatus_CurrentTime, @@ -798,7 +798,7 @@ private async Task> GetValuesAsync(Dictionary outputArguments, StatusCode response = default, StatusCode result = default) + { + return new CallResponse + { + ResponseHeader = new ResponseHeader + { + ServiceResult = response + }, + Results = + [ + new CallMethodResult + { + StatusCode = result, + OutputArguments = outputArguments == null ? + null : + [.. outputArguments.Select(o => new Variant(o))] + } + ] + }; + } + } +} diff --git a/Tests/Opc.Ua.Client.Tests/LruCacheTests.cs b/Tests/Opc.Ua.Client.Tests/LruCacheTests.cs index 7e87882ad2..dc184c0c14 100644 --- a/Tests/Opc.Ua.Client.Tests/LruCacheTests.cs +++ b/Tests/Opc.Ua.Client.Tests/LruCacheTests.cs @@ -30,19 +30,21 @@ public async Task FetchRemainingNodesAsyncShouldHandleErrorsAsync() // Arrange var id = new NodeId("test", 0); - var session = new Mock(); - - session - .Setup(c => - c.ReadNodesAsync( - It.IsAny>(), - NodeClass.Unspecified, - false, - It.IsAny())) - .ReturnsAsync((new List { new() }, new[] { - new ServiceResult(StatusCodes.BadUnexpectedError) })) + var context = new Mock(); + + context + .Setup(c => c.FetchNodesAsync( + It.IsAny(), + It.IsAny>(), + false, + It.IsAny())) + .ReturnsAsync(new ResultSet + { + Results = [new()], + Errors = [new ServiceResult(StatusCodes.BadUnexpectedError)] + }) .Verifiable(Times.Once); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var nodeCache = new LruNodeCache(context.Object, telemetry); // Act IReadOnlyList result = await nodeCache.GetNodesAsync([id], default) @@ -50,7 +52,7 @@ public async Task FetchRemainingNodesAsyncShouldHandleErrorsAsync() // Assert Assert.AreEqual(1, result.Count); - session.Verify(); + context.Verify(); } [Test] @@ -60,11 +62,12 @@ public async Task GetBuiltInTypeAsyncShouldHandleUnknownTypeAsync() // Arrange var datatypeId = new NodeId("unknownType", 0); - var session = new Mock(); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var context = new Mock(); + var nodeCache = new LruNodeCache(context.Object, telemetry); - session + context .Setup(c => c.FetchReferencesAsync( + It.IsAny(), It.Is(i => i == datatypeId), It.IsAny())) .ReturnsAsync([]) @@ -76,7 +79,7 @@ public async Task GetBuiltInTypeAsyncShouldHandleUnknownTypeAsync() // Assert Assert.AreEqual(BuiltInType.Null, result); - session.Verify(); + context.Verify(); } [Test] @@ -86,8 +89,8 @@ public async Task GetBuiltInTypeAsyncShouldReturnBuiltInTypeAsync() // Arrange var datatypeId = new NodeId((uint)BuiltInType.Int32, 0); - var session = new Mock(); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var context = new Mock(); + var nodeCache = new LruNodeCache(context.Object, telemetry); // Act BuiltInType result = await nodeCache.GetBuiltInTypeAsync(datatypeId, default) @@ -103,8 +106,8 @@ public async Task GetNodeAsyncShouldHandleEmptyListAsync() ITelemetryContext telemetry = NUnitTelemetryContext.Create(); // Arrange - var session = new Mock(); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var context = new Mock(); + var nodeCache = new LruNodeCache(context.Object, telemetry); // Act IReadOnlyList result = await nodeCache.GetNodesAsync([], default) @@ -122,16 +125,18 @@ public async Task GetNodeAsyncShouldReturnNodeFromCacheAsync() // Arrange var expected = new Node(); var id = new NodeId("test", 0); - var session = new Mock(); + var context = new Mock(); - session - .Setup( - c => c.ReadNodeAsync( - It.Is(i => i == id), - It.IsAny())) + context + .Setup(c => c.FetchNodeAsync( + It.IsAny(), + It.Is(i => i == id), + NodeClass.Unspecified, + false, + It.IsAny())) .ReturnsAsync(expected) .Verifiable(Times.Once); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var nodeCache = new LruNodeCache(context.Object, telemetry); // Act INode result = await nodeCache.GetNodeAsync(id, default).ConfigureAwait(false); @@ -140,7 +145,7 @@ public async Task GetNodeAsyncShouldReturnNodeFromCacheAsync() Assert.AreEqual(expected, result); result = await nodeCache.GetNodeAsync(id, default).ConfigureAwait(false); Assert.AreEqual(expected, result); - session.Verify(); + context.Verify(); } [Test] @@ -150,16 +155,22 @@ public async Task GetNodeTestAsync() var expected = new Node(); var id = new NodeId("test", 0); - var session = new Mock(); - session - .Setup( - c => c.ReadNodeAsync( - It.Is(i => i == id), - It.IsAny())) - .Returns((nodeId, _) => Task.FromResult(expected)) + var context = new Mock(); + var nsTable = new NamespaceTable(); + context.Setup(c => c.NamespaceUris).Returns(nsTable); + + context + .Setup(c => c.FetchNodeAsync( + It.IsAny(), + It.Is(i => i == id), + NodeClass.Unspecified, + false, + It.IsAny())) + .Returns((_, nodeId, _, _, ct) + => ValueTask.FromResult(expected)) .Verifiable(Times.Once); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var nodeCache = new LruNodeCache(context.Object, telemetry); INode result = await nodeCache.GetNodeAsync(id, default).ConfigureAwait(false); Assert.AreEqual(expected, result); @@ -167,7 +178,7 @@ public async Task GetNodeTestAsync() Assert.AreEqual(expected, result); result = await nodeCache.GetNodeAsync(id, default).ConfigureAwait(false); Assert.AreEqual(expected, result); - session.Verify(); + context.Verify(); } [Test] @@ -177,17 +188,19 @@ public void GetNodeThrowsTest() var expected = new Node(); var id = new NodeId("test", 0); - var session = new Mock(); + var context = new Mock(); - session - .Setup( - c => c.ReadNodeAsync( - It.Is(i => i == id), - It.IsAny())) - .Returns( - (nodeId, ct) => Task.FromException(new ServiceResultException())) + context + .Setup(c => c.FetchNodeAsync( + It.IsAny(), + It.Is(i => i == id), + NodeClass.Unspecified, + false, + It.IsAny())) + .Returns((_, nodeId, _, _, ct) + => ValueTask.FromException(new ServiceResultException())) .Verifiable(Times.Exactly(3)); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var nodeCache = new LruNodeCache(context.Object, telemetry); _ = NUnit.Framework.Assert.ThrowsAsync(async () => await nodeCache.GetNodeAsync(id, default).ConfigureAwait(false)); @@ -195,7 +208,7 @@ public void GetNodeThrowsTest() await nodeCache.GetNodeAsync(id, default).ConfigureAwait(false)); _ = NUnit.Framework.Assert.ThrowsAsync(async () => await nodeCache.GetNodeAsync(id, default).ConfigureAwait(false)); - session.Verify(); + context.Verify(); } [Test] @@ -206,22 +219,23 @@ public async Task GetNodeWithBrowsePathAsyncShouldHandleInvalidBrowsePathAsync() // Arrange var id = new NodeId("test", 0); var browsePath = new QualifiedNameCollection { new QualifiedName("invalid") }; - var session = new Mock(); + var context = new Mock(); - session - .Setup(c => - c.FetchReferencesAsync( - It.Is(i => i == ReferenceTypeIds.HierarchicalReferences), - It.IsAny())) + context + .Setup(c => c.FetchReferencesAsync( + It.IsAny(), + It.Is(i => i == ReferenceTypeIds.HierarchicalReferences), + It.IsAny())) .ReturnsAsync([]) .Verifiable(Times.Once); - session + context .Setup(c => c.FetchReferencesAsync( + It.IsAny(), It.Is(i => i == id), It.IsAny())) .ReturnsAsync([]) .Verifiable(Times.Once); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var nodeCache = new LruNodeCache(context.Object, telemetry); // Act INode result = await nodeCache.GetNodeWithBrowsePathAsync(id, browsePath, default) @@ -229,7 +243,7 @@ public async Task GetNodeWithBrowsePathAsyncShouldHandleInvalidBrowsePathAsync() // Assert Assert.Null(result); - session.Verify(); + context.Verify(); } [Test] @@ -256,31 +270,38 @@ public async Task GetNodeWithBrowsePathAsyncShouldReturnNodeAsync() IsForward = true } }; - var session = new Mock(); - session - .Setup(c => - c.FetchReferencesAsync( - It.Is(i => i == ReferenceTypeIds.HierarchicalReferences), - It.IsAny())) + + var context = new Mock(); + var nsTable = new NamespaceTable(); + context.Setup(c => c.NamespaceUris).Returns(nsTable); + context + .Setup(c => c.FetchReferencesAsync( + It.IsAny(), + It.Is(i => i == ReferenceTypeIds.HierarchicalReferences), + It.IsAny())) .ReturnsAsync([]) .Verifiable(Times.Once); - session + context .Setup(c => c.FetchReferencesAsync( + It.IsAny(), It.Is(i => i == id), It.IsAny())) .ReturnsAsync([.. references]) .Verifiable(Times.Once); - session - .Setup(c => - c.ReadNodesAsync( - It.Is>(n => n.Count == 1 && n[0] == id), - NodeClass.Unspecified, - false, - It.IsAny())) - .ReturnsAsync((new List { expected }, new[] { ServiceResult.Good })) + context + .Setup(c => c.FetchNodesAsync( + It.IsAny(), + It.Is>(n => n.Count == 1 && n[0] == id), + false, + It.IsAny())) + .ReturnsAsync(new ResultSet + { + Results = [expected], + Errors = [ServiceResult.Good] + }) .Verifiable(Times.Once); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var nodeCache = new LruNodeCache(context.Object, telemetry); // Act INode result = await nodeCache.GetNodeWithBrowsePathAsync(id, browsePath, default) @@ -295,7 +316,7 @@ public async Task GetNodeWithBrowsePathAsyncShouldReturnNodeAsync() // Assert Assert.AreEqual(expected, result); - session.Verify(); + context.Verify(); } [Test] @@ -346,47 +367,57 @@ public async Task GetNodeWithBrowsePathAsyncShouldReturnNodeWithMultipleElements NodeClass = NodeClass.Variable }; - var session = new Mock(); + var context = new Mock(); + var nsTable = new NamespaceTable(); + context.Setup(c => c.NamespaceUris).Returns(nsTable); - session - .Setup(c => - c.FetchReferencesAsync( - It.Is(i => i == ReferenceTypeIds.HierarchicalReferences), - It.IsAny())) + context + .Setup(c => c.FetchReferencesAsync( + It.IsAny(), + It.Is(i => i == ReferenceTypeIds.HierarchicalReferences), + It.IsAny())) .ReturnsAsync([]) .Verifiable(Times.Once); - session + context .Setup(c => c.FetchReferencesAsync( + It.IsAny(), It.Is(i => i == rootId), It.IsAny())) .ReturnsAsync([.. rootReferences]) .Verifiable(Times.Once); - session + context .Setup(c => c.FetchReferencesAsync( + It.IsAny(), It.Is(i => i == childId), It.IsAny())) .ReturnsAsync([.. childReferences]) .Verifiable(Times.Once); - session - .Setup(c => - c.ReadNodesAsync( - It.Is>(n => n.Count == 1 && n[0] == childId), - NodeClass.Unspecified, - false, - It.IsAny())) - .ReturnsAsync((new List { childNode }, new[] { ServiceResult.Good })) + context + .Setup(c => c.FetchNodesAsync( + It.IsAny(), + It.Is>(n => n.Count == 1 && n[0] == childId), + false, + It.IsAny())) + .ReturnsAsync(new ResultSet + { + Results = [childNode], + Errors = [ServiceResult.Good] + }) .Verifiable(Times.Once); - session - .Setup(c => - c.ReadNodesAsync( - It.Is>(n => n.Count == 1 && n[0] == grandChildId), - NodeClass.Unspecified, - false, - It.IsAny())) - .ReturnsAsync((new List { expected }, new[] { ServiceResult.Good })) + context + .Setup(c => c.FetchNodesAsync( + It.IsAny(), + It.Is>(n => n.Count == 1 && n[0] == grandChildId), + false, + It.IsAny())) + .ReturnsAsync(new ResultSet + { + Results = [expected], + Errors = [ServiceResult.Good] + }) .Verifiable(Times.Once); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var nodeCache = new LruNodeCache(context.Object, telemetry); // Act INode result = await nodeCache @@ -402,7 +433,7 @@ public async Task GetNodeWithBrowsePathAsyncShouldReturnNodeWithMultipleElements // Assert Assert.AreEqual(expected, result); - session.Verify(); + context.Verify(); } [Test] @@ -411,8 +442,8 @@ public async Task GetReferencesAsyncShouldHandleEmptyListOfNodeIdsAsync() ITelemetryContext telemetry = NUnitTelemetryContext.Create(); // Arrange - var session = new Mock(); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var context = new Mock(); + var nodeCache = new LruNodeCache(context.Object, telemetry); // Act IReadOnlyList result = await nodeCache @@ -442,33 +473,39 @@ public async Task GetReferencesAsyncShouldReturnReferencesFromCacheAsync() } }; var id = new NodeId("test", 0); - var session = new Mock(); - session + var context = new Mock(); + var nsTable = new NamespaceTable(); + context.Setup(c => c.NamespaceUris).Returns(nsTable); + + context .Setup(c => c.FetchReferencesAsync( + It.IsAny(), It.Is(i => i == id), It.IsAny())) .ReturnsAsync([.. expected]) .Verifiable(Times.Once); - session - .Setup(c => - c.ReadNodesAsync( - It.Is>(n => n.Count == 1 && n[0] == targetNodeId), - NodeClass.Unspecified, - false, - It.IsAny())) - .ReturnsAsync( - ( - new List + context + .Setup(c => c.FetchNodesAsync( + It.IsAny(), + It.Is>(n => n.Count == 1 && n[0] == targetNodeId), + false, + It.IsAny())) + .ReturnsAsync(new ResultSet + { + Results = + [ + new VariableNode { - new VariableNode { - NodeId = targetNodeId, - NodeClass = NodeClass.Variable } - }, - new[] { ServiceResult.Good })) + NodeId = targetNodeId, + NodeClass = NodeClass.Variable + } + ], + Errors = [ServiceResult.Good] + }) .Verifiable(Times.Once); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var nodeCache = new LruNodeCache(context.Object, telemetry); // Act IReadOnlyList result1 = await nodeCache @@ -492,7 +529,7 @@ public async Task GetReferencesAsyncShouldReturnReferencesFromCacheAsync() // Assert Assert.AreEqual(1, result1.Count); Assert.IsEmpty(result2); - session.Verify(); + context.Verify(); } [Test] @@ -515,11 +552,14 @@ public async Task GetReferencesAsyncWithMoreThanOneSubtypeShouldReturnReferences } }; var id = new NodeId("test", 0); - var session = new Mock(); - session - .Setup(c => - c.FetchReferencesAsync( + var context = new Mock(); + var nsTable = new NamespaceTable(); + context.Setup(c => c.NamespaceUris).Returns(nsTable); + + context + .Setup(c => c.FetchReferencesAsync( + It.IsAny(), It.Is(i => i == referenceTypeId), It.IsAny())) .ReturnsAsync( @@ -532,26 +572,28 @@ public async Task GetReferencesAsyncWithMoreThanOneSubtypeShouldReturnReferences } ]) .Verifiable(Times.Once); - session - .Setup(c => - c.ReadNodesAsync( - It.Is>(n => n.Count == 1 && n[0] == referenceSubTypeId), - NodeClass.Unspecified, - false, - It.IsAny())) - .ReturnsAsync( - ( - new List + context + .Setup(c => c.FetchNodesAsync( + It.IsAny(), + It.Is>(n => n.Count == 1 && n[0] == referenceSubTypeId), + false, + It.IsAny())) + .ReturnsAsync(new ResultSet + { + Results = + [ + new ReferenceTypeNode { - new ReferenceTypeNode { - NodeId = referenceSubTypeId, - NodeClass = NodeClass.ReferenceType } - }, - new[] { ServiceResult.Good })) + NodeId = referenceSubTypeId, + NodeClass = NodeClass.ReferenceType + } + ], + Errors = [ServiceResult.Good] + }) .Verifiable(Times.Once); - session - .Setup(c => - c.FetchReferencesAsync( + context + .Setup(c => c.FetchReferencesAsync( + It.IsAny(), It.Is(i => i == referenceSubTypeId), It.IsAny())) .ReturnsAsync( @@ -565,31 +607,34 @@ public async Task GetReferencesAsyncWithMoreThanOneSubtypeShouldReturnReferences } ]) .Verifiable(Times.Once); - session + context .Setup(c => c.FetchReferencesAsync( + It.IsAny(), It.Is(i => i == id), It.IsAny())) .ReturnsAsync([.. expected]) .Verifiable(Times.Once); - session - .Setup(c => - c.ReadNodesAsync( - It.Is>(n => n.Count == 1 && n[0] == targetNodeId), - NodeClass.Unspecified, - false, - It.IsAny())) - .ReturnsAsync( - ( - new List + context + .Setup(c => c.FetchNodesAsync( + It.IsAny(), + It.Is>(n => n.Count == 1 && n[0] == targetNodeId), + false, + It.IsAny())) + .ReturnsAsync(new ResultSet + { + Results = + [ + new VariableNode { - new VariableNode { - NodeId = targetNodeId, - NodeClass = NodeClass.Variable } - }, - new[] { ServiceResult.Good })) + NodeId = targetNodeId, + NodeClass = NodeClass.Variable + } + ], + Errors = [ServiceResult.Good] + }) .Verifiable(Times.Once); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var nodeCache = new LruNodeCache(context.Object, telemetry); // Act IReadOnlyList result1 = await nodeCache @@ -613,7 +658,7 @@ public async Task GetReferencesAsyncWithMoreThanOneSubtypeShouldReturnReferences // Assert Assert.AreEqual(1, result1.Count); Assert.IsEmpty(result2); - session.Verify(); + context.Verify(); } [Test] @@ -635,40 +680,46 @@ public async Task GetReferencesAsyncWithSubtypesShouldReturnReferencesFromCacheA } }; var id = new NodeId("test", 0); - var session = new Mock(); - session - .Setup(c => - c.FetchReferencesAsync( + var context = new Mock(); + var nsTable = new NamespaceTable(); + context.Setup(c => c.NamespaceUris).Returns(nsTable); + + context + .Setup(c => c.FetchReferencesAsync( + It.IsAny(), It.Is(i => i == referenceTypeId), It.IsAny())) .ReturnsAsync([]) .Verifiable(Times.Once); - session + context .Setup(c => c.FetchReferencesAsync( + It.IsAny(), It.Is(i => i == id), It.IsAny())) .ReturnsAsync([.. expected]) .Verifiable(Times.Once); - session - .Setup(c => - c.ReadNodesAsync( - It.Is>(n => n.Count == 1 && n[0] == targetNodeId), - NodeClass.Unspecified, - false, - It.IsAny())) - .ReturnsAsync( - ( - new List + context + .Setup(c => c.FetchNodesAsync( + It.IsAny(), + It.Is>(n => n.Count == 1 && n[0] == targetNodeId), + false, + It.IsAny())) + .ReturnsAsync(new ResultSet + { + Results = + [ + new VariableNode { - new VariableNode { - NodeId = targetNodeId, - NodeClass = NodeClass.Variable } - }, - new[] { ServiceResult.Good })) + NodeId = targetNodeId, + NodeClass = NodeClass.Variable + } + ], + Errors = [ServiceResult.Good] + }) .Verifiable(Times.Once); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var nodeCache = new LruNodeCache(context.Object, telemetry); // Act IReadOnlyList result1 = await nodeCache @@ -692,7 +743,7 @@ public async Task GetReferencesAsyncWithSubtypesShouldReturnReferencesFromCacheA // Assert Assert.AreEqual(1, result1.Count); Assert.IsEmpty(result2); - session.Verify(); + context.Verify(); } [Test] @@ -702,15 +753,16 @@ public async Task GetSuperTypeAsyncShouldHandleNoSupertypeAsync() // Arrange var typeId = new NodeId("type", 0); - var session = new Mock(); + var context = new Mock(); - session + context .Setup(c => c.FetchReferencesAsync( + It.IsAny(), It.Is(i => i == typeId), It.IsAny())) .ReturnsAsync([]) .Verifiable(Times.Once); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var nodeCache = new LruNodeCache(context.Object, telemetry); // Act NodeId result = await nodeCache.GetSuperTypeAsync(typeId, default) @@ -718,7 +770,7 @@ public async Task GetSuperTypeAsyncShouldHandleNoSupertypeAsync() // Assert Assert.AreEqual(NodeId.Null, result); - session.Verify(); + context.Verify(); } [Test] @@ -738,15 +790,19 @@ public async Task GetSuperTypeAsyncShouldReturnSuperTypeAsync() IsForward = false } }; - var session = new Mock(); - session + var context = new Mock(); + var nsTable = new NamespaceTable(); + context.Setup(c => c.NamespaceUris).Returns(nsTable); + + context .Setup(c => c.FetchReferencesAsync( + It.IsAny(), It.Is(i => i == subTypeId), It.IsAny())) .ReturnsAsync([.. references]) .Verifiable(Times.Once); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var nodeCache = new LruNodeCache(context.Object, telemetry); // Act NodeId result = await nodeCache.GetSuperTypeAsync(subTypeId, default) @@ -760,7 +816,7 @@ public async Task GetSuperTypeAsyncShouldReturnSuperTypeAsync() // Assert Assert.AreEqual(superTypeId, result); - session.Verify(); + context.Verify(); } [Test] @@ -770,18 +826,21 @@ public void GetValueAsyncShouldHandleErrors() // Arrange var id = new NodeId("test", 0); - var session = new Mock(); + var context = new Mock(); - session - .Setup(c => c.ReadValueAsync(It.IsAny(), It.IsAny())) + context + .Setup(c => c.FetchValueAsync( + It.IsAny(), + It.IsAny(), + It.IsAny())) .ThrowsAsync(new ServiceResultException(StatusCodes.BadUnexpectedError)) .Verifiable(Times.Once); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var nodeCache = new LruNodeCache(context.Object, telemetry); // Act && Assert _ = NUnit.Framework.Assert.ThrowsAsync(async () => await nodeCache.GetValueAsync(id, default).ConfigureAwait(false)); - session.Verify(); + context.Verify(); } [Test] @@ -792,15 +851,16 @@ public async Task GetValueAsyncShouldReturnValueFromCacheAsync() // Arrange var expected = new DataValue(new Variant(123), StatusCodes.Good, DateTime.UtcNow); var id = new NodeId("test", 0); - var session = new Mock(); + var context = new Mock(); - session - .Setup(c => c.ReadValueAsync( + context + .Setup(c => c.FetchValueAsync( + It.IsAny(), It.Is(i => i == id), It.IsAny())) .ReturnsAsync(expected) .Verifiable(Times.Once); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var nodeCache = new LruNodeCache(context.Object, telemetry); // Act DataValue result = await nodeCache.GetValueAsync(id, default).ConfigureAwait(false); @@ -809,7 +869,7 @@ public async Task GetValueAsyncShouldReturnValueFromCacheAsync() Assert.AreEqual(expected, result); result = await nodeCache.GetValueAsync(id, default).ConfigureAwait(false); Assert.AreEqual(expected, result); - session.Verify(); + context.Verify(); } [Test] @@ -819,21 +879,20 @@ public async Task GetValuesAsyncShouldHandleErrorsAsync() // Arrange var ids = new List { new("test1", 0), new("test2", 0) }; - var session = new Mock(); + var context = new Mock(); - session - .Setup( - c => c.ReadValuesAsync( - It.IsAny>(), + context + .Setup(c => c.FetchValuesAsync( + It.IsAny(), + It.IsAny>(), It.IsAny())) - .ReturnsAsync( - ( - new DataValueCollection { new(), new() }, - new[] { - new ServiceResult(StatusCodes.BadUnexpectedError), - ServiceResult.Good })) + .ReturnsAsync(new ResultSet + { + Results = [new(), new()], + Errors = [new ServiceResult(StatusCodes.BadUnexpectedError), ServiceResult.Good] + }) .Verifiable(Times.Once); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var nodeCache = new LruNodeCache(context.Object, telemetry); // Act IReadOnlyList result = await nodeCache.GetValuesAsync(ids, default) @@ -843,7 +902,7 @@ public async Task GetValuesAsyncShouldHandleErrorsAsync() Assert.AreEqual(2, result.Count); Assert.AreEqual(StatusCodes.BadUnexpectedError, (uint)result[0].StatusCode); Assert.AreEqual(StatusCodes.Good, (uint)result[1].StatusCode); - session.Verify(); + context.Verify(); } [Test] @@ -858,18 +917,20 @@ public async Task GetValuesAsyncShouldReturnValuesFromCacheAsync() new(new Variant(456), StatusCodes.Good, DateTime.UtcNow) }; var ids = new List { new("test1", 0), new("test2", 0) }; - var session = new Mock(); + var context = new Mock(); - session - .Setup(c => - c.ReadValuesAsync( - It.Is>(i => i.ToHashSet().SetEquals(ids)), + context + .Setup(c => c.FetchValuesAsync( + It.IsAny(), + It.Is>(i => i.ToHashSet().SetEquals(ids)), It.IsAny())) - .ReturnsAsync((new DataValueCollection(expected), new[] { - ServiceResult.Good, - ServiceResult.Good })) + .ReturnsAsync(new ResultSet + { + Results = expected, + Errors = [ServiceResult.Good, ServiceResult.Good] + }) .Verifiable(Times.Once); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var nodeCache = new LruNodeCache(context.Object, telemetry); // Act IReadOnlyList result = await nodeCache.GetValuesAsync(ids, default) @@ -880,7 +941,7 @@ public async Task GetValuesAsyncShouldReturnValuesFromCacheAsync() result = await nodeCache.GetValuesAsync(ids, default).ConfigureAwait(false); Assert.AreEqual(expected, result); - session.Verify(); + context.Verify(); } [Test] @@ -895,19 +956,20 @@ public async Task GetValuesAsyncShouldReturnValuesFromCacheButHonorStatusOfReadA new(new Variant(456), StatusCodes.Good, DateTime.UtcNow) }; var ids = new List { new("test1", 0), new("test2", 0) }; - var session = new Mock(); + var context = new Mock(); - session - .Setup(c => - c.ReadValuesAsync( - It.Is>(i => i.ToHashSet().SetEquals(ids)), + context + .Setup(c => c.FetchValuesAsync( + It.IsAny(), + It.Is>(i => i.ToHashSet().SetEquals(ids)), It.IsAny())) - .ReturnsAsync( - ( - new DataValueCollection(expected), - new[] { ServiceResult.Good, new ServiceResult(StatusCodes.Bad) })) + .ReturnsAsync(new ResultSet + { + Results = expected, + Errors = [ServiceResult.Good, new ServiceResult(StatusCodes.Bad)] + }) .Verifiable(Times.Once); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var nodeCache = new LruNodeCache(context.Object, telemetry); // Act IReadOnlyList result = await nodeCache.GetValuesAsync(ids, default) @@ -918,16 +980,19 @@ public async Task GetValuesAsyncShouldReturnValuesFromCacheButHonorStatusOfReadA Assert.AreEqual(StatusCodes.Bad, (uint)result[1].StatusCode); Assert.AreEqual(expected[1].Value, result[1].Value); - session - .Setup(c => - c.ReadValuesAsync( - It.Is>(i => i.Count == 1 && i[0] == ids[1]), + context + .Setup(c => c.FetchValuesAsync( + It.IsAny(), + It.Is>(i => i.Count == 1 && i[0] == ids[1]), It.IsAny())) - .ReturnsAsync( - (new DataValueCollection { expected[1] }, new[] { ServiceResult.Good })) + .ReturnsAsync(new ResultSet + { + Results = [expected[1]], + Errors = [ServiceResult.Good] + }) .Verifiable(Times.Once); result = await nodeCache.GetValuesAsync(ids, default).ConfigureAwait(false); - session.Verify(); + context.Verify(); } [Test] @@ -938,22 +1003,23 @@ public void IsTypeOfShouldHandleNoReferences() // Arrange var superTypeId = new NodeId("superType", 0); var subTypeId = new NodeId("subType", 0); - var session = new Mock(); + var context = new Mock(); - session + context .Setup(c => c.FetchReferencesAsync( + It.IsAny(), It.Is(i => i == subTypeId), It.IsAny())) .ReturnsAsync([]) .Verifiable(Times.Once); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var nodeCache = new LruNodeCache(context.Object, telemetry); // Act bool result = nodeCache.IsTypeOf(subTypeId, superTypeId); // Assert Assert.False(result); - session.Verify(); + context.Verify(); } [Test] @@ -973,15 +1039,19 @@ public void IsTypeOfShouldReturnTrueForSuperType() IsForward = false } }; - var session = new Mock(); - session + var context = new Mock(); + var nsTable = new NamespaceTable(); + context.Setup(c => c.NamespaceUris).Returns(nsTable); + + context .Setup(c => c.FetchReferencesAsync( + It.IsAny(), It.Is(i => i == subTypeId), It.IsAny())) .ReturnsAsync([.. references]) .Verifiable(Times.Once); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var nodeCache = new LruNodeCache(context.Object, telemetry); // Act bool result = nodeCache.IsTypeOf(subTypeId, superTypeId); @@ -994,7 +1064,7 @@ public void IsTypeOfShouldReturnTrueForSuperType() // Assert Assert.True(result); - session.Verify(); + context.Verify(); } [Test] @@ -1004,20 +1074,21 @@ public async Task LoadTypeHierarchyAsyncShouldHandleNoSubtypesAsync() // Arrange var typeId = new NodeId("type", 0); - var session = new Mock(); - session + var context = new Mock(); + context .Setup(c => c.FetchReferencesAsync( + It.IsAny(), It.Is(i => i == typeId), It.IsAny())) .ReturnsAsync([]) .Verifiable(Times.Once); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var nodeCache = new LruNodeCache(context.Object, telemetry); // Act await nodeCache.LoadTypeHierarchyAsync([typeId], default).ConfigureAwait(false); // Assert - session.Verify(); + context.Verify(); } [Test] @@ -1037,35 +1108,44 @@ public async Task LoadTypeHierarchyAsyncShouldLoadTypeHierarchyAsync() IsForward = true } }; - var session = new Mock(); - session + + var context = new Mock(); + var nsTable = new NamespaceTable(); + context.Setup(c => c.NamespaceUris).Returns(nsTable); + context .Setup(c => c.FetchReferencesAsync( + It.IsAny(), It.Is(i => i == typeId), It.IsAny())) .ReturnsAsync(references) .Verifiable(Times.Once); - session + context .Setup(c => c.FetchReferencesAsync( + It.IsAny(), It.Is(i => i == subTypeId), It.IsAny())) .ReturnsAsync([]) .Verifiable(Times.Once); - session - .Setup(c => - c.ReadNodesAsync( - It.Is>(n => n.Count == 1 && n[0] == subTypeId), - NodeClass.Unspecified, - false, - It.IsAny())) - .ReturnsAsync( - ( - new List + context + .Setup(c => c.FetchNodesAsync( + It.IsAny(), + It.Is>(n => n.Count == 1 && n[0] == subTypeId), + false, + It.IsAny())) + .ReturnsAsync(new ResultSet + { + Results = + [ + new DataTypeNode { - new DataTypeNode { NodeId = subTypeId, NodeClass = NodeClass.DataType } - }, - new[] { ServiceResult.Good })) + NodeId = subTypeId, + NodeClass = NodeClass.DataType + } + ], + Errors = [ServiceResult.Good] + }) .Verifiable(Times.Once); - var nodeCache = new LruNodeCache(session.Object, telemetry); + var nodeCache = new LruNodeCache(context.Object, telemetry); // Act await nodeCache.LoadTypeHierarchyAsync([typeId], default).ConfigureAwait(false); @@ -1073,7 +1153,7 @@ public async Task LoadTypeHierarchyAsyncShouldLoadTypeHierarchyAsync() await nodeCache.LoadTypeHierarchyAsync([typeId], default).ConfigureAwait(false); // Assert - session.Verify(); + context.Verify(); } } } diff --git a/Tests/Opc.Ua.Client.Tests/NodeCacheContextTests.cs b/Tests/Opc.Ua.Client.Tests/NodeCacheContextTests.cs new file mode 100644 index 0000000000..4a051bea03 --- /dev/null +++ b/Tests/Opc.Ua.Client.Tests/NodeCacheContextTests.cs @@ -0,0 +1,1167 @@ +/* ======================================================================== + * 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; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Moq; +using NUnit.Framework; + +namespace Opc.Ua.Client.Tests +{ + [TestFixture] + [Category("Client")] + [Category("Session")] + [SetCulture("en-us")] + [SetUICulture("en-us")] + public sealed class NodeCacheContextTests + { + [Test] + public async Task FetchValuesAsyncShouldReturnResultSetAsync() + { + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeIds = new List + { + NodeId.Parse("ns=2;s=TestNode1"), + NodeId.Parse("ns=2;s=TestNode2") + }; + var dataValues = new DataValueCollection + { + new DataValue(new Variant(123), StatusCodes.Good, DateTime.UtcNow), + new DataValue(new Variant(456), StatusCodes.Good, DateTime.UtcNow) + }; + var diagnosticInfos = new DiagnosticInfoCollection(); + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new ReadResponse + { + Results = dataValues, + DiagnosticInfos = diagnosticInfos + })) + .Verifiable(Times.Once); + + // Act + ResultSet result = await sut.FetchValuesAsync(null, nodeIds).ConfigureAwait(false); + + // Assert + Assert.That(result.Results, Is.EquivalentTo(dataValues)); + Assert.That(result.Errors, Is.All.EqualTo(ServiceResult.Good)); + } + + [Test] + public async Task FetchValueAsyncShouldReturnDataValueAsync() + { + // Arrange + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + + var nodeId = NodeId.Parse("ns=2;s=TestNode"); + var dataValue = new DataValue(new Variant(123), StatusCodes.Good, DateTime.UtcNow); + var diagnosticInfos = new DiagnosticInfoCollection(); + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new ReadResponse + { + Results = [dataValue], + DiagnosticInfos = diagnosticInfos + })) + .Verifiable(Times.Once); + + // Act + DataValue result = await sut.FetchValueAsync(null, nodeId).ConfigureAwait(false); + + // Assert + Assert.That(result, Is.EqualTo(dataValue)); + + session.Channel.Verify(); + } + + [Test] + public async Task FetchValuesAsyncShouldReturnEmptyResultSetForEmptyNodeIdsAsync() + { + // Arrange + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeIds = new List(); + + // Act + ResultSet result = await sut.FetchValuesAsync(null, nodeIds).ConfigureAwait(false); + + // Assert + Assert.That(result.Results, Is.Empty); + Assert.That(result.Errors, Is.Empty); + } + + [Test] + public void FetchValueAsyncShouldThrowServiceResultExceptionForBadStatusCode() + { + // Arrange + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeId = NodeId.Parse("ns=2;s=TestNode"); + var dataValue = new DataValue(new Variant(123), StatusCodes.Bad, DateTime.UtcNow); + var diagnosticInfos = new DiagnosticInfoCollection(); + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new ReadResponse + { + Results = [dataValue], + DiagnosticInfos = diagnosticInfos + })) + .Verifiable(Times.Once); + + Assert.ThrowsAsync(async () => await sut.FetchValueAsync( + null, + nodeId, + CancellationToken.None).ConfigureAwait(false)); + + session.Channel.Verify(); + } + + [Test] + public async Task FetchValuesAsyncShouldReturnErrorsForBadStatusCodesAsync() + { + // Arrange + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeIds = new List { + NodeId.Parse("ns=2;s=TestNode1"), + NodeId.Parse("ns=2;s=TestNode2") }; + var dataValues = new DataValueCollection + { + new DataValue(new Variant(123), StatusCodes.Bad, DateTime.UtcNow), + new DataValue(new Variant(456), StatusCodes.Good, DateTime.UtcNow) + }; + var diagnosticInfos = new DiagnosticInfoCollection(); + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new ReadResponse + { + Results = dataValues, + DiagnosticInfos = diagnosticInfos + })) + .Verifiable(Times.Once); + + // Act + ResultSet result = await sut.FetchValuesAsync(null, nodeIds).ConfigureAwait(false); + + // Assert + Assert.That(result.Results, Is.EquivalentTo(dataValues)); + Assert.That(result.Errors[0].StatusCode, Is.EqualTo(StatusCodes.Bad)); + Assert.That(result.Errors[1].StatusCode, Is.EqualTo(StatusCodes.Good)); + } + + [Test] + public void FetchValueAsyncShouldHandleCancellation() + { + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeId = NodeId.Parse("ns=2;s=TestNode"); + var cts = new CancellationTokenSource(); + cts.Cancel(); + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.Is(ct => ct.IsCancellationRequested))) + .ThrowsAsync(new OperationCanceledException()) + .Verifiable(Times.Once); + + Assert.ThrowsAsync( + async () => await sut.FetchValueAsync(null, nodeId, cts.Token).ConfigureAwait(false)); + } + + [Test] + public void FetchValuesAsyncShouldHandleCancellation() + { + // Arrange + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeIds = new List + { + NodeId.Parse("ns=2;s=TestNode1"), + NodeId.Parse("ns=2;s=TestNode2") + }; + var cts = new CancellationTokenSource(); + cts.Cancel(); + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.Is(ct => ct.IsCancellationRequested))) + .ThrowsAsync(new OperationCanceledException()) + .Verifiable(Times.Once); + + Assert.ThrowsAsync( + async () => await sut.FetchValuesAsync(null, nodeIds, cts.Token).ConfigureAwait(false)); + } + + [Test] + public async Task FetchValueAsyncShouldProcessDiagnosticInfoAsync() + { + // Arrange + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeId = NodeId.Parse("ns=2;s=TestNode"); + var dataValue = new DataValue(new Variant(123), StatusCodes.Good, DateTime.UtcNow); + var diagnosticInfo = new DiagnosticInfo(); + var diagnosticInfos = new DiagnosticInfoCollection { diagnosticInfo }; + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new ReadResponse + { + Results = [dataValue], + DiagnosticInfos = diagnosticInfos + })) + .Verifiable(Times.Once); + + // Act + DataValue result = await sut.FetchValueAsync(null, nodeId).ConfigureAwait(false); + + // Assert + Assert.That(result, Is.EqualTo(dataValue)); + Assert.That(diagnosticInfos, Contains.Item(diagnosticInfo)); + + session.Channel.Verify(); + } + + [Test] + public async Task FetchValuesAsyncShouldProcessDiagnosticInfoAsync() + { + // Arrange + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeIds = new List + { + NodeId.Parse("ns=2;s=TestNode1"), + NodeId.Parse("ns=2;s=TestNode2") + }; + var dataValues = new DataValueCollection + { + new DataValue(new Variant(123), StatusCodes.Good, DateTime.UtcNow), + new DataValue(new Variant(456), StatusCodes.Good, DateTime.UtcNow) + }; + var diagnosticInfo = new DiagnosticInfo(); + var diagnosticInfos = new DiagnosticInfoCollection { diagnosticInfo, diagnosticInfo }; + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new ReadResponse + { + Results = dataValues, + DiagnosticInfos = diagnosticInfos + })) + .Verifiable(Times.Once); + + // Act + ResultSet result = await sut.FetchValuesAsync(null, nodeIds).ConfigureAwait(false); + + // Assert + Assert.That(result.Results, Is.EquivalentTo(dataValues)); + Assert.That(result.Errors, Is.All.EqualTo(ServiceResult.Good)); + Assert.That(diagnosticInfos, Contains.Item(diagnosticInfo)); + + session.Channel.Verify(); + } + + [Test] + public async Task FetchNodesAsyncShouldReturnResultSetAsync() + { + // Arrange + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeIds = new List + { + NodeId.Parse("ns=2;s=TestNode1"), + NodeId.Parse("ns=2;s=TestNode2") + }; + VariableNode[] nodes = + [ + new VariableNode + { + NodeId = nodeIds[0], + NodeClass = NodeClass.Variable, + AccessLevel = 1, + DataType = NodeId.Parse("ns=2;s=TestDataType1"), + Description = "TestDescription1", + DisplayName = "TestDisplayName1", + BrowseName = "TestBrowseName1", + UserAccessLevel = 1 + }, + new VariableNode + { + NodeId = nodeIds[1], + NodeClass = NodeClass.Variable, + AccessLevel = 1, + DataType = NodeId.Parse("ns=2;s=TestDataType2"), + Description = "TestDescription2", + DisplayName = "TestDisplayName2", + BrowseName = "TestBrowseName2", + UserAccessLevel = 1 + } + ]; + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns((request, ct) => + { + var results = new DataValueCollection(request.NodesToRead + .Select(r => + { + var value = new DataValue(); + if (r.NodeId == nodeIds[0]) + { + nodes[0].Read(null!, r.AttributeId, value); + } + else + { + nodes[1].Read(null!, r.AttributeId, value); + } + return value; + })); + return new ValueTask(new ReadResponse + { + Results = results, + DiagnosticInfos = [] + }); + }) + .Verifiable(Times.Exactly(2)); + + // Act + ResultSet result = await sut.FetchNodesAsync(null, nodeIds).ConfigureAwait(false); + + // Assert + Assert.That(result.Results.Count, Is.EqualTo(2)); + Assert.That(Utils.IsEqual(nodes[0], result.Results[0]), Is.True); + Assert.That(Utils.IsEqual(nodes[1], result.Results[1]), Is.True); + Assert.That(result.Errors, Is.All.EqualTo(ServiceResult.Good)); + } + + [Test] + public async Task FetchNodesAsyncShouldReturnResultSetWhenOptionalAttributesMissingAsync() + { + // Arrange + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeIds = new List + { + NodeId.Parse("ns=2;s=TestNode1"), + NodeId.Parse("ns=2;s=TestNode2") + }; + VariableNode[] nodes = + [ + new VariableNode + { + NodeId = nodeIds[0], + NodeClass = NodeClass.Variable, + AccessLevel = 1, + DataType = NodeId.Parse("ns=2;s=TestDataType1"), + Description = "TestDescription1", + DisplayName = "TestDisplayName1", + BrowseName = "TestBrowseName1", + UserAccessLevel = 1 + }, + new VariableNode + { + NodeId = nodeIds[1], + NodeClass = NodeClass.Variable, + AccessLevel = 1, + DataType = NodeId.Parse("ns=2;s=TestDataType2"), + Description = "TestDescription2", + DisplayName = "TestDisplayName2", + BrowseName = "TestBrowseName2", + UserAccessLevel = 1 + } + ]; + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns((request, ct) => + { + var results = new DataValueCollection(request.NodesToRead + .Select(r => + { + var value = new DataValue(); + if (r.AttributeId == Attributes.MinimumSamplingInterval) + { + return new DataValue(StatusCodes.BadNotReadable); + } + if (r.AttributeId == Attributes.Description) + { + return new DataValue(StatusCodes.BadAttributeIdInvalid); + } + if (r.NodeId == nodeIds[0]) + { + nodes[0].Read(null!, r.AttributeId, value); + } + else + { + nodes[1].Read(null!, r.AttributeId, value); + } + return value; + })); + return new ValueTask(new ReadResponse + { + Results = results, + DiagnosticInfos = [] + }); + }) + .Verifiable(Times.Exactly(2)); + + // Act + ResultSet result = await sut.FetchNodesAsync(null, nodeIds).ConfigureAwait(false); + + // Assert + Assert.That(result.Results.Count, Is.EqualTo(2)); + Assert.That(Utils.IsEqual(nodes[0], result.Results[0]), Is.False); + Assert.That(Utils.IsEqual(nodes[1], result.Results[1]), Is.False); + Assert.That(result.Errors, Is.All.EqualTo(ServiceResult.Good)); + } + + [Test] + public async Task FetchNodeAsyncShouldReturnNodeAsync() + { + // Arrange + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeId = NodeId.Parse("ns=2;s=TestNode"); + var node = new VariableNode + { + NodeId = nodeId, + NodeClass = NodeClass.Variable, + AccessLevel = 1, + DataType = NodeId.Parse("ns=2;s=TestDataType"), + Description = "TestDescription", + DisplayName = "TestDisplayName", + BrowseName = "TestBrowseName", + UserAccessLevel = 1 + }; + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns((request, ct) => + { + var results = new DataValueCollection(request.NodesToRead + .Select(r => + { + var value = new DataValue(); + node.Read(null!, r.AttributeId, value); + return value; + })); + return new ValueTask(new ReadResponse + { + Results = results, + DiagnosticInfos = [] + }); + }) + .Verifiable(Times.Once); + + // Act + Node result = await sut.FetchNodeAsync(null, nodeId).ConfigureAwait(false); + + // Assert + Assert.That(Utils.IsEqual(node, result), Is.True); + + session.Channel.Verify(); + } + + [Test] + public async Task FetchNodesAsyncShouldReturnEmptyResultSetForEmptyNodeIdsAsync() + { + // Arrange + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeIds = new List(); + + // Act + ResultSet result = await sut.FetchNodesAsync(null, nodeIds).ConfigureAwait(false); + + // Assert + Assert.That(result.Results, Is.Empty); + Assert.That(result.Errors, Is.Empty); + } + + [Test] + public void FetchNodeAsyncShouldThrowServiceResultExceptionForBadStatusCode() + { + // Arrange + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeId = NodeId.Parse("ns=2;s=TestNode"); + var node = new VariableNode + { + NodeId = nodeId, + NodeClass = NodeClass.Variable, + AccessLevel = 1, + DataType = NodeId.Parse("ns=2;s=TestDataType"), + Description = "TestDescription", + DisplayName = "TestDisplayName", + BrowseName = "TestBrowseName", + UserAccessLevel = 1 + }; + var diagnosticInfos = new DiagnosticInfoCollection(); + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns((request, ct) => + new ValueTask(new ReadResponse + { + Results = [.. request.NodesToRead + .Select(r => new DataValue(StatusCodes.BadAlreadyExists))], + DiagnosticInfos = [.. request.NodesToRead.Select(_ => new DiagnosticInfo())] + })) + .Verifiable(Times.Once); + + ServiceResultException sre = Assert.ThrowsAsync( + async () => await sut.FetchNodeAsync(null, nodeId, + NodeClass.Unspecified).ConfigureAwait(false)); + Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadAlreadyExists)); + + session.Channel.Verify(); + } + + [Test] + public async Task FetchNodesAsyncShouldReturnErrorsForBadStatusCodesAsync() + { + // Arrange + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeIds = new List + { + NodeId.Parse("ns=2;s=TestNode1"), + NodeId.Parse("ns=2;s=TestNode2") + }; + VariableNode[] nodes = + [ + new VariableNode + { + NodeId = nodeIds[0], + NodeClass = NodeClass.Variable, + AccessLevel = 1, + DataType = NodeId.Parse("ns=2;s=TestDataType1"), + Description = "TestDescription1", + DisplayName = "TestDisplayName1", + BrowseName = "TestBrowseName1", + UserAccessLevel = 1 + }, + new VariableNode + { + NodeId = nodeIds[1], + NodeClass = NodeClass.Variable, + AccessLevel = 1, + DataType = NodeId.Parse("ns=2;s=TestDataType2"), + Description = "TestDescription2", + DisplayName = "TestDisplayName2", + BrowseName = "TestBrowseName2", + UserAccessLevel = 1 + } + ]; + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns((request, ct) => + { + var results = new DataValueCollection(request.NodesToRead + .Select(r => + { + if (r.NodeId == nodeIds[0]) + { + var value = new DataValue(); + nodes[0].Read(null!, r.AttributeId, value); + return value; + } + return new DataValue(StatusCodes.BadUnexpectedError); + })); + return new ValueTask(new ReadResponse + { + Results = results, + DiagnosticInfos = [.. results.Select(r => new DiagnosticInfo())] + }); + }) + .Verifiable(Times.Exactly(2)); + + // Act + ResultSet result = await sut.FetchNodesAsync(null, nodeIds).ConfigureAwait(false); + + // Assert + Assert.That(result.Results.Count, Is.EqualTo(2)); + Assert.That(Utils.IsEqual(nodes[0], result.Results[0]), Is.True); + Assert.That(Utils.IsEqual(nodes[1], result.Results[1]), Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(2)); + Assert.That(result.Errors[0], Is.EqualTo(ServiceResult.Good)); + Assert.That(result.Errors[1].StatusCode, Is.EqualTo(StatusCodes.BadUnexpectedError)); + } + + [Test] + public async Task FetchNodesAsyncShouldReturnErrorsForBadNodeClassTypeAsync() + { + // Arrange + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeIds = new List + { + NodeId.Parse("ns=2;s=TestNode1"), + NodeId.Parse("ns=2;s=TestNode2") + }; + VariableNode[] nodes = + [ + new VariableNode + { + NodeId = nodeIds[0], + NodeClass = NodeClass.Variable, + AccessLevel = 1, + DataType = NodeId.Parse("ns=2;s=TestDataType1"), + Description = "TestDescription1", + DisplayName = "TestDisplayName1", + BrowseName = "TestBrowseName1", + UserAccessLevel = 1 + }, + new VariableNode + { + NodeId = nodeIds[1], + NodeClass = NodeClass.Variable, + AccessLevel = 1, + DataType = NodeId.Parse("ns=2;s=TestDataType2"), + Description = "TestDescription2", + DisplayName = "TestDisplayName2", + BrowseName = "TestBrowseName2", + UserAccessLevel = 1 + } + ]; + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns((request, ct) => + { + var results = new DataValueCollection(request.NodesToRead + .Select(r => + { + if (r.AttributeId == Attributes.NodeClass) + { + return new DataValue(new Variant("Badclass")); + } + var value = new DataValue(); + if (r.NodeId == nodeIds[0]) + { + nodes[0].Read(null!, r.AttributeId, value); + } + else + { + nodes[1].Read(null!, r.AttributeId, value); + } + return value; + })); + return new ValueTask(new ReadResponse + { + Results = results, + DiagnosticInfos = [.. results.Select(r => new DiagnosticInfo())] + }); + }) + .Verifiable(Times.Exactly(2)); + + // Act + ResultSet result = await sut.FetchNodesAsync(null, nodeIds).ConfigureAwait(false); + + // Assert + Assert.That(result.Results.Count, Is.EqualTo(2)); + Assert.That(Utils.IsEqual(nodes[0], result.Results[0]), Is.False); + Assert.That(Utils.IsEqual(nodes[1], result.Results[1]), Is.False); + Assert.That(result.Errors.Count, Is.EqualTo(2)); + Assert.That(result.Errors[0].StatusCode, Is.EqualTo(StatusCodes.BadUnexpectedError)); + Assert.That(result.Errors[1].StatusCode, Is.EqualTo(StatusCodes.BadUnexpectedError)); + } + + [Test] + public void FetchNodeAsyncShouldHandleCancellation() + { + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeId = NodeId.Parse("ns=2;s=TestNode"); + var cts = new CancellationTokenSource(); + cts.Cancel(); + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.Is(ct => ct.IsCancellationRequested))) + .ThrowsAsync(new OperationCanceledException()) + .Verifiable(Times.Once); + + Assert.ThrowsAsync(async () => + await sut.FetchNodeAsync(null, nodeId, ct: cts.Token).ConfigureAwait(false)); + } + + [Test] + public void FetchNodesAsyncShouldHandleCancellation() + { + // Arrange + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeIds = new List + { + NodeId.Parse("ns=2;s=TestNode1"), + NodeId.Parse("ns=2;s=TestNode2") + }; + var cts = new CancellationTokenSource(); + cts.Cancel(); + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.Is(ct => ct.IsCancellationRequested))) + .ThrowsAsync(new OperationCanceledException()) + .Verifiable(Times.Once); + + Assert.ThrowsAsync(async () => await sut.FetchNodesAsync( + null, + nodeIds, + NodeClass.Unspecified, + ct: cts.Token).ConfigureAwait(false)); + } + + [Test] + public async Task FetchNodeAsyncShouldProcessDiagnosticInfoAsync() + { + // Arrange + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeId = NodeId.Parse("ns=2;s=TestNode"); + var node = new VariableNode + { + NodeId = nodeId, + NodeClass = NodeClass.Variable, + AccessLevel = 1, + DataType = NodeId.Parse("ns=2;s=TestDataType"), + Description = "TestDescription", + DisplayName = "TestDisplayName", + BrowseName = "TestBrowseName", + UserAccessLevel = 1 + }; + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns((request, ct) => + { + var results = new DataValueCollection(request.NodesToRead + .Select(r => + { + var value = new DataValue(); + node.Read(null!, r.AttributeId, value); + return value; + })); + return new ValueTask(new ReadResponse + { + Results = results, + DiagnosticInfos = [.. results.Select(_ => new DiagnosticInfo())] + }); + }) + .Verifiable(Times.Once); + + // Act + Node result = await sut.FetchNodeAsync(null, nodeId).ConfigureAwait(false); + + // Assert + Assert.That(Utils.IsEqual(node, result), Is.True); + + session.Channel.Verify(); + } + + [Test] + public async Task FetchNodesAsyncShouldProcessDiagnosticInfoAsync() + { + // Arrange + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeIds = new List + { + NodeId.Parse("ns=2;s=TestNode1"), + NodeId.Parse("ns=2;s=TestNode2") + }; + VariableNode[] nodes = + [ + new VariableNode + { + NodeId = nodeIds[0], + NodeClass = NodeClass.Variable, + AccessLevel = 1, + DataType = NodeId.Parse("ns=2;s=TestDataType1"), + Description = "TestDescription1", + DisplayName = "TestDisplayName1", + BrowseName = "TestBrowseName1", + UserAccessLevel = 1 + }, + new VariableNode + { + NodeId = nodeIds[1], + NodeClass = NodeClass.Variable, + AccessLevel = 1, + DataType = NodeId.Parse("ns=2;s=TestDataType2"), + Description = "TestDescription2", + DisplayName = "TestDisplayName2", + BrowseName = "TestBrowseName2", + UserAccessLevel = 1 + } + ]; + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns((request, ct) => + { + var results = new DataValueCollection(request.NodesToRead + .Select(r => + { + var value = new DataValue(); + if (r.NodeId == nodeIds[0]) + { + nodes[0].Read(null!, r.AttributeId, value); + } + else + { + nodes[1].Read(null!, r.AttributeId, value); + } + return value; + })); + return new ValueTask(new ReadResponse + { + Results = results, + DiagnosticInfos = [.. results.Select(r => new DiagnosticInfo())] + }); + }) + .Verifiable(Times.Exactly(2)); + + // Act + ResultSet result = await sut.FetchNodesAsync(null, nodeIds).ConfigureAwait(false); + + // Assert + Assert.That(result.Results.Count, Is.EqualTo(2)); + Assert.That(Utils.IsEqual(nodes[0], result.Results[0]), Is.True); + Assert.That(Utils.IsEqual(nodes[1], result.Results[1]), Is.True); + Assert.That(result.Errors.Count, Is.EqualTo(2)); + Assert.That(result.Errors, Is.All.EqualTo(ServiceResult.Good)); + + session.Channel.Verify(); + } + + [Test] + public async Task FetchReferencesAsyncShouldReturnResultSetAsync() + { + // Arrange + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeId = NodeId.Parse("ns=2;s=TestNode"); + var references = new ReferenceDescriptionCollection + { + new ReferenceDescription + { + NodeId = new ExpandedNodeId("ns=2;s=TestNode1"), + BrowseName = "TestBrowseName1", + DisplayName = "TestDisplayName1", + NodeClass = NodeClass.Variable + }, + new ReferenceDescription + { + NodeId = new ExpandedNodeId("ns=2;s=TestNode2"), + BrowseName = "TestBrowseName2", + DisplayName = "TestDisplayName2", + NodeClass = NodeClass.Variable + } + }; + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new BrowseResponse + { + Results = + [ + new BrowseResult + { + References = references + } + ], + DiagnosticInfos = [] + })) + .Verifiable(Times.Once); + + // Act + ReferenceDescriptionCollection result = await sut.FetchReferencesAsync(null, nodeId).ConfigureAwait(false); + + // Assert + Assert.That(result, Is.EquivalentTo(references)); + + session.Channel.Verify(); + } + + [Test] + public async Task FetchReferencesAsyncShouldReturnEmptyResultSetForEmptyNodeIdsAsync() + { + // Arrange + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeIds = new List(); + + // Act + ResultSet result = + await sut.FetchReferencesAsync(null, nodeIds).ConfigureAwait(false); + + // Assert + Assert.That(result.Results, Is.Empty); + Assert.That(result.Errors, Is.Empty); + } + + [Test] + public async Task FetchReferencesAsyncShouldReturnErrorsForBadStatusCodesAsync() + { + // Arrange + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeIds = new List + { + NodeId.Parse("ns=2;s=TestNode1"), + NodeId.Parse("ns=2;s=TestNode2") + }; + var references = new ReferenceDescriptionCollection + { + new ReferenceDescription + { + NodeId = new ExpandedNodeId("ns=2;s=TestNode1"), + BrowseName = "TestBrowseName1", + DisplayName = "TestDisplayName1", + NodeClass = NodeClass.Variable + }, + new ReferenceDescription + { + NodeId = new ExpandedNodeId("ns=2;s=TestNode2"), + BrowseName = "TestBrowseName2", + DisplayName = "TestDisplayName2", + NodeClass = NodeClass.Variable + } + }; + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new BrowseResponse + { + Results = + [ + new BrowseResult + { + References = references, + StatusCode = StatusCodes.Bad + }, + new BrowseResult + { + References = references, + StatusCode = StatusCodes.Bad + } + ], + DiagnosticInfos = [] + })) + .Verifiable(Times.Once); + + // Act + ResultSet result = await sut.FetchReferencesAsync(null, nodeIds, + CancellationToken.None).ConfigureAwait(false); + + // Assert + Assert.That(result.Results.Count, Is.EqualTo(2)); + Assert.That(Utils.IsEqual(result.Results[0], references), Is.True); + Assert.That(Utils.IsEqual(result.Results[1], references), Is.True); + Assert.That(result.Errors.Count, Is.EqualTo(2)); + Assert.That(result.Errors[0].StatusCode, Is.EqualTo(StatusCodes.Bad)); + Assert.That(result.Errors[1].StatusCode, Is.EqualTo(StatusCodes.Bad)); + } + + [Test] + public void FetchReferencesAsyncShouldHandleCancellation() + { + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeIds = new List + { + NodeId.Parse("ns=2;s=TestNode1"), + NodeId.Parse("ns=2;s=TestNode2") + }; + var cts = new CancellationTokenSource(); + cts.Cancel(); + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.Is(ct => ct.IsCancellationRequested))) + .ThrowsAsync(new OperationCanceledException()) + .Verifiable(Times.Once); + + Assert.ThrowsAsync( + async () => await sut.FetchReferencesAsync(null, nodeIds, cts.Token).ConfigureAwait(false)); + } + + [Test] + public async Task FetchReferenceAsyncShouldReturnReferenceDescriptionAsync() + { + // Arrange + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeId = NodeId.Parse("ns=2;s=TestNode"); + var reference = new ReferenceDescription + { + NodeId = new ExpandedNodeId("ns=2;s=TestNode1"), + BrowseName = "TestBrowseName1", + DisplayName = "TestDisplayName1", + NodeClass = NodeClass.Variable + }; + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new BrowseResponse + { + Results = + [ + new BrowseResult + { + References = [reference] + } + ], + DiagnosticInfos = [] + })) + .Verifiable(Times.Once); + + // Act + ReferenceDescriptionCollection result = await sut.FetchReferencesAsync(null, nodeId).ConfigureAwait(false); + + // Assert + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result[0], Is.EqualTo(reference)); + + session.Channel.Verify(); + } + + [Test] + public void FetchReferenceAsyncShouldThrowServiceResultExceptionForBadStatusCode() + { + // Arrange + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeId = NodeId.Parse("ns=2;s=TestNode"); + var reference = new ReferenceDescription + { + NodeId = new ExpandedNodeId("ns=2;s=TestNode1"), + BrowseName = "TestBrowseName1", + DisplayName = "TestDisplayName1", + NodeClass = NodeClass.Variable + }; + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new BrowseResponse + { + ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.Bad }, + Results = + [ + new BrowseResult + { + References = [reference] + } + ], + DiagnosticInfos = [] + })) + .Verifiable(Times.Once); + + // Act + Assert.ThrowsAsync( + async () => await sut.FetchReferencesAsync(null, nodeId).ConfigureAwait(false)); + + session.Channel.Verify(); + } + + [Test] + public void FetchReferenceAsyncShouldHandleCancellation() + { + // Arrange + var session = SessionMock.Create(); + var sut = new NodeCacheContext(session); + var nodeId = NodeId.Parse("ns=2;s=TestNode"); + var cts = new CancellationTokenSource(); + cts.Cancel(); + + session.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.Is(ct => ct.IsCancellationRequested))) + .ThrowsAsync(new OperationCanceledException()) + .Verifiable(Times.Once); + + Assert.ThrowsAsync( + async () => await sut.FetchReferencesAsync(null, nodeId, cts.Token).ConfigureAwait(false)); + } + } +} diff --git a/Tests/Opc.Ua.Client.Tests/ReverseConnectTest.cs b/Tests/Opc.Ua.Client.Tests/ReverseConnectTest.cs index 7a0bacde3a..641be85c73 100644 --- a/Tests/Opc.Ua.Client.Tests/ReverseConnectTest.cs +++ b/Tests/Opc.Ua.Client.Tests/ReverseConnectTest.cs @@ -56,7 +56,6 @@ public class ReverseConnectTest : ClientTestFramework [DatapointSource] public static readonly TelemetryParameterizable[] SessionFactories = [ - TelemetryParameterizable.Create(t => new TraceableSessionFactory(t)), TelemetryParameterizable.Create(t => new TestableSessionFactory(t)), TelemetryParameterizable.Create(t => new DefaultSessionFactory(t)) ]; diff --git a/Tests/Opc.Ua.Client.Tests/SessionMock.cs b/Tests/Opc.Ua.Client.Tests/SessionMock.cs new file mode 100644 index 0000000000..9067a38348 --- /dev/null +++ b/Tests/Opc.Ua.Client.Tests/SessionMock.cs @@ -0,0 +1,135 @@ +/* ======================================================================== + * 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 Moq; +using Opc.Ua.Configuration; +using Opc.Ua.Tests; + +namespace Opc.Ua.Client.Tests +{ + /// + /// Session with channel mock + /// + public sealed class SessionMock : Session + { + /// + /// Get private field m_serverNonce from base class using reflection + /// + internal byte[] ServerNonce => + (byte[])typeof(Session) + .GetField( + "m_serverNonce", + System.Reflection.BindingFlags.NonPublic | + System.Reflection.BindingFlags.Instance) + .GetValue(this); + + /// + /// Create the mock + /// + private SessionMock( + Mock channel, + ApplicationConfiguration configuration, + ConfiguredEndpoint endpoint) + : base( + channel.Object, + configuration, + endpoint, + null) + { + Channel = channel; + } + + /// + /// Create default mock + /// + /// + public static SessionMock Create(EndpointDescription endpoint = null) + { + ITelemetryContext telemetry = NUnitTelemetryContext.Create(); + var channel = new Mock(); + channel + .SetupGet(s => s.MessageContext) + .Returns(new ServiceMessageContext(telemetry)); + channel + .SetupGet(s => s.SupportedFeatures) + .Returns(TransportChannelFeatures.Reconnect); + var configuration = new ApplicationConfiguration(telemetry) + { + ClientConfiguration = new ClientConfiguration() // TODO: Reasonable defaults! + }; + + endpoint ??= new EndpointDescription + { + SecurityMode = MessageSecurityMode.None, + SecurityPolicyUri = SecurityPolicies.None, + EndpointUrl = "opc.tcp://localhost:4840", + UserIdentityTokens = + [ + new UserTokenPolicy() + ] + }; + + // TODO: Allow mocking of application certificate loading + var application = new ApplicationInstance(configuration, telemetry); + if (endpoint.SecurityMode != MessageSecurityMode.None) + { + application.CheckApplicationInstanceCertificatesAsync(true).AsTask().GetAwaiter().GetResult(); + } + + return new SessionMock(channel, configuration, + new ConfiguredEndpoint(null, endpoint ?? + new EndpointDescription + { + SecurityMode = MessageSecurityMode.None, + SecurityPolicyUri = SecurityPolicies.None, + EndpointUrl = "opc.tcp://localhost:4840", + UserIdentityTokens = + [ + new UserTokenPolicy() + ] + })); + } + + internal void SetConnected() + { + SessionCreated(new NodeId("s=connected"), new NodeId("s=auth")); + RenewUserIdentity += Sut_RenewUserIdentity; + } + + private IUserIdentity Sut_RenewUserIdentity(ISession session, IUserIdentity identity) + { + return identity ?? new UserIdentity(); + } + + /// + /// Channel mock + /// + public Mock Channel { get; } + } +} diff --git a/Tests/Opc.Ua.Client.Tests/SessionTests.cs b/Tests/Opc.Ua.Client.Tests/SessionTests.cs new file mode 100644 index 0000000000..a06304a614 --- /dev/null +++ b/Tests/Opc.Ua.Client.Tests/SessionTests.cs @@ -0,0 +1,1343 @@ +/* ======================================================================== + * 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; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Moq; +using NUnit.Framework; + +namespace Opc.Ua.Client.Tests +{ + [TestFixture] + [Category("Client")] + [Category("Session")] + [SetCulture("en-us")] + [SetUICulture("en-us")] + public sealed class SessionTests + { + [Test] + public async Task FetchOperationLimitsAsyncShouldFetchAllOperationLimitsAsync() + { + var sut = SessionMock.Create(); + CancellationToken ct = CancellationToken.None; + + var dataValues = new DataValueCollection + { + new DataValue(new Variant(2000u)), + new DataValue(new Variant(3000u)), + new DataValue(new Variant(4000u)), + new DataValue(new Variant(1000u)), + new DataValue(new Variant(5000u)), + new DataValue(new Variant(6000u)), + new DataValue(new Variant(7000u)), + new DataValue(new Variant(8000u)), + new DataValue(new Variant(9000u)), + new DataValue(new Variant(10000u)), + new DataValue(new Variant(11000u)), + new DataValue(new Variant(12000u)), + new DataValue(new Variant((ushort)13000)), + new DataValue(new Variant((ushort)14000u)), + new DataValue(new Variant((ushort)15000u)), + new DataValue(new Variant(16000u)), + new DataValue(new Variant(17000u)), + new DataValue(new Variant(18000u)), + new DataValue(new Variant((double)19000.0)), + new DataValue(new Variant(20000u)), + new DataValue(new Variant(21000u)), + new DataValue(new Variant(22000u)), + new DataValue(new Variant(23000u)), + new DataValue(new Variant(24000u)), + new DataValue(new Variant(25000u)), + new DataValue(new Variant(26000u)), + new DataValue(new Variant(27000u)) + }; + + sut.Channel + .SetupSequence(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new ReadResponse + { + Results = [new DataValue(new Variant(1000u))], + DiagnosticInfos = [] + })) + .Returns(new ValueTask(new ReadResponse + { + Results = dataValues, + DiagnosticInfos = [] + })); + + // Act + await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false); + + // Assert + Assert.That(sut.OperationLimits.MaxNodesPerRead, Is.EqualTo(1000)); + Assert.That(sut.OperationLimits.MaxNodesPerHistoryReadData, Is.EqualTo(2000)); + Assert.That(sut.OperationLimits.MaxNodesPerHistoryReadEvents, Is.EqualTo(3000)); + Assert.That(sut.OperationLimits.MaxNodesPerWrite, Is.EqualTo(4000)); + Assert.That(sut.OperationLimits.MaxNodesPerHistoryUpdateData, Is.EqualTo(5000)); + Assert.That(sut.OperationLimits.MaxNodesPerHistoryUpdateEvents, Is.EqualTo(6000)); + Assert.That(sut.OperationLimits.MaxNodesPerMethodCall, Is.EqualTo(7000)); + Assert.That(sut.OperationLimits.MaxNodesPerBrowse, Is.EqualTo(8000)); + Assert.That(sut.OperationLimits.MaxNodesPerRegisterNodes, Is.EqualTo(9000)); + Assert.That(sut.OperationLimits.MaxNodesPerNodeManagement, Is.EqualTo(10000)); + Assert.That(sut.OperationLimits.MaxMonitoredItemsPerCall, Is.EqualTo(11000)); + Assert.That(sut.OperationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds, Is.EqualTo(12000)); + Assert.That(sut.ServerCapabilities.MaxBrowseContinuationPoints, Is.EqualTo(13000)); + Assert.That(sut.ServerCapabilities.MaxHistoryContinuationPoints, Is.EqualTo(14000)); + Assert.That(sut.ServerCapabilities.MaxQueryContinuationPoints, Is.EqualTo(15000)); + Assert.That(sut.ServerCapabilities.MaxStringLength, Is.EqualTo(16000)); + Assert.That(sut.ServerCapabilities.MaxArrayLength, Is.EqualTo(17000)); + Assert.That(sut.ServerCapabilities.MaxByteStringLength, Is.EqualTo(18000)); + Assert.That(sut.ServerCapabilities.MinSupportedSampleRate, Is.EqualTo(19000.0)); + Assert.That(sut.ServerCapabilities.MaxSessions, Is.EqualTo(20000)); + Assert.That(sut.ServerCapabilities.MaxSubscriptions, Is.EqualTo(21000)); + Assert.That(sut.ServerCapabilities.MaxMonitoredItems, Is.EqualTo(22000)); + Assert.That(sut.ServerCapabilities.MaxMonitoredItemsPerSubscription, Is.EqualTo(23000)); + Assert.That(sut.ServerCapabilities.MaxMonitoredItemsQueueSize, Is.EqualTo(24000)); + Assert.That(sut.ServerCapabilities.MaxSubscriptionsPerSession, Is.EqualTo(25000)); + Assert.That(sut.ServerCapabilities.MaxWhereClauseParameters, Is.EqualTo(26000)); + Assert.That(sut.ServerCapabilities.MaxSelectClauseParameters, Is.EqualTo(27000)); + + sut.Channel.Verify(); + } + + [Test] + public void FetchOperationLimitsAsyncShouldHandleEmptyResponse() + { + // Arrange + var sut = SessionMock.Create(); + CancellationToken ct = CancellationToken.None; + + var dataValues = new DataValueCollection(); + var diagnosticInfos = new DiagnosticInfoCollection(); + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new ReadResponse + { + Results = dataValues, + DiagnosticInfos = diagnosticInfos + })) + .Verifiable(Times.Once); + + Assert.ThrowsAsync( + async () => await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false)); + + sut.Channel.Verify(); + } + + [Test] + public void FetchOperationLimitsAsyncShouldHandlePartialData() + { + // Arrange + var sut = SessionMock.Create(); + CancellationToken ct = CancellationToken.None; + + var dataValues = new DataValueCollection + { + new DataValue(new Variant(1000u)), + new DataValue(new Variant(2000u)), + new DataValue(new Variant(3000u)) + }; + + sut.Channel + .SetupSequence(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new ReadResponse + { + Results = [new DataValue(new Variant(1000u))], + DiagnosticInfos = [] + })) + .Returns(new ValueTask(new ReadResponse + { + Results = dataValues, + DiagnosticInfos = [] + })); + + Assert.ThrowsAsync( + async () => await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false)); + + sut.Channel.Verify(); + } + + [Test] + public void FetchOperationLimitsAsyncShouldHandleErrors() + { + // Arrange + var sut = SessionMock.Create(); + CancellationToken ct = CancellationToken.None; + + var dataValues = new DataValueCollection + { + new DataValue(StatusCodes.BadUnexpectedError) + }; + + var diagnosticInfos = new DiagnosticInfoCollection(); + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new ReadResponse + { + Results = dataValues, + DiagnosticInfos = diagnosticInfos + })) + .Verifiable(Times.Exactly(2)); + + Assert.ThrowsAsync( + async () => await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false)); + + sut.Channel.Verify(); + } + + [Test] + public void FetchOperationLimitsAsyncShouldThrowWhenInvalidDataTypes() + { + // Arrange + var sut = SessionMock.Create(); + CancellationToken ct = CancellationToken.None; + + var dataValues = new DataValueCollection + { + new DataValue("InvalidDataType") + }; + + var diagnosticInfos = new DiagnosticInfoCollection(); + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new ReadResponse + { + Results = dataValues, + DiagnosticInfos = diagnosticInfos + })) + .Verifiable(Times.Exactly(2)); + + Assert.ThrowsAsync( + async () => await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false)); + + sut.Channel.Verify(); + } + + [Test] + public void FetchOperationLimitsAsyncShouldHandleTimeout() + { + var sut = SessionMock.Create(); + CancellationToken ct = new CancellationTokenSource(100).Token; // Set a short timeout + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .ThrowsAsync(new TaskCanceledException()) + .Verifiable(Times.Once); + + Assert.ThrowsAsync( + async () => await sut.FetchOperationLimitsAsync(ct).ConfigureAwait(false)); + + sut.Channel.Verify(); + } + + [Test] + public async Task FetchNamespaceTablesAsyncShouldFetchAndUpdateTablesAsync() + { + var sut = SessionMock.Create(); + CancellationToken ct = CancellationToken.None; + + var namespaceArray = new DataValue(new Variant([Ua.Namespaces.OpcUa, "http://namespace2"])); + var serverArray = new DataValue(new Variant(["http://server1", "http://server2"])); + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new ReadResponse + { + Results = [namespaceArray, serverArray], + DiagnosticInfos = [] + })) + .Verifiable(Times.Once); + + // Act + await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false); + + // Assert + Assert.That(sut.NamespaceUris.ToArray(), Is.EquivalentTo([Ua.Namespaces.OpcUa, "http://namespace2"])); + Assert.That(sut.ServerUris.ToArray(), Is.EquivalentTo(["http://server1", "http://server2"])); + + sut.Channel.Verify(); + } + + [Test] + public async Task FetchNamespaceTablesAsyncShouldFetchAndUpdateTablesAndLogDifferences1Async() + { + // Arrange + var sut = SessionMock.Create(); + CancellationToken ct = CancellationToken.None; + + var namespaceArray1 = new DataValue(new Variant([Ua.Namespaces.OpcUa, "http://namespace2", "http://namespace3"])); + var serverArray1 = new DataValue(new Variant(["http://server1", "http://server2"])); + var namespaceArray2 = new DataValue(new Variant([Ua.Namespaces.OpcUa, "http://namespace3", "http://namespace2"])); + var serverArray2 = new DataValue(new Variant(["http://server1", "http://server2", "http://server3"])); + + sut.Channel + .SetupSequence(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new ReadResponse + { + Results = [namespaceArray1, serverArray1], + DiagnosticInfos = [] + })) + .Returns(new ValueTask(new ReadResponse + { + Results = [namespaceArray2, serverArray2], + DiagnosticInfos = [] + })); + + // Act + await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false); + + // Assert + Assert.That(sut.NamespaceUris.ToArray(), Is.EquivalentTo([Ua.Namespaces.OpcUa, "http://namespace2", "http://namespace3"])); + Assert.That(sut.ServerUris.ToArray(), Is.EquivalentTo(["http://server1", "http://server2"])); + + // Act + await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false); + + // Assert + Assert.That(sut.NamespaceUris.ToArray(), Is.EquivalentTo([Ua.Namespaces.OpcUa, "http://namespace3", "http://namespace2"])); + Assert.That(sut.ServerUris.ToArray(), Is.EquivalentTo(["http://server1", "http://server2", "http://server3"])); + + sut.Channel.Verify(); + } + + [Test] + public async Task FetchNamespaceTablesAsyncShouldFetchAndUpdateTablesAndLogDifferences2Async() + { + // Arrange + var sut = SessionMock.Create(); + CancellationToken ct = CancellationToken.None; + + var namespaceArray1 = new DataValue(new Variant([Ua.Namespaces.OpcUa, "http://namespace2", "http://namespace3"])); + var serverArray1 = new DataValue(new Variant(["http://server1", "http://server2"])); + var namespaceArray2 = new DataValue(new Variant([Ua.Namespaces.OpcUa, "http://namespace3"])); + var serverArray2 = new DataValue(new Variant(["http://server1"])); + + sut.Channel + .SetupSequence(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new ReadResponse + { + Results = [namespaceArray1, serverArray1], + DiagnosticInfos = [] + })) + .Returns(new ValueTask(new ReadResponse + { + Results = [namespaceArray2, serverArray2], + DiagnosticInfos = [] + })); + + // Act + await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false); + + // Assert + Assert.That(sut.NamespaceUris.ToArray(), Is.EquivalentTo([Ua.Namespaces.OpcUa, "http://namespace2", "http://namespace3"])); + Assert.That(sut.ServerUris.ToArray(), Is.EquivalentTo(["http://server1", "http://server2"])); + + // Act + await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false); + + // Assert + Assert.That(sut.NamespaceUris.ToArray(), Is.EquivalentTo([Ua.Namespaces.OpcUa, "http://namespace3"])); + Assert.That(sut.ServerUris.ToArray(), Is.EquivalentTo(["http://server1"])); + + sut.Channel.Verify(); + } + + [Test] + public async Task FetchNamespaceTablesAsyncShouldHandlePartialSuccessAsync() + { + // Arrange + var sut = SessionMock.Create(); + CancellationToken ct = CancellationToken.None; + + var namespaceArray = new DataValue(new Variant([Ua.Namespaces.OpcUa, "http://namespace2"])); + var serverArray = new DataValue(StatusCodes.BadUnexpectedError); + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new ReadResponse + { + Results = [namespaceArray, serverArray], + DiagnosticInfos = [] + })) + .Verifiable(Times.Once); + + // Act + await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false); + + // Assert + Assert.That(sut.NamespaceUris.ToArray(), Is.EquivalentTo([Ua.Namespaces.OpcUa, "http://namespace2"])); + Assert.That(sut.ServerUris.ToArray(), Is.Empty); + + sut.Channel.Verify(); + } + + [Test] + public void FetchNamespaceTablesAsyncShouldThrowInCaseOfBadResponse() + { + // Arrange + var sut = SessionMock.Create(); + CancellationToken ct = CancellationToken.None; + + var namespaceArray = new DataValue(new Variant([Ua.Namespaces.OpcUa, "http://namespace2"])); + var serverArray = new DataValue(new Variant(["http://server1", "http://server2"])); + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new ReadResponse + { + ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.BadUnexpectedError }, + Results = [namespaceArray, serverArray], + DiagnosticInfos = [] + })) + .Verifiable(Times.Once); + + ServiceResultException sre = Assert.ThrowsAsync( + async () => await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false)); + Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadUnexpectedError)); + + sut.Channel.Verify(); + } + + [Test] + public void FetchNamespaceTablesAsyncShouldThrowWhenNamespaceArrayCouldNotBeRetrieved() + { + // Arrange + var sut = SessionMock.Create(); + CancellationToken ct = CancellationToken.None; + + var namespaceArray = new DataValue(StatusCodes.BadUnexpectedError); + var serverArray = new DataValue(StatusCodes.BadUnexpectedError); + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new ReadResponse + { + Results = [namespaceArray, serverArray], + DiagnosticInfos = [] + })) + .Verifiable(Times.Once); + + ServiceResultException sre = Assert.ThrowsAsync( + async () => await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false)); + Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadUnexpectedError)); + + sut.Channel.Verify(); + } + + [Test] + public void FetchNamespaceTablesAsyncShoulThrowWhenEmptyResponse() + { + // Arrange + var sut = SessionMock.Create(); + CancellationToken ct = CancellationToken.None; + + var dataValues = new DataValueCollection(); + var diagnosticInfos = new DiagnosticInfoCollection(); + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new ReadResponse + { + Results = dataValues, + DiagnosticInfos = diagnosticInfos + })) + .Verifiable(Times.Once); + + ServiceResultException sre = Assert.ThrowsAsync( + async () => await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false)); + Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadUnexpectedError)); + + sut.Channel.Verify(); + } + + [Test] + public void FetchNamespaceTablesAsyncShouldThrowWhenInvalidDataTypesForNamespaceTable() + { + // Arrange + var sut = SessionMock.Create(); + CancellationToken ct = CancellationToken.None; + + var namespaceArray = new DataValue(new Variant(12345)); + var serverArray = new DataValue(new Variant(67890)); + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new ReadResponse + { + Results = [namespaceArray, serverArray], + DiagnosticInfos = [] + })) + .Verifiable(Times.Once); + + ServiceResultException sre = Assert.ThrowsAsync( + async () => await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false)); + Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadTypeMismatch)); + + sut.Channel.Verify(); + } + + [Test] + public void FetchNamespaceTablesAsyncShouldThrowWhenInvalidDataTypeForServerUrls() + { + // Arrange + var sut = SessionMock.Create(); + CancellationToken ct = CancellationToken.None; + + var namespaceArray = new DataValue(new Variant([Ua.Namespaces.OpcUa, "http://namespace2"])); + var serverArray = new DataValue(new Variant(67890)); + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new ReadResponse + { + Results = [namespaceArray, serverArray], + DiagnosticInfos = [] + })) + .Verifiable(Times.Once); + + ServiceResultException sre = Assert.ThrowsAsync( + async () => await sut.FetchNamespaceTablesAsync(ct).ConfigureAwait(false)); + Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadTypeMismatch)); + + sut.Channel.Verify(); + } + + [Test] + public async Task CloseAsyncShouldCloseSessionAndChannelSuccessfullyAsync() + { + // Arrange + var sut = SessionMock.Create(); + sut.SetConnected(); + CancellationToken ct = CancellationToken.None; + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new CloseSessionResponse + { + ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.Good } + })) + .Verifiable(Times.Once); + + sut.Channel + .Setup(c => c.CloseAsync(It.IsAny())) + .Returns(new ValueTask()) + .Verifiable(Times.Once); + + // Act + StatusCode result = await sut.CloseAsync(ct).ConfigureAwait(false); + + // Assert + Assert.That(result, Is.EqualTo(StatusCodes.Good)); + sut.Channel.Verify(); + } + + [Test] + public async Task CloseAsyncShouldHandleAlreadyClosedSessionAsync() + { + // Arrange + var sut = SessionMock.Create(); + CancellationToken ct = CancellationToken.None; + + sut.Dispose(); + + // Act + StatusCode result = await sut.CloseAsync(ct).ConfigureAwait(false); + + // Assert + Assert.That(result, Is.EqualTo(StatusCodes.Good)); + } + + [Test] + public async Task CloseAsyncShouldHandleErrorsDuringCloseAsync() + { + // Arrange + var sut = SessionMock.Create(); + sut.SetConnected(); + CancellationToken ct = CancellationToken.None; + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .ThrowsAsync(new ServiceResultException(StatusCodes.BadUnexpectedError)) + .Verifiable(Times.Once); + + sut.Channel + .Setup(c => c.CloseAsync(It.IsAny())) + .Returns(new ValueTask()) + .Verifiable(Times.Once); + + // Act + StatusCode result = await sut.CloseAsync(ct).ConfigureAwait(false); + + // Assert + Assert.That(result, Is.EqualTo(StatusCodes.BadUnexpectedError)); + sut.Channel.Verify(); + } + + [Test] + public async Task CloseAsyncShouldCloseSessionWithoutDeletingSubscriptionsAsync() + { + // Arrange + var sut = SessionMock.Create(); + sut.SetConnected(); + sut.DeleteSubscriptionsOnClose = false; + CancellationToken ct = CancellationToken.None; + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new CloseSessionResponse + { + ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.Good } + })) + .Verifiable(Times.Once); + + sut.Channel + .Setup(c => c.CloseAsync(It.IsAny())) + .Returns(new ValueTask()) + .Verifiable(Times.Once); + + // Act + StatusCode result = await sut.CloseAsync(ct).ConfigureAwait(false); + + // Assert + Assert.That(result, Is.EqualTo(StatusCodes.Good)); + sut.Channel.Verify(); + } + + [Test] + public async Task CloseAsyncShouldHandleChannelErrorsAsync() + { + // Arrange + var sut = SessionMock.Create(); + sut.SetConnected(); + CancellationToken ct = CancellationToken.None; + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new CloseSessionResponse + { + ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.Good } + })) + .Verifiable(Times.Once); + + sut.Channel + .Setup(c => c.CloseAsync(It.IsAny())) + .ThrowsAsync(new ServiceResultException(StatusCodes.BadUnexpectedError)) + .Verifiable(Times.Once); + + StatusCode result = await sut.CloseAsync(ct).ConfigureAwait(false); + + Assert.That(result, Is.EqualTo(StatusCodes.Good)); // Eats the error from channel close + sut.Channel.Verify(); + } + + [Test] + public async Task CloseAsyncShouldCloseSessionSuccessfullyAsync() + { + // Arrange + var sut = SessionMock.Create(); + sut.SetConnected(); + CancellationToken ct = CancellationToken.None; + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new CloseSessionResponse + { + ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.Good } + })) + .Verifiable(Times.Once); + + sut.Channel + .Setup(c => c.CloseAsync(It.IsAny())) + .Returns(new ValueTask()) + .Verifiable(Times.Once); + + // Act + StatusCode result = await sut.CloseAsync(ct).ConfigureAwait(false); + + // Assert + Assert.That(result, Is.EqualTo(StatusCodes.Good)); + sut.Channel.Verify(); + } + + [Test] + public async Task CloseAsyncShouldHandleErrorsDuringCloseSessionAsync() + { + // Arrange + var sut = SessionMock.Create(); + sut.SetConnected(); + CancellationToken ct = CancellationToken.None; + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .ThrowsAsync(new ServiceResultException(StatusCodes.BadUnexpectedError)) + .Verifiable(Times.Once); + sut.Channel + .Setup(c => c.CloseAsync(It.IsAny())) + .Returns(new ValueTask()) + .Verifiable(Times.Once); + + // Act + StatusCode result = await sut.CloseAsync(ct).ConfigureAwait(false); + + // Assert + Assert.That(result, Is.EqualTo(StatusCodes.BadUnexpectedError)); + sut.Channel.Verify(); + } + + [Test] + public async Task ReconnectAsyncShouldReconnectSuccessfullyAsync() + { + // Arrange + var sut = SessionMock.Create(); + sut.SetConnected(); + CancellationToken ct = CancellationToken.None; + + byte[] serverNonce = [1, 2, 3, 4]; + + sut.Channel + .Setup(c => c.ReconnectAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask()) + .Verifiable(Times.Once); + sut.Channel + .Setup(c => c.SupportedFeatures) + .Returns(TransportChannelFeatures.Reconnect); + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new ActivateSessionResponse + { + ServerNonce = serverNonce, + Results = [], + DiagnosticInfos = [] + })) + .Verifiable(Times.Once); + + await sut.ReconnectAsync(ct).ConfigureAwait(false); + + Assert.That(sut.ServerNonce, Is.EquivalentTo(serverNonce)); + sut.Channel.Verify(); + } + + [Test] + public void ReconnectAsyncShouldHandleReconnectFailure() + { + var sut = SessionMock.Create(); + sut.SetConnected(); + CancellationToken ct = CancellationToken.None; + + sut.Channel + .Setup(c => c.ReconnectAsync( + It.IsAny(), + It.IsAny())) + .ThrowsAsync(new ServiceResultException(StatusCodes.BadUnexpectedError)) + .Verifiable(Times.Once); + sut.Channel + .Setup(c => c.SupportedFeatures) + .Returns(TransportChannelFeatures.Reconnect); + + ServiceResultException sre = Assert.ThrowsAsync( + async () => await sut.ReconnectAsync(ct).ConfigureAwait(false)); + Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadUnexpectedError)); + + sut.Channel.Verify(); + } + + [Test] + public void ReconnectAsyncShouldHandleNoSupportedFeatures() + { + var sut = SessionMock.Create(); + sut.SetConnected(); + CancellationToken ct = CancellationToken.None; + + sut.Channel + .Setup(c => c.SupportedFeatures) + .Returns(TransportChannelFeatures.None); + + // TODO: Should properly mock channel creation + + Assert.ThrowsAsync( + async () => await sut.ReconnectAsync(ct).ConfigureAwait(false)); + + sut.Channel.Verify(); + } + + [Test] + public void ReconnectAsyncShouldHandleCancellation() + { + var sut = SessionMock.Create(); + sut.SetConnected(); + + sut.Channel + .Setup(c => c.ReconnectAsync( + It.IsAny(), + It.IsAny())) + .ThrowsAsync(new TaskCanceledException()) + .Verifiable(Times.Once); + sut.Channel + .Setup(c => c.SupportedFeatures) + .Returns(TransportChannelFeatures.Reconnect); + + Assert.ThrowsAsync( + async () => await sut.ReconnectAsync(default).ConfigureAwait(false)); + + sut.Channel.Verify(); + } + + [Test] + public async Task ReconnectAsyncShouldHandleServerResponseWithNullNonceAsync() + { + var sut = SessionMock.Create(); + sut.SetConnected(); + CancellationToken ct = CancellationToken.None; + + sut.Channel + .Setup(c => c.ReconnectAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask()) + .Verifiable(Times.Once); + sut.Channel + .Setup(c => c.SupportedFeatures) + .Returns(TransportChannelFeatures.Reconnect); + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new ActivateSessionResponse + { + ServerNonce = null, + Results = [], + DiagnosticInfos = [] + })) + .Verifiable(Times.Once); + + await sut.ReconnectAsync(ct).ConfigureAwait(false); + + Assert.That(sut.ServerNonce, Is.Null); + sut.Channel.Verify(); + } + + [Test] + public void ReconnectAsyncShouldThrowWithIncompatibleIdentity() + { + var ep = new EndpointDescription + { + SecurityMode = MessageSecurityMode.None, + SecurityPolicyUri = SecurityPolicies.None, + EndpointUrl = "opc.tcp://localhost:4840", + UserIdentityTokens = + [ + new UserTokenPolicy + { + PolicyId = "T", + TokenType = UserTokenType.Certificate + } + ] + }; + var sut = SessionMock.Create(ep); + sut.SetConnected(); + CancellationToken ct = CancellationToken.None; + + ServiceResultException sre = Assert.ThrowsAsync( + async () => await sut.ReconnectAsync(ct).ConfigureAwait(false)); + Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadIdentityTokenRejected)); + + sut.Channel.Verify(); + } + + [Test] + public void ReconnectAsyncShouldThrowWithBadActivationResponse() + { + var sut = SessionMock.Create(); + sut.SetConnected(); + CancellationToken ct = CancellationToken.None; + + sut.Channel + .Setup(c => c.ReconnectAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask()) + .Verifiable(Times.Once); + sut.Channel + .Setup(c => c.SupportedFeatures) + .Returns(TransportChannelFeatures.Reconnect); + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new ActivateSessionResponse + { + ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.BadSessionNotActivated }, + Results = [], + DiagnosticInfos = [] + })) + .Verifiable(Times.Once); + + ServiceResultException sre = Assert.ThrowsAsync( + async () => await sut.ReconnectAsync(ct).ConfigureAwait(false)); + Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadSessionNotActivated)); + + sut.Channel.Verify(); + } + + [Test] + public void ReconnectAsyncShouldThrowWhenTimingOut() + { + var sut = SessionMock.Create(); + sut.SetConnected(); + CancellationToken ct = CancellationToken.None; + + sut.Channel + .Setup(c => c.ReconnectAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask()) + .Verifiable(Times.Once); + sut.Channel + .Setup(c => c.SupportedFeatures) + .Returns(TransportChannelFeatures.Reconnect); + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .ThrowsAsync(new OperationCanceledException()) + .Verifiable(Times.Once); + + Assert.ThrowsAsync( + async () => await sut.ReconnectAsync(ct).ConfigureAwait(false)); + + sut.Channel.Verify(); + } + + [Test] + public async Task OpenAsyncShouldOpenSessionSuccessfullyAsync() + { + // Arrange + var ep = new EndpointDescription + { + SecurityMode = MessageSecurityMode.None, + SecurityPolicyUri = SecurityPolicies.None, + EndpointUrl = "opc.tcp://localhost:4840", + UserIdentityTokens = + [ + new UserTokenPolicy() + ] + }; + var sut = SessionMock.Create(ep); + CancellationToken ct = CancellationToken.None; + byte[] serverNonce = [1, 2, 3, 4]; + var authToken = NodeId.Parse("s=cookie"); + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new CreateSessionResponse + { + ServerNonce = serverNonce, + SessionId = NodeId.Parse("s=connected"), + AuthenticationToken = authToken, + ServerEndpoints = [ep] + })) + .Verifiable(Times.Once); + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.Is(r => r.RequestHeader.AuthenticationToken == authToken), + It.IsAny())) + .Returns(new ValueTask(new ActivateSessionResponse + { + ServerNonce = serverNonce, + Results = [], + DiagnosticInfos = [] + })) + .Verifiable(Times.Once); + + // Read limit and also first keep alive timers + sut.Channel + .Setup(c => c.SendRequestAsync( + It.Is(r => r.NodesToRead.Count == 1), + It.IsAny())) + .Returns(new ValueTask(new ReadResponse + { + Results = + [ + new (new Variant(0u)) + ], + DiagnosticInfos = [] + })) + .Verifiable(Times.AtLeastOnce); + + // Operation limits + sut.Channel + .Setup(c => c.SendRequestAsync( + It.Is(r => r.NodesToRead.Count == 27), + It.IsAny())) + .Returns(new ValueTask(new ReadResponse + { + Results = [.. Enumerable + .Range(0, 27) + .Select(_ => new DataValue(Variant.Null))], + DiagnosticInfos = [] + })) + .Verifiable(Times.Once); + + // Namespaces + sut.Channel + .Setup(c => c.SendRequestAsync( + It.Is(r => r.NodesToRead.Count == 2), + It.IsAny())) + .Returns(new ValueTask(new ReadResponse + { + Results = + [ + new (new[] { Ua.Namespaces.OpcUa }), + new(Array.Empty()) + ], + DiagnosticInfos = [] + })) + .Verifiable(Times.Once); + + await sut.OpenAsync("test", new UserIdentity(), ct).ConfigureAwait(false); + + Assert.That(sut.ServerNonce, Is.EquivalentTo(new byte[] { 1, 2, 3, 4 })); + sut.Channel.Verify(); + } + + [Test] + public void OpenAsyncShouldHandleCreateSessionSuccessButActivationError() + { + // Arrange + var ep = new EndpointDescription + { + SecurityMode = MessageSecurityMode.None, + SecurityPolicyUri = SecurityPolicies.None, + EndpointUrl = "opc.tcp://localhost:4840", + UserIdentityTokens = + [ + new UserTokenPolicy() + ] + }; + var sut = SessionMock.Create(ep); + CancellationToken ct = CancellationToken.None; + byte[] serverNonce = [1, 2, 3, 4]; + var authToken = NodeId.Parse("s=cookie"); + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new CreateSessionResponse + { + ServerNonce = serverNonce, + SessionId = NodeId.Parse("s=connected"), + AuthenticationToken = authToken, + ServerEndpoints = [ep] + })) + .Verifiable(Times.Once); + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.Is(r => r.RequestHeader.AuthenticationToken == authToken), + It.IsAny())) + .Returns(new ValueTask(new ActivateSessionResponse + { + ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.BadSessionNotActivated }, + ServerNonce = serverNonce, + Results = [], + DiagnosticInfos = [] + })) + .Verifiable(Times.Once); + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new CloseSessionResponse + { + ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.Good } + })) + .Verifiable(Times.Once); + + sut.Channel + .Setup(c => c.CloseAsync(It.IsAny())) + .Returns(new ValueTask()) + .Verifiable(Times.Once); + + ServiceResultException sre = Assert.ThrowsAsync( + async () => await sut.OpenAsync("test", new UserIdentity(), default).ConfigureAwait(false)); + Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadSessionNotActivated)); + sut.Channel.Verify(); + } + + [Test] + public void OpenAsyncShouldHandleCreateSessionSuccessButActivationErrorAndThenCloseAlsoFails() + { + // Arrange + var ep = new EndpointDescription + { + SecurityMode = MessageSecurityMode.None, + SecurityPolicyUri = SecurityPolicies.None, + EndpointUrl = "opc.tcp://localhost:4840", + UserIdentityTokens = + [ + new UserTokenPolicy() + ] + }; + var sut = SessionMock.Create(ep); + CancellationToken ct = CancellationToken.None; + byte[] serverNonce = [1, 2, 3, 4]; + var authToken = NodeId.Parse("s=cookie"); + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new CreateSessionResponse + { + ServerNonce = serverNonce, + SessionId = NodeId.Parse("s=connected"), + AuthenticationToken = authToken, + ServerEndpoints = [ep] + })) + .Verifiable(Times.Once); + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.Is(r => r.RequestHeader.AuthenticationToken == authToken), + It.IsAny())) + .Returns(new ValueTask(new ActivateSessionResponse + { + ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.BadSessionNotActivated }, + ServerNonce = serverNonce, + Results = [], + DiagnosticInfos = [] + })) + .Verifiable(Times.Once); + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new CloseSessionResponse + { + ResponseHeader = new ResponseHeader { ServiceResult = StatusCodes.BadNotConnected } + })) + .Verifiable(Times.Once); + + sut.Channel + .Setup(c => c.CloseAsync(It.IsAny())) + .Throws(new ServiceResultException(StatusCodes.BadNotConnected)) + .Verifiable(Times.Once); + + ServiceResultException sre = Assert.ThrowsAsync( + async () => await sut.OpenAsync( + "test", + 60000, + new UserIdentity(), + null, + true, + closeChannel: true, + default).ConfigureAwait(false)); + Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadSessionNotActivated)); + + sut.Channel.Verify(); + } + + [Test] + public void OpenAsyncShouldHandleSessionOpeningFailure() + { + var ep = new EndpointDescription + { + SecurityMode = MessageSecurityMode.None, + SecurityPolicyUri = SecurityPolicies.None, + EndpointUrl = "opc.tcp://localhost:4840", + UserIdentityTokens = + [ + new UserTokenPolicy() + ] + }; + var sut = SessionMock.Create(ep); + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .ThrowsAsync(new ServiceResultException(StatusCodes.BadUnexpectedError)) + .Verifiable(Times.Exactly(2)); + + ServiceResultException sre = Assert.ThrowsAsync( + async () => await sut.OpenAsync("test", new UserIdentity(), default).ConfigureAwait(false)); + Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadUnexpectedError)); + + sut.Channel.Verify(); + } + +#if FALSE // TODO: Enable when moving certificate loading into OpenAsync + [Test] + public void OpenAsyncShouldHandleBadSecurityPolicy() + { + var ep = new EndpointDescription + { + SecurityMode = MessageSecurityMode.None, + SecurityPolicyUri = "Bad", + EndpointUrl = "opc.tcp://localhost:4840", + UserIdentityTokens = + [ + new UserTokenPolicy() + ] + }; + var sut = SessionMock.Create(ep); + + ServiceResultException sre = Assert.ThrowsAsync( + async () => await sut.OpenAsync("test", new UserIdentity(), default).ConfigureAwait(false)); + Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadSecurityPolicyRejected)); + + sut.Channel.Verify(); + } +#endif + + [Test] + public void OpenAsyncShouldHandleBadIdentityTokenPolicy() + { + var ep = new EndpointDescription + { + SecurityMode = MessageSecurityMode.None, + SecurityPolicyUri = SecurityPolicies.None, + EndpointUrl = "opc.tcp://localhost:4840", + UserIdentityTokens = + [ + new UserTokenPolicy + { + PolicyId = "PolicyId", + TokenType = UserTokenType.IssuedToken + } + ] + }; + var sut = SessionMock.Create(ep); + + ServiceResultException sre = Assert.ThrowsAsync( + async () => await sut.OpenAsync("test", new UserIdentity(), default).ConfigureAwait(false)); + Assert.That(sre.StatusCode, Is.EqualTo(StatusCodes.BadIdentityTokenRejected)); + sut.Channel.Verify(); + } + + [Test] + public void OpenAsyncShouldHandleCancellation() + { + var sut = SessionMock.Create(); + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .ThrowsAsync(new TaskCanceledException()) + .Verifiable(Times.Once); + + Assert.ThrowsAsync( + async () => await sut.OpenAsync("test", new UserIdentity(), default).ConfigureAwait(false)); + + sut.Channel.Verify(); + } + + [Test] + public void OpenAsyncShouldHandleInvalidServerResponse() + { + var sut = SessionMock.Create(); + CancellationToken ct = CancellationToken.None; + + sut.Channel + .Setup(c => c.SendRequestAsync( + It.IsAny(), + It.IsAny())) + .Returns(new ValueTask(new CreateSessionResponse + { + ServerNonce = null, + SessionId = NodeId.Parse("s=connected") + })) + .Verifiable(Times.Once); + + Assert.ThrowsAsync( + async () => await sut.OpenAsync("test", new UserIdentity(), ct).ConfigureAwait(false)); + + sut.Channel.Verify(); + } + } +} diff --git a/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs b/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs index 0dcbf2e89b..57556d7ce0 100644 --- a/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs +++ b/Tests/Opc.Ua.Client.Tests/SubscriptionTest.cs @@ -202,8 +202,8 @@ public async Task AddSubscriptionAsync() subscription.Priority = 200; await subscription.ModifyAsync().ConfigureAwait(false); - // save with custom Subscription subclass information - Session.Save(m_subscriptionTestXml, [typeof(TestableSubscription)]); + // save with custom Subscription state subclass information + Session.Save(m_subscriptionTestXml); await Task.Delay(5000).ConfigureAwait(false); OutputSubscriptionInfo(TestContext.Out, subscription); @@ -250,8 +250,7 @@ public async Task LoadSubscriptionAsync() // load IEnumerable subscriptions = Session.Load( m_subscriptionTestXml, - false, - [typeof(TestableSubscription)]); + false); Assert.NotNull(subscriptions); Assert.IsNotEmpty(subscriptions); @@ -507,11 +506,12 @@ public async Task ReconnectWithSavedSessionSecretsAsync( const int kQueueSize = 10; ServiceResultException sre; - UserIdentity userIdentity = anonymous ? new UserIdentity() : new UserIdentity("user1", "password"u8); + ClientFixture.SessionFactory = new TestableSessionFactory(Telemetry); + // the first channel determines the endpoint ConfiguredEndpoint endpoint = await ClientFixture .GetEndpointAsync(ServerUrl, securityPolicy, Endpoints) @@ -578,8 +578,7 @@ await CreateSubscriptionsAsync( var subscriptionStream = new MemoryStream(); session1.Save( subscriptionStream, - session1.Subscriptions, - [typeof(TestableSubscription)]); + session1.Subscriptions); byte[] subscriptionStreamArray = subscriptionStream.ToArray(); TestContext.Out.WriteLine($"Subscriptions: {subscriptionStreamArray.Length} bytes"); @@ -611,7 +610,7 @@ await CreateSubscriptionsAsync( // restore the subscriptions var loadSubscriptionStream = new MemoryStream(subscriptionStreamArray); var restoredSubscriptions = new SubscriptionCollection( - session2.Load(loadSubscriptionStream, true, [typeof(TestableSubscription)])); + session2.Load(loadSubscriptionStream, true, [typeof(SubscriptionState)])); // hook notifications for log output int ii = 0; @@ -986,7 +985,7 @@ await CreateSubscriptionsAsync( originSession.DeleteSubscriptionsOnClose = false; // save with custom Subscription subclass information - originSession.Save(filePath, [typeof(TestableSubscription)]); + originSession.Save(filePath); if (transferType == TransferType.CloseSession) { @@ -1027,8 +1026,7 @@ await CreateSubscriptionsAsync( if (transferType != TransferType.KeepOpen) { // load subscriptions for transfer - transferSubscriptions.AddRange( - targetSession.Load(filePath, true, [typeof(TestableSubscription)])); + transferSubscriptions.AddRange(targetSession.Load(filePath, true)); // hook notifications for log output int ii = 0; diff --git a/Tests/Opc.Ua.Client.Tests/TestableSession.cs b/Tests/Opc.Ua.Client.Tests/TestableSession.cs index 2d0205f115..4990229c4c 100644 --- a/Tests/Opc.Ua.Client.Tests/TestableSession.cs +++ b/Tests/Opc.Ua.Client.Tests/TestableSession.cs @@ -47,49 +47,14 @@ public static class Namespaces /// /// A subclass of a session for testing purposes, e.g. to override some implementations. /// - [DataContract(Namespace = Namespaces.OpcUaClient)] - [KnownType(typeof(TestableSubscription))] - [KnownType(typeof(TestableMonitoredItem))] public class TestableSession : Session { - /// - /// Constructs a new instance of the class. - /// - /// The channel used to communicate with the server. - /// The configuration for the client application. - /// The endpoint use to initialize the channel. - public TestableSession( - ISessionChannel channel, - ApplicationConfiguration configuration, - ConfiguredEndpoint endpoint, - ITelemetryContext telemetry) - : this(channel as ITransportChannel, configuration, endpoint, null, telemetry) - { - } - - /// - /// Constructs a new instance of the class. - /// - /// The channel used to communicate with the server. - /// The configuration for the client application. - /// The endpoint used to initialize the channel. - /// The certificate to use for the client. - /// The list of available endpoints returned by server in GetEndpoints() response. - /// The value of profileUris used in GetEndpoints() request. - /// - /// The application configuration is used to look up the certificate if none is provided. - /// The clientCertificate must have the private key. This will require that the certificate - /// be loaded from a certicate store. Converting a DER encoded blob to a X509Certificate2 - /// will not include a private key. - /// The availableEndpoints and discoveryProfileUris parameters are used to validate - /// that the list of EndpointDescriptions returned at GetEndpoints matches the list returned at CreateSession. - /// + /// public TestableSession( ITransportChannel channel, ApplicationConfiguration configuration, ConfiguredEndpoint endpoint, X509Certificate2 clientCertificate, - ITelemetryContext telemetry, EndpointDescriptionCollection availableEndpoints = null, StringCollection discoveryProfileUris = null) : base( @@ -97,19 +62,21 @@ public TestableSession( configuration, endpoint, clientCertificate, + null, availableEndpoints, discoveryProfileUris) { } - /// - /// Initializes a new instance of the class. - /// - /// The channel. - /// The template session. - /// if set to true the event handlers are copied. - public TestableSession(ITransportChannel channel, Session template, bool copyEventHandlers) - : base(channel, template, copyEventHandlers) + /// + public TestableSession( + ITransportChannel channel, + Session template, + bool copyEventHandlers) + : base( + channel, + template, + copyEventHandlers) { } @@ -137,46 +104,84 @@ public override Session CloneSession(ITransportChannel channel, bool copyEventHa TimestampOffset = TimestampOffset }; } + + /// + protected override Subscription CreateSubscription(SubscriptionOptions options) + { + return new TestableSubscription(MessageContext.Telemetry, options); + } + + /// + public override void Snapshot(out SessionState state) + { + base.Snapshot(out state); + state = new TestableSessionState(state) + { + TimestampOffset = TimestampOffset + }; + } + + /// + public override void Restore(SessionState state) + { + if (state is TestableSessionState s) + { + TimestampOffset = s.TimestampOffset; + } + base.Restore(state); + } } /// - /// A subclass of the subscription for testing purposes. + /// Testable session state /// [DataContract(Namespace = Namespaces.OpcUaClient)] - [KnownType(typeof(TestableMonitoredItem))] - public class TestableSubscription : Subscription + [KnownType(typeof(MonitoredItemState))] + [KnownType(typeof(SubscriptionState))] + public record class TestableSessionState : SessionState { /// - /// Constructs a new instance of the class. + /// Default constructor /// - public TestableSubscription(ITelemetryContext telemetry) - : base(telemetry) + public TestableSessionState() { } /// - /// Constructs a new instance of the class. + /// Create a new instance of the class. /// - public TestableSubscription(Subscription template) - : this(template, false) + /// + public TestableSessionState(SessionState state) + : base(state) { } + /// + /// The timespan offset to be used to modify the request header timestamp. + /// + [DataMember] + public TimeSpan TimestampOffset { get; set; } = new TimeSpan(0); + } + + /// + /// A subclass of the subscription for testing purposes. + /// + public class TestableSubscription : Subscription + { /// /// Constructs a new instance of the class. /// - public TestableSubscription(Subscription template, bool copyEventHandlers) - : base(template, copyEventHandlers) + public TestableSubscription(ITelemetryContext telemetry, SubscriptionOptions options = null) + : base(telemetry, options) { } /// - /// Called by the .NET framework during deserialization. + /// Constructs a new instance of the class. /// - [OnDeserializing] - protected new void Initialize(StreamingContext context) + public TestableSubscription(Subscription template, bool copyEventHandlers = false) + : base(template, copyEventHandlers) { - base.Initialize(context); } /// @@ -184,19 +189,24 @@ public override Subscription CloneSubscription(bool copyEventHandlers) { return new TestableSubscription(this, copyEventHandlers); } + + /// + protected override MonitoredItem CreateMonitoredItem(MonitoredItemOptions options) + { + return new TestableMonitoredItem(Telemetry, options); + } } /// /// A subclass of a monitored item for testing purposes. /// - [DataContract(Namespace = Namespaces.OpcUaClient)] - [KnownType(typeof(TestableMonitoredItem))] public class TestableMonitoredItem : MonitoredItem { /// /// Constructs a new instance of the class. /// - public TestableMonitoredItem() + public TestableMonitoredItem(ITelemetryContext telemetry, MonitoredItemOptions options = null) + : base(telemetry, options) { } @@ -219,23 +229,6 @@ public TestableMonitoredItem( { } - /// - /// Called by the .NET framework during deserialization. - /// - [OnDeserializing] - protected new void Initialize(StreamingContext context) - { - base.Initialize(context); - Initialize(); - } - - /// - /// Sets the private members to default values. - /// - private static void Initialize() - { - } - /// public override MonitoredItem CloneMonitoredItem( bool copyEventHandlers, diff --git a/Tests/Opc.Ua.Client.Tests/TestableSessionFactory.cs b/Tests/Opc.Ua.Client.Tests/TestableSessionFactory.cs index 5ea604f32f..1c2a8cb299 100644 --- a/Tests/Opc.Ua.Client.Tests/TestableSessionFactory.cs +++ b/Tests/Opc.Ua.Client.Tests/TestableSessionFactory.cs @@ -27,10 +27,7 @@ * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ -using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; -using System.Threading; -using System.Threading.Tasks; namespace Opc.Ua.Client.Tests { @@ -47,190 +44,13 @@ public TestableSessionFactory(ITelemetryContext telemetry) { } - /// - public override 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 override 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 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) - { - return await Session - .CreateAsync( - this, - configuration, - connection, - endpoint, - updateBeforeConnect, - checkDomain, - sessionName, - sessionTimeout, - identity, - preferredLocales, - ReturnDiagnostics, - 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) - { - 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 override 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 override Session Create( - ISessionChannel channel, - ApplicationConfiguration configuration, - ConfiguredEndpoint endpoint) - { - return new TestableSession(channel, configuration, endpoint, Telemetry); - } - - /// - public override Session Create( ITransportChannel channel, ApplicationConfiguration configuration, ConfiguredEndpoint endpoint, X509Certificate2 clientCertificate, + X509Certificate2Collection certificateChain, EndpointDescriptionCollection availableEndpoints = null, StringCollection discoveryProfileUris = null) { @@ -239,7 +59,6 @@ public override Session Create( configuration, endpoint, clientCertificate, - Telemetry, availableEndpoints, discoveryProfileUris); } diff --git a/Tests/Opc.Ua.Client.Tests/TestableSessionInstantiator.cs b/Tests/Opc.Ua.Client.Tests/TestableSessionInstantiator.cs deleted file mode 100644 index 02c485c946..0000000000 --- a/Tests/Opc.Ua.Client.Tests/TestableSessionInstantiator.cs +++ /dev/null @@ -1,81 +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.Security.Cryptography.X509Certificates; - -namespace Opc.Ua.Client.Tests -{ - /// - /// Object that creates an instance of a Session object. - /// It can be used to create instances of enhanced Session - /// classes with added functionality or overridden methods. - /// - public class TestableSessionInstantiator : ISessionInstantiator - { - /// - /// Craete a new instance of the instantiator. - /// - /// The telemetry context to use to create obvservability instruments - public TestableSessionInstantiator(ITelemetryContext telemetry) - { - Telemetry = telemetry; - } - - /// - public ITelemetryContext Telemetry { get; } - - /// - public Session Create( - ISessionChannel channel, - ApplicationConfiguration configuration, - ConfiguredEndpoint endpoint) - { - return new TestableSession(channel, configuration, endpoint, Telemetry); - } - - /// - public Session Create( - ITransportChannel channel, - ApplicationConfiguration configuration, - ConfiguredEndpoint endpoint, - X509Certificate2 clientCertificate, - EndpointDescriptionCollection availableEndpoints = null, - StringCollection discoveryProfileUris = null) - { - return new TestableSession( - channel, - configuration, - endpoint, - clientCertificate, - Telemetry, - availableEndpoints, - discoveryProfileUris); - } - } -} diff --git a/Tests/Opc.Ua.Client.Tests/TraceableRequestHeaderClientSession.cs b/Tests/Opc.Ua.Client.Tests/TraceableRequestHeaderClientSession.cs index 84c2913510..a0b9f6277e 100644 --- a/Tests/Opc.Ua.Client.Tests/TraceableRequestHeaderClientSession.cs +++ b/Tests/Opc.Ua.Client.Tests/TraceableRequestHeaderClientSession.cs @@ -37,37 +37,7 @@ namespace Opc.Ua.Client /// public class TraceableRequestHeaderClientSession : Session { - /// - /// Constructs a new instance of the class. - /// - /// The channel used to communicate with the server. - /// The configuration for the client application. - /// The endpoint use to initialize the channel. - public TraceableRequestHeaderClientSession( - ISessionChannel channel, - ApplicationConfiguration configuration, - ConfiguredEndpoint endpoint) - : this(channel as ITransportChannel, configuration, endpoint, null) - { - } - - /// - /// Constructs a new instance of the class. - /// - /// The channel used to communicate with the server. - /// The configuration for the client application. - /// The endpoint used to initialize the channel. - /// The certificate to use for the client. - /// The list of available endpoints returned by server in GetEndpoints() response. - /// The value of profileUris used in GetEndpoints() request. - /// - /// The application configuration is used to look up the certificate if none is provided. - /// The clientCertificate must have the private key. This will require that the certificate - /// be loaded from a certicate store. Converting a DER encoded blob to a X509Certificate2 - /// will not include a private key. - /// The availableEndpoints and discoveryProfileUris parameters are used to validate - /// that the list of EndpointDescriptions returned at GetEndpoints matches the list returned at CreateSession. - /// + /// public TraceableRequestHeaderClientSession( ITransportChannel channel, ApplicationConfiguration configuration, @@ -80,17 +50,13 @@ public TraceableRequestHeaderClientSession( configuration, endpoint, clientCertificate, + null, availableEndpoints, discoveryProfileUris) { } - /// - /// Initializes a new instance of the class. - /// - /// The channel. - /// The template session. - /// if set to true the event handlers are copied. + /// public TraceableRequestHeaderClientSession( ITransportChannel channel, Session template, diff --git a/Tests/Opc.Ua.Client.Tests/TraceableRequestHeaderClientSessionFactory.cs b/Tests/Opc.Ua.Client.Tests/TraceableRequestHeaderClientSessionFactory.cs index 216a0c1193..f97b6c9ed4 100644 --- a/Tests/Opc.Ua.Client.Tests/TraceableRequestHeaderClientSessionFactory.cs +++ b/Tests/Opc.Ua.Client.Tests/TraceableRequestHeaderClientSessionFactory.cs @@ -36,7 +36,7 @@ namespace Opc.Ua.Client /// It can be used to create instances of enhanced Session /// classes with added functionality or overridden methods. /// - public class TraceableRequestHeaderClientSessionFactory : TraceableSessionFactory + public class TraceableRequestHeaderClientSessionFactory : DefaultSessionFactory { public TraceableRequestHeaderClientSessionFactory(ITelemetryContext telemetry) : base(telemetry) @@ -45,22 +45,14 @@ public TraceableRequestHeaderClientSessionFactory(ITelemetryContext telemetry) } /// - public override Session Create( - ISessionChannel channel, - ApplicationConfiguration configuration, - ConfiguredEndpoint endpoint) - { - return new TraceableRequestHeaderClientSession(channel, configuration, endpoint); - } - - /// - public override Session Create( + public override ISession Create( ITransportChannel channel, ApplicationConfiguration configuration, ConfiguredEndpoint endpoint, X509Certificate2 clientCertificate, - EndpointDescriptionCollection availableEndpoints = null, - StringCollection discoveryProfileUris = null) + X509Certificate2Collection clientCertificateChain, + EndpointDescriptionCollection availableEndpoints, + StringCollection discoveryProfileUris) { return new TraceableRequestHeaderClientSession( channel, diff --git a/Tests/Opc.Ua.Core.Tests/Stack/Types/ResultSetTests.cs b/Tests/Opc.Ua.Core.Tests/Stack/Types/ResultSetTests.cs new file mode 100644 index 0000000000..335f7aca08 --- /dev/null +++ b/Tests/Opc.Ua.Core.Tests/Stack/Types/ResultSetTests.cs @@ -0,0 +1,144 @@ +/* ======================================================================== + * Copyright (c) 2005-2018 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.Collections.Generic; +using NUnit.Framework; + +namespace Opc.Ua.Core.Tests.Stack.Types +{ + /// + /// Tests for the result set type + /// + [TestFixture] + [Category("ResultSet")] + [SetCulture("en-us")] + [SetUICulture("en-us")] + [Parallelizable] + public sealed class ResultSetTests + { + [Test] + public void ResultSetConstructorShouldInitializeProperties() + { + // Arrange + var results = new List { 1, 2, 3 }; + var errors = new List { ServiceResult.Good }; + + // Act + var resultSet = new ResultSet(results, errors); + + // Assert + Assert.That(resultSet.Results, Is.EquivalentTo(results)); + Assert.That(resultSet.Errors, Is.EquivalentTo(errors)); + } + + [Test] + public void ResultSetFromShouldInitializeProperties() + { + // Arrange + IEnumerable results = [1, 2, 3]; + var errors = new List { ServiceResult.Good }; + + // Act + var resultSet = ResultSet.From(results, errors); + + // Assert + Assert.That(resultSet.Results, Is.EquivalentTo(results)); + Assert.That(resultSet.Errors, Is.EquivalentTo(errors)); + } + + [Test] + public void ResultSetEmptyShouldReturnEmptyResultSet() + { + // Act + ResultSet emptyResultSet = ResultSet.Empty; + + // Assert + Assert.That(emptyResultSet.Results, Is.Empty); + Assert.That(emptyResultSet.Errors, Is.Empty); + } + + [Test] + public void ResultSetFromShouldInitializePropertiesWithoutErrorList() + { + // Arrange + IReadOnlyList results = [1, 2, 3]; + + // Act + var resultSet = ResultSet.From(results); + + // Assert + Assert.That(resultSet.Results, Is.EquivalentTo(results)); + Assert.That(resultSet.Errors.Count, Is.EqualTo(3)); + Assert.That(resultSet.Errors, Is.All.EqualTo(ServiceResult.Good)); + } + + [Test] + public void ResultSetFromShouldInitializePropertiesWithEnumerableWithoutErrorList() + { + // Arrange + IEnumerable results = [1, 2, 3]; + + // Act + var resultSet = ResultSet.From(results); + + // Assert + Assert.That(resultSet.Results, Is.EquivalentTo(results)); + Assert.That(resultSet.Errors.Count, Is.EqualTo(3)); + Assert.That(resultSet.Errors, Is.All.EqualTo(ServiceResult.Good)); + } + + [Test] + public void ResultSetEmptyShouldReturnAlwaysSameListReferences() + { + // Act + ResultSet emptyResultSet1 = ResultSet.Empty; + ResultSet emptyResultSet2 = ResultSet.Empty; + + // Assert + Assert.That(emptyResultSet2.Results, Is.SameAs(emptyResultSet1.Results)); + Assert.That(emptyResultSet2.Errors, Is.SameAs(emptyResultSet1.Errors)); + } + + [Test] + public void ResultSetCopiedShouldReturnAlwaysSameListReferences() + { + // Arrange + var results = new List { 1, 2, 3 }; + var errors = new List { ServiceResult.Good }; + + // Act + var resultSet1 = ResultSet.From(results, errors); + ResultSet resultSet2 = resultSet1; + + // Assert + Assert.That(resultSet2.Results, Is.SameAs(resultSet1.Results)); + Assert.That(resultSet2.Errors, Is.SameAs(resultSet1.Errors)); + } + } +}