Files
UA-.NETStandard/tools/Opc.Ua.SourceGeneration.Core/Schema/ModelDesignExtensions.cs
T
romanett 442889cba5 Fix source generator audit findings; conform structure-field matrices and type metadata to OPC UA 1.05 (#4569)
# Description

This PR fixes a bug audit of the source generation projects
(`tools/Opc.Ua.SourceGeneration*`: model loading, NodeSet import, all
emitters, the Roslyn hosts and packaging). It also fixes a max-effort
code review of those fixes, and a check of every spec-relevant change
against the OPC UA 1.05 text.

- **Scope:** about 85 audit findings, 48 review findings and 32
spec-grounding items were addressed. Each fix has a regression test that
fails without it.
- **Wire change:** it includes one deliberate, stack-wide wire change:
**structure-field matrices now follow OPC 10000-6 5.2.5 Table 28**.

Most changes are internal correctness fixes. The sections below list
every change a user can observe: wire formats, generated code and API,
generated address-space content, diagnostics and runtime behaviour.

## Behaviour / user-facing changes

### Wire format (encoders)
- **Binary inline matrices** (OPC 10000-6 5.2.5 Table 28).
- **Layout:** a multi-dimensional *structure field* is encoded as
`Int32[] Dimensions`, then the values **without a length prefix**, with
at least 2 dimensions.
- **Empty and null:** any dimension <= 0 means no values follow, and a
null matrix is `-1`. An empty matrix is written as `[0,0]`.
- **Compatibility:** this is **not compatible with earlier builds of
this SDK** for matrix fields. It applies to generated types,
`[DataType]` types, `Structure`, client ComplexTypes and
`WriteEncodeableMatrix`.
  - **Unchanged:** Variant matrices (5.2.2.16) keep their old encoding.
- **Declared rank decides matrix vs array.** A 1-D `MatrixOf<T>` in an
array field or a Variable value is written as an array again.
Matrix-declared fields go through the new public
`EncoderExtensions.WriteInlineMatrixValue`, which generated code also
uses. A non-empty 1-D value in a matrix field throws `BadEncodingError`.
- **Decoder hardening:**
- Before allocating, the decoder bounds all matrix dimensions against
overflow, `MaxArrayLength` and the remaining message length.
  - The decoder rejects:
    - a matrix with fewer than 2 dimensions;
- a populated matrix whose rank differs from the field's declared rank;
    - a union SwitchField greater than the number of fields;
    - an EncodingMask with unassigned bits set.
- **Encoder `MaxArrayLength`:** the encoder enforces `MaxArrayLength`
for inline matrices, as it already did for arrays.
- **XML structure fields** (5.3.1, 5.3.4, 5.3.5).
- **Fields:** `Structure` and client ComplexTypes write fields like
typed fields (`<A>1</A>`, array elements directly) instead of a Variant
body.
- **Matrix fields:** `Dimensions` and `Elements` go directly inside the
field element, with no `<Matrix>` wrapper.
- **Empty matrices:** written as nil, because XML dimensions must be
greater than 0.
  - **Compatibility:** decoders still accept the old shapes.
- **Enum fixes:** enum array elements with value 0 are no longer
dropped, and enum array element names are fixed.
- **Other encoding fixes:**
- A null `DiagnosticInfo` structure field writes `0x00` instead of
nothing.
- A null scalar field in a runtime `Structure` is written as its default
instead of zero bytes, which used to shift every following field.
- A Variant holding an empty matrix encodes as an empty array instead of
throwing.
  - `Variant.Copy` keeps the matrix identity of null and empty matrices.
- **NodeSet export and the DataContract surrogate** write Variable
values with the Variant rules again. A null or empty `MatrixOf` variable
no longer produces invalid XML or unreadable NodeSets.
- **`JsonEncoder.CanOmitFields`** is `true` only for Compact encoding.
Verbose and RawData JSON include default-valued fields.

### `[DataType]` source-generated types
- **Default-value handling** (OPC 10000-6 5.4.1, Table 1).
- **Encode:** a field is omitted only when its value *and* its declared
default are the type's default (0, false, null). Fields with a
non-default initializer or constructor default, and empty strings, are
always written, so configuration files get slightly larger.
- **JSON decode:** a field missing from JSON decodes to the type
default.
- **XML decode:** a field missing from XML keeps its declared default,
which older configuration files rely on.
- **Why:** this fixes silent round-trip loss, for example
`SamplingInterval = 0` read back as `-1`.
- **Nested `[DataType]` types:** they are now generated inside their
containing types. Without an explicit id, their default TypeId and
XmlName are qualified (e.g. `s=Outer.Foo`); **wire identity change** for
nested types. Colliding activator names get a `_2` suffix.
- **Activators:** they are `internal` when the type, or any containing
type, is not public (was CS0060).
- **Matrix properties:**
  - `MatrixOf<T>` properties use the inline matrix encoding.
- Matrices of non-sealed structures, or with
`StructureHandling.ExtensionObject`, are written as ExtensionObjects.
- Their published ValueRank is 2, because the rank can't be known from
`MatrixOf<T>`.
- **Published StructureDefinitions:**
  - They include base-class fields.
  - Nullable properties are no longer marked optional.
  - Inline structure fields name their concrete type.
- **`[Flags]` enums:** they are published as plain enumerations with
their member values, which matches how they are encoded.
- **Other generated code:**
- `Clone` deep-copies arrays and matrices of structures, Variants,
ExtensionObjects and DataValues.
- `IsEqual` and `GetHashCode` are consistent: EncodingMask, active union
member and NaN handling agree.
- Pooled `Reuse()` restores the freshly constructed state through the
new `protected virtual ResetForReuse()`, and returns an instance to its
runtime type's pool.
- **Enum capture:** enum member values are captured culture-invariantly.
Negative values were lost on cultures that use U+2212 as the minus sign.

### Model-driven generator: generated code, API and file names
- **Removed API:** generated `*_Placeholder` proxy accessors (e.g.
`PowerTrainTypeClient.GetMotorIdentifier_PlaceholderAsync`) and fluent
`X_Placeholder()` accessors are removed; they could never resolve
anything.
- Proxy accessors now look children up by BrowseName with the right
namespace.
- `RoboticsClient` finds motors and gears by type and drives via
`IsDrivenBy`. Its topology snapshots used to be empty.
- **Renamed files:** generated files for a bound `[NodeManager]` are now
named `{Namespace}.{Class}.NodeManager.g.cs` (also `.Factory` and
`.FluentBuilders`).
- **Generated NodeState code:**
- Attributes are set through `baseState`/`nodeState` locals, so children
named like base attributes (e.g. `DataType`, `EventNotifier`) compile.
- Children that hide base members are declared `public new`, using names
reflected from the runtime.
  - `global::` qualification is used throughout.
- **Proxies and fluent builders:** a proxy whose supertype is excluded
(e.g. Draft) derives from the nearest emitted ancestor. Fluent wrappers
and NodeState factories fall back the same way; they used to fail with
CS0246.
- **Derived structures:**
  - The optional-field `EncodingMask` continues across inheritance.
- A derived structure over a base type from another model no longer
relies on `base.ResetForReuse`.
- **Default values:**
- NodeId, QualifiedName and ExpandedNodeId defaults resolve by namespace
URI (`context.NamespaceUris.GetIndexOrAppend`) and server URI.
- StatusCode, negative enum, `double`/`float` (shortest round-trip
literal) and escaped string defaults now compile.
- **Event records:**
- Vendor event records have `Decoder.GetStandardFields(NamespaceTable)`,
and their browse paths use the model's namespace.
- A field whose namespace is unknown to the session no longer matches a
namespace-0 field.
  - Companion `StandardFields` is `[Obsolete]`.
- Vendor simple subtypes such as `Percent : Double` map to their
built-in type.
- **Other fixes:**
- `MethodState.CallAsync` passes the real `objectId` to synchronous
`OnCallMethod2` handlers; it used to pass `NodeId.Null`.
- Enumeration fields with an explicit `Identifier` keep it, instead of
taking a trailing `_<digits>`.
  - Explicit NumericIds are no longer reused by automatic numbering.
  - Dependencies are loaded in topological order.

### Generated address-space content
- **Arguments:**
- Methods without arguments no longer get empty
`InputArguments`/`OutputArguments` properties (OPC 10000-3 5.7).
- About 2,660 automatically assigned namespace-0 NodeIds of 15000 and
above shift. Every NodeId in `StandardTypes.csv` is unchanged.
- **Permissions:** type nodes and method declarations on types now carry
the model's `RolePermissions`/`AccessRestrictions`.
- **Restored nodes:** NodeSet import keeps nodes that were silently
dropped before:
- `NamespaceMetadata` objects and the NodeSet's own TypeDictionary
variables;
  - ISA95 placeholders linked only non-hierarchically;
  - instances whose parent is in another namespace.
- **Import details:**
  - ValueRank > 1 without ArrayDimensions keeps its rank.
  - `svr=` indexes map correctly.
  - Invalid MinimumSamplingInterval values import as -1, not 0.
  - Localized texts prefer the invariant or English translation.
- **StructureDefinitions:**
- They include inherited fields, including for subtypes without their
own fields (e.g. `AnonymousIdentityToken` now publishes PolicyId).
  - The StructureType kind covers inherited AllowSubTypes fields.
  - Unions ignore IsOptional.
- StructureFields never publish ValueRank 0; a OneOrMoreDimensions field
without dimensions becomes rank 2.
- Fields with ValueRank -2/-3 are published as scalar BaseDataType,
which is what is on the wire.
- **OptionSets:**
  - Implicit fields get the next free bit.
- `OptionSetValues` uses null for undefined bits instead of "Reserved",
and structure OptionSets publish all their bits (no 32/96-bit cap).
- Numeric OptionSets are limited to their base type's width, and UInt64
bit 63 works.
- **Schemas:**
- Structures with inline-matrix fields are left out of the `.bsd`
(5.2.5).
- In the BSD, Variant-encoded and non-structure subtyped fields are
described as `ua:Variant`.
- In the `.xsd`, matrix fields are typed `ua:Matrix` with
`Dimensions`/`Elements`, and `EncodingMask` is required.

### NodeSet / model input handling
- **Model selection:**
- The newest NodeSet of a model is chosen by `ModelVersion` (SemVer 2.0
precedence), then PublicationDate. The `Version` attribute, which Annex
F says is not for comparison, is only a tie-break.
- The order is total and does not depend on input order; referenced
assemblies use the same rule.
- **Default names:**
- Invalid default Name/Prefix values are repaired (`201301ISA95` becomes
`_201301ISA95`, `class` becomes `class_`).
  - Valid names are unchanged.
  - Sanitising no longer depends on the OS.
- **`ModelUri` metadata** that doesn't match the NodeSet raises a
warning instead of silently losing the model.
- **Input detection:**
- Only XML files whose root is `ModelDesign` or `UANodeSet` are treated
as models.
- A malformed NodeSet stays a NodeSet input, so it no longer breaks the
ModelDesign pass.
- **`IdentifierFile` paths:** they are normalized (`./`, `../`), and the
suffix match requires a folder boundary.
- **Dependency payloads** (`ModelDependencyV1`) carry field IsOptional,
AllowSubTypes, ArrayDimensions and the producer's namespace tables in
skippable trailers, so old and new readers stay compatible.

### Roslyn host, diagnostics, packaging
- **New diagnostics:**
- MODELGEN036: `[NodeManager]` target not supported (nested, generic,
file-local or global namespace).
- MODELGEN037: `[DataType]` target not supported (struct, generic,
abstract, file-local or global namespace).
- MODELGEN038: the definition of an encodeable base type can't be found.
- **New validator errors:**
- a structure with both optional and subtyped fields (OPC 10000-6 F.13);
  - multi-bit or duplicate OptionSet masks.
- **Diagnostic locations:** `[DataType]`/`[NodeManager]` diagnostics
point at real source locations again, so `#pragma` and `.editorconfig`
apply. MODELGEN010 is the exception.
- **Incremental caching:** pipeline values compare by value, so
unrelated edits no longer regenerate the whole model or all
`[DataType]`s.
- **`ModelSourceGeneratorIgnore`** on a ModelDesign input is honoured;
the design still resolves as a dependency.
- **Cancellation:** a cancelled generation run propagates
`OperationCanceledException` instead of reporting a MODELGEN003 error.
- **Stack package:** the `Stack` NuGet package now ships
`build/OPCFoundation.NetStandard.Opc.Ua.SourceGeneration.Stack.props`,
so `StackSourceGeneratorMode` and `StackSourceGeneratorExclude` reach
the compiler.

## Related Issues

- No tracking issue. The audit, review and spec-grounding reports are
available on request. Contains the matrix wire-format change discussed
for D-5 (OPC 10000-6 5.2.5 Table 28).

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
(`docs/SourceGeneratedDataTypes.md`, `docs/NodeManagers.md`, generator
readme)
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings. (`UA.slnx` 0 warnings / 0 errors on net48 and
net10.0)
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed. The
affected suites were run on net10.0: SourceGeneration Core/Roslyn/Stack,
Types, Core.Encoders, Encoders.Fuzz, Client, Client.ComplexTypes,
PubSub, Core, Server, GDS, Configuration, Di, Robotics, ISA95,
Positioning, OpenUsd, XRegistry. net48 was built; its test legs, and the
post-rebase verification, run in CI.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:47:40 +02:00

3798 lines
152 KiB
C#

/* ========================================================================
* 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;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Xml;
using Opc.Ua.SourceGeneration;
using Opc.Ua.Types;
namespace Opc.Ua.Schema.Model
{
/// <summary>
/// Defines where to add nullable annotations
/// </summary>
internal enum NullableAnnotation
{
/// <summary>
/// Types should be non nullable
/// </summary>
NonNullable,
/// <summary>
/// Types are nullable except for data types
/// </summary>
NullableExceptDataTypes,
/// <summary>
/// All types are nullable
/// </summary>
Nullable
}
/// <summary>
/// Identifies one generated method-argument symbol scope and the names
/// reserved by that scope's emitted scaffolding.
/// </summary>
internal sealed class MethodArgumentCodeNameScope
{
public MethodArgumentCodeNameScope(params string[] reservedNames)
: this(null, reservedNames)
{
}
public MethodArgumentCodeNameScope(
Func<string, bool, IEnumerable<string>> getCollisionIdentifiers,
params string[] reservedNames)
{
ReservedNames = reservedNames == null ? [] : [.. reservedNames];
m_getCollisionIdentifiers = getCollisionIdentifiers;
}
public IReadOnlyCollection<string> ReservedNames { get; }
public IEnumerable<string> GetCollisionIdentifiers(
string identifier,
bool output)
{
return m_getCollisionIdentifiers?.Invoke(identifier, output) ?? [identifier];
}
private readonly Func<string, bool, IEnumerable<string>> m_getCollisionIdentifiers;
}
/// <summary>
/// Dotnet code generation support.
/// </summary>
internal static class ModelDesignExtensions
{
/// <summary>
/// Get basic data type from data type design.
/// </summary>
/// <param name="dataType"></param>
/// <returns></returns>
public static BasicDataType DetermineBasicDataType(this DataTypeDesign dataType)
{
if (dataType == null)
{
return BasicDataType.BaseDataType;
}
if (dataType.IsOptionSet)
{
return BasicDataType.Enumeration;
}
// check if it is a built in data type.
if (dataType.IsBasicDataType(out BasicDataType basicDataType))
{
return basicDataType;
}
// recursively search hierarchy if not base data type yet
BasicDataType basicType = DetermineBasicDataType(
dataType.BaseTypeNode as DataTypeDesign);
// data type is user defined if a sub-type of structure.
if (basicType == BasicDataType.Structure)
{
return BasicDataType.UserDefined;
}
return basicType;
}
/// <summary>
/// Get type node state class name
/// </summary>
public static string GetNodeStateClassName(
this TypeDesign type,
Namespace[] namespaces)
{
string className = type.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
return CoreUtils.Format("{0}State", className);
}
/// <summary>
/// Returns the class name to use when creating an instance of the type.
/// </summary>
public static string GetNodeStateClassName(
this InstanceDesign instance,
string targetNamespace,
Namespace[] namespaces,
bool asFactory = false,
bool applyStandardFallback = true)
{
if (instance is MethodDesign method)
{
string className;
XmlQualifiedName methodStateIdentity =
MethodDesignArgumentResolver.ResolveMethodStateIdentity(method);
if (methodStateIdentity != null)
{
className =
methodStateIdentity.AsFullyQualifiedTypeSymbol(namespaces);
if (className.EndsWith("MethodType", StringComparison.Ordinal))
{
className = className[..^"MethodType".Length];
}
else if (className.EndsWith("Type", StringComparison.Ordinal))
{
className = className[..^"Type".Length];
}
}
else
{
className = method.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
}
if (MethodDesignArgumentResolver.HasMethodArguments(method))
{
// Only method types have a typed class. A reference to a
// method that declares its arguments inline uses the base
// MethodState; declaration names keep the typed name.
if (applyStandardFallback &&
!MethodDesignArgumentResolver.HasTypedMethodState(method))
{
return CoreUtils.Format(
"{0}global::Opc.Ua.MethodState",
asFactory ? "new " : string.Empty);
}
string typedClassName = CoreUtils.Format(
"{0}{1}MethodState",
asFactory ? "new " : string.Empty,
className);
// Degrade a reference to a standard global::Opc.Ua.*MethodState
// to the base MethodState when a curated Opc.Ua.Core omits the
// typed class and the current model pass does not declare it
// (e.g. the FileDirectoryType methods on a Robotics Programs
// object). Model-generation-scoped; see
// StandardMethodStateFallback.
if (applyStandardFallback &&
StandardMethodStateFallback.ShouldFallBackToBase(typedClassName))
{
return CoreUtils.Format(
"{0}global::Opc.Ua.MethodState",
asFactory ? "new " : string.Empty);
}
return typedClassName;
}
return CoreUtils.Format(
"{0}global::Opc.Ua.MethodState",
asFactory ? "new " : string.Empty);
}
if (instance is not VariableDesign variable)
{
// No state class is emitted for an excluded ObjectType: use
// the nearest emitted supertype's.
TypeDesign typeDefinition = instance.TypeDefinitionNode;
while (typeDefinition is ObjectTypeDesign { IsExcludedFromGeneration: true } &&
typeDefinition.BaseTypeNode != null)
{
typeDefinition = typeDefinition.BaseTypeNode;
}
return GetNodeStateNameSimple(typeDefinition);
}
var variableType = instance.TypeDefinitionNode as VariableTypeDesign;
// check if the variable type restricted the datatype to eliminate the
// need for a template parameter.
if (variableType.DataTypeNode.IsTemplateParameterRequired(variableType.ValueRank))
{
return GetNodeStateNameSimple(variableType);
}
// check if the variable instance did not restrict the datatype.
if (!variable.DataTypeNode.IsTemplateParameterRequired(variable.ValueRank))
{
return GetNodeStateNameSimple(variableType);
}
// instance restricted the datatype but the type did not.
string scalarName = GetDotNetTypeName(
variable.DataTypeNode,
targetNamespace,
namespaces,
nullable: NullableAnnotation.NonNullable,
true);
if (variable.ValueRank == ValueRank.Array)
{
if (asFactory)
{
return CoreUtils.Format(
"{0}State<global::Opc.Ua.ArrayOf<{1}>>.With<{2}>",
GetClassName(variableType, namespaces),
scalarName,
GetVariantBuilder(variable.DataTypeNode, scalarName));
}
return CoreUtils.Format(
"{0}State<global::Opc.Ua.ArrayOf<{1}>>",
GetClassName(variableType, namespaces),
scalarName);
}
if (variable.ValueRank == ValueRank.OneOrMoreDimensions &&
variable.DataTypeNode.SupportsMatrixOf() &&
variable.DataTypeNode.BasicDataType
is not BasicDataType.BaseDataType
and not BasicDataType.Number
and not BasicDataType.UInteger
and not BasicDataType.Integer)
{
if (asFactory)
{
return CoreUtils.Format(
"{0}State<global::Opc.Ua.MatrixOf<{1}>>.With<{2}>",
GetClassName(variableType, namespaces),
scalarName,
GetVariantBuilder(variable.DataTypeNode, scalarName));
}
return CoreUtils.Format(
"{0}State<global::Opc.Ua.MatrixOf<{1}>>",
GetClassName(variableType, namespaces),
scalarName);
}
if (IsIndeterminateType(variable))
{
return GetNodeStateNameSimple(variableType);
}
// add hack for TwoStateDiscreteType which always has to be bool.
if (variableType.SymbolicName ==
new XmlQualifiedName("TwoStateDiscreteType", Namespaces.OpcUa))
{
return GetNodeStateNameSimple(variableType);
}
if (asFactory)
{
return CoreUtils.Format(
"{0}State<{1}>.With<{2}>",
GetClassName(variableType, namespaces),
scalarName,
GetVariantBuilder(variable.DataTypeNode, scalarName));
}
return CoreUtils.Format(
"{0}State<{1}>",
GetClassName(variableType, namespaces),
scalarName);
string GetNodeStateNameSimple(TypeDesign variableType)
{
return CoreUtils.Format(
"{0}{1}State",
asFactory ? "new " : string.Empty,
GetClassName(variableType, namespaces));
}
}
/// <summary>
/// Get the variant builder to use to access or mutate a variant of type T
/// </summary>
public static string GetVariantBuilder(
this DataTypeDesign datatype,
string typeName)
{
switch (datatype.BasicDataType)
{
case BasicDataType.UserDefined:
if (datatype.IsEnumeration)
{
return CoreUtils.Format("global::Opc.Ua.EnumerationBuilder<{0}>", typeName);
}
return CoreUtils.Format("global::Opc.Ua.StructureBuilder<{0}>", typeName);
case BasicDataType.Enumeration:
if (datatype.SymbolicId ==
new XmlQualifiedName("Enumeration", Namespaces.OpcUa))
{
return "global::Opc.Ua.VariantBuilder";
}
if (datatype.IsOptionSet)
{
return GetVariantBuilder((DataTypeDesign)datatype.BaseTypeNode, typeName);
}
return CoreUtils.Format("global::Opc.Ua.EnumerationBuilder<{0}>", typeName);
default:
return "global::Opc.Ua.VariantBuilder";
}
}
/// <summary>
/// Returns the class name to use when creating an instance of the type.
/// </summary>
/// <exception cref="ArgumentNullException"></exception>
public static string GetBaseClassName(this TypeDesign type, Namespace[] namespaces)
{
if (type?.BaseTypeNode?.SymbolicName == null)
{
throw new ArgumentNullException(nameof(type));
}
if (type is not DataTypeDesign dataType ||
dataType.BaseTypeNode is not DataTypeDesign dtd)
{
return type.BaseTypeNode.GetClassName(namespaces);
}
if (dtd.BasicDataType == BasicDataType.Structure)
{
return "global::Opc.Ua.IEncodeable";
}
return dtd.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
}
/// <summary>
/// Fully qualify a symbol
/// </summary>
/// <returns></returns>
/// <exception cref="ArgumentException"></exception>
public static string AsFullyQualifiedTypeSymbol(
this XmlQualifiedName symbol,
Namespace[] namespaces,
string alternativeName = null)
{
string name = alternativeName ?? symbol?.Name;
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("Symbol name is empty");
}
string ns = namespaces.GetNamespacePrefix(symbol?.Namespace);
if (string.IsNullOrEmpty(ns))
{
return name;
}
return CoreUtils.Format("global::{0}.{1}", ns, name);
}
/// <summary>
/// Returns the class name to use when creating an instance of the type.
/// </summary>
/// <exception cref="ArgumentNullException"></exception>
public static bool IsDerivedDataType(this TypeDesign type)
{
if (type is null)
{
throw new ArgumentNullException(nameof(type));
}
if (type is not DataTypeDesign ||
type.BaseTypeNode is not DataTypeDesign dtd)
{
return true;
}
return dtd.BasicDataType != BasicDataType.Structure;
}
/// <summary>
/// Returns a qualifier for the namespace to use in code.
/// </summary>
public static string GetNamespacePrefix(this Namespace[] namespaces, string namespaceUri)
{
if (namespaces != null)
{
foreach (Namespace ns in namespaces)
{
if (ns.Value == namespaceUri)
{
return CoreUtils.Format("{0}", ns.Prefix);
}
}
}
return null;
}
/// <summary>
/// Returns a constant string for the namespace uri.
/// </summary>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ArgumentException"></exception>
public static string GetConstantSymbolForNamespace(
this Namespace[] namespaces,
string namespaceUri)
{
if (namespaces is null)
{
throw new ArgumentNullException(nameof(namespaces));
}
Namespace ns = namespaces.GetNamespace(namespaceUri);
if (ns == null)
{
return null;
}
if (string.IsNullOrWhiteSpace(ns.Name))
{
throw new ArgumentException("Found namespace has no name");
}
if (string.IsNullOrWhiteSpace(ns.Prefix)) // TODO: should this work?
{
return CoreUtils.Format("Namespaces.{0}", ns.Name);
}
return CoreUtils.Format("global::{0}.Namespaces.{1}", ns.Prefix, ns.Name);
}
public static bool IsOverriddenWithSameClass(
this InstanceDesign instance,
string targetNamespace,
Namespace[] namespaces)
{
if (instance is null)
{
throw new ArgumentNullException(nameof(instance));
}
if (!instance.IsOverridden())
{
return false;
}
string x = instance.GetMergedInstance().GetNodeStateClassName(targetNamespace, namespaces);
string y = instance.OveriddenNode.GetNodeStateClassName(targetNamespace, namespaces);
// Only the exact same class counts. An override that refines the
// generic argument (BaseDataVariableState<double> overridden as
// BaseDataVariableState<int>) is a different class and has to be
// redeclared, or the subtype keeps exposing the base's value type.
// (A special case treating every BaseDataVariableState<...> pair as
// equal used to compare against the unqualified class name, which
// GetNodeStateClassName never returns - it had no effect and this
// is the behaviour the generated code has always had.)
return x == y;
}
private static bool IsIndeterminateType(this InstanceDesign instance)
{
if (instance is VariableDesign variable)
{
if (variable.ValueRank is not ValueRank.Scalar and not ValueRank.Array)
{
// Matrix-rank with a concrete data type that supports
// `MatrixOf<T>` is determinate - the generator emits a
// typed `State<MatrixOf<T>>` for it. Everything else
// (ScalarOrArray, ScalarOrOneDimension, Any, or matrix
// over an abstract numeric base) stays on the Variant
// fallback.
return variable.ValueRank != ValueRank.OneOrMoreDimensions ||
variable.DataTypeNode == null ||
!variable.DataTypeNode.SupportsMatrixOf() ||
!(variable.DataTypeNode.BasicDataType
is not BasicDataType.BaseDataType
and not BasicDataType.Number
and not BasicDataType.UInteger
and not BasicDataType.Integer);
}
if (variable.DataType ==
new XmlQualifiedName(BrowseNames.Enumeration, Namespaces.OpcUa))
{
return true;
}
}
return false;
}
public static string EnsureUniqueEnumName(this Parameter target)
{
if (target?.Parent is DataTypeDesign dt && dt.HasFields && dt.Fields != null)
{
HashSet<string> names = [];
foreach (Parameter field in dt.Fields)
{
int count = 1;
string check = field.Name;
if (names.Contains(check))
{
check = field.Name + "_" + field.Identifier;
}
while (names.Contains(check))
{
check = field.Name + "_v" + count++;
}
if (target.Identifier == field.Identifier)
{
return check;
}
names.Add(check);
}
}
return target?.Name;
}
/// <summary>
/// Returns the canonical C# identifier for a method argument.
/// </summary>
internal static string GetGeneratedCodeIdentifier(
this Parameter field,
bool upperCamelCase = false,
MethodArgumentCodeNameScope scope = null)
{
string name = field?.Name;
scope ??= s_defaultMethodArgumentCodeNameScope;
if (field != null)
{
lock (s_generatedCodeNamesLock)
{
if (s_generatedCodeNames.TryGetValue(field, out GeneratedCodeNameState state) &&
state.TryGetName(scope, out string generatedName) &&
!string.IsNullOrEmpty(generatedName))
{
name = generatedName;
}
}
}
return name.ToCSharpIdentifier(upperCamelCase);
}
/// <summary>
/// Assigns unique generated-code names against the requested scope while
/// preserving the authored OPC UA names.
/// </summary>
internal static void AssignMethodArgumentCodeNames(
this MethodDesign method,
MethodArgumentCodeNameScope scope = null,
params string[] additionalReservedNames)
{
AssignMethodArgumentCodeNames(
method?.InputArguments,
method?.OutputArguments,
scope,
additionalReservedNames);
}
/// <summary>
/// Assigns unique generated-code names for an effective method signature.
/// </summary>
internal static void AssignMethodArgumentCodeNames(
Parameter[] inputArguments,
Parameter[] outputArguments,
MethodArgumentCodeNameScope scope = null,
params string[] additionalReservedNames)
{
scope ??= s_defaultMethodArgumentCodeNameScope;
lock (s_generatedCodeNamesLock)
{
ClearGeneratedCodeNames(inputArguments, scope);
ClearGeneratedCodeNames(outputArguments, scope);
var usedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
AddReservedMethodArgumentNames(usedNames, scope.ReservedNames);
AddReservedMethodArgumentNames(usedNames, additionalReservedNames);
AssignUniqueGeneratedCodeNames(
inputArguments,
usedNames,
output: false,
scope);
AssignUniqueGeneratedCodeNames(
outputArguments,
usedNames,
output: true,
scope);
}
}
private static void AddReservedMethodArgumentNames(
HashSet<string> usedNames,
IEnumerable<string> reservedNames)
{
if (reservedNames == null)
{
return;
}
foreach (string reservedName in reservedNames)
{
if (!string.IsNullOrWhiteSpace(reservedName))
{
usedNames.Add(GetIdentifierCollisionKey(
reservedName.ToCSharpIdentifier()));
}
}
}
private static void AssignUniqueGeneratedCodeNames(
Parameter[] arguments,
HashSet<string> usedNames,
bool output,
MethodArgumentCodeNameScope scope)
{
if (arguments == null)
{
return;
}
foreach (Parameter argument in arguments)
{
if (argument == null)
{
continue;
}
string identifier = argument.Name.ToCSharpIdentifier();
if (TryReserveGeneratedIdentifier(
identifier,
output,
scope,
usedNames))
{
SetGeneratedCodeName(argument, identifier, scope);
continue;
}
string stem = identifier.TrimStart('@');
int suffix = output ? 1 : 2;
while (true)
{
string suffixText;
if (output)
{
suffixText = suffix == 1 ?
"Out" :
"Out" + suffix.ToString(CultureInfo.InvariantCulture);
}
else
{
suffixText = suffix.ToString(CultureInfo.InvariantCulture);
}
string candidate = (stem + suffixText).ToCSharpIdentifier();
suffix++;
if (TryReserveGeneratedIdentifier(
candidate,
output,
scope,
usedNames))
{
SetGeneratedCodeName(argument, candidate, scope);
break;
}
}
}
}
private static bool TryReserveGeneratedIdentifier(
string identifier,
bool output,
MethodArgumentCodeNameScope scope,
HashSet<string> usedNames)
{
string[] collisionKeys =
[
.. scope.GetCollisionIdentifiers(identifier, output)
.Where(name => !string.IsNullOrWhiteSpace(name))
.Select(GetIdentifierCollisionKey)
.Distinct(StringComparer.OrdinalIgnoreCase)
];
if (collisionKeys.Any(usedNames.Contains))
{
return false;
}
foreach (string collisionKey in collisionKeys)
{
usedNames.Add(collisionKey);
}
return true;
}
private static void ClearGeneratedCodeNames(
Parameter[] arguments,
MethodArgumentCodeNameScope scope)
{
if (arguments == null)
{
return;
}
foreach (Parameter argument in arguments)
{
if (argument != null &&
s_generatedCodeNames.TryGetValue(
argument,
out GeneratedCodeNameState state))
{
state.ClearName(scope);
}
}
}
private static string GetIdentifierCollisionKey(string identifier)
{
return identifier.TrimStart('@');
}
private static void SetGeneratedCodeName(
Parameter argument,
string name,
MethodArgumentCodeNameScope scope)
{
s_generatedCodeNames.GetOrCreateValue(argument).SetName(scope, name);
}
private sealed class GeneratedCodeNameState
{
public bool TryGetName(
MethodArgumentCodeNameScope scope,
out string name)
{
return m_names.TryGetValue(scope, out name);
}
public void SetName(
MethodArgumentCodeNameScope scope,
string name)
{
m_names[scope] = name;
}
public void ClearName(MethodArgumentCodeNameScope scope)
{
m_names.Remove(scope);
}
private readonly Dictionary<MethodArgumentCodeNameScope, string> m_names = [];
}
private static readonly MethodArgumentCodeNameScope
s_defaultMethodArgumentCodeNameScope = new();
private static readonly System.Threading.Lock s_generatedCodeNamesLock = new();
private static readonly ConditionalWeakTable<Parameter, GeneratedCodeNameState>
s_generatedCodeNames = new();
/// <summary>
/// Returns the field name of a child node.
/// </summary>
public static string GetChildFieldName(this Parameter field)
{
if (string.IsNullOrEmpty(field?.Name))
{
return string.Empty;
}
// Derived from the property name, which is sanitized and already
// disambiguated against its siblings. Mapping the authored name
// directly is lossy in a way the property mapping is not: "Value Id"
// and "ValueId" both collapse onto "m_valueId" (CS0102, even though
// their properties differ), and characters that are legal in a
// BrowseName but not in an identifier survive into the field name -
// "$Value" became "m_$Value", which does not compile at all.
return field.GetPropertyName().TrimStart('@').ToSafeSymbolName(true, "m_");
}
/// <summary>
/// Returns the C# identifier of the property a structure field is
/// generated as. Structure field names are authored data, so they can be
/// C# keywords or collide with the members every generated data type
/// carries; neither would compile when used verbatim. The wire name is
/// unaffected - it is emitted from <see cref="Parameter.Name"/>.
/// </summary>
public static string GetPropertyName(this Parameter field)
{
return GetGeneratedName(field, s_propertyNames, BuildPropertyNames);
}
/// <summary>
/// Looks a field's generated name up in the map computed once for its
/// whole structure. Deciding one field at a time cannot work: three
/// authored names that sanitize alike ("A-", "A?", "A!" all give "A_")
/// each see only the others' pre-disambiguation names, so the second and
/// third both pick the same replacement.
/// </summary>
private static string GetGeneratedName(
Parameter field,
ConditionalWeakTable<DataTypeDesign, string[]> cache,
ConditionalWeakTable<DataTypeDesign, string[]>.CreateValueCallback build)
{
if (string.IsNullOrEmpty(field?.Name))
{
return string.Empty;
}
if (field.Parent is not DataTypeDesign dataType || dataType.Fields == null)
{
return field.Name.ToCSharpIdentifierPreserveCase();
}
int index = IndexOfField(dataType.Fields, field);
if (index < 0)
{
return field.Name.ToCSharpIdentifierPreserveCase();
}
return cache.GetValue(dataType, build)[index];
}
private static int IndexOfField(Parameter[] fields, Parameter field)
{
for (int ii = 0; ii < fields.Length; ii++)
{
if (ReferenceEquals(fields[ii], field))
{
return ii;
}
}
return -1;
}
/// <summary>
/// Assigns every field of a structure its generated property name in one
/// pass, in declaration order, so each name is unique within the class.
/// A name is taken by an earlier property, by the backing field of an
/// earlier property, by a member the templates declare, by the enclosing
/// type's own name, or by an inherited property.
/// </summary>
private static string[] BuildPropertyNames(DataTypeDesign dataType)
{
Parameter[] fields = dataType.Fields;
var names = new string[fields.Length];
var taken = new HashSet<string>(StringComparer.Ordinal);
taken.UnionWith(s_reservedDataTypeMembers);
taken.UnionWith(s_reservedDataTypeFields);
if (!string.IsNullOrEmpty(dataType.SymbolicName?.Name))
{
// A member may not carry the name of its enclosing type.
taken.Add(dataType.SymbolicName.Name);
}
// Inherited properties, keyed by generated name. A derived field
// that carries the *same* authored name as an inherited one is a
// deliberate redeclaration (the generator emits it as an override),
// so only a different wire name landing on the same identifier is a
// collision - that would be CS0108 on the generated class.
Dictionary<string, string> inherited = CollectInheritedPropertyNames(dataType);
for (int ii = 0; ii < fields.Length; ii++)
{
Parameter field = fields[ii];
if (string.IsNullOrEmpty(field?.Name))
{
names[ii] = string.Empty;
continue;
}
string candidate = SanitizeFieldName(field.Name);
string suffixed = candidate + "Field";
while (IsTaken(taken, inherited, candidate, field.Name))
{
candidate = suffixed;
suffixed += "_";
}
taken.Add(candidate);
// Reserve the backing field the property is stored in, so two
// properties cannot share one ("Value" and "value" both map to
// "m_value", and a field literally named "m_value" would take
// the name of another field's store).
taken.Add(ToBackingFieldName(candidate));
// Re-apply the keyword escape: the replacement may no longer be
// one, and the original may still be.
names[ii] = candidate.ToCSharpIdentifierPreserveCase();
}
return names;
}
private static bool IsTaken(
HashSet<string> taken,
Dictionary<string, string> inherited,
string candidate,
string authoredName)
{
return taken.Contains(candidate) ||
taken.Contains(ToBackingFieldName(candidate)) ||
(inherited.TryGetValue(candidate, out string inheritedFrom) &&
!string.Equals(inheritedFrom, authoredName, StringComparison.Ordinal));
}
/// <summary>
/// The generated property names of every field inherited through the
/// base type chain, mapped to the authored name each came from.
/// </summary>
private static Dictionary<string, string> CollectInheritedPropertyNames(
DataTypeDesign dataType)
{
var result = new Dictionary<string, string>(StringComparer.Ordinal);
var visited = new HashSet<XmlQualifiedName>();
for (var baseType = dataType.BaseTypeNode as DataTypeDesign;
baseType?.Fields != null;
baseType = baseType.BaseTypeNode as DataTypeDesign)
{
if (baseType.SymbolicId != null && !visited.Add(baseType.SymbolicId))
{
// A cyclic base chain is reported elsewhere; do not hang.
break;
}
foreach (Parameter field in baseType.Fields)
{
if (string.IsNullOrEmpty(field?.Name))
{
continue;
}
string name = field.GetPropertyName().TrimStart('@');
if (!result.ContainsKey(name))
{
result[name] = field.Name;
}
}
}
return result;
}
private static string SanitizeFieldName(string name)
{
return name.ToCSharpIdentifierPreserveCase().TrimStart('@');
}
private static string ToBackingFieldName(string propertyName)
{
return propertyName.ToSafeSymbolName(true, "m_");
}
private static readonly ConditionalWeakTable<DataTypeDesign, string[]>
s_propertyNames = new();
private static readonly ConditionalWeakTable<DataTypeDesign, string[]>
s_fieldsEnumMemberNames = new();
/// <summary>
/// Backing fields the data type templates declare themselves, which a
/// generated property therefore cannot be named after.
/// </summary>
private static readonly HashSet<string> s_reservedDataTypeFields =
new(StringComparer.Ordinal) { "m_FieldNames", "m_pooledSentinel" };
/// <summary>
/// Returns the C# identifier of the member a structure field is given in
/// the generated <c>{ClassName}Fields</c> enumeration. Same reasoning as
/// <see cref="GetPropertyName(Parameter)"/>, but the only name already
/// taken in that scope is <c>None</c>.
/// </summary>
public static string GetFieldsEnumMemberName(this Parameter field)
{
return GetGeneratedName(
field, s_fieldsEnumMemberNames, BuildFieldsEnumMemberNames);
}
/// <summary>
/// Assigns every field its member name in the generated
/// <c>{ClassName}Fields</c> enumeration, in one pass for the same reason
/// as the property names. The enumeration is its own scope, so the only
/// name taken up front is <c>None</c>.
/// </summary>
private static string[] BuildFieldsEnumMemberNames(DataTypeDesign dataType)
{
Parameter[] fields = dataType.Fields;
var names = new string[fields.Length];
var taken = new HashSet<string>(StringComparer.Ordinal) { "None" };
for (int ii = 0; ii < fields.Length; ii++)
{
Parameter field = fields[ii];
if (string.IsNullOrEmpty(field?.Name))
{
names[ii] = string.Empty;
continue;
}
string candidate = SanitizeFieldName(field.Name);
string suffixed = candidate + "Field";
while (taken.Contains(candidate))
{
candidate = suffixed;
suffixed += "_";
}
taken.Add(candidate);
names[ii] = candidate.ToCSharpIdentifierPreserveCase();
}
return names;
}
private static readonly HashSet<string> s_reservedDataTypeMembers =
new(StringComparer.Ordinal)
{
"SwitchField",
"EncodingMask",
"EncodingMaskFieldNames",
"Reuse",
"ResetForReuse",
"TypeId",
"BinaryEncodingId",
"XmlEncodingId",
"JsonEncodingId",
"Encode",
"Decode",
"IsEqual",
"Equals",
"GetHashCode",
"GetType",
"ToString",
"Clone",
"MemberwiseClone",
"Initialize"
};
/// <summary>
/// Returns the field name of a child node.
/// </summary>
public static string GetChildFieldName(this InstanceDesign instance)
{
if (string.IsNullOrEmpty(instance?.SymbolicName?.Name))
{
return string.Empty;
}
string name = instance.SymbolicName.Name.ToSafeSymbolName(true, "m_");
if (instance is MethodDesign)
{
return CoreUtils.Format("{0}Method", name);
}
return name;
}
/// <summary>
/// Fixes class names for nodes.
/// </summary>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ArgumentException"></exception>
public static string GetClassName(this TypeDesign node, Namespace[] namespaces)
{
if (node is null)
{
throw new ArgumentNullException(nameof(node));
}
if (node is DataTypeDesign)
{
return node.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
}
if (node is ObjectTypeDesign objectType &&
objectType.ClassName == "ObjectSource")
{
return "global::Opc.Ua.BaseObject";
}
if (node is VariableTypeDesign variableType &&
variableType.ClassName is "DataVariable" or "BaseVariable")
{
return "global::Opc.Ua.BaseDataVariable";
}
if (!string.IsNullOrEmpty(node.ClassName))
{
return node.SymbolicName.AsFullyQualifiedTypeSymbol(
namespaces, node.ClassName);
}
return node.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
}
/// <summary>
/// Returns the NodeClass of a Node
/// </summary>
public static string GetNodeClassAsString(this NodeDesign node)
{
if (node is VariableDesign)
{
return "Variable";
}
if (node is VariableTypeDesign)
{
return "VariableType";
}
if (node is ObjectDesign)
{
return "Object";
}
if (node is ObjectTypeDesign)
{
return "ObjectType";
}
if (node is ReferenceTypeDesign)
{
return "ReferenceType";
}
if (node is DataTypeDesign)
{
return "DataType";
}
if (node is MethodDesign)
{
return "Method";
}
if (node is ViewDesign)
{
return "View";
}
return "Node";
}
/// <summary>
/// Maps the event notifier flag onto a string.
/// </summary>
/// <exception cref="ArgumentNullException"></exception>
public static string GetEventNotifierAsCode(this ObjectTypeDesign objectType)
{
if (objectType is null)
{
throw new ArgumentNullException(nameof(objectType));
}
if (objectType.SupportsEvents)
{
return "global::Opc.Ua.EventNotifiers.SubscribeToEvents";
}
return "global::Opc.Ua.EventNotifiers.None";
}
/// <summary>
/// Maps the value rank enumeration onto a string.
/// </summary>
public static string GetValueRankAsCode(this ValueRank valueRank, string arrayDimensions)
{
switch (valueRank)
{
case ValueRank.Array:
return "global::Opc.Ua.ValueRanks.OneDimension";
case ValueRank.Scalar:
return "global::Opc.Ua.ValueRanks.Scalar";
case ValueRank.Any:
case ValueRank.ScalarOrArray:
return "global::Opc.Ua.ValueRanks.Any";
case ValueRank.ScalarOrOneDimension:
return "global::Opc.Ua.ValueRanks.ScalarOrOneDimension";
case ValueRank.OneOrMoreDimensions:
if (string.IsNullOrWhiteSpace(arrayDimensions))
{
return "global::Opc.Ua.ValueRanks.OneOrMoreDimensions";
}
// The number of entries in ArrayDimensions is the number of
// dimensions, which equals the ValueRank for a fixed-size
// multi-dimensional array (e.g. "3,3" is a two-dimensional
// array with ValueRank 2).
string[] dimensions = arrayDimensions.Split([','], StringSplitOptions.RemoveEmptyEntries);
int dims = dimensions.Length;
if (dims == 0)
{
return "global::Opc.Ua.ValueRanks.OneOrMoreDimensions";
}
if (dims == 1)
{
return "global::Opc.Ua.ValueRanks.OneDimension";
}
if (dims == 2)
{
return "global::Opc.Ua.ValueRanks.TwoDimensions";
}
return CoreUtils.Format("{0}", dims);
}
return "global::Opc.Ua.ValueRanks.Any";
}
/// <summary>
/// The bit an OptionSet field mask (the design Identifier) selects.
/// An OptionSet field names exactly one bit (OPC 10000-3 8.40, 8.52);
/// false for a zero mask (the "no bits set" value) or a mask of
/// several bits.
/// </summary>
public static bool TryGetOptionSetBit(decimal mask, out int bit)
{
bit = 0;
if (mask <= 0 || decimal.Truncate(mask) != mask)
{
return false;
}
while (decimal.Remainder(mask, 2) == 0)
{
mask /= 2;
bit++;
}
return mask == 1;
}
/// <summary>
/// The bit an OptionSet field names: its explicit
/// <see cref="Parameter.OptionSetBit"/> (any position, a subtype of
/// the OptionSet structure has no upper bit, OPC 10000-3 8.40) or
/// else the bit of its Identifier mask. False for the zero ("no bits
/// set") value and a mask of several bits.
/// </summary>
public static bool TryGetOptionSetBit(this Parameter field, out int bit)
{
if (field?.OptionSetBit is int explicitBit && explicitBit >= 0)
{
bit = explicitBit;
return true;
}
bit = 0;
return field != null && TryGetOptionSetBit(field.Identifier, out bit);
}
/// <summary>
/// The decimal mask of an OptionSet bit, or 0 when the bit is beyond
/// what a decimal represents exactly (bit 96 and above); such a bit
/// is carried by <see cref="Parameter.OptionSetBit"/>. Computed in
/// decimal since a signed shift turns bit 63 into a negative mask.
/// </summary>
public static decimal GetOptionSetMask(int bit)
{
if (bit is < 0 or >= kMaxDecimalMaskBits)
{
return 0;
}
decimal mask = 1m;
for (int ii = 0; ii < bit; ii++)
{
mask *= 2;
}
return mask;
}
/// <summary>
/// The hexadecimal BitMask of an OptionSet bit (at least 8 digits),
/// for any bit position.
/// </summary>
public static string GetOptionSetBitMask(int bit)
{
string mask = "1248"[bit % 4] + new string('0', bit / 4);
return mask.Length < 8 ? mask.PadLeft(8, '0') : mask;
}
/// <summary>
/// The bit a hexadecimal BitMask of any length selects; false when
/// the mask is not a single bit.
/// </summary>
public static bool TryGetOptionSetBitFromBitMask(string bitMask, out int bit)
{
bit = 0;
string mask = bitMask?.Trim().TrimStart('0');
if (string.IsNullOrEmpty(mask))
{
return false;
}
int zeros = 0;
for (int ii = mask.Length - 1; ii > 0; ii--)
{
if (mask[ii] != '0')
{
return false;
}
zeros++;
}
int lead = mask[0] switch
{
'1' => 0,
'2' => 1,
'4' => 2,
'8' => 3,
_ => -1
};
if (lead < 0)
{
return false;
}
bit = (zeros * 4) + lead;
return true;
}
/// <summary>
/// The bits a decimal OptionSet mask can represent exactly.
/// </summary>
private const int kMaxDecimalMaskBits = 96;
/// <summary>
/// The ArrayDimensions a StructureField publishes for the field. A
/// StructureField ValueRank is -1 or &gt;= 1, never 0 (OPC 10000-3
/// 8.51), and a multi-dimensional field has at least two dimensions
/// (OPC 10000-6 5.2.5): a <see cref="ValueRank.OneOrMoreDimensions"/>
/// field without ArrayDimensions is a two dimensional matrix of
/// unknown lengths. (The validator already normalizes design fields
/// that way; this also covers fields it did not see.)
/// </summary>
public static string GetStructureFieldArrayDimensions(this Parameter field)
{
if (field.ValueRank == ValueRank.OneOrMoreDimensions &&
string.IsNullOrWhiteSpace(field.ArrayDimensions))
{
return "0,0";
}
return field.ArrayDimensions;
}
/// <summary>
/// Maps the MinimumSamplingInterval onto a constant.
/// </summary>
public static string GetMinimumSamplingIntervalAsCode(this VariableTypeDesign variableType)
{
return variableType.MinimumSamplingInterval switch
{
-1 => "global::Opc.Ua.MinimumSamplingIntervals.Indeterminate",
0 => "global::Opc.Ua.MinimumSamplingIntervals.Continuous",
_ => variableType.MinimumSamplingInterval.ToString(CultureInfo.InvariantCulture)
};
}
/// <summary>
/// Whether this is a reference type and implicitly nullable
/// </summary>
public static bool IsDotNetReferenceType(
this DataTypeDesign dataType,
ValueRank valueRank)
{
return !IsDotNetValueType(dataType, valueRank);
}
/// <summary>
/// Whether the data type supports equality comparison in .NET.
/// </summary>
public static bool IsDotNetEqualityComparable(
this DataTypeDesign dataType,
ValueRank valueRank)
{
if (IsDotNetValueType(dataType, valueRank))
{
return true;
}
if (valueRank == ValueRank.Scalar)
{
switch (dataType.BasicDataType)
{
case BasicDataType.String:
// Use equality for strings
return true;
default:
return false;
}
}
return false;
}
/// <summary>
/// If the data type is a value type in .NET.
/// </summary>
/// <exception cref="ArgumentNullException"></exception>
public static bool IsDotNetValueType(
this DataTypeDesign dataType,
ValueRank valueRank)
{
if (dataType is null)
{
throw new ArgumentNullException(nameof(dataType));
}
if (valueRank != ValueRank.Scalar)
{
return true;
// ArrayOf/MatrixOf/Variant;
}
switch (dataType.BasicDataType)
{
case BasicDataType.Boolean:
case BasicDataType.SByte:
case BasicDataType.Byte:
case BasicDataType.Int16:
case BasicDataType.UInt16:
case BasicDataType.Int32:
case BasicDataType.UInt32:
case BasicDataType.Int64:
case BasicDataType.UInt64:
case BasicDataType.Float:
case BasicDataType.Double:
case BasicDataType.DateTime:
case BasicDataType.Guid:
case BasicDataType.NodeId:
case BasicDataType.ExpandedNodeId:
case BasicDataType.QualifiedName:
case BasicDataType.LocalizedText:
case BasicDataType.StatusCode:
case BasicDataType.Structure: // Extension object
case BasicDataType.Number:
case BasicDataType.Integer:
case BasicDataType.UInteger:
case BasicDataType.BaseDataType: // Variant
case BasicDataType.Enumeration when !dataType.IsOptionSet:
case BasicDataType.UserDefined when dataType.IsEnumeration:
case BasicDataType.ByteString:
case BasicDataType.XmlElement:
return true;
default:
return false;
}
}
/// <summary>
/// If the type needs to be cloned
/// </summary>
/// <exception cref="ArgumentNullException"></exception>
public static bool NeedsCloning(this DataTypeDesign dataType)
{
if (dataType is null)
{
throw new ArgumentNullException(nameof(dataType));
}
switch (dataType.BasicDataType)
{
case BasicDataType.DataValue:
case BasicDataType.Structure: // Extension object
case BasicDataType.BaseDataType: // Variant
case BasicDataType.UserDefined when !dataType.IsEnumeration:
case BasicDataType.Enumeration when dataType.IsOptionSet:
return true;
default:
return false;
}
}
/// <summary>
/// Whether the data type can be expressed as a typed
/// <see cref="MatrixOf{T}"/> in generated code, i.e. whether
/// <c>Variant.From(MatrixOf&lt;T&gt;)</c> /
/// <c>Variant.GetXxxMatrix()</c> round-trip APIs exist.
/// All BasicDataType values except <see cref="BasicDataType.DiagnosticInfo"/>
/// (which is not a valid structure field per OPC UA Part 5) are
/// supported.
/// </summary>
/// <exception cref="ArgumentNullException"></exception>
public static bool SupportsMatrixOf(this DataTypeDesign dataType)
{
if (dataType is null)
{
throw new ArgumentNullException(nameof(dataType));
}
return dataType.BasicDataType != BasicDataType.DiagnosticInfo;
}
/// <summary>
/// True if a field or a nested field of the structure (including the
/// fields inherited from its base types) is a multi-dimensional array,
/// i.e. encoded with the inline matrix representation. Such a
/// structure "is not compatible with the deprecated DataTypeDictionary
/// mechanism, and shall not be included in a DataTypeDictionary"
/// (OPC 10000-6 5.2.5): the dictionary has no way to describe values
/// whose count is the product of the dimensions. A field that allows
/// subtypes is an ExtensionObject in the dictionary and does not nest.
/// </summary>
public static bool HasInlineMatrixField(this DataTypeDesign dataType)
{
return HasInlineMatrixField(dataType, []);
static bool HasInlineMatrixField(DataTypeDesign dataType, HashSet<DataTypeDesign> visited)
{
if (dataType == null || !visited.Add(dataType))
{
return false;
}
for (DataTypeDesign type = dataType;
type != null;
type = type.BaseTypeNode as DataTypeDesign)
{
if (type.Fields == null || type.IsOptionSet || type.IsEnumeration)
{
continue;
}
foreach (Parameter field in type.Fields)
{
if (field == null)
{
continue;
}
if (field.ValueRank == ValueRank.OneOrMoreDimensions)
{
return true;
}
if (field.ValueRank is ValueRank.Scalar or ValueRank.Array &&
!field.AllowSubTypes &&
field.DataTypeNode is DataTypeDesign fieldType &&
fieldType.BasicDataType == BasicDataType.UserDefined &&
!fieldType.IsEnumeration &&
HasInlineMatrixField(fieldType, visited))
{
return true;
}
}
}
return false;
}
}
/// <summary>
/// Emits the default value as a C# expression.
/// </summary>
/// <param name="dataType">The data type of the value.</param>
/// <param name="valueRank">The value rank.</param>
/// <param name="defaultValue">The authored value, decoded here when
/// <paramref name="decodedValue"/> is null.</param>
/// <param name="decodedValue">The already decoded value.</param>
/// <param name="valueAsVariant">Wrap the expression in a Variant.</param>
/// <param name="targetNamespace">The target namespace.</param>
/// <param name="namespaces">The design's namespaces.</param>
/// <param name="context">The context used to decode
/// <paramref name="defaultValue"/>.</param>
/// <param name="onUnknownElement">Fallback for values that have no
/// expression form.</param>
/// <param name="decodedValueNamespaceUris">The table the namespace
/// indexes in <paramref name="decodedValue"/> refer to (the NodeSet's);
/// null for a value authored in the design.</param>
/// <param name="namespaceTableVariable">A NamespaceTable expression in
/// scope where the code is emitted (e.g. <c>context.NamespaceUris</c>),
/// or null when there is none.</param>
public static string GetValueAsCode(
this DataTypeDesign dataType,
ValueRank valueRank,
System.Xml.XmlElement defaultValue,
object decodedValue,
bool valueAsVariant,
string targetNamespace,
Namespace[] namespaces,
IServiceMessageContext context,
Func<string> onUnknownElement = null,
NamespaceTable decodedValueNamespaceUris = null,
string namespaceTableVariable = null)
{
TypeInfo decodedValueType = default;
string defaultString = valueAsVariant ?
"global::Opc.Ua.Variant.Null" :
"default";
if (decodedValue == null && defaultValue != null)
{
// <opc:DefaultValue>
// <uax:Boolean>true</uax:Boolean>
// </opc:DefaultValue >
using var decoder = new XmlDecoder((XmlElement)defaultValue, context);
Variant variant = decoder.ReadVariantValue(null, default);
decodedValueType = variant.TypeInfo;
decodedValue = variant.AsBoxedObject(Variant.BoxingBehavior.Legacy);
// Authored in the design: its indexes are the design's own.
decodedValueNamespaceUris = null;
}
if (valueRank == ValueRank.Array)
{
return dataType.GetArrayValueAsCode(
defaultValue,
decodedValue,
valueAsVariant,
targetNamespace,
namespaces,
onUnknownElement)
?? defaultString;
}
if (valueRank == ValueRank.Scalar &&
dataType.BasicDataType != BasicDataType.BaseDataType)
{
return dataType.GetScalarValueAsCode(
defaultValue,
decodedValue,
decodedValueType,
valueAsVariant,
targetNamespace,
namespaces,
onUnknownElement,
decodedValueNamespaceUris,
namespaceTableVariable)
?? defaultString;
}
return onUnknownElement?.Invoke() ?? defaultString;
}
/// <summary>
/// Get default for a scalar type
/// </summary>
internal static string GetScalarValueAsCode(
this DataTypeDesign dataType,
System.Xml.XmlElement defaultValue,
object decodedValue,
TypeInfo decodedValueType,
bool valueAsVariant,
string targetNamespace,
Namespace[] namespaces,
Func<string> onUnknownElement = null,
NamespaceTable decodedValueNamespaceUris = null,
string namespaceTableVariable = null)
{
// Note that we return a typed value since we initialize a object/variant
switch (dataType.BasicDataType)
{
case BasicDataType.Boolean:
if (decodedValue is not bool boolValue)
{
boolValue = false;
}
return MakeReturnType(boolValue ? "true" : "false");
case BasicDataType.SByte:
if (decodedValue is not sbyte sbyteValue)
{
sbyteValue = 0;
}
return MakeReturnType(CoreUtils.Format("(sbyte){0}", sbyteValue));
case BasicDataType.Byte:
if (decodedValue is not byte byteValue)
{
byteValue = 0;
}
return MakeReturnType(CoreUtils.Format("(byte){0}", byteValue));
case BasicDataType.Int16:
if (decodedValue is not short shortValue)
{
shortValue = 0;
}
return MakeReturnType(CoreUtils.Format("(short){0}", shortValue));
case BasicDataType.UInt16:
if (decodedValue is not ushort ushortValue)
{
ushortValue = 0;
}
return MakeReturnType(CoreUtils.Format("(ushort){0}", ushortValue));
case BasicDataType.Int32:
if (decodedValue is not int intValue)
{
intValue = 0;
}
return MakeReturnType(CoreUtils.Format("(int){0}", intValue));
case BasicDataType.UInt32:
if (decodedValue is not uint uintValue)
{
uintValue = 0;
}
return MakeReturnType(CoreUtils.Format("(uint){0}", uintValue));
case BasicDataType.Integer:
case BasicDataType.Int64:
if (decodedValue is not long longValue)
{
longValue = 0;
}
return MakeReturnType(CoreUtils.Format("(long){0}", longValue));
case BasicDataType.UInteger:
case BasicDataType.UInt64:
if (decodedValue is not ulong ulongValue)
{
ulongValue = 0;
}
return MakeReturnType(CoreUtils.Format("(ulong){0}", ulongValue));
case BasicDataType.Float:
if (decodedValue is not float floatValue)
{
floatValue = 0;
}
// NaN and the infinities have no literal form in C# - they
// format as "NaN" / "Infinity", which would not compile.
if (float.IsNaN(floatValue))
{
return MakeReturnType("float.NaN");
}
if (float.IsPositiveInfinity(floatValue))
{
return MakeReturnType("float.PositiveInfinity");
}
if (float.IsNegativeInfinity(floatValue))
{
return MakeReturnType("float.NegativeInfinity");
}
return MakeReturnType("(float)" + FormatFloatLiteral(floatValue));
case BasicDataType.Number:
case BasicDataType.Double:
if (decodedValue is not double doubleValue)
{
doubleValue = 0;
}
if (double.IsNaN(doubleValue))
{
return MakeReturnType("double.NaN");
}
if (double.IsPositiveInfinity(doubleValue))
{
return MakeReturnType("double.PositiveInfinity");
}
if (double.IsNegativeInfinity(doubleValue))
{
return MakeReturnType("double.NegativeInfinity");
}
return MakeReturnType("(double)" + FormatDoubleLiteral(doubleValue));
case BasicDataType.String:
if (decodedValue is not string stringValue)
{
return MakeReturnType("default(string)");
}
return MakeReturnType(stringValue.AsStringLiteral());
case BasicDataType.DateTime:
if (decodedValue is DateTimeUtc dtutc)
{
if (dtutc.IsNull)
{
return MakeReturnType("global::Opc.Ua.DateTimeUtc.MinValue");
}
decodedValue = (DateTime)dtutc;
}
if (decodedValue is not DateTime dateTimeValue ||
dateTimeValue == DateTime.MinValue)
{
return MakeReturnType("global::Opc.Ua.DateTimeUtc.MinValue");
}
return MakeReturnType(CoreUtils.Format(
"global::Opc.Ua.DateTimeUtc.From(\"{0:yyyy-MM-dd HH:mm:ss}\")",
dateTimeValue));
case BasicDataType.Guid:
if (decodedValue is Uuid uuid)
{
decodedValue = uuid.Guid;
}
if (decodedValue is not Guid guidValue ||
guidValue == Guid.Empty)
{
return MakeReturnType("global::Opc.Ua.Uuid.Empty");
}
return MakeReturnType(CoreUtils.Format(
"global::Opc.Ua.Uuid.Parse(\"{0}\")",
guidValue));
case BasicDataType.ByteString:
if (decodedValue is ByteString byteStringValue)
{
decodedValue = byteStringValue.ToArray();
}
if (decodedValue is not byte[] byteArray)
{
return MakeReturnType("global::Opc.Ua.ByteString.Empty");
}
return MakeReturnType(CoreUtils.Format(
"global::Opc.Ua.ByteString.FromHexString(\"{0}\")",
CoreUtils.ToHexString(byteArray)));
case BasicDataType.NodeId:
if (decodedValue is not NodeId nodeId ||
nodeId.IsNull)
{
return MakeReturnType("global::Opc.Ua.NodeId.Null");
}
// The namespace index is one of the table the value was
// decoded with, not of the server the code runs in: resolve
// it to its URI and look that up at run time. Without a
// namespace table in scope (a structure field initializer)
// or for an index nothing declares, the literal id is all
// that can be emitted.
string nodeIdNamespaceUri = nodeId.NamespaceIndex == 0
? null
: ResolveDecodedNamespaceUri(
nodeId.NamespaceIndex,
decodedValueNamespaceUris,
namespaces);
if (nodeIdNamespaceUri == null || namespaceTableVariable == null)
{
return MakeReturnType(CoreUtils.Format(
"global::Opc.Ua.NodeId.Parse({0})",
nodeId.ToString().AsStringLiteral()));
}
return MakeReturnType(CoreUtils.Format(
"global::Opc.Ua.NodeId.Parse({0}).WithNamespaceIndex({1}.GetIndexOrAppend({2}))",
nodeId.WithNamespaceIndex(0).ToString().AsStringLiteral(),
namespaceTableVariable,
nodeIdNamespaceUri.AsStringLiteral()));
case BasicDataType.ExpandedNodeId:
if (decodedValue is not ExpandedNodeId expandedNodeId ||
expandedNodeId.IsNull)
{
return MakeReturnType("global::Opc.Ua.ExpandedNodeId.Null");
}
// An ExpandedNodeId can carry the namespace URI itself, so an
// index-only value is emitted in its absolute "nsu=" form -
// the index would otherwise be read against the server's
// namespace table.
if (string.IsNullOrEmpty(expandedNodeId.NamespaceUri) &&
expandedNodeId.NamespaceIndex != 0)
{
string expandedNamespaceUri = ResolveDecodedNamespaceUri(
expandedNodeId.NamespaceIndex,
decodedValueNamespaceUris,
namespaces);
if (expandedNamespaceUri != null)
{
expandedNodeId = new ExpandedNodeId(
expandedNodeId.InnerNodeId,
expandedNamespaceUri,
expandedNodeId.ServerIndex);
}
}
// A server index is one of the NodeSet's ServerUris table
// (OPC 10000-6 F.2, F.14), not of the server the code runs
// in, and the ExpandedNodeId text has no server URI form:
// map the URI through the context's ServerUris at run time
// (the literal index stays the fallback without a table).
string serverUri = expandedNodeId.ServerIndex == 0 ||
decodedValueNamespaceUris is not DecodedValueNamespaceTable decodedTable
? null
: decodedTable.ServerUris?.GetString(expandedNodeId.ServerIndex);
string serverTableVariable = GetServerTableVariable(namespaceTableVariable);
if (serverUri != null && serverTableVariable != null)
{
return MakeReturnType(CoreUtils.Format(
"global::Opc.Ua.ExpandedNodeId.Parse({0}).WithServerIndex({1}?.GetIndexOrAppend({2}) ?? {3}u)",
expandedNodeId.WithServerIndex(0).ToString().AsStringLiteral(),
serverTableVariable,
serverUri.AsStringLiteral(),
expandedNodeId.ServerIndex));
}
return MakeReturnType(CoreUtils.Format(
"global::Opc.Ua.ExpandedNodeId.Parse({0})",
expandedNodeId.ToString().AsStringLiteral()));
case BasicDataType.QualifiedName:
if (decodedValue is not QualifiedName qualifiedName ||
qualifiedName.IsNull)
{
return MakeReturnType("global::Opc.Ua.QualifiedName.Null");
}
string qualifiedNameNamespaceUri = qualifiedName.NamespaceIndex == 0
? null
: ResolveDecodedNamespaceUri(
qualifiedName.NamespaceIndex,
decodedValueNamespaceUris,
namespaces);
if (qualifiedNameNamespaceUri == null || namespaceTableVariable == null)
{
return MakeReturnType(CoreUtils.Format(
"global::Opc.Ua.QualifiedName.Parse({0})",
qualifiedName.ToString().AsStringLiteral()));
}
return MakeReturnType(CoreUtils.Format(
"new global::Opc.Ua.QualifiedName({0}, {1}.GetIndexOrAppend({2}))",
qualifiedName.Name.AsStringLiteral(),
namespaceTableVariable,
qualifiedNameNamespaceUri.AsStringLiteral()));
case BasicDataType.LocalizedText:
if (decodedValue is not Ua.LocalizedText localizedText ||
localizedText.IsNullOrEmpty)
{
return MakeReturnType("global::Opc.Ua.LocalizedText.Null");
}
return MakeReturnType(CoreUtils.Format(
"new global::Opc.Ua.LocalizedText({0}, {1})",
localizedText.Locale.AsStringLiteral(),
localizedText.Text.AsStringLiteral()));
case BasicDataType.StatusCode:
if (decodedValue is not StatusCode statusCode ||
statusCode.Code == 0)
{
return MakeReturnType("default(global::Opc.Ua.StatusCode)");
}
// The numeric code, not StatusCode.ToString(): that yields
// "BadNodeIdUnknown [0x80340000]", which is no C# expression.
return MakeReturnType(CoreUtils.Format(
"new global::Opc.Ua.StatusCode(0x{0:X8}u)",
statusCode.Code));
case BasicDataType.Enumeration:
if (dataType.BaseTypeNode?.SymbolicId ==
new XmlQualifiedName("OptionSet", Namespaces.OpcUa))
{
// OptionSet subtypes inherit from Opc.Ua.OptionSet (a
// class implementing IEncodeable), so Variant.From<T>
// (where T : struct, Enum) does not apply. Route them
// through Variant.FromStructure<T> (where T : IEncodeable).
return MakeReturnType(
CoreUtils.Format("new {0}()",
dataType.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces)),
"Structure");
}
if (!dataType.IsOptionSet && dataType.Fields?.Length > 0)
{
if (decodedValue is int enumValue)
{
return MakeReturnType(CoreUtils.Format("({0})({1})",
dataType.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces),
enumValue));
}
if (decodedValue is null)
{
// Enum type declared - use first field as default
return MakeReturnType(CoreUtils.Format("{0}.{1}",
dataType.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces),
dataType.Fields[0].Name));
}
// TODO: change to initialize enum with the value
return onUnknownElement?.Invoke() ??
MakeReturnType(CoreUtils.Format("default({0})",
dataType.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces)));
}
// Default to enum with value 0 which should be "none" for option set
return onUnknownElement?.Invoke();
case BasicDataType.DataValue:
return MakeReturnType("new global::Opc.Ua.DataValue()");
case BasicDataType.UserDefined:
if (defaultValue == null)
{
return MakeReturnType(CoreUtils.Format(
"new {0}()",
GetDotNetTypeName(
dataType,
ValueRank.Scalar,
targetNamespace,
namespaces,
nullable: NullableAnnotation.NonNullable)),
"Structure");
}
return onUnknownElement?.Invoke();
default:
return onUnknownElement?.Invoke();
}
string MakeReturnType(string value, string prefix = null)
{
if (valueAsVariant)
{
return CoreUtils.Format(
"global::Opc.Ua.Variant.From{0}({1})",
prefix ?? string.Empty,
value);
}
return value;
}
}
/// <summary>
/// The run-time server URI table that sits next to a namespace table
/// expression: "context.NamespaceUris" pairs with "context.ServerUris".
/// Null when the expression is not a member access on a context.
/// </summary>
private static string GetServerTableVariable(string namespaceTableVariable)
{
const string suffix = ".NamespaceUris";
if (namespaceTableVariable == null ||
!namespaceTableVariable.EndsWith(suffix, StringComparison.Ordinal))
{
return null;
}
return namespaceTableVariable[..(namespaceTableVariable.Length - suffix.Length)] +
".ServerUris";
}
/// <summary>
/// Resolves a namespace index found inside a decoded default value to
/// its namespace URI. A value read from a NodeSet carries the table of
/// that NodeSet; a value authored in a ModelDesign numbers OPC UA 0 and
/// then the design's namespaces in declaration order. Returns null when
/// the index is not in the table.
/// </summary>
internal static string ResolveDecodedNamespaceUri(
ushort namespaceIndex,
NamespaceTable decodedValueNamespaceUris,
Namespace[] namespaces)
{
if (namespaceIndex == 0)
{
return Namespaces.OpcUa;
}
NamespaceTable table = decodedValueNamespaceUris ??
CreateDesignNamespaceTable(namespaces);
return table.GetString(namespaceIndex);
}
/// <summary>
/// The table the namespace indexes of a value authored in a ModelDesign
/// refer to: OPC UA 0 and then the design's namespaces in declaration
/// order.
/// </summary>
internal static NamespaceTable CreateDesignNamespaceTable(Namespace[] namespaces)
{
var table = new NamespaceTable();
foreach (Namespace ns in namespaces ?? [])
{
if (!string.IsNullOrEmpty(ns?.Value))
{
table.GetIndexOrAppend(ns.Value);
}
}
return table;
}
/// <summary>
/// Formats a double as the digits of a C# literal that evaluates to
/// exactly the same value. The default "G" format depends on the
/// runtime the generator runs in: 15 significant digits on .NET
/// Framework (Visual Studio), the shortest round-trip form on .NET -
/// so the generated code differed between the two, lost precision on
/// .NET Framework and turned double.MaxValue into an out-of-range
/// literal (CS0594). The shortest of G15..G17 that parses back to the
/// value is the same on every runtime.
/// </summary>
internal static string FormatDoubleLiteral(double value)
{
if (value == 0)
{
// "(double)-0" would be integer zero cast to +0.
return BitConverter.DoubleToInt64Bits(value) < 0 ? "-0.0" : "0";
}
for (int precision = 15; precision < 17; precision++)
{
string text = value.ToString(
"G" + precision.ToString(CultureInfo.InvariantCulture),
CultureInfo.InvariantCulture);
if (double.TryParse(
text,
NumberStyles.Float,
CultureInfo.InvariantCulture,
out double parsed) &&
parsed.Equals(value))
{
return text;
}
}
return value.ToString("G17", CultureInfo.InvariantCulture);
}
/// <summary>
/// Formats a float as the digits of a literal that, cast to float,
/// evaluates to exactly the same value. The generated expression is
/// <c>(float)digits</c>, i.e. the digits are parsed as a double and then
/// converted, so the round trip is checked the same way.
/// See <see cref="FormatDoubleLiteral"/> for why "G" is not used.
/// </summary>
internal static string FormatFloatLiteral(float value)
{
if (value == 0)
{
return BitConverter.DoubleToInt64Bits(value) < 0 ? "-0.0" : "0";
}
for (int precision = 6; precision < 9; precision++)
{
string text = value.ToString(
"G" + precision.ToString(CultureInfo.InvariantCulture),
CultureInfo.InvariantCulture);
if (double.TryParse(
text,
NumberStyles.Float,
CultureInfo.InvariantCulture,
out double parsed) &&
((float)parsed).Equals(value))
{
return text;
}
}
return value.ToString("G9", CultureInfo.InvariantCulture);
}
/// <summary>
/// Get default for a array type
/// </summary>
internal static string GetArrayValueAsCode(
this DataTypeDesign dataType,
System.Xml.XmlElement defaultValue,
object decodedValue,
bool valueAsVariant,
string targetNamespace,
Namespace[] namespaces,
Func<string> onUnknownElement = null)
{
if ((decodedValue is not null &&
(decodedValue is not IList l || l.Count != 0) &&
(decodedValue is not Array a || a.Length != 0)) ||
(decodedValue is null && defaultValue is not null))
{
// TODO: change to initialize simple arrays with values
return onUnknownElement?.Invoke();
}
// Emit Array.Empty<T>() for empty arrays
switch (dataType.BasicDataType)
{
case BasicDataType.Boolean:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<bool>()");
case BasicDataType.SByte:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<sbyte>()");
case BasicDataType.Byte:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<byte>()");
case BasicDataType.Int16:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<short>()");
case BasicDataType.UInt16:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<ushort>()");
case BasicDataType.Int32:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<int>()");
case BasicDataType.UInt32:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<uint>()");
case BasicDataType.Int64:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<long>()");
case BasicDataType.UInt64:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<ulong>()");
case BasicDataType.Float:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<float>()");
case BasicDataType.Double:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<double>()");
case BasicDataType.String:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<string>()");
case BasicDataType.Guid:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.Uuid>()");
case BasicDataType.DateTime:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.DateTimeUtc>()");
case BasicDataType.ByteString:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.ByteString>()");
case BasicDataType.XmlElement:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.XmlElement>()");
case BasicDataType.NodeId:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.NodeId>()");
case BasicDataType.ExpandedNodeId:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.ExpandedNodeId>()");
case BasicDataType.QualifiedName:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.QualifiedName>()");
case BasicDataType.LocalizedText:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.LocalizedText>()");
case BasicDataType.StatusCode:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.StatusCode>()");
case BasicDataType.DataValue:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.DataValue>()");
case BasicDataType.Structure:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.ExtensionObject>()");
case BasicDataType.Enumeration:
case BasicDataType.UserDefined:
return MakeReturnType(
CoreUtils.Format(
"global::Opc.Ua.ArrayOf.Empty<{0}>()",
GetDotNetTypeName(
dataType,
ValueRank.Scalar,
targetNamespace,
namespaces,
nullable: NullableAnnotation.NonNullable)),
dataType.BasicDataType == BasicDataType.UserDefined ? "Structure" : null);
case BasicDataType.DiagnosticInfo:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.DiagnosticInfo>()");
case BasicDataType.Number:
case BasicDataType.Integer:
case BasicDataType.UInteger:
case BasicDataType.BaseDataType:
return MakeReturnType("global::Opc.Ua.ArrayOf.Empty<global::Opc.Ua.Variant>()");
default:
return null;
}
string MakeReturnType(string value, string prefix = null)
{
if (valueAsVariant)
{
return CoreUtils.Format(
"global::Opc.Ua.Variant.From{0}({1})",
prefix ?? string.Empty,
value);
}
return value;
}
}
/// <summary>
/// Get code string to load a variant from XML resource stream.
/// </summary>
/// <returns></returns>
public static string GetVariantValueFromXmlAsCode(
this DataTypeDesign dataType,
ValueRank valueRank,
string targetNamespace,
Namespace[] namespaces,
string xmlStreamResource,
string contextVariable = null)
{
contextVariable ??= "context";
switch (dataType.BasicDataType)
{
case BasicDataType.BaseDataType:
return CoreUtils.Format(
"global::Opc.Ua.Variant.FromXml({0}AsStream, {1})",
xmlStreamResource,
contextVariable);
case BasicDataType.UserDefined:
case BasicDataType.Structure:
case BasicDataType.Enumeration:
string dataTypeString = dataType.GetDotNetTypeName(
targetNamespace,
namespaces,
NullableAnnotation.NonNullable,
false);
return CoreUtils.Format(
"global::Opc.Ua.Variant.FromXml({0}AsStream, {1}).Get{2}{3}<{4}>()",
xmlStreamResource,
contextVariable,
dataType.BasicDataType == BasicDataType.Enumeration ? "Enumeration" : "Structure",
valueRank == ValueRank.Array ? "Array" : string.Empty,
dataTypeString);
default:
return CoreUtils.Format(
"global::Opc.Ua.Variant.FromXml({0}AsStream, {1}).Get{2}{3}()",
xmlStreamResource,
contextVariable,
dataType.BasicDataType.ToString(),
valueRank == ValueRank.Array ? "Array" : string.Empty);
}
}
/// <summary>
/// Returns the data type for a method argument.
/// </summary>
public static string GetMethodArgumentTypeAsCode(
this DataTypeDesign datatype,
ValueRank valueRank,
string targetNamespace,
Namespace[] namespaces,
bool isOptional)
{
string typeName = GetDotNetTypeName(
datatype,
targetNamespace,
namespaces,
// Make scalar optional nullable, but array/matrix elements non nullable
// TODO: Revisit this and make input non nullable but output parameter T
// nullable when we get rid of collections
nullable: isOptional && valueRank == ValueRank.Scalar ?
NullableAnnotation.Nullable : NullableAnnotation.NonNullable,
// Use raw arguments
false);
if (typeName is "global::Opc.Ua.IEncodeable" or "global::Opc.Ua.IEncodeable?")
{
typeName = "global::Opc.Ua.ExtensionObject";
}
if (typeName is "object" or "object?")
{
typeName = "global::Opc.Ua.Variant";
}
switch (valueRank)
{
case ValueRank.Scalar:
return typeName;
case ValueRank.Array:
return CoreUtils.Format("global::Opc.Ua.ArrayOf<{0}>", typeName);
case ValueRank.OneOrMoreDimensions:
return CoreUtils.Format("global::Opc.Ua.MatrixOf<{0}>", typeName);
default:
return "global::Opc.Ua.Variant";
}
}
/// <summary>
/// Returns true if the type is nullable
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public static bool IsXmlNillable(this BasicDataType type)
{
switch (type)
{
case BasicDataType.Boolean:
case BasicDataType.SByte:
case BasicDataType.Byte:
case BasicDataType.Int16:
case BasicDataType.UInt16:
case BasicDataType.Int32:
case BasicDataType.UInt32:
case BasicDataType.Int64:
case BasicDataType.UInt64:
case BasicDataType.Float:
case BasicDataType.Double:
case BasicDataType.StatusCode:
case BasicDataType.Enumeration:
return false;
}
return true;
}
/// <summary>
/// Returns system type for a basic data type.
/// </summary>
/// <exception cref="ArgumentException"></exception>
public static string GetDotNetTypeName(
this DataTypeDesign datatype,
string targetNamespace,
Namespace[] namespaces,
NullableAnnotation nullable = NullableAnnotation.NonNullable,
bool useBuiltInWrapperTypes = false)
{
switch (datatype.BasicDataType)
{
case BasicDataType.Boolean:
return "bool";
case BasicDataType.SByte:
return "sbyte";
case BasicDataType.Byte:
return "byte";
case BasicDataType.Int16:
return "short";
case BasicDataType.UInt16:
return "ushort";
case BasicDataType.Int32:
return "int";
case BasicDataType.UInt32:
return "uint";
case BasicDataType.Int64:
return "long";
case BasicDataType.UInt64:
return "ulong";
case BasicDataType.Float:
return "float";
case BasicDataType.Double:
return "double";
case BasicDataType.String:
return nullable == NullableAnnotation.NonNullable ?
"string" :
"string?";
case BasicDataType.DateTime:
return "global::Opc.Ua.DateTimeUtc";
case BasicDataType.Guid:
return "global::Opc.Ua.Uuid";
case BasicDataType.ByteString:
return "global::Opc.Ua.ByteString";
case BasicDataType.XmlElement:
return "global::Opc.Ua.XmlElement";
case BasicDataType.NodeId:
return "global::Opc.Ua.NodeId";
case BasicDataType.ExpandedNodeId:
return "global::Opc.Ua.ExpandedNodeId";
case BasicDataType.StatusCode:
return "global::Opc.Ua.StatusCode";
case BasicDataType.DiagnosticInfo:
return nullable == NullableAnnotation.NonNullable ?
"global::Opc.Ua.DiagnosticInfo" :
"global::Opc.Ua.DiagnosticInfo?";
case BasicDataType.QualifiedName:
return "global::Opc.Ua.QualifiedName";
case BasicDataType.LocalizedText:
return "global::Opc.Ua.LocalizedText";
case BasicDataType.DataValue:
return "global::Opc.Ua.DataValue";
case BasicDataType.Structure:
if (!useBuiltInWrapperTypes)
{
// return nullable == NullableAnnotation.NonNullable ?
// "global::Opc.Ua.IEncodeable" :
// "global::Opc.Ua.IEncodeable?";
}
return "global::Opc.Ua.ExtensionObject";
case BasicDataType.Enumeration:
if (datatype.SymbolicId ==
new XmlQualifiedName("Enumeration", Namespaces.OpcUa))
{
return "int";
}
if (datatype.IsOptionSet)
{
return GetDotNetTypeName(
(DataTypeDesign)datatype.BaseTypeNode,
targetNamespace,
namespaces,
nullable, // Should it always be non nullable?
useBuiltInWrapperTypes);
}
return datatype.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
case BasicDataType.UserDefined:
string typeName = datatype.SymbolicName.AsFullyQualifiedTypeSymbol(namespaces);
if (datatype.IsEnumeration)
{
return typeName;
}
// All of these are always set to default type in properties when null
// is passed therefore we want to maintain non nullability no matter
return nullable != NullableAnnotation.Nullable ? typeName : typeName + "?";
}
if (!useBuiltInWrapperTypes)
{
return nullable == NullableAnnotation.NonNullable ? "object" : "object?";
}
return "global::Opc.Ua.Variant";
}
/// <summary>
/// Returns the type name for the data type with the provided value rank.
/// </summary>
public static string GetDotNetTypeName(
this DataTypeDesign dataType,
ValueRank valueRank,
string targetNamespace,
Namespace[] namespaces,
NullableAnnotation nullable = NullableAnnotation.NonNullable,
bool useMatrixTypeInsteadOfVariant = false)
{
string typeName = dataType.GetDotNetTypeName(
targetNamespace,
namespaces,
// Make scalar optional nullable, but array/matrix elements non nullable
// TODO: Revisit this when we get rid of generated collections
valueRank == ValueRank.Scalar ? nullable : NullableAnnotation.NonNullable,
true);
if (typeName != null)
{
switch (valueRank)
{
case ValueRank.Scalar:
return typeName;
case ValueRank.Array:
return CoreUtils.Format("global::Opc.Ua.ArrayOf<{0}>", typeName);
case ValueRank.OneOrMoreDimensions when useMatrixTypeInsteadOfVariant:
return CoreUtils.Format("global::Opc.Ua.MatrixOf<{0}>", typeName);
}
}
return "global::Opc.Ua.Variant";
}
/// <summary>
/// Whether a template parameter is required.
/// </summary>
public static bool IsTemplateParameterRequired(
this DataTypeDesign dataType,
ValueRank valueRank)
{
// DiagnosticInfo is not a valid Variant value (OPC UA Part 6) and
// therefore has no IVariantBuilder<DiagnosticInfo>. A typed
// State<DiagnosticInfo> (scalar or ArrayOf/MatrixOf) cannot be
// emitted, so fall back to the non-generic, Variant-backed state.
// Mirrors the DiagnosticInfo exclusion in SupportsMatrixOf.
if (dataType.BasicDataType == BasicDataType.DiagnosticInfo)
{
return false;
}
if (dataType.BasicDataType
is not BasicDataType.BaseDataType
and not BasicDataType.Number
and not BasicDataType.UInteger
and not BasicDataType.Integer &&
valueRank
is not ValueRank.OneOrMoreDimensions
and not ValueRank.ScalarOrOneDimension
and not ValueRank.ScalarOrArray)
{
return true;
}
if (valueRank == ValueRank.Array)
{
return true;
}
// Matrix-rank with a concrete data type that supports
// `MatrixOf<T>` is fully determinate (the generator produces
// `State<MatrixOf<T>>` rather than `State<T>`/`State<Variant>`),
// so the template parameter is in fact required to specialize
// the inherited State class.
if (valueRank == ValueRank.OneOrMoreDimensions &&
dataType.SupportsMatrixOf() &&
dataType.BasicDataType
is not BasicDataType.BaseDataType
and not BasicDataType.Number
and not BasicDataType.UInteger
and not BasicDataType.Integer)
{
return true;
}
return false;
}
/// <summary>
/// Is overridden instance.
/// </summary>
/// <exception cref="ArgumentNullException"></exception>
public static bool IsOverridden(this InstanceDesign instance)
{
if (instance is null)
{
throw new ArgumentNullException(nameof(instance));
}
return instance.OveriddenNode != null &&
instance.ModellingRule != ModellingRule.None &&
instance.ModellingRule != ModellingRule.ExposesItsArray &&
instance.ModellingRule != ModellingRule.MandatoryPlaceholder &&
instance.ModellingRule != ModellingRule.OptionalPlaceholder;
}
/// <summary>
/// Determines whether a placeholder redeclares one its base type already
/// declares.
/// </summary>
/// <remarks>
/// A placeholder is not overridden in the sense
/// <see cref="IsOverridden"/> means, because it names a slot rather than
/// a child, and merging it with the base declaration would be wrong. It
/// does however emit an Add method named after the slot, so a subtype
/// that redeclares the slot hides its base method and has to say so.
/// This method detects that redeclaration so the generated member can
/// be marked appropriately.
/// </remarks>
public static bool HidesBaseTypePlaceholder(this InstanceDesign instance)
{
if (instance is null)
{
throw new ArgumentNullException(nameof(instance));
}
if (instance.ModellingRule != ModellingRule.MandatoryPlaceholder &&
instance.ModellingRule != ModellingRule.OptionalPlaceholder)
{
return false;
}
// OveriddenNode is not linked for a placeholder, so the base chain
// is walked for a slot of the same name.
if (instance.Parent is not TypeDesign declaringType)
{
return false;
}
for (TypeDesign baseType = declaringType.BaseTypeNode;
baseType != null;
baseType = baseType.BaseTypeNode)
{
InstanceDesign[] children = baseType.Children?.Items;
if (children == null)
{
continue;
}
foreach (InstanceDesign child in children)
{
if (child.SymbolicName == instance.SymbolicName)
{
return true;
}
}
}
return false;
}
/// <summary>
/// Returns the merged instance for an overriden node.
/// </summary>
public static InstanceDesign GetMergedInstance(this InstanceDesign instance)
{
for (NodeDesign parent = instance.Parent; parent != null; parent = parent.Parent)
{
if (parent.Parent == null && parent.Hierarchy != null)
{
// The hierarchy is keyed by the path relative to the root, so
// the root's own symbolic name has to come off the front.
// Strip that exact prefix rather than everything up to the
// first '_', which mangles the path whenever the root type's
// name itself contains an underscore.
string relativePath = instance.SymbolicId.Name;
string rootPrefix = parent.SymbolicId?.Name + "_";
if (rootPrefix.Length > 1 &&
relativePath.StartsWith(rootPrefix, StringComparison.Ordinal))
{
relativePath = relativePath[rootPrefix.Length..];
}
else
{
int index = relativePath.IndexOf('_', StringComparison.Ordinal);
if (index != -1)
{
relativePath = relativePath[(index + 1)..];
}
}
if (parent.Hierarchy.Nodes.TryGetValue(relativePath,
out HierarchyNode hierarchyNode) &&
hierarchyNode.Instance is InstanceDesign instanceDesign)
{
return instanceDesign;
}
break;
}
}
return instance;
}
/// <summary>
/// Returns a name qualified with a namespace prefix.
/// </summary>
/// <exception cref="ArgumentNullException"></exception>
public static string GetPrefixedName(this XmlQualifiedName qname, List<string> namespaceUris)
{
if (qname.IsNull())
{
return string.Empty;
}
if (qname.Namespace == Namespaces.OpcUaBuiltInTypes)
{
return CoreUtils.Format("ua:{0}", qname.Name);
}
if (namespaceUris is null)
{
throw new ArgumentNullException(nameof(namespaceUris));
}
int index = namespaceUris.IndexOf(qname.Namespace);
if (index > 0)
{
return CoreUtils.Format("s{0}:{1}", index - 1, qname.Name);
}
return qname.Name;
}
/// <summary>
/// Returns the data type to use for the value of a variable or the argument of a method.
/// </summary>
public static string GetBinaryDataType(
this DataTypeDesign dataType,
string targetNamespace,
Namespace[] namespaces)
{
switch (dataType.BasicDataType)
{
case BasicDataType.Boolean:
return "opc:Boolean";
case BasicDataType.SByte:
return "opc:SByte";
case BasicDataType.Byte:
return "opc:Byte";
case BasicDataType.Int16:
return "opc:Int16";
case BasicDataType.UInt16:
return "opc:UInt16";
case BasicDataType.Int32:
return "opc:Int32";
case BasicDataType.UInt32:
return "opc:UInt32";
case BasicDataType.Int64:
return "opc:Int64";
case BasicDataType.UInt64:
return "opc:UInt64";
case BasicDataType.Float:
return "opc:Float";
case BasicDataType.Double:
return "opc:Double";
case BasicDataType.String:
return "opc:CharArray";
case BasicDataType.DateTime:
return "opc:DateTime";
case BasicDataType.Guid:
return "opc:Guid";
case BasicDataType.ByteString:
return "opc:ByteString";
case BasicDataType.XmlElement:
return "ua:XmlElement";
case BasicDataType.NodeId:
return "ua:NodeId";
case BasicDataType.ExpandedNodeId:
return "ua:ExpandedNodeId";
case BasicDataType.StatusCode:
return "ua:StatusCode";
case BasicDataType.DiagnosticInfo:
return "ua:DiagnosticInfo";
case BasicDataType.QualifiedName:
return "ua:QualifiedName";
case BasicDataType.LocalizedText:
return "ua:LocalizedText";
case BasicDataType.DataValue:
return "ua:DataValue";
case BasicDataType.Number:
case BasicDataType.Integer:
case BasicDataType.UInteger:
case BasicDataType.BaseDataType:
return "ua:Variant";
default:
if (dataType.SymbolicName ==
new XmlQualifiedName("Structure", Namespaces.OpcUa))
{
return CoreUtils.Format("ua:ExtensionObject");
}
if (dataType.SymbolicName ==
new XmlQualifiedName("Enumeration", Namespaces.OpcUa))
{
if (dataType.IsOptionSet)
{
return GetBinaryDataType(
(DataTypeDesign)dataType.BaseTypeNode,
targetNamespace,
namespaces);
}
return CoreUtils.Format("opc:Int32");
}
string prefix = "tns";
if (dataType.SymbolicName.Namespace != targetNamespace)
{
if (dataType.SymbolicName.Namespace == Namespaces.OpcUa)
{
prefix = "ua";
}
else
{
prefix = GetXmlNamespacePrefix(
namespaces,
dataType.SymbolicName.Namespace);
}
}
return CoreUtils.Format("{0}:{1}", prefix, dataType.SymbolicName.Name);
}
}
/// <summary>
/// Returns the data type to use for the value of a variable or the argument of a method.
/// </summary>
public static string GetXmlDataType(
this DataTypeDesign dataType,
ValueRank valueRank,
string targetNamespace,
Namespace[] namespaces)
{
if (valueRank != ValueRank.Scalar)
{
switch (dataType.BasicDataType)
{
case BasicDataType.Boolean:
return "ua:ListOfBoolean";
case BasicDataType.SByte:
return "ua:ListOfSByte";
case BasicDataType.Byte:
return "ua:ListOfByte";
case BasicDataType.Int16:
return "ua:ListOfInt16";
case BasicDataType.UInt16:
return "ua:ListOfUInt16";
case BasicDataType.Int32:
return "ua:ListOfInt32";
case BasicDataType.UInt32:
return "ua:ListOfUInt32";
case BasicDataType.Int64:
return "ua:ListOfInt64";
case BasicDataType.UInt64:
return "ua:ListOfUInt64";
case BasicDataType.Float:
return "ua:ListOfFloat";
case BasicDataType.Double:
return "ua:ListOfDouble";
case BasicDataType.String:
return "ua:ListOfString";
case BasicDataType.DateTime:
return "ua:ListOfDateTime";
case BasicDataType.Guid:
return "ua:ListOfGuid";
case BasicDataType.ByteString:
return "ua:ListOfByteString";
case BasicDataType.XmlElement:
return "ua:ListOfXmlElement";
case BasicDataType.NodeId:
return "ua:ListOfNodeId";
case BasicDataType.ExpandedNodeId:
return "ua:ListOfExpandedNodeId";
case BasicDataType.StatusCode:
return "ua:ListOfStatusCode";
case BasicDataType.DiagnosticInfo:
return "ua:ListOfDiagnosticInfo";
case BasicDataType.QualifiedName:
return "ua:ListOfQualifiedName";
case BasicDataType.LocalizedText:
return "ua:ListOfLocalizedText";
case BasicDataType.DataValue:
return "ua:ListOfDataValue";
case BasicDataType.Number:
case BasicDataType.Integer:
case BasicDataType.UInteger:
case BasicDataType.BaseDataType:
return "ua:ListOfVariant";
default:
if (dataType.SymbolicName ==
new XmlQualifiedName("Structure", Namespaces.OpcUa))
{
return CoreUtils.Format("ua:ListOfExtensionObject");
}
if (dataType.SymbolicName ==
new XmlQualifiedName("Enumeration", Namespaces.OpcUa))
{
if (dataType.IsOptionSet)
{
return GetXmlDataType(
(DataTypeDesign)dataType.BaseTypeNode,
valueRank,
targetNamespace,
namespaces);
}
return CoreUtils.Format("ua:ListOfInt32");
}
string prefix = "tns";
if (dataType.SymbolicName.Namespace != targetNamespace)
{
if (dataType.SymbolicName.Namespace == Namespaces.OpcUa)
{
if (dataType.SymbolicName.Name == "Enumeration")
{
if (dataType.IsOptionSet)
{
return GetXmlDataType(
(DataTypeDesign)dataType.BaseTypeNode,
valueRank,
targetNamespace,
namespaces);
}
return CoreUtils.Format("ua:ListOfInt32");
}
prefix = "ua";
}
else
{
prefix = GetXmlNamespacePrefix(namespaces, dataType.SymbolicName.Namespace);
}
}
return CoreUtils.Format("{0}:ListOf{1}", prefix, dataType.SymbolicName.Name);
}
}
switch (dataType.BasicDataType)
{
case BasicDataType.Boolean:
return "xs:boolean";
case BasicDataType.SByte:
return "xs:byte";
case BasicDataType.Byte:
return "xs:unsignedByte";
case BasicDataType.Int16:
return "xs:short";
case BasicDataType.UInt16:
return "xs:unsignedShort";
case BasicDataType.Int32:
return "xs:int";
case BasicDataType.UInt32:
return "xs:unsignedInt";
case BasicDataType.Int64:
return "xs:long";
case BasicDataType.UInt64:
return "xs:unsignedLong";
case BasicDataType.Float:
return "xs:float";
case BasicDataType.Double:
return "xs:double";
case BasicDataType.String:
return "xs:string";
case BasicDataType.DateTime:
return "xs:dateTime";
case BasicDataType.Guid:
return "ua:Guid";
case BasicDataType.ByteString:
return "xs:base64Binary";
case BasicDataType.XmlElement:
return "ua:XmlElement";
case BasicDataType.NodeId:
return "ua:NodeId";
case BasicDataType.ExpandedNodeId:
return "ua:ExpandedNodeId";
case BasicDataType.StatusCode:
return "ua:StatusCode";
case BasicDataType.DiagnosticInfo:
return "ua:DiagnosticInfo";
case BasicDataType.QualifiedName:
return "ua:QualifiedName";
case BasicDataType.LocalizedText:
return "ua:LocalizedText";
case BasicDataType.DataValue:
return "ua:DataValue";
case BasicDataType.Number:
case BasicDataType.Integer:
case BasicDataType.UInteger:
case BasicDataType.BaseDataType:
return "ua:Variant";
default:
if (dataType.SymbolicName ==
new XmlQualifiedName("Structure", Namespaces.OpcUa))
{
return CoreUtils.Format("ua:ExtensionObject");
}
if (dataType.SymbolicName ==
new XmlQualifiedName("Enumeration", Namespaces.OpcUa))
{
if (dataType.IsOptionSet)
{
return GetXmlDataType(
(DataTypeDesign)dataType.BaseTypeNode,
valueRank,
targetNamespace,
namespaces);
}
return CoreUtils.Format("xs:int");
}
string prefix = null;
if (dataType.SymbolicName.Namespace != targetNamespace)
{
if (dataType.SymbolicName.Namespace == Namespaces.OpcUa)
{
if (dataType.SymbolicName.Name == "Enumeration")
{
if (dataType.IsOptionSet)
{
return GetXmlDataType(
(DataTypeDesign)dataType.BaseTypeNode,
valueRank,
targetNamespace,
namespaces);
}
return CoreUtils.Format("xs:int");
}
prefix = "ua";
}
else
{
prefix = GetXmlNamespacePrefix(namespaces, dataType.SymbolicName.Namespace);
}
}
return CoreUtils.Format("{0}:{1}", prefix ?? "tns", dataType.SymbolicName.Name);
}
}
/// <summary>
/// Returns a constant for the namespace uri.
/// </summary>
public static Namespace GetNamespace(this Namespace[] namespaces, string namespaceUri)
{
if (namespaces != null)
{
foreach (Namespace ns in namespaces)
{
if (ns.Value == namespaceUri)
{
return ns;
}
}
}
return null;
}
/// <summary>
/// Returns a constant for the namespace uri.
/// </summary>
public static string GetConstantForXmlNamespace(this Namespace[] namespaces, string namespaceUri)
{
Namespace ns = GetNamespace(namespaces, namespaceUri);
if (ns != null)
{
// The "...Xsd" companion constant only exists when the XML
// namespace differs from the namespace URI - ConstantsGenerator
// emits it under exactly that condition. Referencing it whenever
// XmlNamespace is merely non-empty names a constant that was
// never declared (CS0117 in the generated sources).
if (!string.IsNullOrEmpty(ns.XmlNamespace) &&
!string.Equals(ns.XmlNamespace, ns.Value, StringComparison.Ordinal))
{
return CoreUtils.Format("{1}.Namespaces.{0}Xsd", ns.Name, ns.Prefix);
}
return CoreUtils.Format("{1}.Namespaces.{0}", ns.Name, ns.Prefix);
}
return null;
}
/// <summary>
/// Returns the XML prefix for the specified namespace.
/// </summary>
public static string GetXmlNamespacePrefix(this Namespace[] namespaces, string namespaceUri)
{
if (namespaceUri == null)
{
return null;
}
if (namespaces != null)
{
for (int ii = 0; ii < namespaces.Length; ii++)
{
if (namespaces[ii].Value == namespaceUri)
{
if (string.IsNullOrEmpty(namespaces[ii].XmlPrefix))
{
return CoreUtils.Format("s{0}", ii);
}
return namespaces[ii].XmlPrefix;
}
}
}
return null;
}
public static bool IsMethodTypeDesign(this NodeDesign node)
{
if (node?.SymbolicId is null)
{
return false;
}
if (node is not MethodDesign)
{
return false;
}
string symbol = node.SymbolicId.Name;
int index = symbol.IndexOf('_', StringComparison.Ordinal);
if (index > 0)
{
symbol = symbol[..index];
}
return symbol.EndsWith("MethodType", StringComparison.Ordinal);
}
public static bool IsPartOfOpcUaTypesLibrary(this DataTypeDesign dataType)
{
if (dataType != null &&
dataType.SymbolicId.Namespace == Namespaces.OpcUa)
{
switch (dataType.NumericId)
{
case DataTypes.AccessRestrictionType:
case DataTypes.ReferenceDescription:
case DataTypes.AttributeWriteMask:
case DataTypes.Argument:
case DataTypes.IdType:
case DataTypes.RolePermissionType:
case DataTypes.PermissionType:
case DataTypes.ViewDescription:
case DataTypes.BrowseDescription:
case DataTypes.StructureDefinition:
case DataTypes.StructureType:
case DataTypes.StructureField:
case DataTypes.InstanceNode:
case DataTypes.ReferenceTypeNode:
case DataTypes.ReferenceNode:
case DataTypes.DataTypeDefinition:
case DataTypes.EnumDefinition:
case DataTypes.EnumField:
case DataTypes.EnumValueType:
case DataTypes.RelativePath:
case DataTypes.BrowseDirection:
case DataTypes.RelativePathElement:
case DataTypes.NodeClass:
case DataTypes.Node:
case DataTypes.ViewNode:
case DataTypes.ObjectNode:
case DataTypes.MethodNode:
case DataTypes.TypeNode:
case DataTypes.ObjectTypeNode:
case DataTypes.DataTypeNode:
case DataTypes.VariableTypeNode:
case DataTypes.VariableNode:
return true;
}
}
return false;
}
public static Export.ReleaseStatus ToNodeSetReleaseStatus(
this ReleaseStatus releaseStatus)
{
switch (releaseStatus)
{
case ReleaseStatus.Deprecated:
return Export.ReleaseStatus.Deprecated;
case ReleaseStatus.RC:
case ReleaseStatus.Draft:
return Export.ReleaseStatus.Draft;
default:
return Export.ReleaseStatus.Released;
}
}
/// <summary>
/// Maps the access level enumeration onto a code string.
/// </summary>
public static string GetAccessLevelAsCode(this AccessLevel accessLevel)
{
return accessLevel switch
{
AccessLevel.Read => "global::Opc.Ua.AccessLevels.CurrentRead",
AccessLevel.Write => "global::Opc.Ua.AccessLevels.CurrentWrite",
AccessLevel.ReadWrite => "global::Opc.Ua.AccessLevels.CurrentReadOrWrite",
AccessLevel.HistoryRead => "global::Opc.Ua.AccessLevels.HistoryRead",
AccessLevel.HistoryWrite => "global::Opc.Ua.AccessLevels.HistoryWrite",
AccessLevel.HistoryReadWrite => "global::Opc.Ua.AccessLevels.HistoryReadOrWrite",
_ => "global::Opc.Ua.AccessLevels.None"
};
}
/// <summary>
/// Maps the AccessLevel of a variable onto code. NodeSet2-sourced
/// designs carry the verbatim bitmask, which is preferred because
/// the ModelDesign <see cref="AccessLevel"/> enumeration cannot
/// represent combinations such as <c>CurrentRead | HistoryRead</c>.
/// </summary>
public static string GetAccessLevelAsCode(this VariableDesign variable)
{
uint? rawAccessLevel = variable?.RawAccessLevel;
return rawAccessLevel != null
? GetAccessLevelBitsAsCode(rawAccessLevel.Value)
: GetAccessLevelAsCode(variable?.AccessLevel ?? AccessLevel.None);
}
/// <summary>
/// Maps the UserAccessLevel of a variable onto code. NodeSet2-sourced
/// designs carry the verbatim UserAccessLevel bitmask when the
/// attribute is explicitly present and it is preferred; otherwise the
/// UserAccessLevel is derived from the AccessLevel, matching the
/// runtime NodeSet2 importer in <c>UANodeSetHelpers</c>.
/// </summary>
public static string GetUserAccessLevelAsCode(this VariableDesign variable)
{
uint? rawUserAccessLevel = variable?.RawUserAccessLevel;
return rawUserAccessLevel != null
? GetAccessLevelBitsAsCode(rawUserAccessLevel.Value)
: GetAccessLevelAsCode(variable);
}
/// <summary>
/// Maps a verbatim OPC UA AccessLevel bitmask onto code, emitting
/// the named <c>AccessLevels</c> constants for every set bit.
/// </summary>
private static string GetAccessLevelBitsAsCode(uint accessLevel)
{
byte bits = (byte)accessLevel;
if (bits == 0)
{
return "global::Opc.Ua.AccessLevels.None";
}
var names = new List<string>();
AppendBit(names, bits, 0x1, "CurrentRead");
AppendBit(names, bits, 0x2, "CurrentWrite");
AppendBit(names, bits, 0x4, "HistoryRead");
AppendBit(names, bits, 0x8, "HistoryWrite");
AppendBit(names, bits, 0x10, "SemanticChange");
AppendBit(names, bits, 0x20, "StatusWrite");
AppendBit(names, bits, 0x40, "TimestampWrite");
if (names.Count == 0)
{
return CoreUtils.Format("(byte)0x{0:X2}", bits);
}
if (names.Count == 1)
{
return names[0];
}
return CoreUtils.Format("(byte)({0})", string.Join(" | ", names));
}
private static void AppendBit(List<string> names, byte bits, byte mask, string name)
{
if ((bits & mask) != 0)
{
names.Add("global::Opc.Ua.AccessLevels." + name);
}
}
/// <summary>
/// Maps the WriteAccess (WriteMask) of a node onto code. The verbatim
/// OPC UA WriteMask bitmask is emitted so that attributes advertised as
/// writable in the NodeSet (for example DisplayName and Description)
/// stay writable at runtime instead of defaulting to None.
/// </summary>
public static string GetWriteMaskAsCode(this NodeDesign node)
{
uint writeMask = node?.WriteAccess ?? 0;
if (writeMask == 0)
{
return "global::Opc.Ua.AttributeWriteMask.None";
}
return CoreUtils.Format(
"(global::Opc.Ua.AttributeWriteMask){0}",
writeMask);
}
public static string GetLocalizedTextAsCode(this string localizedText)
{
return GetLocalizedTextAsCode(new LocalizedText
{
Value = localizedText
}, noAutoGen: false);
}
public static string GetLocalizedTextAsCode(this LocalizedText localizedText, bool noAutoGen = false)
{
if (localizedText?.Value != null && (!noAutoGen || !localizedText.IsAutogenerated))
{
return CoreUtils.Format(
"new global::Opc.Ua.LocalizedText({0}, string.Empty, {1})",
localizedText.Key.AsStringLiteral(),
localizedText.Value.Trim().AsStringLiteral());
}
return "global::Opc.Ua.LocalizedText.Null";
}
/// <summary>
/// Maps the array dimensions onto code that creates the array.
/// </summary>
public static string GetArrayDimensionsAsCode(this ValueRank valueRank, string arrayDimensions)
{
// Part 3 5.6.2: ArrayDimensions is null unless the ValueRank names an
// array of a specific dimension. ModelDesign's ValueRank is the XSD
// enum (Scalar = 0 ... Any = 5), not the numeric OPC UA rank, so a
// "< 0" test never fired and a scalar declared with
// ArrayDimensions="0" (as NodeSet exporters write it) got them.
if (valueRank is ValueRank.Scalar or
ValueRank.ScalarOrArray or
ValueRank.ScalarOrOneDimension or
ValueRank.Any)
{
return null;
}
if (string.IsNullOrEmpty(arrayDimensions))
{
if (valueRank == ValueRank.Array)
{
return "new uint[] { 0 }";
}
return null;
}
var tokens = arrayDimensions
.Split([','], StringSplitOptions.RemoveEmptyEntries)
.Select(t => t.Trim())
.Where(t => t.Length > 0)
.ToList();
if (tokens.Count == 0)
{
return null;
}
return CoreUtils.Format(
"new uint[] {{ {0} }}",
string.Join(", ", tokens.Select(t =>
{
if (uint.TryParse(t, out uint val))
{
return val.ToString(CultureInfo.InvariantCulture);
}
return "0";
})));
}
public static string GetAccessRestrictionsAsCode(
this AccessRestrictions restrictions,
bool restrictionsSpecified)
{
if (!restrictionsSpecified)
{
return null;
}
return restrictions switch
{
AccessRestrictions.SigningRequired =>
"global::Opc.Ua.AccessRestrictionType.SigningRequired",
AccessRestrictions.EncryptionRequired =>
"global::Opc.Ua.AccessRestrictionType.EncryptionRequired",
AccessRestrictions.SessionRequired =>
"global::Opc.Ua.AccessRestrictionType.SessionRequired",
AccessRestrictions.SessionWithSigningRequired =>
"global::Opc.Ua.AccessRestrictionType.SigningRequired | " +
"global::Opc.Ua.AccessRestrictionType.SessionRequired",
AccessRestrictions.SessionWithEncryptionRequired =>
"global::Opc.Ua.AccessRestrictionType.EncryptionRequired | " +
"global::Opc.Ua.AccessRestrictionType.SessionRequired",
AccessRestrictions.SigningAndApplyToBrowseRequired =>
"global::Opc.Ua.AccessRestrictionType.SigningRequired | " +
"global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse",
AccessRestrictions.EncryptionAndApplyToBrowseRequired =>
"global::Opc.Ua.AccessRestrictionType.EncryptionRequired | " +
"global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse",
AccessRestrictions.SessionAndApplyToBrowseRequired =>
"global::Opc.Ua.AccessRestrictionType.SessionRequired | " +
"global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse",
AccessRestrictions.SessionWithSigningAndApplyToBrowseRequired =>
"global::Opc.Ua.AccessRestrictionType.SigningRequired | " +
"global::Opc.Ua.AccessRestrictionType.SessionRequired | " +
"global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse",
AccessRestrictions.SessionWithEncryptionAndApplyToBrowseRequired =>
"global::Opc.Ua.AccessRestrictionType.EncryptionRequired | " +
"global::Opc.Ua.AccessRestrictionType.SessionRequired | " +
"global::Opc.Ua.AccessRestrictionType.ApplyRestrictionsToBrowse",
_ => null
};
}
public static string GetPermissionTypeAsCode(this Permissions[] permissions)
{
if (permissions == null || permissions.Length == 0)
{
return "global::Opc.Ua.PermissionType.None";
}
var parts = new HashSet<string>();
foreach (Permissions p in permissions)
{
switch (p)
{
case Permissions.All:
AddPermissionStrings(Permissions.WriteAttribute, parts);
AddPermissionStrings(Permissions.WriteRolePermissions, parts);
AddPermissionStrings(Permissions.WriteHistorizing, parts);
AddPermissionStrings(Permissions.Write, parts);
AddPermissionStrings(Permissions.InsertHistory, parts);
AddPermissionStrings(Permissions.ModifyHistory, parts);
AddPermissionStrings(Permissions.DeleteHistory, parts);
AddPermissionStrings(Permissions.Call, parts);
AddPermissionStrings(Permissions.AddReference, parts);
AddPermissionStrings(Permissions.RemoveReference, parts);
AddPermissionStrings(Permissions.DeleteNode, parts);
AddPermissionStrings(Permissions.AddNode, parts);
goto case Permissions.AllRead;
case Permissions.AllRead:
AddPermissionStrings(Permissions.Browse, parts);
AddPermissionStrings(Permissions.Read, parts);
AddPermissionStrings(Permissions.ReadHistory, parts);
AddPermissionStrings(Permissions.ReceiveEvents, parts);
AddPermissionStrings(Permissions.ReadRolePermissions, parts);
break;
default:
AddPermissionStrings(p, parts);
break;
}
}
return parts.Count > 0 ?
string.Join(" | ", parts) :
"global::Opc.Ua.PermissionType.None";
static void AddPermissionStrings(Permissions p, HashSet<string> parts)
{
string part = p switch
{
Permissions.Browse =>
"global::Opc.Ua.PermissionType.Browse",
Permissions.Read =>
"global::Opc.Ua.PermissionType.Read",
Permissions.Write =>
"global::Opc.Ua.PermissionType.Write",
Permissions.Call =>
"global::Opc.Ua.PermissionType.Call",
Permissions.ReadHistory =>
"global::Opc.Ua.PermissionType.ReadHistory",
Permissions.InsertHistory =>
"global::Opc.Ua.PermissionType.InsertHistory",
Permissions.ModifyHistory =>
"global::Opc.Ua.PermissionType.ModifyHistory",
Permissions.DeleteHistory =>
"global::Opc.Ua.PermissionType.DeleteHistory",
Permissions.ReceiveEvents =>
"global::Opc.Ua.PermissionType.ReceiveEvents",
Permissions.AddNode =>
"global::Opc.Ua.PermissionType.AddNode",
Permissions.DeleteNode =>
"global::Opc.Ua.PermissionType.DeleteNode",
Permissions.AddReference =>
"global::Opc.Ua.PermissionType.AddReference",
Permissions.RemoveReference =>
"global::Opc.Ua.PermissionType.RemoveReference",
Permissions.ReadRolePermissions =>
"global::Opc.Ua.PermissionType.ReadRolePermissions",
Permissions.WriteRolePermissions =>
"global::Opc.Ua.PermissionType.WriteRolePermissions",
Permissions.WriteAttribute =>
"global::Opc.Ua.PermissionType.WriteAttribute",
Permissions.WriteHistorizing =>
"global::Opc.Ua.PermissionType.WriteHistorizing",
_ => null
};
if (part != null)
{
parts.Add(part);
}
}
}
/// <summary>
/// Returns the identifier assigned to the node as one of the four
/// identifier types defined by OPC 10000-3 5.2.2, or <c>null</c> when
/// the node does not carry an explicit identifier.
/// </summary>
public static object GetIdentifier(this NodeDesign node)
{
if (node == null)
{
return null;
}
if (node.NumericIdSpecified)
{
return node.NumericId;
}
if (node.StringId != null)
{
return node.StringId;
}
if (node.GuidIdSpecified)
{
return node.GuidId;
}
if (node.OpaqueId != null)
{
return ByteString.From(node.OpaqueId);
}
return null;
}
/// <summary>
/// Assigns the identifier of the node, clearing the identifiers of all
/// other identifier types. Passing <c>null</c> clears the identifier.
/// </summary>
public static void SetIdentifier(this NodeDesign node, object identifier)
{
if (node == null)
{
return;
}
node.NumericId = 0;
node.NumericIdSpecified = false;
node.StringId = null;
node.GuidId = Guid.Empty;
node.GuidIdSpecified = false;
node.OpaqueId = null;
switch (identifier)
{
case uint numericId:
node.NumericId = numericId;
node.NumericIdSpecified = true;
break;
case string stringId:
node.StringId = stringId;
break;
case Guid guidId:
node.GuidId = guidId;
node.GuidIdSpecified = true;
break;
case ByteString opaqueId:
node.OpaqueId = opaqueId.ToArray();
break;
}
}
/// <summary>
/// Returns true when the node carries an explicit identifier of any of
/// the four identifier types defined by OPC 10000-3 5.2.2.
/// </summary>
public static bool HasIdentifier(this NodeDesign node)
{
return node.GetIdentifier() != null;
}
/// <summary>
/// Returns true when the node carries a Guid or Opaque identifier.
/// These cannot be emitted as a C# <c>const</c> and are not part of the
/// generated identifier reflection tables.
/// </summary>
public static bool HasNonConstantIdentifier(this NodeDesign node)
{
return node != null &&
!node.NumericIdSpecified &&
string.IsNullOrEmpty(node.StringId) &&
(node.GuidIdSpecified || node.OpaqueId != null);
}
/// <summary>
/// Returns the C# literal for an identifier together with the C# type
/// the literal has.
/// </summary>
public static string GetIdentifierAsCode(object identifier, out string type)
{
switch (identifier)
{
case uint numericId:
type = "uint";
return CoreUtils.Format("{0}u", numericId);
case string stringId:
type = "string";
return stringId.AsStringLiteral();
case Guid guidId:
type = "global::System.Guid";
return CoreUtils.Format(
"new global::System.Guid({0})",
guidId.ToString("D", CultureInfo.InvariantCulture).AsStringLiteral());
case ByteString opaqueId:
type = "global::Opc.Ua.ByteString";
return CoreUtils.Format(
"global::Opc.Ua.ByteString.FromBase64({0})",
opaqueId.ToBase64().AsStringLiteral());
default:
type = null;
return null;
}
}
public static string GetNodeIdAsCode(
this NodeDesign node,
Namespace[] namespaces,
string namespaceTableVariable)
{
string identifier;
if (node == null)
{
return "global::Opc.Ua.NodeId.Null";
}
if (node.NumericIdSpecified)
{
identifier = CoreUtils.Format("{0}u", node.NumericId);
}
else if (!string.IsNullOrEmpty(node.StringId))
{
identifier = node.StringId.AsStringLiteral();
}
else if (node.HasNonConstantIdentifier())
{
identifier = GetIdentifierAsCode(node.GetIdentifier(), out _);
}
else
{
uint? numericId = FindNumericIdentifier(node);
if (numericId.HasValue)
{
identifier = CoreUtils.Format("{0}u", numericId.Value);
}
else
{
if (string.IsNullOrEmpty(node.SymbolicId.Name))
{
return "global::Opc.Ua.NodeId.Null";
}
identifier = node.SymbolicId.Name.AsStringLiteral();
}
}
return CoreUtils.Format(
"global::Opc.Ua.NodeId.Create({0}, {1}, {2})",
identifier,
namespaces.GetConstantSymbolForNamespace(
node.SymbolicId.Namespace),
namespaceTableVariable);
}
public static string GetExpandedNodeIdAsCode(
this NodeDesign node,
Namespace[] namespaces)
{
string identifier;
if (node == null)
{
return "global::Opc.Ua.ExpandedNodeId.Null";
}
if (node.NumericIdSpecified)
{
identifier = CoreUtils.Format("{0}u", node.NumericId);
}
else if (!string.IsNullOrEmpty(node.StringId))
{
identifier = node.StringId.AsStringLiteral();
}
else if (node.HasNonConstantIdentifier())
{
identifier = GetIdentifierAsCode(node.GetIdentifier(), out _);
}
else
{
uint? numericId = FindNumericIdentifier(node);
if (numericId.HasValue)
{
identifier = CoreUtils.Format("{0}u", numericId.Value);
}
else
{
if (string.IsNullOrEmpty(node.SymbolicId.Name))
{
return "global::Opc.Ua.ExpandedNodeId.Null";
}
identifier = node.SymbolicId.Name.AsStringLiteral();
}
}
return CoreUtils.Format(
"new global::Opc.Ua.ExpandedNodeId({0}, {1})",
identifier,
namespaces.GetConstantSymbolForNamespace(
node.SymbolicId.Namespace));
}
public static uint? FindNumericIdentifier(this NodeDesign node)
{
uint? numericId = node.NumericIdSpecified ? node.NumericId : null;
for (NodeDesign parent = node.Parent;
!numericId.HasValue && parent != null;
parent = parent.Parent)
{
if (parent.Hierarchy == null)
{
continue;
}
string browsePath = node.SymbolicId.Name;
string parentPath = parent.SymbolicId.Name;
if (browsePath.StartsWith(parentPath, StringComparison.InvariantCulture) &&
browsePath[parentPath.Length] == NodeDesign.PathChar)
{
browsePath = browsePath[(parentPath.Length + 1)..];
}
if (parent.Hierarchy.Nodes.TryGetValue(browsePath, out HierarchyNode instance) &&
instance.Instance.NumericId != 0)
{
numericId = instance.Instance.NumericId;
}
}
return numericId;
}
public static string GetNodeIdConstant(
this IModelDesign design,
XmlQualifiedName symbolidId,
string type,
string namespaceTableVariable)
{
if (symbolidId == null)
{
return "global::Opc.Ua.NodeId.Null";
}
NodeDesign node = design.FindNode(
symbolidId,
symbolidId.Name,
type);
if (node == null)
{
return "global::Opc.Ua.NodeId.Null";
}
return node.GetNodeIdAsCode(design.Namespaces, namespaceTableVariable);
}
public static void SetDefaultValue(
this VariableTypeDesign node,
object value,
IServiceMessageContext context)
{
if (value != null)
{
node.DefaultValue = AsXmlElement(value, context);
node.DecodedValue = value;
node.DecodedValueNamespaceUris = null;
}
else
{
node.DefaultValue = null;
node.DecodedValue = null;
node.DecodedValueNamespaceUris = null;
}
}
public static void SetDefaultValue(
this VariableDesign node,
object value,
IServiceMessageContext context)
{
if (value != null)
{
node.DefaultValue = AsXmlElement(value, context);
node.DecodedValue = value;
node.DecodedValueNamespaceUris = null;
}
else
{
node.DefaultValue = null;
node.DecodedValue = null;
node.DecodedValueNamespaceUris = null;
}
}
private static System.Xml.XmlElement AsXmlElement(
object value,
IServiceMessageContext context)
{
if (VariantHelper.TryCastFromWithReflectionFallback(value, out Variant variant) &&
!variant.IsNull)
{
using var encoder = new XmlEncoder(context);
encoder.WriteVariantValue(null, variant);
var xmlDoc = new XmlDocument();
xmlDoc.LoadInnerXml(encoder.CloseAndReturnText());
return xmlDoc.DocumentElement;
}
return null;
}
private static bool IsBasicDataType(
this DataTypeDesign dataType,
out BasicDataType basicDataType)
{
if (dataType.SymbolicName.Namespace == "http://opcfoundation.org/UA/")
{
#if NET8_0_OR_GREATER
foreach (string name in Enum.GetNames<BasicDataType>())
#else
foreach (string name in Enum.GetNames(typeof(BasicDataType)))
#endif
{
if (name == dataType.SymbolicName.Name)
{
#if NET8_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
basicDataType = Enum.Parse<BasicDataType>(
dataType.SymbolicName.Name);
#else
basicDataType = (BasicDataType)Enum.Parse(
typeof(BasicDataType),
dataType.SymbolicName.Name);
#endif
return true;
}
}
}
basicDataType = default;
return false;
}
}
}