Files
UA-.NETStandard/samples/PumpDeviceIntegrationServer
Marc Schier e5cb64cb62 Make fluent sample security settings explicit (#4087)
# Description

Makes security-critical configuration explicit in the modern fluent
client and server samples instead of relying on hidden defaults.

- Adds an application-wide `ConfigureSecurity` DI callback for advanced
certificate validation settings.
- Makes PKI roots and certificate validation settings explicit in the
Pump, Boiler, Calculator, and Minimal Client samples.
- Makes the Calculator sample's endpoint policies and anonymous
authentication choice explicit.
- Adds consolidated developer guidance for application certificates,
endpoint security, identity, resource controls, and every advanced
security-builder setting.
- Preserves the samples' existing automatic-trust behavior while clearly
marking it as sample-only.

## Related Issues

- Fixes #4085

## 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.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

Copilot-Session: 833a8855-af3c-4a85-887b-32bf38d92cfa
2026-07-24 12:56:51 +02:00
..

PumpDeviceIntegrationServer

A self-contained, NativeAOT-friendly OPC UA server that demonstrates the OPC 40223 Pumps companion specification with a full live simulation, wired through the fluent INodeManagerBuilder API and the additive OPC 10000-100 topology-element builder integration.

The pump sample is the integration test for every fluent API extension shipped under src/Opc.Ua.Server/Fluent/. Each extension is documented in Source-generated NodeManagers — Building richer node managers.

Running the sample

cd samples/PumpDeviceIntegrationServer
dotnet run -c Release

The server listens on opc.tcp://localhost:62542/PumpDeviceIntegrationServer by default. Override with --port 62550.

Sample console output:

info: Opc.Ua.Server.MasterNodeManager
      MasterNodeManager.Startup - NodeManagers=3
info: Pumps.PumpNodeManager
      Configuring PumpNodeManager fluent wiring...
info: Pumps.PumpNodeManager
      PumpNodeManager: address space ready (10330 predefined nodes).
info: Opc.Ua.Server.StandardServer
      OPC UA server listening at opc.tcp://localhost:62542/PumpDeviceIntegrationServer.

Browse to Objects > DeviceSet > Pump #1 in any OPC UA client (e.g. UaExpert) to explore the simulated pump. A second declarative pump, Pump #2, is organized alongside it by the same DeviceSet — it demonstrates the DI hosting ConfigureDevicesFor flow without the hand-wired fluent simulation.

Running in Docker

A Dockerfile is provided that builds the Release publish output on the .NET AzureLinux 3 base images and runs it as a non-root user.

Build from the repository root, not from this folder. The image needs the full source tree (src/, src/, tools/), so the Docker build context must be the repo root and the Dockerfile is selected with -f. Running docker build . from inside this folder fails fast with a message telling you the correct command.

# from the repository root:
docker build -f samples/PumpDeviceIntegrationServer/Dockerfile `
             -t pumpdeviceintegrationserver:local .

Run it, publishing the OPC UA port:

docker run --rm -p 62542:62542 pumpdeviceintegrationserver:local

Inside the container the endpoint binds to 0.0.0.0 so it is reachable from the host. Override the bind host and port via environment variables:

docker run --rm -p 62550:62550 `
           -e host=0.0.0.0 -e port=62550 `
           pumpdeviceintegrationserver:local

The server creates its certificate/PKI store under /app at runtime. To persist certificates across container restarts, mount a volume:

docker run --rm -p 62542:62542 `
           -v pump-pki:/app/pki `
           pumpdeviceintegrationserver:local

The image is built and published to the GitHub Container Registry by the pump-device-integration-server-docker.yml workflow on every push to master and on manual dispatch.

What the sample demonstrates

Feature Where
AddOpcUa().AddServer(...).AddNodeManager<T>() hosting Program.cs
Multi-model composition (DI library + locally source-generated Machinery + Pumps) PumpNodeManager.cs LoadPredefinedNodesAsync
Identification properties via WithProperty(name, value) PumpNodeManager.Configure.cs WithIdentification
Optional-child materialisation via generator-emitted AddXxx(context) helpers (Operational / Measurements / Events / SupervisionProcessFluid / SupervisionPumpOperation / Maintenance) PumpNodeManager.cs MaterialisePumpOptionalChildren
Engineering units / EURange via WithEngineeringUnits / WithEURange WithMeasurements
Discrete NumberOfStarts counter wired via Variable<uint>(...).OnRead(...) WithMeasurements
250 ms simulation tick via Simulation(...).OnTick(...) Configure → AdvanceSimulation
Limit alarm with thresholds and acknowledge handler via CreateLimitAlarm(...).WithLimits(...) WithSupervision
Boolean supervision (TwoStateDiscreteState) → alarm activation via .ActivatesAlarm(...) WithSupervision
Cross-namespace path resolution (Pump #1 in Pumps NS → Operational in Machinery NS → Measurements in Pumps NS, all in one unqualified browse path) src/Opc.Ua.Server/Fluent/BrowsePathResolver.cs
Generated PumpType instance + typed Identification group configuration Program.cs (Pump #2)

Architecture

PumpDeviceIntegrationServer/
├── Program.cs                          # AddOpcUa().AddServer(...).AddNodeManager<T>()
│                                       # + ConfigureDevicesFor declarative Pump #2
├── PumpNodeManager.cs                  # Hand-written FluentNodeManagerBase
│                                       # + LoadPredefinedNodesAsync (multi-model)
│                                       # + CreateAddressSpaceAsync (builder setup)
├── PumpNodeManager.Configure.cs        # partial — fluent wiring + simulation tick
├── PumpDeviceIntegrationServer.csproj  # ProjectReference to Opc.Ua.Di model lib
│                                       # AdditionalFiles for Machinery + Pumps
│                                       # NodeSet2 (consumed by source generator)
├── Model/
│   ├── Opc.Ua.Machinery.NodeSet2.xml   # AdditionalFiles — build-time only
│   └── Opc.Ua.Pumps.NodeSet2.xml       # AdditionalFiles — build-time only
└── Properties/AssemblyInfo.cs

The Opc.Ua.Di model library is consumed as a project reference (its types live under the Opc.Ua.Di namespace and are source-generated from the ModelDesign XML). Cross-namespace references from Machinery and Pumps to DI types resolve through the [assembly: ModelDependencyAttribute] carried in the Opc.Ua.Di assembly — no DI NodeSet2 XML needed in this project. The unified attribute carries the compact type-table payload that the consumer's source generator imports at compile time; see ModelDependencies.md for the wire format and consumer-side flow.

The Machinery and Pumps NodeSet2 XMLs are source-generated locally inside this assembly via the <AdditionalFiles> plumbing in the .csproj. The generator emits typed *State classes, NodeId tables, and the AddOpcUaMachinery / AddOpcUaPumps extension methods that LoadPredefinedNodesAsync calls. No runtime XML loading happens — the Model/ folder is a build-time input only. Consumer assemblies that want to reference Machinery or Pumps the same way they reference Opc.Ua.Di should source-generate against the model XML inside their own assembly using the same <AdditionalFiles> pattern.

Extending the sample

  • Add a measurement: open PumpNodeManager.Configure.cs, add a call to AddMeasurement(builder, browsePath, getter, units, min, max) inside WithMeasurements, then add a field + line to AdvanceSimulation that updates the value each tick.
  • Add an alarm: inside WithSupervision, chain another builder.Node("Pump #1/Events").CreateLimitAlarm(...).WithLimits(...) and wire the triggering boolean variable via .ActivatesAlarm(...).
  • Add a second pump: two patterns are demonstrated in the sample.
    • Hand-rolled (used for Pump #1): in PumpNodeManager.CreatePumpAsync, create the generated PumpState, attach it to the DI DeviceSet with Organizes, and register it. The fluent Configure.cs then wires its measurements, alarms, and simulation by browse path.
    • DI declarative (used for Pump #2): in Program.cs, call PumpNodeManager.CreatePumpAsync(...) from a ConfigureDevicesFor<PumpNodeManager> block, wrap the generated PumpState with ctx.TopologyElement<PumpState>(...), then configure the mandatory Identification group. This preserves the PumpType type definition while exposing only topology-element operations.

NativeAOT publishing

cd samples/PumpDeviceIntegrationServer
dotnet publish -c Release -r win-x64

The pump server publishes cleanly under NativeAOT — no trim or AOT warnings — because every fluent extension is reflection-free and the generated model factories are statically rooted.

See also

  • docs/DeviceIntegration.md — full developer guide for the DI library trio (device builder, hosting integration, lock service, software-update package store, client helpers).