mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-19 07:02:16 +08:00
889572472f
Convert `DataValue` from a mutable reference type (`class`) to a `readonly struct` to eliminate per-allocation GC pressure on every attribute read, subscription publish, and history sample. ### Key API changes (user-visible) | Before | After | Pattern | |---|---|---| | `class DataValue` | `readonly struct DataValue : INullable` | Foundation | | `Equals(DataValue?)` / `operator ==(DataValue?, DataValue?)` | `Equals(DataValue)` / `operator ==(DataValue, DataValue)` | Value semantics | | `IEncoder.WriteDataValue(string?, DataValue?)` | `IEncoder.WriteDataValue(string?, DataValue)` | Non-nullable wire API | | `IDecoder.ReadDataValue(...) -> DataValue?` | `IDecoder.ReadDataValue(...) -> DataValue` | Same | | `IDataChangeMonitoredItemQueue.PeekLast/Oldest()` | `TryPeekLastValue/Oldest(out DataValue)` | TryGet pattern | | `IAggregateCalculator.GetProcessedValue(bool)` | `TryGetProcessedValue(bool, out DataValue)` | Same | | `IUaPubSubDataStore.ReadPublishedDataItem(...)` | `TryReadPublishedDataItem(NodeId, uint, out DataValue)` | Same | | `Dictionary<uint, DataValue?>` (NodeCacheContext) | `Dictionary<uint, DataValue>` | Default + IsNull | | `IHistoryDataSource.FirstRaw/NextRaw` | `TryFirstRaw/TryNextRaw(out DataValue)` | TryGet pattern | | `NodeState.ReadAttribute(... DataValue)` | `NodeState.ReadAttribute(... ref DataValue)` | Sync mutation via ref | | `NodeState.ReadAttributeAsync(... DataValue)` | `NodeState.ReadAttributeAsync(...) -> ValueTask<(ServiceResult, DataValue)>` | Async return tuple | | `obj as DataValue` (heterogeneous Dictionary<string, object>) | `obj is DataValue dv ? dv : default(DataValue)` | Safe struct cast | | `Assert.That(dv, Is.Not.Null)` (tests) | `Assert.That(dv.IsNull, Is.False)` | Struct test pattern | | `[Obsolete] Value` setter, all property setters | Removed; use ctor or `With*()` | Immutability | | `[DataContract]` + `[DataMember]` | Removed; surrogate via `SerializableDataValue` + `DataValueSurrogateProvider` | Serialization | ### Wire format Wire format is **byte-identical** to the previous class-based implementation across binary / JSON / XML encoders. Default DataValue (`.IsNull == true`) is treated as the equivalent of the old null reference (writes JSON `null`, binary `byte 0`, XML omitted element). ### Why TryGet pattern (instead of leaving `DataValue?`) For methods that previously returned `DataValue?` to signal "no value available" (queue peek, aggregate result, data store lookup, history navigation), the conversion to `bool TryGet(out DataValue)` matches BCL conventions (`Dictionary.TryGetValue`, `Queue.TryPeek`, `Int32.TryParse`) and gives the caller an explicit "may be absent" affordance without `Nullable<DataValue>` overhead. ### Breaking changes summary - `DataValue` is now a `readonly struct` - boxing/unboxing semantics change for callers using `object`-typed APIs. - `IEncoder.WriteDataValue` / `IDecoder.ReadDataValue` / `ReadDataValueArray` interface signatures lose the `Nullable` wrapper. - `IDataChangeMonitoredItemQueue.PeekLastValue` / `PeekOldestValue` -> `TryPeekLastValue` / `TryPeekOldestValue` (out parameter). - `IAggregateCalculator.GetProcessedValue` -> `TryGetProcessedValue` (out parameter). - `IUaPubSubDataStore.ReadPublishedDataItem` -> `TryReadPublishedDataItem` (out parameter). - `NodeState.ReadAttribute` takes `ref DataValue`; `ReadAttributeAsync` returns `ValueTask<(ServiceResult, DataValue)>`. - `DataValue` property setters removed (use constructor + `With*` mutators).
1068 lines
42 KiB
C#
1068 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.Collections.Generic;
|
|
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<ArrayOf<ReferenceDescription>> 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<ArrayOf<ReferenceDescription>> results =
|
|
await browser.BrowseAsync([nodeId], ct).ConfigureAwait(false);
|
|
return results.Results[0];
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ValueTask<ResultSet<ArrayOf<ReferenceDescription>>> FetchReferencesAsync(
|
|
RequestHeader? requestHeader,
|
|
ArrayOf<NodeId> nodeIds,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (nodeIds.Count == 0)
|
|
{
|
|
return new ValueTask<ResultSet<ArrayOf<ReferenceDescription>>>(
|
|
ResultSet<ArrayOf<ReferenceDescription>>.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,
|
|
ArrayOf<NodeId> nodeIds,
|
|
bool skipOptionalAttributes = false,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (nodeIds.Count == 0)
|
|
{
|
|
return ResultSet<Node>.Empty;
|
|
}
|
|
|
|
var nodeCollection = new List<Node>(nodeIds.Count);
|
|
|
|
// first read only nodeclasses for nodes from server.
|
|
ArrayOf<ReadValueId> itemsToRead = nodeIds
|
|
.ConvertAll(nodeId => new ReadValueId
|
|
{
|
|
NodeId = nodeId,
|
|
AttributeId = Attributes.NodeClass
|
|
});
|
|
|
|
ReadResponse readResponse = await m_session.ReadAsync(
|
|
null,
|
|
0,
|
|
TimestampsToReturn.Neither,
|
|
itemsToRead,
|
|
ct)
|
|
.ConfigureAwait(false);
|
|
|
|
ArrayOf<DataValue> nodeClassValues = readResponse.Results;
|
|
ArrayOf<DiagnosticInfo> 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 List<ReadValueId>();
|
|
|
|
CreateAttributesReadNodesRequest(
|
|
readResponse.ResponseHeader,
|
|
itemsToRead,
|
|
nodeClassValues,
|
|
diagnosticInfos,
|
|
attributesToRead,
|
|
attributesPerNodeId,
|
|
nodeCollection,
|
|
serviceResults,
|
|
skipOptionalAttributes);
|
|
|
|
if (attributesToRead.Count > 0)
|
|
{
|
|
itemsToRead = attributesToRead.ToArrayOf();
|
|
readResponse = await m_session.ReadAsync(
|
|
null,
|
|
0,
|
|
TimestampsToReturn.Neither,
|
|
itemsToRead,
|
|
ct)
|
|
.ConfigureAwait(false);
|
|
|
|
ArrayOf<DataValue> values = readResponse.Results;
|
|
diagnosticInfos = readResponse.DiagnosticInfos;
|
|
|
|
ClientBase.ValidateResponse(values, itemsToRead);
|
|
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, itemsToRead);
|
|
|
|
ProcessAttributesReadNodesResponse(
|
|
readResponse.ResponseHeader,
|
|
itemsToRead,
|
|
attributesPerNodeId,
|
|
values,
|
|
diagnosticInfos,
|
|
nodeCollection,
|
|
serviceResults);
|
|
}
|
|
|
|
return ResultSet.From(nodeCollection, serviceResults);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<ResultSet<Node>> FetchNodesAsync(
|
|
RequestHeader? requestHeader,
|
|
ArrayOf<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 List<Node>(nodeIds.Count);
|
|
// determine attributes to read for nodeclass
|
|
var attributesPerNodeId = new List<IDictionary<uint, DataValue>?>(nodeIds.Count);
|
|
var attributesToRead = new List<ReadValueId>();
|
|
|
|
CreateNodeClassAttributesReadNodesRequest(
|
|
nodeIds,
|
|
nodeClass,
|
|
attributesToRead,
|
|
attributesPerNodeId,
|
|
nodeCollection,
|
|
skipOptionalAttributes);
|
|
|
|
var itemsToRead = attributesToRead.ToArrayOf();
|
|
ReadResponse readResponse = await m_session.ReadAsync(
|
|
requestHeader,
|
|
0,
|
|
TimestampsToReturn.Neither,
|
|
itemsToRead,
|
|
ct)
|
|
.ConfigureAwait(false);
|
|
|
|
ArrayOf<DataValue> values = readResponse.Results;
|
|
ArrayOf<DiagnosticInfo> diagnosticInfos = readResponse.DiagnosticInfos;
|
|
|
|
ClientBase.ValidateResponse(values, itemsToRead);
|
|
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, itemsToRead);
|
|
|
|
List<ServiceResult> serviceResults = new ServiceResult[nodeIds.Count].ToList();
|
|
ProcessAttributesReadNodesResponse(
|
|
readResponse.ResponseHeader,
|
|
itemsToRead,
|
|
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 = attributes.Keys
|
|
.Select(attributeId => new ReadValueId
|
|
{
|
|
NodeId = nodeId,
|
|
AttributeId = attributeId
|
|
}).ToArrayOf();
|
|
|
|
// read from server.
|
|
ReadResponse readResponse = await m_session.ReadAsync(
|
|
null,
|
|
0,
|
|
TimestampsToReturn.Neither,
|
|
itemsToRead,
|
|
ct)
|
|
.ConfigureAwait(false);
|
|
|
|
ArrayOf<DataValue> values = readResponse.Results;
|
|
ArrayOf<DiagnosticInfo> 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)
|
|
{
|
|
ArrayOf<ReadValueId> itemsToRead =
|
|
[
|
|
new ReadValueId
|
|
{
|
|
NodeId = nodeId,
|
|
AttributeId = Attributes.Value
|
|
}
|
|
];
|
|
|
|
// read from server.
|
|
ReadResponse readResponse = await m_session.ReadAsync(
|
|
null,
|
|
0,
|
|
TimestampsToReturn.Both,
|
|
itemsToRead,
|
|
ct)
|
|
.ConfigureAwait(false);
|
|
|
|
ArrayOf<DataValue> values = readResponse.Results;
|
|
ArrayOf<DiagnosticInfo> 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,
|
|
ArrayOf<NodeId> nodeIds,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (nodeIds.Count == 0)
|
|
{
|
|
return ResultSet<DataValue>.Empty;
|
|
}
|
|
|
|
// read all values from server.
|
|
ArrayOf<ReadValueId> itemsToRead = nodeIds
|
|
.ConvertAll(nodeId => new ReadValueId
|
|
{
|
|
NodeId = nodeId,
|
|
AttributeId = Attributes.Value
|
|
});
|
|
|
|
// read from server.
|
|
var errors = new ServiceResult[itemsToRead.Count];
|
|
|
|
ReadResponse readResponse = await m_session.ReadAsync(
|
|
null,
|
|
0,
|
|
TimestampsToReturn.Both,
|
|
itemsToRead,
|
|
ct)
|
|
.ConfigureAwait(false);
|
|
|
|
ArrayOf<DataValue> values = readResponse.Results;
|
|
ArrayOf<DiagnosticInfo> diagnosticInfos = readResponse.DiagnosticInfos;
|
|
|
|
ClientBase.ValidateResponse(values, itemsToRead);
|
|
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, itemsToRead);
|
|
|
|
for (int ii = 0; ii < values.Count; ii++)
|
|
{
|
|
ServiceResult result = ServiceResult.Good;
|
|
if (StatusCode.IsBad(values[ii].StatusCode))
|
|
{
|
|
result = ClientBase.GetResult(
|
|
values[ii].StatusCode,
|
|
ii,
|
|
diagnosticInfos,
|
|
readResponse.ResponseHeader);
|
|
}
|
|
errors[ii] = result;
|
|
}
|
|
|
|
return ResultSet.From(values, errors);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a read request with attributes determined by the NodeClass.
|
|
/// </summary>
|
|
private static void CreateAttributesReadNodesRequest(
|
|
ResponseHeader responseHeader,
|
|
ArrayOf<ReadValueId> itemsToRead,
|
|
ArrayOf<DataValue> nodeClassValues,
|
|
ArrayOf<DiagnosticInfo> diagnosticInfos,
|
|
List<ReadValueId> attributesToRead,
|
|
List<IDictionary<uint, DataValue>?> attributesPerNodeId,
|
|
List<Node> nodeCollection,
|
|
List<ServiceResult> errors,
|
|
bool skipOptionalAttributes)
|
|
{
|
|
for (int ii = 0; ii < itemsToRead.Count; ii++)
|
|
{
|
|
var node = new Node { NodeId = itemsToRead[ii].NodeId };
|
|
if (!nodeClassValues[ii].IsGood)
|
|
{
|
|
nodeCollection.Add(node);
|
|
errors.Add(
|
|
new ServiceResult(
|
|
nodeClassValues[ii].StatusCode,
|
|
ii,
|
|
diagnosticInfos,
|
|
responseHeader.StringTable));
|
|
attributesPerNodeId.Add(null);
|
|
continue;
|
|
}
|
|
|
|
// check for valid node class.
|
|
if (!nodeClassValues[ii].WrappedValue.TryGetValue(out NodeClass nodeClass))
|
|
{
|
|
if (nodeClassValues[ii].WrappedValue.TryGetValue(out 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].WrappedValue));
|
|
attributesPerNodeId.Add(null);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
node.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);
|
|
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,
|
|
ArrayOf<ReadValueId> attributesToRead,
|
|
List<IDictionary<uint, DataValue>?> attributesPerNodeId,
|
|
ArrayOf<DataValue> values,
|
|
ArrayOf<DiagnosticInfo> diagnosticInfos,
|
|
List<Node> 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;
|
|
ArrayOf<ReadValueId> subRangeAttributes = attributesToRead.Slice(readIndex, readCount);
|
|
ArrayOf<DataValue> subRangeValues = values.Slice(readIndex, readCount);
|
|
ArrayOf<DiagnosticInfo> subRangeDiagnostics =
|
|
diagnosticInfos.Count > 0
|
|
? diagnosticInfos.Slice(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,
|
|
ArrayOf<ReadValueId> itemsToRead,
|
|
ArrayOf<DataValue> values,
|
|
ArrayOf<DiagnosticInfo> diagnosticInfos)
|
|
{
|
|
// process results.
|
|
NodeClass nodeClass = default;
|
|
|
|
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 (!values[ii].IsGood)
|
|
{
|
|
throw ServiceResultException.Create(
|
|
values[ii].StatusCode,
|
|
ii,
|
|
diagnosticInfos,
|
|
responseHeader.StringTable);
|
|
}
|
|
|
|
// check for valid node class.
|
|
if (!values[ii].WrappedValue.TryGetValue(out nodeClass))
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"Node does not have a valid value for NodeClass: {0}.",
|
|
values[ii]);
|
|
}
|
|
}
|
|
else if (!values[ii].IsGood)
|
|
{
|
|
// 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.IsNull)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"Object does not support the EventNotifier attribute.");
|
|
}
|
|
|
|
objectNode.EventNotifier = value.WrappedValue.GetByte();
|
|
node = objectNode;
|
|
break;
|
|
case NodeClass.ObjectType:
|
|
var objectTypeNode = new ObjectTypeNode();
|
|
|
|
value = attributes[Attributes.IsAbstract];
|
|
if (value.IsNull)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"ObjectType does not support the IsAbstract attribute.");
|
|
}
|
|
|
|
objectTypeNode.IsAbstract = value.WrappedValue.GetBoolean();
|
|
node = objectTypeNode;
|
|
break;
|
|
case NodeClass.Variable:
|
|
var variableNode = new VariableNode();
|
|
|
|
// DataType Attribute
|
|
value = attributes[Attributes.DataType];
|
|
if (value.IsNull)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"Variable does not support the DataType attribute.");
|
|
}
|
|
|
|
variableNode.DataType = value.WrappedValue.GetNodeId();
|
|
|
|
// ValueRank Attribute
|
|
value = attributes[Attributes.ValueRank];
|
|
if (value.IsNull)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"Variable does not support the ValueRank attribute.");
|
|
}
|
|
|
|
variableNode.ValueRank = value.WrappedValue.GetInt32();
|
|
|
|
// ArrayDimensions Attribute
|
|
value = attributes[Attributes.ArrayDimensions];
|
|
|
|
if (!value.IsNull)
|
|
{
|
|
if (!value.WrappedValue.TryGetValue(out ArrayOf<uint> arrayDimensions1))
|
|
{
|
|
variableNode.ArrayDimensions = [];
|
|
}
|
|
else
|
|
{
|
|
variableNode.ArrayDimensions = arrayDimensions1;
|
|
}
|
|
}
|
|
|
|
// AccessLevel Attribute
|
|
value = attributes[Attributes.AccessLevel];
|
|
if (value.IsNull)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"Variable does not support the AccessLevel attribute.");
|
|
}
|
|
|
|
variableNode.AccessLevel = value.WrappedValue.GetByte();
|
|
|
|
// UserAccessLevel Attribute
|
|
value = attributes[Attributes.UserAccessLevel];
|
|
if (value.IsNull)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"Variable does not support the UserAccessLevel attribute.");
|
|
}
|
|
|
|
variableNode.UserAccessLevel = value.WrappedValue.GetByte();
|
|
|
|
// Historizing Attribute
|
|
value = attributes[Attributes.Historizing];
|
|
if (value.IsNull)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"Variable does not support the Historizing attribute.");
|
|
}
|
|
|
|
variableNode.Historizing = value.WrappedValue.GetBoolean();
|
|
|
|
// MinimumSamplingInterval Attribute
|
|
value = attributes[Attributes.MinimumSamplingInterval];
|
|
if (!value.IsNull)
|
|
{
|
|
variableNode.MinimumSamplingInterval =
|
|
(double)value.WrappedValue.ConvertToDouble();
|
|
}
|
|
|
|
// AccessLevelEx Attribute
|
|
value = attributes[Attributes.AccessLevelEx];
|
|
if (!value.IsNull)
|
|
{
|
|
variableNode.AccessLevelEx = value.WrappedValue.GetUInt32();
|
|
}
|
|
|
|
node = variableNode;
|
|
break;
|
|
case NodeClass.VariableType:
|
|
var variableTypeNode = new VariableTypeNode();
|
|
|
|
// IsAbstract Attribute
|
|
value = attributes[Attributes.IsAbstract];
|
|
if (value.IsNull)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"VariableType does not support the IsAbstract attribute.");
|
|
}
|
|
|
|
variableTypeNode.IsAbstract = value.WrappedValue.GetBoolean();
|
|
|
|
// DataType Attribute
|
|
value = attributes[Attributes.DataType];
|
|
if (value.IsNull)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"VariableType does not support the DataType attribute.");
|
|
}
|
|
|
|
variableTypeNode.DataType = value.WrappedValue.GetNodeId();
|
|
|
|
// ValueRank Attribute
|
|
value = attributes[Attributes.ValueRank];
|
|
if (value.IsNull)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"VariableType does not support the ValueRank attribute.");
|
|
}
|
|
|
|
variableTypeNode.ValueRank = value.WrappedValue.GetInt32();
|
|
|
|
// ArrayDimensions Attribute
|
|
value = attributes[Attributes.ArrayDimensions];
|
|
|
|
if (!value.IsNull &&
|
|
value.WrappedValue.TryGetValue(out ArrayOf<uint> arrayDimensions2))
|
|
{
|
|
variableTypeNode.ArrayDimensions = arrayDimensions2;
|
|
}
|
|
|
|
node = variableTypeNode;
|
|
break;
|
|
case NodeClass.Method:
|
|
var methodNode = new MethodNode();
|
|
|
|
// Executable Attribute
|
|
value = attributes[Attributes.Executable];
|
|
if (value.IsNull)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"Method does not support the Executable attribute.");
|
|
}
|
|
|
|
methodNode.Executable = value.WrappedValue.GetBoolean();
|
|
|
|
// UserExecutable Attribute
|
|
value = attributes[Attributes.UserExecutable];
|
|
if (value.IsNull)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"Method does not support the UserExecutable attribute.");
|
|
}
|
|
|
|
methodNode.UserExecutable = value.WrappedValue.GetBoolean();
|
|
|
|
node = methodNode;
|
|
break;
|
|
case NodeClass.DataType:
|
|
var dataTypeNode = new DataTypeNode();
|
|
|
|
// IsAbstract Attribute
|
|
value = attributes[Attributes.IsAbstract];
|
|
if (value.IsNull)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"DataType does not support the IsAbstract attribute.");
|
|
}
|
|
|
|
dataTypeNode.IsAbstract = value.WrappedValue.GetBoolean();
|
|
|
|
// DataTypeDefinition Attribute
|
|
value = attributes[Attributes.DataTypeDefinition];
|
|
|
|
if (!value.IsNull)
|
|
{
|
|
dataTypeNode.DataTypeDefinition =
|
|
value.WrappedValue.TryGetValue(out ExtensionObject eo) ? eo : default;
|
|
}
|
|
|
|
node = dataTypeNode;
|
|
break;
|
|
case NodeClass.ReferenceType:
|
|
var referenceTypeNode = new ReferenceTypeNode();
|
|
|
|
// IsAbstract Attribute
|
|
value = attributes[Attributes.IsAbstract];
|
|
if (value.IsNull)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"ReferenceType does not support the IsAbstract attribute.");
|
|
}
|
|
|
|
referenceTypeNode.IsAbstract = value.WrappedValue.GetBoolean();
|
|
|
|
// Symmetric Attribute
|
|
value = attributes[Attributes.Symmetric];
|
|
if (value.IsNull)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"ReferenceType does not support the Symmetric attribute.");
|
|
}
|
|
|
|
referenceTypeNode.Symmetric = value.WrappedValue.GetBoolean();
|
|
|
|
// InverseName Attribute
|
|
value = attributes[Attributes.InverseName];
|
|
|
|
if (!value.IsNull &&
|
|
value.WrappedValue.TryGetValue(out LocalizedText inverseName))
|
|
{
|
|
referenceTypeNode.InverseName = inverseName;
|
|
}
|
|
|
|
node = referenceTypeNode;
|
|
break;
|
|
case NodeClass.View:
|
|
var viewNode = new ViewNode();
|
|
|
|
// EventNotifier Attribute
|
|
value = attributes[Attributes.EventNotifier];
|
|
if (value.IsNull)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"View does not support the EventNotifier attribute.");
|
|
}
|
|
|
|
viewNode.EventNotifier = value.WrappedValue.GetByte();
|
|
|
|
// ContainsNoLoops Attribute
|
|
value = attributes[Attributes.ContainsNoLoops];
|
|
if (value.IsNull)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"View does not support the ContainsNoLoops attribute.");
|
|
}
|
|
|
|
viewNode.ContainsNoLoops = value.WrappedValue.GetBoolean();
|
|
|
|
node = viewNode;
|
|
break;
|
|
case NodeClass.Unspecified:
|
|
throw ServiceResultException.Unexpected(
|
|
"Node does not have a valid value for NodeClass: {0}.",
|
|
nodeClass);
|
|
default:
|
|
throw ServiceResultException.Unexpected(
|
|
$"Unexpected NodeClass: {nodeClass}.");
|
|
}
|
|
|
|
// NodeId Attribute
|
|
value = attributes[Attributes.NodeId];
|
|
if (value.IsNull)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"Node does not support the NodeId attribute.");
|
|
}
|
|
|
|
node.NodeId = value.WrappedValue.GetNodeId();
|
|
node.NodeClass = nodeClass;
|
|
|
|
// BrowseName Attribute
|
|
value = attributes[Attributes.BrowseName];
|
|
if (value.IsNull)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"Node does not support the BrowseName attribute.");
|
|
}
|
|
|
|
node.BrowseName = value.WrappedValue.GetQualifiedName();
|
|
|
|
// DisplayName Attribute
|
|
value = attributes[Attributes.DisplayName];
|
|
if (value.IsNull)
|
|
{
|
|
throw ServiceResultException.Unexpected(
|
|
"Node does not support the DisplayName attribute.");
|
|
}
|
|
|
|
node.DisplayName = value.WrappedValue.GetLocalizedText();
|
|
|
|
// all optional attributes follow
|
|
|
|
// Description Attribute
|
|
if (attributes.TryGetValue(Attributes.Description, out value) &&
|
|
!value.IsNull &&
|
|
!value.WrappedValue.IsNull)
|
|
{
|
|
node.Description = value.WrappedValue.GetLocalizedText();
|
|
}
|
|
|
|
// WriteMask Attribute
|
|
if (attributes.TryGetValue(Attributes.WriteMask, out value) &&
|
|
!value.IsNull)
|
|
{
|
|
node.WriteMask = value.WrappedValue.GetUInt32();
|
|
}
|
|
|
|
// UserWriteMask Attribute
|
|
if (attributes.TryGetValue(Attributes.UserWriteMask, out value) &&
|
|
!value.IsNull)
|
|
{
|
|
node.UserWriteMask = value.WrappedValue.GetUInt32();
|
|
}
|
|
|
|
// RolePermissions Attribute
|
|
if (attributes.TryGetValue(Attributes.RolePermissions, out value) &&
|
|
!value.IsNull)
|
|
{
|
|
if (value.WrappedValue.TryGetValue(out ArrayOf<ExtensionObject> rolePermissions))
|
|
{
|
|
// Preserve original behavior of inserting null entries when an
|
|
// ExtensionObject cannot be decoded into a RolePermissionType. The
|
|
// declared element type of node.RolePermissions is non-nullable so
|
|
// the failure branch uses null! to suppress nullability metadata.
|
|
var rolePermissionList = new List<RolePermissionType>();
|
|
foreach (ExtensionObject rolePermission in rolePermissions)
|
|
{
|
|
rolePermissionList.Add(rolePermission.TryGetValue(
|
|
out RolePermissionType? rolePermissionType) ? rolePermissionType! : null!);
|
|
}
|
|
node.RolePermissions = rolePermissionList;
|
|
}
|
|
}
|
|
|
|
// UserRolePermissions Attribute
|
|
if (attributes.TryGetValue(Attributes.UserRolePermissions, out value) &&
|
|
!value.IsNull)
|
|
{
|
|
if (value.WrappedValue.TryGetValue(out ArrayOf<ExtensionObject> userRolePermissions))
|
|
{
|
|
// See RolePermissions above for the rationale behind the null! casts.
|
|
var userRolePermissionList = new List<RolePermissionType>();
|
|
foreach (ExtensionObject rolePermission in userRolePermissions)
|
|
{
|
|
userRolePermissionList.Add(rolePermission.TryGetValue(
|
|
out RolePermissionType? rolePermissionType) ? rolePermissionType! : null!);
|
|
}
|
|
node.UserRolePermissions = userRolePermissionList;
|
|
}
|
|
}
|
|
|
|
// AccessRestrictions Attribute
|
|
if (attributes.TryGetValue(Attributes.AccessRestrictions, out value) &&
|
|
!value.IsNull)
|
|
{
|
|
node.AccessRestrictions = value.WrappedValue.GetUInt16();
|
|
}
|
|
|
|
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, default },
|
|
{ Attributes.NodeClass, default },
|
|
{ Attributes.BrowseName, default },
|
|
{ Attributes.DisplayName, default }
|
|
};
|
|
|
|
switch (nodeClass)
|
|
{
|
|
case NodeClass.Object:
|
|
attributes.Add(Attributes.EventNotifier, default);
|
|
break;
|
|
case NodeClass.Variable:
|
|
attributes.Add(Attributes.DataType, default);
|
|
attributes.Add(Attributes.ValueRank, default);
|
|
attributes.Add(Attributes.ArrayDimensions, default);
|
|
attributes.Add(Attributes.AccessLevel, default);
|
|
attributes.Add(Attributes.UserAccessLevel, default);
|
|
attributes.Add(Attributes.Historizing, default);
|
|
attributes.Add(Attributes.MinimumSamplingInterval, default);
|
|
attributes.Add(Attributes.AccessLevelEx, default);
|
|
break;
|
|
case NodeClass.Method:
|
|
attributes.Add(Attributes.Executable, default);
|
|
attributes.Add(Attributes.UserExecutable, default);
|
|
break;
|
|
case NodeClass.ObjectType:
|
|
attributes.Add(Attributes.IsAbstract, default);
|
|
break;
|
|
case NodeClass.VariableType:
|
|
attributes.Add(Attributes.IsAbstract, default);
|
|
attributes.Add(Attributes.DataType, default);
|
|
attributes.Add(Attributes.ValueRank, default);
|
|
attributes.Add(Attributes.ArrayDimensions, default);
|
|
break;
|
|
case NodeClass.ReferenceType:
|
|
attributes.Add(Attributes.IsAbstract, default);
|
|
attributes.Add(Attributes.Symmetric, default);
|
|
attributes.Add(Attributes.InverseName, default);
|
|
break;
|
|
case NodeClass.DataType:
|
|
attributes.Add(Attributes.IsAbstract, default);
|
|
attributes.Add(Attributes.DataTypeDefinition, default);
|
|
break;
|
|
case NodeClass.View:
|
|
attributes.Add(Attributes.EventNotifier, default);
|
|
attributes.Add(Attributes.ContainsNoLoops, default);
|
|
break;
|
|
case NodeClass.Unspecified:
|
|
// build complete list of attributes.
|
|
attributes.Add(Attributes.DataType, default);
|
|
attributes.Add(Attributes.ValueRank, default);
|
|
attributes.Add(Attributes.ArrayDimensions, default);
|
|
attributes.Add(Attributes.AccessLevel, default);
|
|
attributes.Add(Attributes.UserAccessLevel, default);
|
|
attributes.Add(Attributes.MinimumSamplingInterval, default);
|
|
attributes.Add(Attributes.Historizing, default);
|
|
attributes.Add(Attributes.EventNotifier, default);
|
|
attributes.Add(Attributes.Executable, default);
|
|
attributes.Add(Attributes.UserExecutable, default);
|
|
attributes.Add(Attributes.IsAbstract, default);
|
|
attributes.Add(Attributes.InverseName, default);
|
|
attributes.Add(Attributes.Symmetric, default);
|
|
attributes.Add(Attributes.ContainsNoLoops, default);
|
|
attributes.Add(Attributes.DataTypeDefinition, default);
|
|
attributes.Add(Attributes.AccessLevelEx, default);
|
|
break;
|
|
default:
|
|
throw ServiceResultException.Unexpected(
|
|
$"Unexpected NodeClass: {nodeClass}.");
|
|
}
|
|
|
|
if (!skipOptionalAttributes)
|
|
{
|
|
attributes.Add(Attributes.Description, default);
|
|
attributes.Add(Attributes.WriteMask, default);
|
|
attributes.Add(Attributes.UserWriteMask, default);
|
|
attributes.Add(Attributes.RolePermissions, default);
|
|
attributes.Add(Attributes.UserRolePermissions, default);
|
|
attributes.Add(Attributes.AccessRestrictions, default);
|
|
}
|
|
|
|
return attributes;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a read request with attributes determined by the NodeClass.
|
|
/// </summary>
|
|
private static void CreateNodeClassAttributesReadNodesRequest(
|
|
ArrayOf<NodeId> nodeIds,
|
|
NodeClass nodeClass,
|
|
List<ReadValueId> attributesToRead,
|
|
List<IDictionary<uint, DataValue>?> attributesPerNodeId,
|
|
List<Node> 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;
|
|
}
|
|
}
|