# Description Nullable reference types are now enabled once, in `common.props`, and every project in the repository builds warning-clean with them. **Configuration** - `<Nullable>enable</Nullable>` lives in `common.props`. All per-project `<Nullable>` settings are removed: 178 project files (enable, annotations, and the `disable` in the three Aot test projects). - 419 per-file `#nullable enable`/`restore` directives and the 4 `#nullable disable` directives are removed. - Kept on purpose: - The 5 `<auto-generated>` schema files (`UANodeSet.cs`, `UA Type Dictionary.cs`, `OPCBinarySchema.cs`, `SecuredApplication.cs`, `PInvokeHelper.cs`) keep `#nullable enable`. The compiler ignores the project setting in generated code, so the directive is required there. - `tests/Opc.Ua.Interop.LegacyPeer` keeps `disable`. It builds against the 1.5.378 packages and does not import the repository props. - `docs/DeveloperGuide.md` and `.github/copilot-instructions.md` now describe the central setting. **Migration** (the 43 unset and 13 annotations-only projects, roughly 10k diagnostics) - **Production code: annotated at the declarations.** Covers `tools/Opc.Ua.SourceGeneration.Core`, `src/Opc.Ua.Lds.Server` and the LDS sample. - Members, parameters and returns are nullable where null actually flows. - Null-check helpers carry `[NotNullWhen]` (`IsNull`, `IsValidXmlName`, `IsMethodTypeDesign`). - `CoreUtils.Format`, `Utils.Format`, `ITemplateWriter.WriteLine` take `params object?[]`. `Template.AddReplacement` takes `string?` (rendering already skips null replacements). - `IRegisteredServerStore.RegisterAsync` returns `Task<RegistrationEntry?>`: it returns null when a registration is removed. - Dereferences backed by an invariant (for example, the validated model) use `!`, so runtime behaviour is unchanged. - **Behavioural changes in production code** (small, deliberate): - `NodeSetToModelDesign` throws `InvalidOperationException` when the file is not a NodeSet. Before, it threw a NullReferenceException on the next line. - `ModelDesignValidator` starts its lookup tables empty instead of null. A redundant `if (m_nodes == null) return;` guard is gone. - **Test code: minimal annotations.** - `?` on values that can be null, `!` where the test asserts non-null. - `= null!` for fixture members assigned in SetUp. - `TryGetStructure<T>(out T? x)` where the inferred `T?` broke the `IEncodeable` constraint. - A few redundant `== null` checks on structs (`NodeId`, `ArrayOf<T>`) folded into the adjacent `IsNull`/`Count` checks. **Notes for reviewers** - The diff is large (~1,500 files) but almost entirely mechanical: `?`, `!` and directive removal. The non-annotation changes are listed above. - Known follow-up, not done here: `IModelDesign.Namespaces/TargetNamespace/NamespaceUris` stay non-nullable even though the validator returns null before it loads a dictionary. Making them nullable would ripple through every generator call site. ## Related Issues - Fixes #4439 ## 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. - [ ] 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. The "no new warnings" box covers net10.0 and net48: full non-incremental builds of `UA.slnx`, `tools/SourceGeneration.slnx`, `tools/MigrationAnalyzer.slnx` and the Aot test projects finished with 0 warnings and 0 errors under the repository's warnings-as-errors settings. net9.0 and net8.0 also had no nullable diagnostics, but they were not rebuilt after the final fixes. Tests have not been run locally yet. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
OPC 40223 Pumps samples
Two console applications that show the Opc.Ua.Pumps* libraries end to end:
a server that publishes pumps with the fluent builders, and a client that
discovers and reads them with PumpsClient.
| Sample | What it does |
|---|---|
| PumpsServer | Serves a duty/standby pair of cooling-water pumps and simulates them once a second |
| PumpsClient | Discovers every pump, prints the whole pump, then streams live values and supervision changes |
The Pumps developer guide explains every API used here.
Run
In two terminals:
dotnet run --project samples/Pumps/PumpsServer -- --port 62547
dotnet run --project samples/Pumps/PumpsClient -- --insecure --seconds 30
--insecure makes the client accept the server's self-signed certificate;
never use it in production. The client also takes --url <endpoint> (default
opc.tcp://localhost:62547/PumpsServer) and --seconds <n> (how long to
stream, 0 to skip).
The client prints something like:
Pump CoolingPump_A (ns=1;i=3788608060)
Acme Pumps CW-80, serial CW80-0042, built 2024/3 in DE
location: Plant 1 / Cooling skid
Design:
ClockwiseRotation True
MaximumAllowableContinuousSpeed 3000 1/min
MaximumAllowableWorkingPressure 1.6E+06 Pa
Implementation:
PumpBestEfficiency 78 %
RatedFlow 25 kg/s
RatedSpeed 2900 1/min
SystemRequirements:
Fluid Cooling water
MaximumFlow 32 kg/s
MinimumFlow 6 kg/s
Measurements:
BearingTemperature 27.92 °C
DifferentialPressure 367831 Pa [0 … 1600000]
MassFlow 21.58 kg/s [0 … 32]
NumberOfStarts 0
PumpEfficiency 75.08 % [0 … 100]
PumpPowerInput 9.64 kW
Speed 2821.17 1/min [0 … 3000]
Measurements/DriveEndBearing:
OverallVibrationVelocityRMS 2.88 mm/s [0 … 11.2]
ReferenceStandardForVibrationMeasurement ISO 10816-3
Signals:
PumpOperation True
StandBy False
MultiPump: role Master, mode RedundancyOperation, 1 of 2 pumps in operation
supervision: no active signals
maintenance: OperatingState, Level1, failure False, operating time 12480 h
port Suction: InletConnection, direction In, DN100 PN16 flange
port Discharge: OutletConnection, direction Out, DN80 PN16 flange
port Motor: Drive, direction -, no category
Motor/Measurements:
MotorCurrent 18.84 A
MotorTemperature 35.92 °C
document OperationManualLink: https://pumps.example.com/manuals/cw-80.pdf
document TechnicalDataLink: https://pumps.example.com/datasheets/cw-80.pdf
Pump CoolingPump_B (ns=1;i=1919434908)
…
Polled 7 measurements with PumpReadOptions.ValuesOnly; MassFlow = 21.58
Watching 8 values for 45s (the pumps swap duty every 90 s; cavitation appears once a minute) …
00:04:19 CoolingPump_A DriveEndBearing vRMS 2.91
00:04:19 CoolingPump_A Cavitation False
00:04:19 CoolingPump_B PumpOperation False
00:04:19 CoolingPump_B Cavitation False
00:04:19 CoolingPump_A MassFlow 21.75
00:04:19 CoolingPump_B MassFlow 0
…
00:04:56 CoolingPump_A DriveEndBearing vRMS 7.1
00:04:56 CoolingPump_A Cavitation True
00:05:02 CoolingPump_A Cavitation False
NodeIds and namespace indices are whatever the server assigned; only the browse names are stable.
What the server shows
| File | Shows |
|---|---|
Program.cs |
AddPumps() and ConfigurePumps(...) on the generic host |
PumpFleet.cs |
CreatePumpAsync, WithNameplate, every group builder (Design, Implementation, SystemRequirements, Measurements, Signals, Supervision(...), MultiPump, MaintenanceCategory(...), AddPort, Documentation), engineering units and ranges |
PumpFleet.cs |
A vibration measurement from the <Vibration> placeholder with AddVibration, and the PumpUnit holder of the builders the simulation writes through |
PumpSimulation.cs |
A BackgroundService driving values, a duty/standby exchange every 90 s, and a cavitation episode once a minute |
PumpDatasheet.cs |
Static pump data and UNECE engineering units built per OPC 10000-8 §5.6.3 |
Runtime updates. The simulation writes through the builders it kept at
configuration time. Every Set/SetAnalog/SetDiscrete publishes the
change, so subscribed clients see each update without further code.
What the client shows
Code in Program.cs |
Shows |
|---|---|
connection.Session.Pumps(telemetry) |
Creating the client; registers the OPC 40223 structured types |
pumps.IsSupported |
Detecting whether the server publishes OPC 40223 |
EnumeratePumpsAsync |
Discovery from the DI DeviceSet and the Machinery Machines folder |
ReadPumpAsync → PumpSnapshot |
The whole pump in one pass; each group has its own Read…Async too |
PumpValueSet / PumpValue |
Values with engineering unit and range, AsDouble, AsString |
PumpValueSet.Groups |
Nested groups such as the DriveEndBearing vibration measurement |
PumpSupervisionStatus.ActiveSignals |
Only the raised supervision signals |
PumpMaintenanceData |
StateOfTheItem, MaintenanceLevel, HasFailure |
ReadMeasurementsAsync(…, PumpReadOptions.ValuesOnly, …) |
Cheap polling once the metadata is known |
Streaming.SubscribeDataChangesAsync(value.NodeId, …) |
Subscribing to a reading; PumpValue.NodeId is the node that carries it, also for discrete signals |
SampleSession.cs is the session setup. It
keeps the ApplicationInstance alive until the session closes, because the
instance owns the CertificateManager the session validates with.
Publish as NativeAOT
Both samples are NativeAOT-compatible on net10.0:
dotnet publish samples/Pumps/PumpsServer -c Release -f net10.0 -r osx-arm64
Troubleshooting
| Symptom | Fix |
|---|---|
Client reports BadCertificateUntrusted |
Pass --insecure for the demo, or trust the server certificate in the client's PKI (under the local application data folder, OPC Foundation/PumpsClient/pki) |
The server publishes no pumps. |
Wait for the server log line Materialised pump … before starting the client |
| Port already in use | Start the server with --port <n> and the client with --url opc.tcp://localhost:<n>/PumpsServer |