Files
UA-.NETStandard/samples/FluentApi/BasicFluentApiServer/BasicFluentNodeManager.cs
romanett 96c5ef1623 Run the fluent configure pipeline in FluentNodeManagerBase (#4537) (#4538)
## Proposed changes

Closes #4537. The configure pipeline now lives in
`FluentNodeManagerBase`, so a hand-written fluent node manager only
overrides `ConfigureAsync` and no longer writes
`CreateAddressSpaceAsync` itself.

`FluentNodeManagerBase.CreateAddressSpaceAsync` now:
1. loads the predefined nodes (as `AsyncCustomNodeManager` did before),
2. creates a builder for `NamespaceIndex` with `CreateFluentBuilder`,
3. awaits `ConfigureAsync(builder, ct)`,
4. runs `RegisterAuthoredNodesAsync` and then `CompleteConfigureAsync`,
5. finishes with `SealConfigurationAsync`, which seals the builder,
replays `NotifyNodeAdded`, then activates behaviors and simulations.

### Breaking change (2.0)
Some managers need a different order, for example a post-setup runner
before sealing, a builder for another namespace, or work after sealing.
They must stop calling `base.CreateAddressSpaceAsync`, because that
would now run a second, empty pipeline and seal the manager-owned
registries too early. These managers now call
`LoadPredefinedNodesAsync(SystemContext, externalReferences, ct)` and
keep driving their own pipeline:
- the source-generated `NodeManager` template (the ordering test in
`NodeManagerGeneratorTests` is updated)
- `DiNodeManager`, `Isa95NodeManager`, `PositioningNodeManager`,
`RobotIntentNodeManager`, `VisionNodeManager`
- `samples/OpenUsd/SiteCompositionServer/SiteNodeManager`

These now use the base pipeline:
- `samples/FluentApi/BasicFluentApiServer/BasicFluentNodeManager`
becomes a single `ConfigureAsync` override. Before this change it never
sealed its builder.
- the hosting `FluentNodeManager` (from `AddNodeManager(namespaceUri,
build)`) becomes a `ConfigureAsync` override. It now seals through
`SealConfigurationAsync`, so `OnNodeAdded` handlers wired in the build
delegate fire. Before, it called `builder.SealAsync()` directly and they
did not.

Docs (`NodeManagers.md`, `DeviceIntegration.md`) are updated.
`NodeManagers.md` gets a short hand-written `ConfigureAsync` example.

## Types of changes

- [x] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Enhancement (non-breaking change which adds functionality)

## Checklist

- [x] I have read the CONTRIBUTING doc.
- [x] I have signed the CLA.
- [x] I ran tests locally with my changes, all passed.
- [x] I fixed all failing tests in the CI pipelines.
- [x] I fixed all introduced issues with CodeQL and LGTM.
- [x] I have added tests that prove my fix is effective or that my
feature works.
- [x] I have added necessary documentation (if appropriate).

## Tests

New `FluentNodeManagerBasePipelineTests`: nodes authored in
`ConfigureAsync` get registered, the Objects-folder reference is
mirrored into `externalReferences`, `OnNodeAdded` fires once, and the
builder is sealed afterwards.

Local run on net10.0, all passing:
- Opc.Ua.Server.Tests (Fluent/Hosting/StateMachine/RuntimeNodeSet
filter): 1286
- Opc.Ua.SourceGeneration.Core.Tests (NodeManager filter): 40
- Opc.Ua.Di.Tests 426, ISA95 136, Positioning 69, Robotics 636,
Robotics.Intent 22, Vision 484 (+1 skipped), Vision.Intent 14, OpenUsd
921

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

---------

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

195 lines
6.8 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.Generic;
using System.Threading;
using System.Threading.Tasks;
using Opc.Ua;
using Opc.Ua.Server;
using Opc.Ua.Server.Fluent;
namespace BasicFluentApiServer
{
public sealed class BasicFluentNodeManager : FluentNodeManagerBase
{
public const string NamespaceUri = "urn:localhost:OPCFoundation:BasicFluentApiServer";
private BaseVariableState? lastResult;
public BasicFluentNodeManager(
IServerInternal server,
ApplicationConfiguration configuration)
: base(server, configuration, NamespaceUri)
{
}
protected override ValueTask ConfigureAsync(
INodeManagerBuilder builder,
CancellationToken cancellationToken)
{
FolderState devices = builder.AddFolder("Devices").Node;
lastResult = builder
.AddVariable<int>("LastResult", devices.NodeId)
.Writable()
.OnWrite(OnWriteLastResult)
.AsVariable<int>().Node;
builder
.AddMethod("Add", devices.NodeId)
.OnCall(OnAdd)
.AddInputArguments(args => args
.Add(arg => arg
.WithName("a")
.WithDataType(DataTypeIds.Int32)
.WithValueRank(ValueRanks.Scalar)
.WithDescription("The first integer to add."))
.Add(arg => arg
.WithName("b")
.WithDataType(DataTypeIds.Int32)
.WithValueRank(ValueRanks.Scalar)
.WithDescription("The second integer to add."))
)
.AddOutputArguments(arg => arg
.WithName("sum")
.WithDataType<int>(SystemContext)
.WithValueRank(ValueRanks.Scalar)
.WithDescription("The sum of the two integers.")
);
builder
.AddMethod("Multiply", devices.NodeId)
.OnCall(OnMultiply)
.AddInputArguments(
arg => arg
.WithName("a")
.WithDataType(DataTypeIds.Int32)
.WithValueRank(ValueRanks.Scalar)
.WithDescription("The first integer to multiply."),
arg => arg
.WithName("b")
.WithDataType(DataTypeIds.Int32)
.WithValueRank(ValueRanks.Scalar)
.WithDescription("The second integer to multiply.")
)
.AddOutputArguments(arg => arg
.WithName("product")
.WithDataType<int>(SystemContext)
.WithValueRank(ValueRanks.Scalar)
.WithDescription("The product of the two integers.")
);
return default;
}
private ServiceResult OnAdd(
ISystemContext context,
MethodState method,
NodeId oid,
ArrayOf<Variant> inputArguments,
List<Variant> outputArguments)
{
if (inputArguments.Count < 2 ||
!inputArguments[0].TryGetValue(out int a) ||
!inputArguments[1].TryGetValue(out int b))
{
return StatusCodes.BadInvalidArgument;
}
int result = a + b;
outputArguments[0] = result;
if (lastResult is null)
{
return new(StatusCodes.UncertainNotAllNodesAvailable);
}
lastResult.Value = result;
lastResult.Timestamp = DateTime.UtcNow;
lastResult.StatusCode = StatusCodes.Good;
lastResult.ClearChangeMasks(context, false);
return ServiceResult.Good;
}
private ServiceResult OnMultiply(
ISystemContext context,
MethodState method,
NodeId oid,
ArrayOf<Variant> inputArguments,
List<Variant> outputArguments)
{
if (inputArguments.Count < 2 ||
!inputArguments[0].TryGetValue(out int a) ||
!inputArguments[1].TryGetValue(out int b))
{
return StatusCodes.BadInvalidArgument;
}
int result = a * b;
outputArguments[0] = result;
if (lastResult is null)
{
return new(StatusCodes.UncertainNotAllNodesAvailable);
}
lastResult.Value = result;
lastResult.Timestamp = DateTime.UtcNow;
lastResult.StatusCode = StatusCodes.Good;
lastResult.ClearChangeMasks(context, false);
return ServiceResult.Good;
}
private ServiceResult OnWriteLastResult(
ISystemContext context,
NodeState node,
ref Variant value)
{
try
{
if (!value.TryGetValue(out int lastResult) || lastResult == 0)
{
return StatusCodes.BadOutOfRange;
}
return ServiceResult.Good;
}
catch (Exception e)
{
return ServiceResult.Create(e, StatusCodes.Bad, "Error writing LastResult variable.");
}
}
}
}