/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Opc.Ua.Redaction;
namespace Opc.Ua.Client
{
///
/// An implementation of a client side nodecache.
///
public class NodeCache : INodeCache, IDisposable
{
///
/// Initializes the object with default values.
///
public NodeCache(INodeCacheContext context, ITelemetryContext telemetry)
{
m_context = context ?? throw new ArgumentNullException(nameof(context));
m_logger = telemetry.CreateLogger();
m_typeTree = new TypeTable(m_context.NamespaceUris);
m_nodes = new NodeTable(
m_context.NamespaceUris,
m_context.ServerUris,
m_typeTree);
m_uaTypesLoaded = false;
m_cacheLock = new ReaderWriterLockSlim();
}
///
/// An overrideable version of the Dispose.
///
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
m_cacheLock?.Dispose();
}
}
///
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
///
public NamespaceTable NamespaceUris => m_context.NamespaceUris;
///
public StringTable ServerUris => m_context.ServerUris;
///
IAsyncTypeTable IAsyncNodeTable.TypeTree => this;
///
/// Legacy type table adapter for backwards compatibility.
///
public ITypeTable TypeTree => this.AsTypeTable();
///
public async ValueTask FindAsync(
ExpandedNodeId nodeId,
CancellationToken ct = default)
{
// check for null.
if (NodeId.IsNull(nodeId))
{
return null;
}
m_cacheLock.EnterReadLock();
INode node;
try
{
// check if node already exists.
node = m_nodes.Find(nodeId);
}
finally
{
m_cacheLock.ExitReadLock();
}
if (node != null)
{
// do not return temporary nodes created after a Browse().
if (node.GetType() != typeof(Node))
{
return node;
}
}
// fetch node from server.
try
{
return await FetchNodeAsync(nodeId, ct).ConfigureAwait(false);
}
catch (Exception e)
{
m_logger.LogError(
"Could not fetch node from server: NodeId={NodeId}, Reason='{Error}'.",
nodeId,
Redact.Create(e));
// m_nodes[nodeId] = null;
return null;
}
}
///
public async Task> FindAsync(
IList nodeIds,
CancellationToken ct = default)
{
// check for null.
if (nodeIds == null || nodeIds.Count == 0)
{
return [];
}
int count = nodeIds.Count;
var nodes = new List(count);
var fetchNodeIds = new ExpandedNodeIdCollection();
int ii;
for (ii = 0; ii < count; ii++)
{
INode node;
m_cacheLock.EnterReadLock();
try
{
// check if node already exists.
node = m_nodes.Find(nodeIds[ii]);
}
finally
{
m_cacheLock.ExitReadLock();
}
// do not return temporary nodes created after a Browse().
if (node != null && node.GetType() != typeof(Node))
{
nodes.Add(node);
}
else
{
nodes.Add(null);
fetchNodeIds.Add(nodeIds[ii]);
}
}
if (fetchNodeIds.Count == 0)
{
return nodes;
}
// fetch missing nodes from server.
IList fetchedNodes;
try
{
fetchedNodes = await FetchNodesAsync(fetchNodeIds, ct).ConfigureAwait(false);
}
catch (Exception e)
{
m_logger.LogError("Could not fetch nodes from server: Reason='{Error}'.", e.Message);
// m_nodes[nodeId] = null;
return nodes;
}
ii = 0;
foreach (Node? fetchedNode in fetchedNodes)
{
while (ii < count && nodes[ii] != null)
{
ii++;
}
if (ii < count && nodes[ii] == null)
{
nodes[ii++] = fetchedNode;
}
else
{
m_logger.LogError(
"Inconsistency fetching nodes from server. Not all nodes could be assigned.");
break;
}
}
return nodes;
}
///
public async ValueTask FindAsync(
ExpandedNodeId sourceId,
NodeId referenceTypeId,
bool isInverse,
bool includeSubtypes,
QualifiedName browseName,
CancellationToken ct = default)
{
// find the source.
if (await FindAsync(sourceId, ct).ConfigureAwait(false)
is not Node source)
{
return null;
}
IList references;
m_cacheLock.EnterReadLock();
try
{
// find all references.
references = source.ReferenceTable
.Find(referenceTypeId, isInverse, includeSubtypes, m_typeTree);
}
finally
{
m_cacheLock.ExitReadLock();
}
foreach (IReference reference in references)
{
INode? target = await FindAsync(reference.TargetId, ct)
.ConfigureAwait(false);
if (target == null)
{
continue;
}
if (target.BrowseName == browseName)
{
return target;
}
}
// target not found.
return null;
}
///
public async ValueTask FindSuperTypeAsync(
ExpandedNodeId typeId,
CancellationToken ct = default)
{
INode? type = await FindAsync(typeId, ct).ConfigureAwait(false);
if (type == null)
{
return NodeId.Null;
}
m_cacheLock.EnterReadLock();
try
{
return m_typeTree.FindSuperType(typeId);
}
finally
{
m_cacheLock.ExitReadLock();
}
}
///
public async ValueTask> FindAsync(
ExpandedNodeId sourceId,
NodeId referenceTypeId,
bool isInverse,
bool includeSubtypes,
CancellationToken ct = default)
{
var hits = new List();
// find the source.
if (await FindAsync(sourceId, ct).ConfigureAwait(false)
is not Node source)
{
return hits;
}
IList references;
m_cacheLock.EnterReadLock();
try
{
// find all references.
references = source.ReferenceTable
.Find(referenceTypeId, isInverse, includeSubtypes, m_typeTree);
}
finally
{
m_cacheLock.ExitReadLock();
}
foreach (IReference reference in references)
{
INode? target =
await FindAsync(reference.TargetId, ct).ConfigureAwait(false);
if (target == null)
{
continue;
}
hits.Add(target);
}
return hits;
}
///
public async ValueTask FindSuperTypeAsync(
NodeId typeId,
CancellationToken ct = default)
{
INode? type = await FindAsync(typeId, ct).ConfigureAwait(false);
if (type == null)
{
return NodeId.Null;
}
m_cacheLock.EnterReadLock();
try
{
return m_typeTree.FindSuperType(typeId);
}
finally
{
m_cacheLock.ExitReadLock();
}
}
///
public async Task FetchNodeAsync(ExpandedNodeId nodeId, CancellationToken ct)
{
var localId = ExpandedNodeId.ToNodeId(nodeId, m_context.NamespaceUris);
if (localId == null)
{
return null;
}
// fetch node from server.
Node source = await m_context.FetchNodeAsync(null, localId, ct: ct).ConfigureAwait(false);
try
{
// fetch references from server.
ReferenceDescriptionCollection references = await m_context
.FetchReferencesAsync(null, localId, ct)
.ConfigureAwait(false);
m_cacheLock.EnterUpgradeableReadLock();
try
{
foreach (ReferenceDescription reference in references)
{
// create a placeholder for the node if it does not already exist.
if (!m_nodes.Exists(reference.NodeId))
{
// transform absolute identifiers.
if (reference.NodeId != null && reference.NodeId.IsAbsolute)
{
reference.NodeId = ExpandedNodeId.ToNodeId(
reference.NodeId,
NamespaceUris);
}
var target = new Node(reference);
InternalWriteLockedAttach(target);
}
// add the reference.
source.ReferenceTable
.Add(reference.ReferenceTypeId, !reference.IsForward, reference.NodeId);
}
}
finally
{
m_cacheLock.ExitUpgradeableReadLock();
}
}
catch (Exception e)
{
m_logger.LogError(
"Could not fetch references for valid node with NodeId = {NodeId}. Error = {Error}",
nodeId,
Redact.Create(e));
}
InternalWriteLockedAttach(source);
return source;
}
///
public async Task> FetchNodesAsync(
IList nodeIds,
CancellationToken ct)
{
int count = nodeIds.Count;
if (count == 0)
{
return [];
}
var localIds = new NodeIdCollection(
nodeIds.Select(nodeId => ExpandedNodeId.ToNodeId(nodeId, m_context.NamespaceUris)));
// fetch nodes and references from server.
(IReadOnlyList sourceNodes, IReadOnlyList readErrors) = await m_context
.FetchNodesAsync(null, localIds, NodeClass.Unspecified, ct: ct)
.ConfigureAwait(false);
(IReadOnlyList referenceCollectionList, IReadOnlyList fetchErrors) =
await m_context.FetchReferencesAsync(null, localIds, ct).ConfigureAwait(false);
int ii = 0;
for (ii = 0; ii < count; ii++)
{
if (ServiceResult.IsBad(readErrors[ii]))
{
continue;
}
if (!ServiceResult.IsBad(fetchErrors[ii]))
{
// fetch references from server.
foreach (ReferenceDescription reference in referenceCollectionList[ii])
{
m_cacheLock.EnterUpgradeableReadLock();
try
{
// create a placeholder for the node if it does not already exist.
if (!m_nodes.Exists(reference.NodeId))
{
// transform absolute identifiers.
if (reference.NodeId != null && reference.NodeId.IsAbsolute)
{
reference.NodeId = ExpandedNodeId.ToNodeId(
reference.NodeId,
NamespaceUris);
}
var target = new Node(reference);
InternalWriteLockedAttach(target);
}
}
finally
{
m_cacheLock.ExitUpgradeableReadLock();
}
// add the reference.
sourceNodes[ii]
.ReferenceTable.Add(
reference.ReferenceTypeId,
!reference.IsForward,
reference.NodeId);
}
}
InternalWriteLockedAttach(sourceNodes[ii]);
}
return [.. sourceNodes];
}
///
public async Task> FindReferencesAsync(
ExpandedNodeId nodeId,
NodeId referenceTypeId,
bool isInverse,
bool includeSubtypes,
CancellationToken ct)
{
IList targets = [];
if (await FindAsync(nodeId, ct).ConfigureAwait(false) is not Node source)
{
return targets;
}
IList references;
m_cacheLock.EnterReadLock();
try
{
references = source.ReferenceTable
.Find(referenceTypeId, isInverse, includeSubtypes, m_typeTree);
}
finally
{
m_cacheLock.ExitReadLock();
}
var targetIds = new ExpandedNodeIdCollection(
references.Select(reference => reference.TargetId));
IList result = await FindAsync(targetIds, ct).ConfigureAwait(false);
foreach (INode? target in result)
{
if (target != null)
{
targets.Add(target);
}
}
return targets;
}
///
public async Task> FindReferencesAsync(
IList nodeIds,
IList referenceTypeIds,
bool isInverse,
bool includeSubtypes,
CancellationToken ct)
{
IList targets = [];
if (nodeIds.Count == 0 || referenceTypeIds.Count == 0)
{
return targets;
}
var targetIds = new ExpandedNodeIdCollection();
IList sources = await FindAsync(nodeIds, ct).ConfigureAwait(false);
foreach (INode? source in sources)
{
if (source is not Node node)
{
continue;
}
foreach (NodeId referenceTypeId in referenceTypeIds)
{
IList references;
m_cacheLock.EnterReadLock();
try
{
references = node.ReferenceTable
.Find(referenceTypeId, isInverse, includeSubtypes, m_typeTree);
}
finally
{
m_cacheLock.ExitReadLock();
}
targetIds.AddRange(references.Select(reference => reference.TargetId));
}
}
IList result = await FindAsync(targetIds, ct).ConfigureAwait(false);
foreach (INode? target in result)
{
if (target != null)
{
targets.Add(target);
}
}
return targets;
}
///
public async Task FetchSuperTypesAsync(ExpandedNodeId nodeId, CancellationToken ct)
{
// find the target node,
if (await FindAsync(nodeId, ct).ConfigureAwait(false) is not ILocalNode source)
{
return;
}
// follow the tree.
ILocalNode? subType = source;
while (subType != null)
{
ILocalNode? superType = null;
// Get super type (should be 1 or none)
IList references = await FindReferencesAsync(
subType.NodeId,
ReferenceTypeIds.HasSubtype,
true,
true,
ct).ConfigureAwait(false);
if (references != null && references.Count > 0)
{
superType = references[0] as ILocalNode;
}
subType = superType;
}
}
///
public async ValueTask ExistsAsync(
ExpandedNodeId nodeId,
CancellationToken ct = default)
{
return await FindAsync(nodeId, ct).ConfigureAwait(false) != null;
}
///
public async ValueTask IsKnownAsync(
ExpandedNodeId typeId,
CancellationToken ct = default)
{
INode? type = await FindAsync(typeId, ct).ConfigureAwait(false);
if (type == null)
{
return false;
}
m_cacheLock.EnterReadLock();
try
{
return m_typeTree.IsKnown(typeId);
}
finally
{
m_cacheLock.ExitReadLock();
}
}
///
public async ValueTask IsKnownAsync(
NodeId typeId,
CancellationToken ct = default)
{
INode? type = await FindAsync(typeId, ct).ConfigureAwait(false);
if (type == null)
{
return false;
}
m_cacheLock.EnterReadLock();
try
{
return m_typeTree.IsKnown(typeId);
}
finally
{
m_cacheLock.ExitReadLock();
}
}
///
public async ValueTask> FindSubTypesAsync(
ExpandedNodeId typeId,
CancellationToken ct = default)
{
var subtypes = new List();
if (await FindAsync(typeId, ct).ConfigureAwait(false)
is not ILocalNode type)
{
return subtypes;
}
IList references;
m_cacheLock.EnterReadLock();
try
{
references = type.References
.Find(ReferenceTypeIds.HasSubtype, false, true, m_typeTree);
}
finally
{
m_cacheLock.ExitReadLock();
}
foreach (IReference reference in references)
{
if (!reference.TargetId.IsAbsolute)
{
subtypes.Add((NodeId)reference.TargetId);
}
}
return subtypes;
}
///
public async ValueTask IsTypeOfAsync(
ExpandedNodeId subTypeId,
ExpandedNodeId superTypeId,
CancellationToken ct = default)
{
if (subTypeId == superTypeId)
{
return true;
}
if (await FindAsync(subTypeId, ct).ConfigureAwait(false)
is not ILocalNode subtype)
{
return false;
}
ILocalNode? supertype = subtype;
while (supertype != null)
{
ExpandedNodeId currentId;
m_cacheLock.EnterReadLock();
try
{
currentId = supertype.References
.FindTarget(ReferenceTypeIds.HasSubtype, true, true, m_typeTree, 0);
}
finally
{
m_cacheLock.ExitReadLock();
}
if (currentId == superTypeId)
{
return true;
}
supertype = await FindAsync(currentId, ct).ConfigureAwait(false) as ILocalNode;
}
return false;
}
///
public async ValueTask IsTypeOfAsync(
NodeId subTypeId,
NodeId superTypeId,
CancellationToken ct)
{
if (subTypeId == superTypeId)
{
return true;
}
if (await FindAsync(subTypeId, ct).ConfigureAwait(false)
is not ILocalNode subtype)
{
return false;
}
ILocalNode? supertype = subtype;
while (supertype != null)
{
ExpandedNodeId currentId;
m_cacheLock.EnterReadLock();
try
{
currentId = supertype.References
.FindTarget(ReferenceTypeIds.HasSubtype, true, true, m_typeTree, 0);
}
finally
{
m_cacheLock.ExitReadLock();
}
if (currentId == superTypeId)
{
return true;
}
supertype = await FindAsync(currentId, ct).ConfigureAwait(false) as
ILocalNode;
}
return false;
}
///
public ValueTask FindReferenceTypeNameAsync(
NodeId referenceTypeId,
CancellationToken ct = default)
{
QualifiedName? typeName;
m_cacheLock.EnterReadLock();
try
{
typeName = m_typeTree.FindReferenceTypeName(referenceTypeId);
}
finally
{
m_cacheLock.ExitReadLock();
}
return new ValueTask(typeName);
}
///
public ValueTask FindReferenceTypeAsync(
QualifiedName browseName,
CancellationToken ct = default)
{
NodeId referenceType;
m_cacheLock.EnterReadLock();
try
{
referenceType = m_typeTree.FindReferenceType(browseName);
}
finally
{
m_cacheLock.ExitReadLock();
}
return new ValueTask(referenceType);
}
///
public async ValueTask IsEncodingOfAsync(
ExpandedNodeId encodingId,
ExpandedNodeId datatypeId,
CancellationToken ct = default)
{
if (await FindAsync(encodingId, ct).ConfigureAwait(false)
is not ILocalNode encoding)
{
return false;
}
IList references;
m_cacheLock.EnterReadLock();
try
{
references = encoding.References
.Find(ReferenceTypeIds.HasEncoding, true, true, m_typeTree);
}
finally
{
m_cacheLock.ExitReadLock();
}
foreach (IReference reference in references)
{
if (reference.TargetId == datatypeId)
{
return true;
}
}
// no match.
return false;
}
///
public async ValueTask IsEncodingForAsync(
NodeId expectedTypeId,
ExtensionObject value,
CancellationToken ct = default)
{
// no match on null values.
if (value == null)
{
return false;
}
// check for exact match.
if (expectedTypeId == value.TypeId)
{
return true;
}
// find the encoding.
if (await FindAsync(value.TypeId, ct).ConfigureAwait(false)
is not ILocalNode encoding)
{
return false;
}
IList references;
m_cacheLock.EnterReadLock();
try
{
references = encoding.References
.Find(ReferenceTypeIds.HasEncoding, true, true, m_typeTree);
}
finally
{
m_cacheLock.ExitReadLock();
}
// find data type.
foreach (IReference reference in references)
{
if (reference.TargetId == expectedTypeId)
{
return true;
}
}
// no match.
return false;
}
///
public async ValueTask IsEncodingForAsync(
NodeId expectedTypeId,
object value,
CancellationToken ct = default)
{
// null actual datatype matches nothing.
if (value == null)
{
return false;
}
// null expected datatype matches everything.
if (NodeId.IsNull(expectedTypeId))
{
return true;
}
// get the actual datatype.
NodeId actualTypeId = TypeInfo.GetDataTypeId(value);
// value is valid if the expected datatype is same as or a supertype of the actual datatype
// for example: expected datatype of 'Integer' matches an actual datatype of 'UInt32'.
if (await IsTypeOfAsync(actualTypeId, expectedTypeId, ct).ConfigureAwait(false))
{
return true;
}
// allow matches non-structure values where the actual datatype
// is a supertype of the expected datatype.
// for example: expected datatype of 'UtcTime' matches an actual
// datatype of 'DateTime'.
if (actualTypeId != DataTypes.Structure)
{
return await IsTypeOfAsync(
expectedTypeId,
actualTypeId,
ct).ConfigureAwait(false);
}
// for structure types must try to determine the subtype.
if (value is ExtensionObject extension)
{
return await IsEncodingForAsync(
expectedTypeId,
extension,
ct).ConfigureAwait(false);
}
// every element in an array must match.
if (value is ExtensionObject[] extensions)
{
for (int ii = 0; ii < extensions.Length; ii++)
{
if (!await IsEncodingForAsync(
expectedTypeId,
extensions[ii],
ct).ConfigureAwait(false))
{
return false;
}
}
return true;
}
// can only get here if the value is an unrecognized data type.
return false;
}
///
public async ValueTask FindDataTypeIdAsync(
ExpandedNodeId encodingId,
CancellationToken ct = default)
{
if (await FindAsync(encodingId, ct).ConfigureAwait(false)
is not ILocalNode encoding)
{
return NodeId.Null;
}
IList references;
m_cacheLock.EnterReadLock();
try
{
references = encoding.References
.Find(ReferenceTypeIds.HasEncoding, true, true, m_typeTree);
}
finally
{
m_cacheLock.ExitReadLock();
}
if (references.Count > 0)
{
return ExpandedNodeId.ToNodeId(references[0].TargetId, m_context.NamespaceUris);
}
return NodeId.Null;
}
///
public async ValueTask FindDataTypeIdAsync(
NodeId encodingId,
CancellationToken ct = default)
{
if (await FindAsync(encodingId, ct).ConfigureAwait(false)
is not ILocalNode encoding)
{
return NodeId.Null;
}
IList references;
m_cacheLock.EnterReadLock();
try
{
references = encoding.References
.Find(ReferenceTypeIds.HasEncoding, true, true, m_typeTree);
}
finally
{
m_cacheLock.ExitReadLock();
}
if (references.Count > 0)
{
return ExpandedNodeId.ToNodeId(references[0].TargetId, m_context.NamespaceUris);
}
return NodeId.Null;
}
///
public void LoadUaDefinedTypes(ISystemContext context)
{
if (m_uaTypesLoaded)
{
return;
}
var predefinedNodes = new NodeStateCollection();
Assembly assembly = typeof(ReadRequest).GetTypeInfo().Assembly;
predefinedNodes.LoadFromBinaryResource(
context,
"Opc.Ua.Stack.Generated.Opc.Ua.PredefinedNodes.uanodes",
assembly,
true);
m_cacheLock.EnterWriteLock();
try
{
for (int ii = 0; ii < predefinedNodes.Count; ii++)
{
if (predefinedNodes[ii] is not BaseTypeState type)
{
continue;
}
type.Export(context, m_nodes);
}
}
finally
{
m_cacheLock.ExitWriteLock();
}
m_uaTypesLoaded = true;
}
///
public void Clear()
{
m_cacheLock.EnterWriteLock();
try
{
m_uaTypesLoaded = false;
m_nodes.Clear();
}
finally
{
m_cacheLock.ExitWriteLock();
}
}
///
public async ValueTask GetDisplayTextAsync(
INode node,
CancellationToken ct = default)
{
// check for null.
if (node == null)
{
return string.Empty;
}
// check for remote node.
if (node is not Node target)
{
return node.ToString();
}
string? displayText = null;
// use the modelling rule to determine which parent to follow.
NodeId modellingRule = target.ModellingRule;
IList references;
m_cacheLock.EnterReadLock();
try
{
references = target.ReferenceTable
.Find(ReferenceTypeIds.Aggregates, true, true, m_typeTree);
}
finally
{
m_cacheLock.ExitReadLock();
}
foreach (IReference reference in references)
{
var parent = await FindAsync(
reference.TargetId,
ct).ConfigureAwait(false) as Node;
// use the first parent if modelling rule is new.
if (modellingRule == Objects.ModellingRule_Mandatory)
{
displayText = parent == null ? null : await GetDisplayTextAsync(
parent,
ct).ConfigureAwait(false);
break;
}
// use the type node as the parent for other modelling rules.
if (parent is VariableTypeNode or ObjectTypeNode)
{
displayText = await GetDisplayTextAsync(
parent,
ct).ConfigureAwait(false);
break;
}
}
// prepend the parent display name.
if (displayText != null)
{
return Utils.Format("{0}.{1}", displayText, node);
}
// simply use the node name.
return node.ToString();
}
///
public async ValueTask GetDisplayTextAsync(
ExpandedNodeId nodeId,
CancellationToken ct = default)
{
if (NodeId.IsNull(nodeId))
{
return string.Empty;
}
INode? node = await FindAsync(
nodeId,
ct).ConfigureAwait(false);
if (node != null)
{
return await GetDisplayTextAsync(
node,
ct).ConfigureAwait(false);
}
return Utils.Format("{0}", nodeId);
}
///
public async ValueTask GetDisplayTextAsync(
ReferenceDescription reference,
CancellationToken ct = default)
{
if (reference == null || NodeId.IsNull(reference.NodeId))
{
return string.Empty;
}
INode? node = await FindAsync(
reference.NodeId,
ct).ConfigureAwait(false);
if (node != null)
{
return await GetDisplayTextAsync(
node,
ct).ConfigureAwait(false);
}
return reference.ToString();
}
private void InternalWriteLockedAttach(ILocalNode node)
{
m_cacheLock.EnterWriteLock();
try
{
// add to cache.
m_nodes.Attach(node);
}
finally
{
m_cacheLock.ExitWriteLock();
}
}
private readonly ReaderWriterLockSlim m_cacheLock = new();
private readonly ILogger m_logger;
private readonly INodeCacheContext m_context;
private readonly TypeTable m_typeTree;
private readonly NodeTable m_nodes;
private bool m_uaTypesLoaded;
}
}