mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-20 16:52:07 +08:00
43ce7ed229
This pull request modernizes the OPC UA .NET codebase and tests, removes .NET Standard 2.0 support from certificate/security code, and updates test code to use C# 12 and .NET 8+ features for clarity and maintainability. - Certificate and BouncyCastle interop files (e.g., CertificateBuilder.cs, X509Extensions.cs, X509SignatureFactory.cs) are refactored to drop NETSTANDARD2_0 support and use modern .NET APIs and conditional compilation. - Test code across the codebase is updated to use default, strongly-typed Parse methods, collection expressions, and improved async/await patterns. - Exception handling and assertions in tests are modernized, and test method naming is made consistent with Async suffixes. - The project and build files are updated to clarify Roslynator analyzer usage and remove obsolete netstandard tags. - General code and test cleanup includes improved null safety, type safety, and removal of deprecated or unused code.
1088 lines
42 KiB
C#
1088 lines
42 KiB
C#
// ------------------------------------------------------------
|
|
// 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
|
|
{
|
|
/// <summary>
|
|
/// Node cache context provides
|
|
/// </summary>
|
|
public sealed class NodeCacheContext : INodeCacheContext
|
|
{
|
|
/// <summary>
|
|
/// Create node cache context
|
|
/// </summary>
|
|
/// <param name="session"></param>
|
|
public NodeCacheContext(ISessionClient session)
|
|
{
|
|
m_session = session;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public NamespaceTable NamespaceUris => m_session.MessageContext.NamespaceUris;
|
|
|
|
/// <inheritdoc/>
|
|
public StringTable ServerUris => m_session.MessageContext.ServerUris;
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<ReferenceDescriptionCollection> 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<ReferenceDescriptionCollection> results =
|
|
await browser.BrowseAsync([nodeId], ct).ConfigureAwait(false);
|
|
return results.Results[0];
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask<ResultSet<ReferenceDescriptionCollection>> FetchReferencesAsync(
|
|
RequestHeader? requestHeader,
|
|
IReadOnlyList<NodeId> nodeIds,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (nodeIds.Count == 0)
|
|
{
|
|
return new ValueTask<ResultSet<ReferenceDescriptionCollection>>(
|
|
ResultSet<ReferenceDescriptionCollection>.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);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<ResultSet<Node>> FetchNodesAsync(
|
|
RequestHeader? requestHeader,
|
|
IReadOnlyList<NodeId> nodeIds,
|
|
bool skipOptionalAttributes = false,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (nodeIds.Count == 0)
|
|
{
|
|
return ResultSet<Node>.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<IDictionary<uint, DataValue?>?>(nodeIds.Count);
|
|
var serviceResults = new List<ServiceResult>(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);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<ResultSet<Node>> FetchNodesAsync(
|
|
RequestHeader? requestHeader,
|
|
IReadOnlyList<NodeId> nodeIds,
|
|
NodeClass nodeClass,
|
|
bool skipOptionalAttributes = false,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (nodeIds.Count == 0)
|
|
{
|
|
return ResultSet<Node>.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<IDictionary<uint, DataValue?>?>(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<ServiceResult> serviceResults = new ServiceResult[nodeIds.Count].ToList();
|
|
ProcessAttributesReadNodesResponse(
|
|
readResponse.ResponseHeader,
|
|
attributesToRead,
|
|
attributesPerNodeId,
|
|
values,
|
|
diagnosticInfos,
|
|
nodeCollection,
|
|
serviceResults);
|
|
|
|
return ResultSet.From(nodeCollection, serviceResults);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<Node> FetchNodeAsync(
|
|
RequestHeader? requestHeader,
|
|
NodeId nodeId,
|
|
NodeClass nodeClass = NodeClass.Unspecified,
|
|
bool skipOptionalAttributes = false,
|
|
CancellationToken ct = default)
|
|
{
|
|
// build list of attributes.
|
|
IDictionary<uint, DataValue?> 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);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<DataValue> 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];
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<ResultSet<DataValue>> FetchValuesAsync(
|
|
RequestHeader? requestHeader,
|
|
IReadOnlyList<NodeId> nodeIds,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (nodeIds.Count == 0)
|
|
{
|
|
return ResultSet<DataValue>.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<ServiceResult>(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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a read request with attributes determined by the NodeClass.
|
|
/// </summary>
|
|
private static void CreateAttributesReadNodesRequest(
|
|
ResponseHeader responseHeader,
|
|
ReadValueIdCollection itemsToRead,
|
|
DataValueCollection nodeClassValues,
|
|
DiagnosticInfoCollection diagnosticInfos,
|
|
ReadValueIdCollection attributesToRead,
|
|
List<IDictionary<uint, DataValue?>?> attributesPerNodeId,
|
|
NodeCollection nodeCollection,
|
|
List<ServiceResult> 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<uint, DataValue?> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds the node collection results based on the attribute values of the read response.
|
|
/// </summary>
|
|
/// <param name="responseHeader">The response requestHeader of the read request.</param>
|
|
/// <param name="attributesToRead">The collection of all attributes to read passed in the read request.</param>
|
|
/// <param name="attributesPerNodeId">The attributes requested per NodeId</param>
|
|
/// <param name="values">The attribute values returned by the read request.</param>
|
|
/// <param name="diagnosticInfos">The diagnostic info returned by the read request.</param>
|
|
/// <param name="nodeCollection">The node collection which holds the results.</param>
|
|
/// <param name="errors">The service results for each node.</param>
|
|
private static void ProcessAttributesReadNodesResponse(
|
|
ResponseHeader responseHeader,
|
|
ReadValueIdCollection attributesToRead,
|
|
List<IDictionary<uint, DataValue?>?> attributesPerNodeId,
|
|
DataValueCollection values,
|
|
DiagnosticInfoCollection diagnosticInfos,
|
|
NodeCollection nodeCollection,
|
|
List<ServiceResult> errors)
|
|
{
|
|
int readIndex = 0;
|
|
for (int ii = 0; ii < nodeCollection.Count; ii++)
|
|
{
|
|
IDictionary<uint, DataValue?>? 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a Node based on the read response.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
private static Node ProcessReadResponse(
|
|
ResponseHeader responseHeader,
|
|
IDictionary<uint, DataValue?> 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<byte>();
|
|
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<bool>();
|
|
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<int>();
|
|
|
|
// ArrayDimensions Attribute
|
|
value = attributes[Attributes.ArrayDimensions];
|
|
|
|
if (value != null)
|
|
{
|
|
if (value.Value == null)
|
|
{
|
|
variableNode.ArrayDimensions = Array.Empty<uint>();
|
|
}
|
|
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<byte>();
|
|
|
|
// UserAccessLevel Attribute
|
|
value = attributes[Attributes.UserAccessLevel];
|
|
|
|
if (value == null)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"Variable does not support the UserAccessLevel attribute.");
|
|
}
|
|
|
|
variableNode.UserAccessLevel = value.GetValueOrDefault<byte>();
|
|
|
|
// Historizing Attribute
|
|
value = attributes[Attributes.Historizing];
|
|
|
|
if (value == null)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"Variable does not support the Historizing attribute.");
|
|
}
|
|
|
|
variableNode.Historizing = value.GetValueOrDefault<bool>();
|
|
|
|
// 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<uint>();
|
|
}
|
|
|
|
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<bool>();
|
|
|
|
// 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<int>();
|
|
|
|
// 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<bool>();
|
|
|
|
// UserExecutable Attribute
|
|
value = attributes[Attributes.UserExecutable];
|
|
|
|
if (value == null)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"Method does not support the UserExecutable attribute.");
|
|
}
|
|
|
|
methodNode.UserExecutable = value.GetValueOrDefault<bool>();
|
|
|
|
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<bool>();
|
|
|
|
// 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<bool>();
|
|
|
|
// Symmetric Attribute
|
|
value = attributes[Attributes.Symmetric];
|
|
|
|
if (value == null)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"ReferenceType does not support the Symmetric attribute.");
|
|
}
|
|
|
|
referenceTypeNode.Symmetric = value.GetValueOrDefault<bool>();
|
|
|
|
// 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<byte>();
|
|
|
|
// ContainsNoLoops Attribute
|
|
value = attributes[Attributes.ContainsNoLoops];
|
|
|
|
if (value == null)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"View does not support the ContainsNoLoops attribute.");
|
|
}
|
|
|
|
viewNode.ContainsNoLoops = value.GetValueOrDefault<bool>();
|
|
|
|
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<uint>();
|
|
}
|
|
|
|
// UserWriteMask Attribute
|
|
if (attributes.TryGetValue(Attributes.UserWriteMask, out value) && value != null)
|
|
{
|
|
node.UserWriteMask = value.GetValueOrDefault<uint>();
|
|
}
|
|
|
|
// 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<ushort>();
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a dictionary of attributes to read for a nodeclass.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
private static Dictionary<uint, DataValue?> CreateAttributes(
|
|
NodeClass nodeClass,
|
|
bool skipOptionalAttributes)
|
|
{
|
|
// Attributes to read for all types of nodes
|
|
var attributes = new Dictionary<uint, DataValue?>(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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a read request with attributes determined by the NodeClass.
|
|
/// </summary>
|
|
private static void CreateNodeClassAttributesReadNodesRequest(
|
|
IReadOnlyList<NodeId> nodeIds,
|
|
NodeClass nodeClass,
|
|
ReadValueIdCollection attributesToRead,
|
|
List<IDictionary<uint, DataValue?>?> attributesPerNodeId,
|
|
NodeCollection nodeCollection,
|
|
bool skipOptionalAttributes)
|
|
{
|
|
for (int ii = 0; ii < nodeIds.Count; ii++)
|
|
{
|
|
var node = new Node { NodeId = nodeIds[ii], NodeClass = nodeClass };
|
|
|
|
Dictionary<uint, DataValue?> 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;
|
|
}
|
|
}
|