Add pinned UAFX Part 81 generated model contracts

This commit is contained in:
Marc
2026-10-24 17:14:25 +02:00
parent 9ce8192ebe
commit f9c2ddf142
15 changed files with 19557 additions and 2 deletions
+1
View File
@@ -48,6 +48,7 @@ OPCFoundation.NetStandard.Opc.Ua.ECM
OPCFoundation.NetStandard.Opc.Ua.Extrusion
OPCFoundation.NetStandard.Opc.Ua.Extrusion.Client
OPCFoundation.NetStandard.Opc.Ua.Extrusion.Server
OPCFoundation.NetStandard.Opc.Ua.Fx
OPCFoundation.NetStandard.Opc.Ua.Gds
OPCFoundation.NetStandard.Opc.Ua.Gds.Client
OPCFoundation.NetStandard.Opc.Ua.Gds.Server
+6
View File
@@ -36,6 +36,12 @@ src/Opc.Ua.AMB/Model/Opc.Ua.AMB.NodeSet2.xml text eol=lf
src/Opc.Ua.AutoID/Model/Opc.Ua.AutoID.NodeSet2.xml text eol=lf whitespace=-blank-at-eol
src/Opc.Ua.AutoID/Model/Opc.Ua.AutoID.NodeIds.csv text eol=lf
# Preserve the three release-pinned UAFX NodeSets and their upstream Git blob identities.
src/Opc.Ua.Fx/Model/*.nodeset2.xml -text
# The Data NodeSet contains trailing whitespace in a published Description.
# TODO: remove this exception when a newer pinned upstream input removes it.
src/Opc.Ua.Fx/Model/opc.ua.fx.data.nodeset2.xml whitespace=-blank-at-eol
# Denote all files that should not have line endings normalized, should not be
# merged, and should not show in a textual diff.
*.docm binary
+2
View File
@@ -221,6 +221,7 @@
<Project Path="src/Opc.Ua.Extrusion.Client/Opc.Ua.Extrusion.Client.csproj" />
<Project Path="src/Opc.Ua.Extrusion.Server/Opc.Ua.Extrusion.Server.csproj" />
<Project Path="src/Opc.Ua.Extrusion/Opc.Ua.Extrusion.csproj" />
<Project Path="src/Opc.Ua.Fx/Opc.Ua.Fx.csproj" />
<Project Path="src/Opc.Ua.Gds.Client/Opc.Ua.Gds.Client.csproj" />
<Project Path="src/Opc.Ua.Gds.Server/Opc.Ua.Gds.Server.csproj" />
<Project Path="src/Opc.Ua.ISA95.Client/Opc.Ua.ISA95.Client.csproj" />
@@ -417,6 +418,7 @@
<Project Path="tests/Opc.Ua.Subscriptions.Tests/Opc.Ua.Subscriptions.Tests.csproj" />
<Project Path="tests/Opc.Ua.Subscriptions.Classic.Tests/Opc.Ua.Subscriptions.Classic.Tests.csproj" />
<Project Path="tests/Opc.Ua.Subscriptions.Durable.Tests/Opc.Ua.Subscriptions.Durable.Tests.csproj" />
<Project Path="tests/Opc.Ua.Fx.Tests/Opc.Ua.Fx.Tests.csproj" />
<Project Path="tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj" />
<Project Path="tests/Opc.Ua.ISA95.Tests/Opc.Ua.ISA95.Tests.csproj" />
<Project Path="tests/Opc.Ua.Lens.Workflow.Tests/Opc.Ua.Lens.Workflow.Tests.csproj" />
+4 -1
View File
@@ -441,6 +441,8 @@ substituting an encoding or ignoring incompatible settings. This is only the
transport/config bridge: Part 81 ConnectionManager procedures,
endpoint ownership, role claims, authorization, status, persistence and
recovery remain unimplemented, so this API does not establish UAFX conformance.
The [UAFX model package](UAFX.md) supplies the standard generated Part 81
contracts independently of this transport bridge.
The PubSub adapter rejects `AllowQueueExpiry=true`. Dropping a key frame or
metadata can invalidate later messages, so enabling expiry requires a
@@ -701,7 +703,8 @@ or empty test run is not native acceptance.
TCP evidence does not establish the complete transport assertion matrix.
- **UAFX integration is partial.** The standard `pubsub-uasc` profile, strict
local-policy validation and an opt-in shared-UASC PubSub transport bridge are
implemented. Part 81 ConnectionManager/AutomationComponent configuration,
implemented. The [UAFX model package](UAFX.md) supplies generated Part 81
contracts. ConnectionManager/AutomationComponent configuration,
endpoint ownership, authorization, status, persistence and recovery are not.
This does not establish the Data Channel UAFX Binding Facet or a UAFX role
claim. The binding selects ordered, confidential PubSub over UASC; it does
+4 -1
View File
@@ -128,7 +128,7 @@ started exercise and the other two paths are optional here.
encodings, and security.
4. [Transport selection](PubSub.md#transports) — UDP, Ethernet, MQTT, and Kafka.
5. [Brokerless PubSub over a SecureChannel](../samples/PubSub/ConsoleSecureChannelPubSub/README.md)
(experimental) - run complete UADP or JSON NetworkMessages over a dedicated
(experimental) - run complete UADP NetworkMessages over a dedicated
flow, or share the connection with ordinary Read/Publish traffic.
## 3. Add data types and application services
@@ -192,6 +192,9 @@ models that each model depends on.
Integration first.
- [ISA-95](ISA95.md) (OPC 10030 and OPC 10031-4) — the common
model and job control.
- [UAFX information models](UAFX.md) (OPC 10000-81) - generated Data,
AutomationComponent and ConnectionManager contracts, without a lifecycle
implementation or conformance claim.
- [Positioning](Positioning.md) (OPC 10000-210 and OPC 10000-211; preview
package) — relative frames and geographic positions.
- [Machinery](Machinery.md) (OPC 40001-1, -2, -3, -4 and -101) —
+67
View File
@@ -0,0 +1,67 @@
# UAFX information models
`OPCFoundation.NetStandard.Opc.Ua.Fx` supplies generated OPC 10000-81 model
contracts for the FX Data, AutomationComponent and ConnectionManager
namespaces. It uses the existing Device Integration model and the stack's
source generator; it adds no native runtime or third-party package.
## Register the models
Register dependencies before their consumers. The same registration order
applies to encodeable factories, node-state factories and predefined nodes.
The namespace table may already contain application namespaces.
```csharp
using Opc.Ua;
using Opc.Ua.Di;
using Opc.Ua.Fx.Data;
using Opc.Ua.Fx.Ac;
using Opc.Ua.Fx.Cm;
messageContext.Factory.Builder
.AddOpcUaDi()
.AddOpcUaFxData()
.AddOpcUaFxAc()
.AddOpcUaFxCm()
.Commit();
nodes.AddOpcUaDi(systemContext)
.AddOpcUaFxData(systemContext)
.AddOpcUaFxAc(systemContext)
.AddOpcUaFxCm(systemContext);
```
The generated types include asynchronous `EstablishConnections` and
`CloseConnections` Method handlers and client proxies, endpoint configuration
and result structures, and PubSub communication-identifier reservation
structures. `PubSubReserveCommunicationIds2DataType` and
`PubSubReserveCommunicationIdsResult2DataType` preserve the standard
`RequestTransportSpecificInfo` and `TransportSpecificInfo` fields.
## Source provenance
The three unmodified UAFX 1.00.04 NodeSets in `src/Opc.Ua.Fx/Model` come from
[UA-Nodeset at dd178cf655ed3d74f1d085ef04e0e226a1d3ed9f](https://github.com/OPCFoundation/UA-Nodeset/tree/dd178cf655ed3d74f1d085ef04e0e226a1d3ed9f/UAFX).
Their Git blob identifiers are:
| Input | Git blob |
| --- | --- |
| `opc.ua.fx.data.nodeset2.xml` | `12212843b7499c48059ca081963d349cd06d19a4` |
| `opc.ua.fx.ac.nodeset2.xml` | `224276cd06fb200f7421d5370698fd187b199893` |
| `opc.ua.fx.cm.nodeset2.xml` | `674ed1aa7ec9baad7119eba35ed1b87c10d2f0a8` |
Generated C# is produced during compilation and is not checked in.
## Implementation boundary
These are model contracts, not a ConnectionManager or AutomationComponent
lifecycle implementation. Applications must implement authorization,
verification, reservation, endpoint association, enablement, status,
persistence and cleanup according to Part 81 and their selected Part 84
profiles. Loading a model or completing a Method call is not evidence of
valid PubSub DataSet reception or UAFX conformance.
The experimental [Data Channel binding](DataChannels.md) provides the
`ConfigureUafx` transport-configuration bridge. It does not install Part 81
configuration or grant a ConnectionManager authority over authenticated data
peers.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
# OPC UA FX information models
`OPCFoundation.NetStandard.Opc.Ua.Fx` provides source-generated Part 81 Data,
AutomationComponent and ConnectionManager types, identifiers, node states and
asynchronous client proxies. It reuses the stack's Device Integration model.
The published UAFX 1.00.04 NodeSets are pinned to
`OPCFoundation/UA-Nodeset@dd178cf655ed3d74f1d085ef04e0e226a1d3ed9f`.
The XML files are unmodified upstream inputs; generated C# is not checked in.
This package supplies model contracts, not a ConnectionManager implementation
or a UAFX conformance claim. Application authorization, commissioning,
identifier reservation, endpoint lifecycle and network-service support require
an implementation of the corresponding Part 81 and selected Part 84 behavior.
See [UAFX information models](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/UAFX.md)
for registration examples and the pinned input blob identifiers.
+46
View File
@@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>$(AssemblyPrefix).Fx</AssemblyName>
<TargetFrameworks>$(LibTargetFrameworks)</TargetFrameworks>
<PackageId>$(PackagePrefix).Opc.Ua.Fx</PackageId>
<RootNamespace>Opc.Ua.Fx</RootNamespace>
<Description>OPC UA FX Part 81 Data, AutomationComponent and ConnectionManager information models.</Description>
<IsPackable>true</IsPackable>
<PackageReadmeFile>NugetREADME.md</PackageReadmeFile>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IsAotCompatible
Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0'))">true</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<PackageId>$(PackageId).Debug</PackageId>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Opc.Ua.Di\Opc.Ua.Di.csproj" />
<ProjectReference Include="..\..\tools\Opc.Ua.SourceGeneration\Opc.Ua.SourceGeneration.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<Import Project="..\..\tools\Opc.Ua.SourceGeneration\OPCFoundation.Opc.Ua.SourceGeneration.props" />
<PropertyGroup>
<ModelSourceGeneratorVersion>v105</ModelSourceGeneratorVersion>
<ModelSourceGeneratorOmitFluentApi>true</ModelSourceGeneratorOmitFluentApi>
<ModelSourceGeneratorUseAllowSubtypes>true</ModelSourceGeneratorUseAllowSubtypes>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="Model\opc.ua.fx.data.nodeset2.xml">
<ModelSourceGeneratorModelUri>http://opcfoundation.org/UA/FX/Data/</ModelSourceGeneratorModelUri>
<ModelSourceGeneratorName>FxData</ModelSourceGeneratorName>
<ModelSourceGeneratorPrefix>Opc.Ua.Fx.Data</ModelSourceGeneratorPrefix>
</AdditionalFiles>
<AdditionalFiles Include="Model\opc.ua.fx.ac.nodeset2.xml">
<ModelSourceGeneratorModelUri>http://opcfoundation.org/UA/FX/AC/</ModelSourceGeneratorModelUri>
<ModelSourceGeneratorName>FxAc</ModelSourceGeneratorName>
<ModelSourceGeneratorPrefix>Opc.Ua.Fx.Ac</ModelSourceGeneratorPrefix>
</AdditionalFiles>
<AdditionalFiles Include="Model\opc.ua.fx.cm.nodeset2.xml">
<ModelSourceGeneratorModelUri>http://opcfoundation.org/UA/FX/CM/</ModelSourceGeneratorModelUri>
<ModelSourceGeneratorName>FxCm</ModelSourceGeneratorName>
<ModelSourceGeneratorPrefix>Opc.Ua.Fx.Cm</ModelSourceGeneratorPrefix>
</AdditionalFiles>
<None Include="NugetREADME.md" Pack="true" PackagePath="\" />
</ItemGroup>
</Project>
+97
View File
@@ -0,0 +1,97 @@
/* ========================================================================
* Copyright (c) 2005-2026 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 Microsoft.Extensions.DependencyInjection;
using Opc.Ua.Di;
using Opc.Ua.Fx.Ac;
using Opc.Ua.Fx.Cm;
using Opc.Ua.Fx.Data;
namespace Opc.Ua.Aot.Tests
{
/// <summary>
/// Exercises generated UAFX model activation and binary decoding after native trimming.
/// </summary>
public sealed class FxAotTests
{
/// <summary>
/// Published models and derived reservation results require no reflection-based activation.
/// </summary>
[Test]
public async Task GeneratedFxModelsAndReservationResultsAreAotSafeAsync()
{
var services = new ServiceCollection();
services.AddOpcUa();
using ServiceProvider provider = services.BuildServiceProvider();
ITelemetryContext telemetry = provider.GetRequiredService<ITelemetryContext>();
ServiceMessageContext message = ServiceMessageContext.Create(telemetry);
message.NamespaceUris.GetIndexOrAppend("urn:uafx-aot:application");
message.NamespaceUris.GetIndexOrAppend(Fx.Cm.Namespaces.FxCm);
message.NamespaceUris.GetIndexOrAppend(Fx.Ac.Namespaces.FxAc);
message.NamespaceUris.GetIndexOrAppend(Fx.Data.Namespaces.FxData);
message.NamespaceUris.GetIndexOrAppend(Di.Namespaces.OpcUaDi);
message.Factory.Builder.AddOpcUaDi().AddOpcUaFxData().AddOpcUaFxAc().AddOpcUaFxCm().Commit();
var context = new SystemContext(telemetry)
{
NamespaceUris = message.NamespaceUris,
EncodeableFactory = message.Factory
};
var nodes = new NodeStateCollection();
nodes.AddOpcUaDi(context).AddOpcUaFxData(context).AddOpcUaFxAc(context).AddOpcUaFxCm(context);
await Assert.That(nodes.Any(node => node.NodeId ==
NodeId.Create(2, Fx.Ac.Namespaces.FxAc, context.NamespaceUris))).IsTrue();
await Assert.That(nodes.Any(node => node.NodeId ==
NodeId.Create(1002, Fx.Cm.Namespaces.FxCm, context.NamespaceUris))).IsTrue();
var result = new PubSubReserveCommunicationIdsResult2DataType
{
DefaultPublisherId = new Variant((uint)23),
WriterGroupIds = [4],
DataSetWriterIds = [5],
TransportSpecificInfo = new Variant("opc.tcp://localhost:4840/uafx")
};
using var stream = new MemoryStream();
using (var encoder = new BinaryEncoder(stream, message, true))
{
encoder.WriteExtensionObject("Result", new ExtensionObject(result));
}
stream.Position = 0;
using var decoder = new BinaryDecoder(stream, message, true);
ExtensionObject decodedObject = decoder.ReadExtensionObject("Result");
bool decoded = decodedObject.TryGetValue(out PubSubReserveCommunicationIdsResult2DataType? value);
await Assert.That(decoded).IsTrue();
await Assert.That(value!.DefaultPublisherId).IsEqualTo(new Variant((uint)23));
await Assert.That(value.WriterGroupIds[0]).IsEqualTo((ushort)4);
await Assert.That(value.DataSetWriterIds[0]).IsEqualTo((ushort)5);
await Assert.That(value.TransportSpecificInfo)
.IsEqualTo(new Variant("opc.tcp://localhost:4840/uafx"));
await Assert.That(stream.Position).IsEqualTo(stream.Length);
}
}
}
@@ -53,6 +53,7 @@
<ProjectReference Include="..\..\src\Opc.Ua.AI.Client\Opc.Ua.AI.Client.csproj" />
<ProjectReference Include="..\..\src\Opc.Ua.Core\Opc.Ua.Core.csproj" />
<ProjectReference Include="..\..\src\Opc.Ua.Core.Schema\Opc.Ua.Core.Schema.csproj" />
<ProjectReference Include="..\..\src\Opc.Ua.Fx\Opc.Ua.Fx.csproj" />
<ProjectReference Include="..\..\src\Opc.Ua.Bindings.Https\Opc.Ua.Bindings.Https.csproj" />
<ProjectReference Include="..\..\src\Opc.Ua.Client\Opc.Ua.Client.csproj" />
<ProjectReference Include="..\..\src\Opc.Ua.Client.ComplexTypes\Opc.Ua.Client.ComplexTypes.csproj" />
+278
View File
@@ -0,0 +1,278 @@
/* ========================================================================
* Copyright (c) 2005-2026 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.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using NUnit.Framework;
using Opc.Ua.Di;
using Opc.Ua.Fx.Ac;
using Opc.Ua.Fx.Cm;
using Opc.Ua.Fx.Data;
using Opc.Ua.Tests;
namespace Opc.Ua.Fx.Tests
{
/// <summary>
/// Exercises the generated Part 81 model contracts, independently of a connection lifecycle implementation.
/// </summary>
[TestFixture]
[Category("UAFX")]
public sealed class FxModelTests
{
/// <summary>
/// Model inputs remain byte-identical to the pinned published files on every supported host.
/// </summary>
[TestCase("data", "80FFA5EAE396FB2ED6EA7FC8647013DC6BDFBD939B12249A3B711542E4881339")]
[TestCase("ac", "35586E374EEA321291AEE9E46731D3CA547CC5A73626F99260D20254DA60B941")]
[TestCase("cm", "6ABFB436BFC07051D7C972DC36A4D8744A760CD1B94A72D37D621A4C272F7929")]
public void ModelInputsRemainByteIdentical(string model, string expectedSha256)
{
string path = Path.Combine(
TestContext.CurrentContext.TestDirectory, "Model", $"opc.ua.fx.{model}.nodeset2.xml");
using var stream = File.OpenRead(path);
#if NET8_0_OR_GREATER
string actual = CoreUtils.ToHexString(SHA256.HashData(stream));
#else
using var sha256 = SHA256.Create();
string actual = CoreUtils.ToHexString(sha256.ComputeHash(stream));
#endif
Assert.That(actual, Is.EqualTo(expectedSha256));
}
/// <summary>
/// Published declarations retain their NodeIds even when the caller changes namespace indexes.
/// </summary>
[TestCase(false)]
[TestCase(true)]
public void PublishedModelsMaterializeWithRemappedNamespaces(bool reverseNamespaces)
{
ServiceMessageContext message = CreateContext(reverseNamespaces);
var context = new SystemContext(NUnitTelemetryContext.Create())
{
NamespaceUris = message.NamespaceUris,
EncodeableFactory = message.Factory
};
var nodes = new NodeStateCollection();
nodes.AddOpcUaDi(context).AddOpcUaFxData(context).AddOpcUaFxAc(context).AddOpcUaFxCm(context);
Assert.Multiple(() =>
{
Assert.That(nodes.Any(node => node.NodeId ==
NodeId.Create(2, Ac.Namespaces.FxAc, context.NamespaceUris)), Is.True,
"AutomationComponentType");
Assert.That(nodes.Any(node => node.NodeId ==
NodeId.Create(1002, Ac.Namespaces.FxAc, context.NamespaceUris)), Is.True,
"ConnectionEndpointType");
Assert.That(nodes.Any(node => node.NodeId ==
NodeId.Create(1002, Cm.Namespaces.FxCm, context.NamespaceUris)), Is.True,
"ConnectionManagerType");
Assert.That(nodes.Any(node => node.NodeId ==
NodeId.Create(3005, Data.Namespaces.FxData, context.NamespaceUris)), Is.True,
"PubSubReserveCommunicationIds2DataType");
Assert.That(nodes.Any(node => node.NodeId ==
NodeId.Create(3013, Data.Namespaces.FxData, context.NamespaceUris)), Is.True,
"PubSubReserveCommunicationIdsResult2DataType");
});
}
/// <summary>
/// Standard lifecycle Methods expose their published arguments rather than Data Channel-specific schemas.
/// </summary>
[Test]
public void LifecycleMethodsRetainPublishedArgumentContracts()
{
ServiceMessageContext message = CreateContext(true);
var context = new SystemContext(NUnitTelemetryContext.Create())
{
NamespaceUris = message.NamespaceUris,
EncodeableFactory = message.Factory
};
var nodes = new NodeStateCollection();
nodes.AddOpcUaFxAc(context);
NodeState automationComponent = nodes.Single(node => node.NodeId ==
NodeId.Create(2, Ac.Namespaces.FxAc, context.NamespaceUris));
var children = new List<BaseInstanceState>();
automationComponent.GetChildren(context, children);
var establish = (EstablishConnectionsMethodState)children.Single(node => node.NodeId ==
NodeId.Create(292, Ac.Namespaces.FxAc, context.NamespaceUris));
var close = (CloseConnectionsMethodState)children.Single(node => node.NodeId ==
NodeId.Create(293, Ac.Namespaces.FxAc, context.NamespaceUris));
Argument[] inputs = establish.InputArguments!.Value.ToArray()!;
Argument[] outputs = establish.OutputArguments!.Value.ToArray()!;
string[] inputNames =
[
"CommandMask", "AssetVerifications", "ConnectionEndpointConfigurations",
"ReserveCommunicationIds", "CommunicationConfigurations"
];
string[] outputNames =
[
"AssetVerificationResults", "ConnectionEndpointConfigurationResults",
"ReserveCommunicationIdsResults", "CommunicationConfigurationResults"
];
string[] closeInputNames = ["ConnectionEndpoints", "Remove"];
Assert.Multiple(() =>
{
Assert.That(inputs.Select(argument => argument.Name), Is.EqualTo(inputNames));
Assert.That(inputs.Select(argument => argument.ValueRank), Is.EqualTo(new[] { -1, 1, 1, 1, 1 }));
Assert.That(inputs.Select(argument => argument.DataType), Is.EqualTo(
new uint[] { 1024, 1048, 1044, 3017, 1046 }
.Select(id => NodeId.Create(id, Data.Namespaces.FxData, context.NamespaceUris))));
Assert.That(outputs.Select(argument => argument.Name), Is.EqualTo(outputNames));
Assert.That(outputs.Select(argument => argument.ValueRank), Is.All.EqualTo(ValueRanks.OneDimension));
Assert.That(outputs.Select(argument => argument.DataType), Is.EqualTo(
new uint[] { 1038, 3008, 3019, 1033 }
.Select(id => NodeId.Create(id, Data.Namespaces.FxData, context.NamespaceUris))));
Assert.That(close.InputArguments!.Value.ToArray()!.Select(argument => argument.Name),
Is.EqualTo(closeInputNames));
Assert.That(close.InputArguments.Value.ToArray()!.Select(argument => argument.DataType),
Is.EqualTo(new[] { DataTypeIds.NodeId, DataTypeIds.Boolean }));
Assert.That(close.InputArguments.Value.ToArray()!.Select(argument => argument.ValueRank),
Is.EqualTo(new[] { ValueRanks.OneDimension, ValueRanks.Scalar }));
Assert.That(close.OutputArguments!.Value.Count, Is.EqualTo(1));
Assert.That(close.OutputArguments!.Value[0].Name, Is.EqualTo("Results"));
Assert.That(close.OutputArguments.Value[0].DataType, Is.EqualTo(DataTypeIds.StatusCode));
});
}
/// <summary>
/// Reservation contracts preserve standard identifier counts and transport-specific request and result fields.
/// </summary>
[TestCase(false)]
[TestCase(true)]
public void ReservationContractsRoundTripWithoutReplacingPubSubIdentifiers(bool reverseNamespaces)
{
ServiceMessageContext context = CreateContext(reverseNamespaces);
var request = new PubSubReserveCommunicationIds2DataType
{
TransportProfileUri = "http://opcfoundation.org/UA-Profile/Transport/pubsub-uasc",
NumReqWriterGroupIds = 2,
NumReqDataSetWriterIds = 3,
RequestTransportSpecificInfo = true
};
var result = new PubSubReserveCommunicationIdsResult2DataType
{
DefaultPublisherId = new Variant((uint)17),
WriterGroupIds = [4, 5],
DataSetWriterIds = [10, 11, 12],
TransportSpecificInfo = new Variant("opc.tcp://localhost:4840/uafx")
};
using var stream = new MemoryStream();
using (var encoder = new BinaryEncoder(stream, context, true))
{
encoder.WriteExtensionObject("Request", new ExtensionObject(request));
encoder.WriteExtensionObject("Result", new ExtensionObject(result));
}
stream.Position = 0;
using var decoder = new BinaryDecoder(stream, context, true);
ExtensionObject requestObject = decoder.ReadExtensionObject("Request");
ExtensionObject resultObject = decoder.ReadExtensionObject("Result");
Assert.That(requestObject.TryGetValue(out PubSubReserveCommunicationIds2DataType? decodedRequest),
Is.True);
Assert.That(resultObject.TryGetValue(out PubSubReserveCommunicationIdsResult2DataType? decodedResult),
Is.True);
Assert.Multiple(() =>
{
Assert.That(decodedRequest!.TransportProfileUri, Is.EqualTo(request.TransportProfileUri));
Assert.That(decodedRequest.NumReqWriterGroupIds, Is.EqualTo(2));
Assert.That(decodedRequest.NumReqDataSetWriterIds, Is.EqualTo(3));
Assert.That(decodedRequest.RequestTransportSpecificInfo, Is.True);
Assert.That(decodedResult!.DefaultPublisherId, Is.EqualTo(new Variant((uint)17)));
Assert.That(decodedResult.WriterGroupIds.ToArray(), Is.EqualTo(new ushort[] { 4, 5 }));
Assert.That(decodedResult.DataSetWriterIds.ToArray(), Is.EqualTo(new ushort[] { 10, 11, 12 }));
Assert.That(decodedResult.TransportSpecificInfo,
Is.EqualTo(new Variant("opc.tcp://localhost:4840/uafx")));
Assert.That(stream.Position, Is.EqualTo(stream.Length));
});
}
/// <summary>
/// Generated structures retain persistence, cleanup and variable associations without runtime admissions.
/// </summary>
[TestCase(false, 0)]
[TestCase(true, 1234.5)]
public void EndpointParametersPreservePersistenceAndVariableAssociations(bool persistent, double timeout)
{
ServiceMessageContext context = CreateContext(false);
var parameters = new PubSubConnectionEndpointParameterDataType
{
Name = "Endpoint",
ConnectionEndpointTypeId = NodeId.Create(1002, Ac.Namespaces.FxAc, context.NamespaceUris),
InputVariableIds = [new NodeId("input", 1)],
OutputVariableIds = [new NodeId("output", 1)],
IsPersistent = persistent,
CleanupTimeout = timeout
};
using var stream = new MemoryStream();
using (var encoder = new BinaryEncoder(stream, context, true))
{
parameters.Encode(encoder);
}
stream.Position = 0;
using var decoder = new BinaryDecoder(stream, context, true);
var decoded = new PubSubConnectionEndpointParameterDataType();
decoded.Decode(decoder);
Assert.Multiple(() =>
{
Assert.That(decoded.Name, Is.EqualTo("Endpoint"));
Assert.That(decoded.ConnectionEndpointTypeId, Is.EqualTo(parameters.ConnectionEndpointTypeId));
Assert.That(decoded.InputVariableIds.ToArray(), Is.EqualTo(parameters.InputVariableIds.ToArray()));
Assert.That(decoded.OutputVariableIds.ToArray(), Is.EqualTo(parameters.OutputVariableIds.ToArray()));
Assert.That(decoded.IsPersistent, Is.EqualTo(persistent));
Assert.That(decoded.CleanupTimeout, Is.EqualTo(timeout));
Assert.That(stream.Position, Is.EqualTo(stream.Length));
});
}
/// <summary>
/// Creates an isolated model-aware context with noncanonical namespace ordering.
/// </summary>
private static ServiceMessageContext CreateContext(bool reverseNamespaces)
{
ServiceMessageContext context = ServiceMessageContext.Create(NUnitTelemetryContext.Create());
context.NamespaceUris.GetIndexOrAppend("urn:uafx-tests:application");
string[] namespaces =
[
Di.Namespaces.OpcUaDi,
Data.Namespaces.FxData,
Ac.Namespaces.FxAc,
Cm.Namespaces.FxCm
];
foreach (string uri in reverseNamespaces ? namespaces.Reverse() : namespaces)
{
context.NamespaceUris.GetIndexOrAppend(uri);
}
context.Factory.Builder.AddOpcUaDi().AddOpcUaFxData().AddOpcUaFxAc().AddOpcUaFxCm().Commit();
return context;
}
}
}
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>$(TestsTargetFrameworks)</TargetFrameworks>
<RootNamespace>Opc.Ua.Fx.Tests</RootNamespace>
<AssemblyName>Opc.Ua.Fx.Tests</AssemblyName>
<GenerateProgramFile>false</GenerateProgramFile>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="NUnit" />
<PackageReference Include="NUnit.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="NUnit3TestAdapter">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Compile Include="..\Common\Main.cs" />
<ProjectReference Include="..\..\src\Opc.Ua.Fx\Opc.Ua.Fx.csproj" />
<ProjectReference Include="..\Opc.Ua.Test.Common\Opc.Ua.Test.Common.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="..\..\src\Opc.Ua.Fx\Model\*.nodeset2.xml"
Link="Model\%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>