/* ========================================================================
* 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.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Microsoft.Extensions.Logging;
namespace Opc.Ua.Client.ComplexTypes
{
///
/// Manages the custom types of a server for a client session.
/// Loads the custom types into the type factory
/// of a client session, to allow for decoding and encoding
/// of custom enumeration types and structured types.
///
///
/// Support for V1.03 dictionaries and all V1.04 data type definitions
/// with the following known restrictions:
/// - Support only for V1.03 structured types which can be mapped to the V1.04
/// structured type definition. Unsupported V1.03 types are ignored.
/// - V1.04 OptionSet does not create the enumeration flags.
///
public class ComplexTypeSystem
{
///
/// an internal limit to prevent the retry
/// datatype loader mechanism to loop forever
///
internal const int MaxLoopCount = 100;
///
/// The data type systems that were loaded
///
public NodeIdDictionary DataTypeSystem
=> m_complexTypeResolver.DataTypeSystem;
///
/// Initializes the type system with a session to load the custom types.
///
public ComplexTypeSystem(ISession session)
: this(session, session.MessageContext.Telemetry)
{
}
///
/// Initializes the type system with a session to load the custom types.
///
public ComplexTypeSystem(ISession session, ITelemetryContext telemetry)
: this(session, new ComplexTypeBuilderFactory(), telemetry)
{
}
///
/// Obsolete constructor
///
[Obsolete("Use ComplexTypeSystem(IComplexTypeResolver, ITelemetryContext) instead.")]
public ComplexTypeSystem(IComplexTypeResolver complexTypeResolver)
: this(complexTypeResolver, (ITelemetryContext)null)
{
}
///
/// Initializes the type system with a complex type resolver to load the custom types.
///
public ComplexTypeSystem(
IComplexTypeResolver complexTypeResolver,
ITelemetryContext telemetry)
: this(complexTypeResolver, new ComplexTypeBuilderFactory(), telemetry)
{
}
///
/// Create complex type system with session and custom type builder factory
///
public ComplexTypeSystem(
ISession session,
IComplexTypeFactory complexTypeBuilderFactory)
: this(session, complexTypeBuilderFactory, session.MessageContext.Telemetry)
{
}
///
/// Initializes the type system with a session to load the custom types
/// and a customized type builder factory
///
public ComplexTypeSystem(
ISession session,
IComplexTypeFactory complexTypeBuilderFactory,
ITelemetryContext telemetry)
: this(new NodeCacheResolver(session, telemetry), complexTypeBuilderFactory, telemetry)
{
}
///
/// Obsolete constructor
///
[Obsolete("Use ComplexTypeSystem(IComplexTypeResolver, IComplexTypeFactory, ITelemetryContext) instead.")]
public ComplexTypeSystem(
IComplexTypeResolver complexTypeResolver,
IComplexTypeFactory complexTypeBuilderFactory)
: this(complexTypeResolver, complexTypeBuilderFactory, null)
{
}
///
/// Initializes the type system with a complex type resolver to load the custom types.
///
public ComplexTypeSystem(
IComplexTypeResolver complexTypeResolver,
IComplexTypeFactory complexTypeBuilderFactory,
ITelemetryContext telemetry)
{
m_complexTypeResolver = complexTypeResolver;
m_complexTypeBuilderFactory = complexTypeBuilderFactory;
m_logger = telemetry.CreateLogger();
}
///
/// Load a single custom type with subtypes.
///
///
/// Uses inverse references on the server to find the super type(s).
/// If the new structure contains a type dependency to a yet
/// unknown type, it loads also the dependent type(s).
/// For servers without DataTypeDefinition support, all
/// custom types are loaded.
///
public async Task LoadTypeAsync(
ExpandedNodeId nodeId,
bool subTypes = false,
bool throwOnError = false,
CancellationToken ct = default)
{
try
{
// add fast path, if no subTypes are requested
if (!subTypes)
{
Type systemType = GetSystemType(nodeId);
if (systemType != null)
{
return systemType;
}
}
// cache the server type system
_ = await m_complexTypeResolver
.LoadDataTypesAsync(DataTypeIds.BaseDataType, true, ct: ct)
.ConfigureAwait(false);
IList subTypeNodes = await m_complexTypeResolver
.LoadDataTypesAsync(nodeId, subTypes, true, ct: ct)
.ConfigureAwait(false);
List subTypeNodesWithoutKnownTypes = RemoveKnownTypes(subTypeNodes);
if (subTypeNodesWithoutKnownTypes.Count > 0)
{
IList serverEnumTypes = [];
IList serverStructTypes = [];
foreach (INode node in subTypeNodesWithoutKnownTypes)
{
await AddEnumerationOrStructureTypeAsync(
node,
serverEnumTypes,
serverStructTypes,
ct)
.ConfigureAwait(false);
}
// load server types
if (DisableDataTypeDefinition ||
!await LoadBaseDataTypesAsync(serverEnumTypes, serverStructTypes, ct)
.ConfigureAwait(false))
{
if (!DisableDataTypeDictionary)
{
_ = await LoadDictionaryDataTypesAsync(serverEnumTypes, false, ct)
.ConfigureAwait(false);
}
}
}
// Commit the changes to the factory
m_complexTypeResolver.FactoryBuilder.Commit();
return GetSystemType(nodeId);
}
catch (Exception ex)
{
m_logger.LogError(ex, "Failed to load the custom type {NodeId}.", nodeId);
if (throwOnError)
{
throw;
}
return null;
}
}
///
/// Load all custom types of a namespace.
///
///
/// If a new type in the namespace contains a type dependency to an
/// unknown type in another namespace, it loads also the dependent type(s).
/// For servers without DataTypeDefinition support all
/// custom types are loaded.
///
///
public async Task LoadNamespaceAsync(
string ns,
bool throwOnError = false,
CancellationToken ct = default)
{
try
{
int index = m_complexTypeResolver.NamespaceUris.GetIndex(ns);
if (index < 0)
{
throw new ServiceResultException(
$"Bad argument {ns}. Namespace not found.");
}
ushort nameSpaceIndex = (ushort)index;
_ = await m_complexTypeResolver
.LoadDataTypesAsync(DataTypeIds.BaseDataType, true, ct: ct)
.ConfigureAwait(false);
IList serverEnumTypes = await m_complexTypeResolver
.LoadDataTypesAsync(DataTypeIds.Enumeration, ct: ct)
.ConfigureAwait(false);
IList serverStructTypes = await m_complexTypeResolver
.LoadDataTypesAsync(DataTypeIds.Structure, true, ct: ct)
.ConfigureAwait(false);
// filter for namespace
serverEnumTypes = [.. serverEnumTypes.Where(
rd => rd.NodeId.NamespaceIndex == nameSpaceIndex)];
serverStructTypes = [.. serverStructTypes.Where(
rd => rd.NodeId.NamespaceIndex == nameSpaceIndex)];
// load types
bool allTypesLoaded;
if (DisableDataTypeDefinition ||
!await LoadBaseDataTypesAsync(serverEnumTypes, serverStructTypes, ct)
.ConfigureAwait(false))
{
if (DisableDataTypeDictionary)
{
return false;
}
allTypesLoaded = await LoadDictionaryDataTypesAsync(serverEnumTypes, false, ct)
.ConfigureAwait(false);
}
else
{
allTypesLoaded = true;
}
// Commit the changes to the factory
m_complexTypeResolver.FactoryBuilder.Commit();
return allTypesLoaded;
}
catch (Exception ex)
{
m_logger.LogError(ex, "Failed to load the custom type namespace {Namespace}.", ns);
if (throwOnError)
{
throw;
}
return false;
}
}
///
///
///
[Obsolete("Use LoadAsync instead.")]
public Task Load(
bool onlyEnumTypes = false,
bool throwOnError = false,
CancellationToken ct = default)
{
return LoadAsync(onlyEnumTypes, throwOnError, ct).AsTask();
}
///
/// Load all custom types from a server into the session system type factory.
///
///
/// The loader follows the following strategy:
/// - Load all DataType nodes of the Enumeration subtypes.
/// - Load all DataType nodes of the Structure subtypes.
/// - Create all enumerated custom types using the DataTypeDefinion attribute, if available.
/// - Create all remaining enumerated custom types using the EnumValues or EnumStrings property, if available.
/// - Create all structured types using the DataTypeDefinion attribute, if available.
/// if there are type definitions remaining
/// - Load the binary schema dictionaries with type definitions.
/// - Create all remaining enumerated custom types using the dictionaries.
/// - Convert all structured types in the dictionaries to the DataTypeDefinion attribute, if possible.
/// - Create all structured types from the dictionaries using the converted DataTypeDefinion attribute.
///
/// true if all DataTypes were loaded.
public async ValueTask LoadAsync(
bool onlyEnumTypes = false,
bool throwOnError = false,
CancellationToken ct = default)
{
try
{
// load server types in cache
_ = await m_complexTypeResolver
.LoadDataTypesAsync(DataTypeIds.BaseDataType, true, ct: ct)
.ConfigureAwait(false);
IList serverEnumTypes = await m_complexTypeResolver
.LoadDataTypesAsync(DataTypeIds.Enumeration, ct: ct)
.ConfigureAwait(false);
IList serverStructTypes = onlyEnumTypes
? []
: await m_complexTypeResolver
.LoadDataTypesAsync(DataTypeIds.Structure, true, ct: ct)
.ConfigureAwait(false);
bool allTypesLoaded;
if (DisableDataTypeDefinition ||
!await LoadBaseDataTypesAsync(serverEnumTypes, serverStructTypes, ct)
.ConfigureAwait(false))
{
if (DisableDataTypeDictionary)
{
return false;
}
allTypesLoaded = await LoadDictionaryDataTypesAsync(serverEnumTypes, true, ct)
.ConfigureAwait(false);
}
else
{
allTypesLoaded = true;
}
// Commit the changes to the factory
m_complexTypeResolver.FactoryBuilder.Commit();
return allTypesLoaded;
}
catch (Exception ex)
{
m_logger.LogError(ex, "Failed to load the custom types.");
if (throwOnError)
{
throw;
}
return false;
}
}
///
/// Get the types defined in this type system.
///
public Type[] GetDefinedTypes()
{
return m_complexTypeBuilderFactory.GetTypes();
}
///
/// Returns data types node ids for everything that was defined.
///
public IEnumerable GetDefinedDataTypeIds()
{
return m_dataTypeDefinitionCache.Keys.Select(nodeId =>
NodeId.ToExpandedNodeId(nodeId, m_complexTypeResolver.NamespaceUris));
}
///
/// Get the data type definition and dependent definitions for a data type node id.
/// Recursive through the cache to find all dependent types for structures fields
/// contained in the cache.
///
public NodeIdDictionary GetDataTypeDefinitionsForDataType(
ExpandedNodeId dataTypeId)
{
var dataTypeDefinitions = new NodeIdDictionary();
var dataTypeNodeId = ExpandedNodeId.ToNodeId(
dataTypeId,
m_complexTypeResolver.NamespaceUris);
if (!NodeId.IsNull(dataTypeNodeId))
{
CollectAllDataTypeDefinitions(dataTypeNodeId, dataTypeDefinitions);
}
return dataTypeDefinitions;
void CollectAllDataTypeDefinitions(
NodeId nodeId,
NodeIdDictionary collect)
{
if (NodeId.IsNull(nodeId))
{
return;
}
if (m_dataTypeDefinitionCache.TryGetValue(
nodeId,
out DataTypeDefinition dataTypeDefinition))
{
collect[nodeId] = dataTypeDefinition;
if (dataTypeDefinition is StructureDefinition structureDefinition)
{
foreach (StructureField field in structureDefinition.Fields)
{
if (!IsRecursiveDataType(nodeId, field.DataType) &&
!collect.ContainsKey(nodeId))
{
CollectAllDataTypeDefinitions(field.DataType, collect);
}
}
}
}
}
}
///
/// Clear references in datatype cache.
///
public void ClearDataTypeCache()
{
m_dataTypeDefinitionCache.Clear();
}
///
/// Disable the use of DataTypeDefinition to create the complex type definition.
///
internal bool DisableDataTypeDefinition { get; set; }
///
/// Disable the use of DataType Dictionaries to create the complex type definition.
///
internal bool DisableDataTypeDictionary { get; set; }
///
/// Load listed custom types from dictionaries
/// into the sessions system type factory.
///
///
/// Loads all custom types at this time to avoid
/// complexity when resolving type dependencies.
///
private async Task LoadDictionaryDataTypesAsync(
IList serverEnumTypes,
bool fullTypeList,
CancellationToken ct = default)
{
// build a type dictionary with all known new types
IList allEnumTypes = fullTypeList
? serverEnumTypes
: await m_complexTypeResolver.LoadDataTypesAsync(DataTypeIds.Enumeration, ct: ct)
.ConfigureAwait(false);
var typeDictionary = new Dictionary();
// strip known types from list
serverEnumTypes = RemoveKnownTypes(allEnumTypes);
// load the binary schema dictionaries from the server
IReadOnlyDictionary typeSystem = await m_complexTypeResolver
.LoadDataTypeSystem(ct: ct)
.ConfigureAwait(false);
// sort dictionaries with import dependencies to the end of the list
var sortedTypeSystem = typeSystem.OrderBy(t => t.Value.TypeDictionary?.Import?.Length)
.ToList();
bool allTypesLoaded = true;
// create custom types for all dictionaries
foreach (KeyValuePair dictionaryId in sortedTypeSystem)
{
try
{
DataDictionary dictionary = dictionaryId.Value;
if (dictionary.TypeDictionary == null ||
dictionary.TypeDictionary.Items == null)
{
continue;
}
string targetDictionaryNamespace = dictionary.TypeDictionary.TargetNamespace;
int targetNamespaceIndex = m_complexTypeResolver.NamespaceUris
.GetIndex(targetDictionaryNamespace);
var structureList = new List();
var enumList = new List();
// split into enumeration and structure types and sort
// types with dependencies to the end of the list.
SplitAndSortDictionary(dictionary, structureList, enumList);
// create assembly for all types in the same module
IComplexTypeBuilder complexTypeBuilder = m_complexTypeBuilderFactory.Create(
targetDictionaryNamespace,
targetNamespaceIndex,
dictionary.Name);
// Add all unknown enumeration types in dictionary
await AddEnumTypesAsync(
complexTypeBuilder,
typeDictionary,
enumList,
allEnumTypes,
serverEnumTypes,
ct)
.ConfigureAwait(false);
// handle structures
int loopCounter = 0;
int lastStructureCount = 0;
while (
structureList.Count > 0 &&
structureList.Count != lastStructureCount &&
loopCounter < MaxLoopCount)
{
loopCounter++;
lastStructureCount = structureList.Count;
var retryStructureList = new List();
// build structured types
foreach (Schema.Binary.TypeDescription item in structureList)
{
if (item is Schema.Binary.StructuredType structuredObject)
{
NodeId nodeId = dictionary.DataTypes
.FirstOrDefault(d => d.Value.Name == item.Name)
.Key;
if (nodeId == null)
{
m_logger.LogError(
Utils.TraceMasks.Error,
"Skip the type definition of {DataType} because the data type node was not found.",
item.Name);
continue;
}
// find the data type node and the binary encoding id
(ExpandedNodeId typeId, ExpandedNodeId binaryEncodingId, DataTypeNode dataTypeNode) =
await m_complexTypeResolver
.BrowseTypeIdsForDictionaryComponentAsync(nodeId, ct)
.ConfigureAwait(false);
if (dataTypeNode == null)
{
m_logger.LogError(
Utils.TraceMasks.Error,
"Skip the type definition of {DataType} because the data type node was not found.",
item.Name);
continue;
}
if (GetSystemType(typeId) != null)
{
XmlQualifiedName qName =
structuredObject.QName
?? new XmlQualifiedName(
structuredObject.Name,
targetDictionaryNamespace);
typeDictionary[qName] = ExpandedNodeId.ToNodeId(
typeId,
m_complexTypeResolver.NamespaceUris);
m_logger.LogInformation(
"Skip the type definition of {DataType} because the type already exists.",
item.Name);
continue;
}
// Use DataTypeDefinition attribute, if available (>=V1.04)
StructureDefinition structureDefinition = null;
if (!DisableDataTypeDefinition)
{
structureDefinition = GetStructureDefinition(dataTypeNode);
}
if (structureDefinition == null)
{
try
{
// convert the binary schema description to a StructureDefinition
structureDefinition = structuredObject
.ToStructureDefinition(
binaryEncodingId,
typeDictionary,
m_complexTypeResolver.NamespaceUris,
dataTypeNode.NodeId);
}
catch (DataTypeNotSupportedException)
{
m_logger.LogError(
"Skipped the type definition of {DataType} because it is not supported.",
item.Name);
continue;
}
catch (ServiceResultException sre)
{
m_logger.LogError(
sre,
"Skip the type definition of {DataType}.",
item.Name);
continue;
}
}
ExpandedNodeIdCollection missingTypeIds = null;
Type complexType = null;
if (structureDefinition != null)
{
IList encodingIds;
ExpandedNodeId xmlEncodingId;
(encodingIds, binaryEncodingId, xmlEncodingId)
= await m_complexTypeResolver
.BrowseForEncodingsAsync(typeId, s_supportedEncodings, ct)
.ConfigureAwait(false);
try
{
// build the actual .NET structured type in assembly
(complexType, missingTypeIds)
= await AddStructuredTypeAsync(
complexTypeBuilder,
structureDefinition,
dataTypeNode.BrowseName,
typeId,
binaryEncodingId,
xmlEncodingId,
ct)
.ConfigureAwait(false);
}
catch (DataTypeNotSupportedException typeNotSupportedException)
{
m_logger.LogInformation(
typeNotSupportedException,
"Skipped the type definition of {DataType} because it is not supported.",
item.Name);
continue;
}
// Add new type to factory
if (complexType != null)
{
// match namespace and add new type to type factory
foreach (NodeId encodingId in encodingIds)
{
AddEncodeableType(encodingId, complexType);
}
AddEncodeableType(typeId, complexType);
XmlQualifiedName qName =
structuredObject.QName
?? new XmlQualifiedName(
structuredObject.Name,
targetDictionaryNamespace);
typeDictionary[qName] = ExpandedNodeId.ToNodeId(
typeId,
m_complexTypeResolver.NamespaceUris);
}
}
if (complexType == null)
{
retryStructureList.Add(item);
m_logger.LogTrace(
"Skipped the type definition of {DataType}, missing {MissingTypeIds}. Retry in next round.",
item.Name,
missingTypeIds == null ?
string.Empty :
string.Join(",", [.. missingTypeIds.Select(id => id.ToString())]));
}
}
}
structureList = retryStructureList;
}
allTypesLoaded = allTypesLoaded && structureList.Count == 0;
}
catch (ServiceResultException sre)
{
m_logger.LogError(
sre,
"Unexpected error processing ditionary {DictionaryName}.",
dictionaryId.Value.Name);
}
}
return allTypesLoaded;
}
///
/// Load all custom types with DataTypeDefinition into the type factory.
///
/// true if all types were loaded, false otherwise
private async Task LoadBaseDataTypesAsync(
IList serverEnumTypes,
IList serverStructTypes,
CancellationToken ct = default)
{
IList enumTypesToDoList = [];
IList structTypesToDoList = [];
bool repeatDataTypeLoad;
do
{
// strip known types
serverEnumTypes = RemoveKnownTypes(serverEnumTypes);
serverStructTypes = RemoveKnownTypes(serverStructTypes);
repeatDataTypeLoad = false;
try
{
enumTypesToDoList = await LoadBaseEnumDataTypesAsync(serverEnumTypes, ct)
.ConfigureAwait(false);
structTypesToDoList = await LoadBaseStructureDataTypesAsync(
serverStructTypes,
ct)
.ConfigureAwait(false);
}
catch (DataTypeNotFoundException dtnfex)
{
m_logger.LogWarning("Data type not found: {Message}", dtnfex.Message);
foreach (ExpandedNodeId nodeId in dtnfex.NodeIds)
{
// add missing types to list
INode dataTypeNode = await m_complexTypeResolver.FindAsync(nodeId, ct)
.ConfigureAwait(false);
if (dataTypeNode != null)
{
await AddEnumerationOrStructureTypeAsync(
dataTypeNode,
serverEnumTypes,
serverStructTypes,
ct)
.ConfigureAwait(false);
repeatDataTypeLoad = true;
}
else
{
m_logger.LogWarning("Datatype {NodeId} was not found.", nodeId);
}
}
}
} while (repeatDataTypeLoad);
// all types loaded
if (enumTypesToDoList.Count == 0 && structTypesToDoList.Count == 0)
{
return true;
}
// Provide some diagnostics so users understand why types could not be serialized.
if (enumTypesToDoList.Count > 0)
{
m_logger.LogWarning(
"{TodoCount} enum types could not be loaded from the server.",
enumTypesToDoList.Count);
if (enumTypesToDoList.Count < 10)
{
m_logger.LogInformation(
"Missing enum types: {MissingEnumTypes}",
string.Join(", ", enumTypesToDoList.Select(e => e.BrowseName)));
}
}
if (structTypesToDoList.Count > 0)
{
m_logger.LogWarning(
"{TodoCount} structure types could not be loaded from the server.",
structTypesToDoList.Count);
if (structTypesToDoList.Count < 10)
{
m_logger.LogInformation(
"Missing structure types: {MissingStructureTypes}",
string.Join(", ", structTypesToDoList.Select(e => e.BrowseName)));
}
}
return false;
}
///
/// Load all custom types with DataTypeDefinition into the type factory.
///
/// true if all types were loaded, false otherwise
private async Task> LoadBaseEnumDataTypesAsync(
IList serverEnumTypes,
CancellationToken ct = default)
{
// strip known types
serverEnumTypes = RemoveKnownTypes(serverEnumTypes);
// add new enum Types for all namespaces
var enumTypesToDoList = new List();
int namespaceCount = m_complexTypeResolver.NamespaceUris.Count;
// create enumeration types for all namespaces
for (uint i = 0; i < namespaceCount; i++)
{
IComplexTypeBuilder complexTypeBuilder = null;
var enumTypes = serverEnumTypes.Where(node => node.NodeId.NamespaceIndex == i)
.ToList();
if (enumTypes.Count != 0)
{
if (complexTypeBuilder == null)
{
string targetNamespace = m_complexTypeResolver.NamespaceUris.GetString(i);
complexTypeBuilder = m_complexTypeBuilderFactory.Create(
targetNamespace,
(int)i);
}
foreach (INode enumType in enumTypes)
{
Type newType = await AddEnumTypeAsync(
complexTypeBuilder,
enumType as DataTypeNode,
ct)
.ConfigureAwait(false);
if (newType != null)
{
// match namespace and add to type factory
AddEncodeableType(enumType.NodeId, newType);
}
else
{
enumTypesToDoList.Add(enumType);
}
}
}
}
// all types loaded, return remaining
return enumTypesToDoList;
}
///
/// Load all structure custom types with DataTypeDefinition into the type factory.
///
/// true if all types were loaded, false otherwise
private async Task> LoadBaseStructureDataTypesAsync(
IList serverStructTypes,
CancellationToken ct = default)
{
// strip known types
serverStructTypes = RemoveKnownTypes(serverStructTypes);
// add new enum Types for all namespaces
int namespaceCount = m_complexTypeResolver.NamespaceUris.Count;
bool retryAddStructType;
var structTypesToDoList = new List();
IList structTypesWorkList = serverStructTypes;
// allow the loader to cache the encodings
IList nodeIds = [.. serverStructTypes.Select(n => n.NodeId)];
_ = await m_complexTypeResolver
.BrowseForEncodingsAsync(nodeIds, s_supportedEncodings, ct)
.ConfigureAwait(false);
// create structured types for all namespaces
int loopCounter = 0;
do
{
loopCounter++;
retryAddStructType = false;
for (uint i = 0; i < namespaceCount; i++)
{
IComplexTypeBuilder complexTypeBuilder = null;
var structTypes = structTypesWorkList.Where(
node => node.NodeId.NamespaceIndex == i).ToList();
if (structTypes.Count != 0)
{
if (complexTypeBuilder == null)
{
string targetNamespace = m_complexTypeResolver.NamespaceUris
.GetString(i);
complexTypeBuilder = m_complexTypeBuilderFactory.Create(
targetNamespace,
(int)i);
}
foreach (INode structType in structTypes)
{
Type newType = null;
if (structType is not DataTypeNode dataTypeNode ||
dataTypeNode.IsAbstract)
{
continue;
}
StructureDefinition structureDefinition = GetStructureDefinition(
dataTypeNode);
if (structureDefinition != null)
{
(
IList encodingIds,
ExpandedNodeId binaryEncodingId,
ExpandedNodeId xmlEncodingId
) = await m_complexTypeResolver
.BrowseForEncodingsAsync(
structType.NodeId,
s_supportedEncodings,
ct)
.ConfigureAwait(false);
try
{
ExpandedNodeId typeId = NormalizeExpandedNodeId(
structType.NodeId);
ExpandedNodeIdCollection missingTypeIds;
(newType, missingTypeIds) = await AddStructuredTypeAsync(
complexTypeBuilder,
structureDefinition,
dataTypeNode.BrowseName,
typeId,
binaryEncodingId,
xmlEncodingId,
ct)
.ConfigureAwait(false);
if (missingTypeIds?.Count > 0)
{
var missingTypeIdsFromWorkList
= new ExpandedNodeIdCollection();
foreach (ExpandedNodeId missingTypeId in missingTypeIds)
{
INode typeMatch = structTypesWorkList
.FirstOrDefault(n =>
n.NodeId == missingTypeId);
if (typeMatch == null)
{
missingTypeIdsFromWorkList.Add(missingTypeId);
}
}
foreach (ExpandedNodeId id in missingTypeIdsFromWorkList)
{
if (!structTypesToDoList.Any(n => n.NodeId == id))
{
structTypesToDoList.Add(
await m_complexTypeResolver.FindAsync(id, ct)
.ConfigureAwait(false));
}
retryAddStructType = true;
}
}
}
catch (DataTypeNotSupportedException)
{
m_logger.LogError(
"Skipped the type definition of {DataType} because it is not supported.",
dataTypeNode.BrowseName.Name);
}
catch
{
// creating the new type failed, likely a missing dependency, retry later
retryAddStructType = true;
}
if (newType != null)
{
foreach (NodeId encodingId in encodingIds)
{
AddEncodeableType(encodingId, newType);
}
AddEncodeableType(structType.NodeId, newType);
}
}
if (newType == null)
{
structTypesToDoList.Add(structType);
if (structureDefinition != null)
{
retryAddStructType = true;
}
}
}
}
}
// due to type dependencies, retry missing types until there is no more progress
if (retryAddStructType &&
structTypesWorkList.Count != structTypesToDoList.Count &&
loopCounter < MaxLoopCount)
{
structTypesWorkList = structTypesToDoList;
structTypesToDoList = [];
}
else
{
break;
}
} while (retryAddStructType);
// all types loaded
return structTypesToDoList;
}
///
/// Return the structure definition from a DataTypeDefinition
///
private static StructureDefinition GetStructureDefinition(DataTypeNode dataTypeNode)
{
if (dataTypeNode.DataTypeDefinition?.Body is StructureDefinition structureDefinition)
{
// Validate the DataTypeDefinition structure,
// but not if the type is supported
if (structureDefinition.Fields == null ||
structureDefinition.BaseDataType.IsNullNodeId ||
structureDefinition.BinaryEncodingId.IsNull)
{
return null;
}
// Validate the structure according to Part3, Table 36
foreach (StructureField field in structureDefinition.Fields)
{
// validate if the DataTypeDefinition is correctly
// filled out, some servers don't do it yet...
if (field.BinaryEncodingId.IsNull ||
field.DataType.IsNullNodeId ||
field.TypeId.IsNull ||
field.Name == null)
{
return null;
}
if (field.ValueRank is not (ValueRanks.Scalar or >= ValueRanks.OneDimension))
{
return null;
}
}
return structureDefinition;
}
return null;
}
///
/// Helper to ensure the expanded nodeId contains a valid namespaceUri.
///
/// The expanded nodeId.
/// The normalized expanded nodeId.
private ExpandedNodeId NormalizeExpandedNodeId(ExpandedNodeId expandedNodeId)
{
var nodeId = ExpandedNodeId.ToNodeId(
expandedNodeId,
m_complexTypeResolver.NamespaceUris);
return NodeId.ToExpandedNodeId(nodeId, m_complexTypeResolver.NamespaceUris);
}
///
/// Add data type to enumeration or structure base type list depending on supertype.
///
///
private async Task AddEnumerationOrStructureTypeAsync(
INode dataTypeNode,
IList serverEnumTypes,
IList serverStructTypes,
CancellationToken ct = default)
{
var superType = ExpandedNodeId.ToNodeId(
dataTypeNode.NodeId,
m_complexTypeResolver.NamespaceUris);
while (true)
{
superType = await m_complexTypeResolver.FindSuperTypeAsync(superType, ct)
.ConfigureAwait(false);
if (superType.IsNullNodeId)
{
throw new ServiceResultException(
StatusCodes.BadNodeIdInvalid,
$"SuperType for {dataTypeNode.NodeId} not found.");
}
if (superType == DataTypeIds.Enumeration)
{
serverEnumTypes.Insert(0, dataTypeNode);
break;
}
else if (superType == DataTypeIds.Structure)
{
serverStructTypes.Insert(0, dataTypeNode);
break;
}
else if (TypeInfo.GetBuiltInType(superType) != BuiltInType.Null)
{
break;
}
}
}
///
/// Remove all known types in the type factory from a list of DataType nodes.
///
private List RemoveKnownTypes(IList nodeList)
{
return [.. nodeList.Where(node => GetSystemType(node.NodeId) == null).Distinct()];
}
///
/// Get the factory system type for an expanded node id.
///
private Type GetSystemType(ExpandedNodeId nodeId)
{
if (!nodeId.IsAbsolute)
{
nodeId = NormalizeExpandedNodeId(nodeId);
}
return m_complexTypeResolver.FactoryBuilder.GetSystemType(nodeId);
}
///
/// Add an enum type defined in a binary schema dictionary.
///
private async Task AddEnumTypesAsync(
IComplexTypeBuilder complexTypeBuilder,
Dictionary typeDictionary,
IList enumList,
IList allEnumerationTypes,
IList enumerationTypes,
CancellationToken ct = default)
{
foreach (Schema.Binary.TypeDescription item in enumList)
{
Type newType = null;
DataTypeNode enumDescription = null;
var enumType =
enumerationTypes.FirstOrDefault(node =>
node.BrowseName.Name == item.Name &&
(
node.BrowseName.NamespaceIndex == complexTypeBuilder
.TargetNamespaceIndex ||
complexTypeBuilder.TargetNamespaceIndex == -1)
) as DataTypeNode;
if (enumType != null)
{
enumDescription = enumType;
// try dictionary enum definition
if (item is Schema.Binary.EnumeratedType enumeratedObject)
{
// 1. use Dictionary entry
var enumDefinition = enumeratedObject.ToEnumDefinition(
enumeratedObject.Name);
// Add EnumDefinition to cache
m_dataTypeDefinitionCache[enumType.NodeId] = enumDefinition;
newType = complexTypeBuilder.AddEnumType(
enumeratedObject.Name,
enumDefinition);
}
if (newType == null)
{
// 2. use node cache
var dataTypeNode =
await m_complexTypeResolver.FindAsync(enumType.NodeId, ct)
.ConfigureAwait(false) as
DataTypeNode;
newType = await AddEnumTypeAsync(complexTypeBuilder, dataTypeNode, ct)
.ConfigureAwait(false);
}
}
else
{
enumDescription =
allEnumerationTypes.FirstOrDefault(node =>
node.BrowseName.Name == item.Name &&
(
node.BrowseName.NamespaceIndex == complexTypeBuilder
.TargetNamespaceIndex ||
complexTypeBuilder.TargetNamespaceIndex == -1)
) as DataTypeNode;
}
if (enumDescription != null)
{
var qName = new XmlQualifiedName(item.Name, complexTypeBuilder.TargetNamespace);
typeDictionary[qName] = enumDescription.NodeId;
}
if (newType != null)
{
// match namespace and add to type factory
AddEncodeableType(enumType.NodeId, newType);
}
}
}
///
/// Helper to add new type with absolute ExpandedNodeId.
///
private void AddEncodeableType(ExpandedNodeId nodeId, Type type)
{
if (NodeId.IsNull(nodeId) || type == null)
{
return;
}
ExpandedNodeId internalNodeId = NormalizeExpandedNodeId(nodeId);
m_logger.LogDebug("Adding Type {DataType} as: {NodeId}", type.FullName, internalNodeId);
m_complexTypeResolver.FactoryBuilder.AddEncodeableType(internalNodeId, type);
}
///
/// Add an enum type defined in a DataType node.
///
private async Task AddEnumTypeAsync(
IComplexTypeBuilder complexTypeBuilder,
DataTypeNode enumTypeNode,
CancellationToken ct = default)
{
Type newType = null;
if (enumTypeNode != null)
{
QualifiedName name = enumTypeNode.BrowseName;
// 1. use DataTypeDefinition
if (DisableDataTypeDefinition ||
enumTypeNode.DataTypeDefinition?.Body is not EnumDefinition enumDefinition)
{
// browse for EnumFields or EnumStrings property
object enumTypeArray = await m_complexTypeResolver
.GetEnumTypeArrayAsync(enumTypeNode.NodeId, ct)
.ConfigureAwait(false);
if (enumTypeArray is ExtensionObject[] extensionObject)
{
// 2. use EnumValues
enumDefinition = extensionObject.ToEnumDefinition(name.Name);
}
else if (enumTypeArray is LocalizedText[] localizedText)
{
// 3. use EnumStrings
enumDefinition = localizedText.ToEnumDefinition(name.Name);
}
else
{
// 4. Give up
enumDefinition = null;
}
}
if (enumDefinition != null)
{
// Add EnumDefinition to cache
m_dataTypeDefinitionCache[enumTypeNode.NodeId] = enumDefinition;
newType = complexTypeBuilder.AddEnumType(name, enumDefinition);
}
}
return newType;
}
///
/// Add structured type to assembly with StructureDefinition.
///
private async Task<(Type structureType, ExpandedNodeIdCollection missingTypes)> AddStructuredTypeAsync(
IComplexTypeBuilder complexTypeBuilder,
StructureDefinition structureDefinition,
QualifiedName typeName,
ExpandedNodeId complexTypeId,
ExpandedNodeId binaryEncodingId,
ExpandedNodeId xmlEncodingId,
CancellationToken ct = default)
{
// init missing type list
ExpandedNodeIdCollection missingTypes = null;
var localDataTypeId = ExpandedNodeId.ToNodeId(
complexTypeId,
m_complexTypeResolver.NamespaceUris);
bool allowSubTypes = IsAllowSubTypes(structureDefinition);
// check all types
var typeList = new List();
foreach (StructureField field in structureDefinition.Fields)
{
Type newType = await GetFieldTypeAsync(field, allowSubTypes, ct).ConfigureAwait(
false);
if (newType == null && !IsRecursiveDataType(localDataTypeId, field.DataType))
{
if (missingTypes == null)
{
missingTypes = [field.DataType];
}
else if (!missingTypes.Contains(field.DataType))
{
missingTypes.Add(field.DataType);
}
}
else
{
typeList.Add(newType);
}
}
if (missingTypes != null)
{
return (null, missingTypes);
}
// Add StructureDefinition to cache
m_dataTypeDefinitionCache[localDataTypeId] = structureDefinition;
IComplexTypeFieldBuilder fieldBuilder = complexTypeBuilder.AddStructuredType(
typeName,
structureDefinition);
fieldBuilder.AddTypeIdAttribute(complexTypeId, binaryEncodingId, xmlEncodingId);
int order = 1;
List.Enumerator typeListEnumerator = typeList.GetEnumerator();
foreach (StructureField field in structureDefinition.Fields)
{
_ = typeListEnumerator.MoveNext();
// check for recursive data type:
// field has the same data type as the parent structure
var nodeId = ExpandedNodeId.ToNodeId(
complexTypeId,
m_complexTypeResolver.NamespaceUris);
bool isRecursiveDataType = IsRecursiveDataType(nodeId, field.DataType);
if (isRecursiveDataType)
{
fieldBuilder.AddField(
field,
fieldBuilder.GetStructureType(field.ValueRank),
order);
}
else
{
fieldBuilder.AddField(field, typeListEnumerator.Current, order);
}
order++;
}
return (fieldBuilder.CreateType(), missingTypes);
}
private static bool IsAllowSubTypes(StructureDefinition structureDefinition)
{
switch (structureDefinition.StructureType)
{
case StructureType.UnionWithSubtypedValues:
case StructureType.StructureWithSubtypedValues:
return true;
case StructureType.Structure:
case StructureType.StructureWithOptionalFields:
case StructureType.Union:
return false;
default:
Debug.Fail($"Unexpected StructureType {structureDefinition.StructureType}.");
return false;
}
}
private async Task IsAbstractTypeAsync(
NodeId fieldDataType,
CancellationToken ct = default)
{
var dataTypeNode =
await m_complexTypeResolver.FindAsync(fieldDataType, ct)
.ConfigureAwait(false) as DataTypeNode;
return dataTypeNode?.IsAbstract == true;
}
private static bool IsRecursiveDataType(NodeId structureDataType, NodeId fieldDataType)
{
return fieldDataType.Equals(structureDataType);
}
///
/// Determine the type of a field in a StructureField definition.
///
///
private async Task GetFieldTypeAsync(
StructureField field,
bool allowSubTypes,
CancellationToken ct = default)
{
if (field.ValueRank is not ValueRanks.Scalar and < ValueRanks.OneDimension)
{
throw new DataTypeNotSupportedException(
field.DataType,
$"The ValueRank {field.ValueRank} is not supported.");
}
Type fieldType =
field.DataType.NamespaceIndex == 0
? TypeInfo.GetSystemType(field.DataType, m_complexTypeResolver.FactoryBuilder)
: GetSystemType(field.DataType);
if (fieldType == null)
{
NodeId superType = await GetBuiltInSuperTypeAsync(
field.DataType,
allowSubTypes,
field.IsOptional,
ct)
.ConfigureAwait(false);
if (superType?.IsNullNodeId == false)
{
field.DataType = superType;
return await GetFieldTypeAsync(field, allowSubTypes, ct).ConfigureAwait(false);
}
return null;
}
if (field.ValueRank == ValueRanks.OneDimension)
{
return fieldType.MakeArrayType();
}
else if (field.ValueRank >= ValueRanks.TwoDimensions)
{
return fieldType.MakeArrayType(field.ValueRank);
}
return fieldType;
}
///
/// Find superType for a datatype.
///
///
private async Task GetBuiltInSuperTypeAsync(
NodeId dataType,
bool allowSubTypes,
bool isOptional,
CancellationToken ct = default)
{
const int maxSuperTypes = 100;
int iterations = 0;
NodeId superType = dataType;
while (iterations++ < maxSuperTypes)
{
superType = await m_complexTypeResolver.FindSuperTypeAsync(superType, ct)
.ConfigureAwait(false);
if (superType?.IsNullNodeId != false)
{
return default;
}
if (superType.NamespaceIndex == 0)
{
if (superType == DataTypeIds.Enumeration && dataType.NamespaceIndex == 0)
{
// enumerations of namespace 0 in a structure
// which are not in the type system are encoded as UInt32
return DataTypeIds.UInt32;
}
if (superType == DataTypeIds.Enumeration)
{
return default;
}
else if (superType == DataTypeIds.Structure)
{
// throw on invalid combinations of allowSubTypes, isOptional and abstract types
// in such case the encoding as ExtensionObject is undetermined and not specified
if ((dataType != DataTypeIds.Structure) &&
((allowSubTypes && !isOptional) || !allowSubTypes) &&
await IsAbstractTypeAsync(dataType, ct).ConfigureAwait(false))
{
throw new DataTypeNotSupportedException(
"Invalid definition of a abstract subtype of a structure.");
}
if (allowSubTypes && isOptional)
{
return superType;
}
return default;
}
// end search if a valid BuiltInType is found. Treat type as opaque.
else if (superType.IdType == IdType.Numeric &&
(uint)superType.Identifier >= (uint)BuiltInType.Boolean &&
(uint)superType.Identifier <= (uint)BuiltInType.DiagnosticInfo)
{
return superType;
}
// no valid supertype found
else if (superType == DataTypeIds.BaseDataType)
{
break;
}
}
}
return default;
}
///
/// Split the dictionary types into a list of structures and enumerations.
/// Sort the structures by dependencies, with structures with dependent
/// types at the end of the list, so they can be added to the factory in order.
///
///
private static void SplitAndSortDictionary(
DataDictionary dictionary,
List structureList,
List enumList)
{
foreach (Schema.Binary.TypeDescription item in dictionary.TypeDictionary.Items)
{
if (item is Schema.Binary.StructuredType structuredObject)
{
IEnumerable dependentFields = structuredObject.Field
.Where(f =>
f.TypeName.Namespace == dictionary.TypeDictionary.TargetNamespace);
if (!dependentFields.Any())
{
structureList.Insert(0, structuredObject);
}
else
{
structureList.Add(structuredObject);
}
}
else if (item is Schema.Binary.EnumeratedType)
{
enumList.Add(item);
}
else if (item is Schema.Binary.OpaqueType)
{
// no need to handle Opaque types
}
else
{
throw ServiceResultException.Unexpected(
"Unexpected Type in binary schema: {0}.",
item.GetType().Name);
}
}
}
private readonly ILogger m_logger;
private readonly IComplexTypeResolver m_complexTypeResolver;
private readonly IComplexTypeFactory m_complexTypeBuilderFactory;
private readonly NodeIdDictionary m_dataTypeDefinitionCache = [];
private static readonly string[] s_supportedEncodings =
[
BrowseNames.DefaultBinary,
BrowseNames.DefaultXml,
BrowseNames.DefaultJson
];
}
}