// ------------------------------------------------------------ // Copyright (c) 2005-2025 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 is ExtensionObject eo ? eo : default; } 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; } }