# 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 40001 Machinery samples
Two console applications that show the whole OPC 40001 series end to end.
| Sample | What it does |
|---|---|
| MachineryServer | Serves one simulated hydraulic press with every part of the series |
| MachineryClient | Walks the Machines folder, prints what it finds, downloads a result and streams state |
Run them in two terminals:
dotnet run --project samples/Machinery/MachineryServer -- --port 62546
dotnet run --project samples/Machinery/MachineryClient -- --insecure
The client prints something like:
Machine 12:HydraPress-1 (ns=12;i=1)
Acme Machine Tools HydraPress 500 — serial HP500-2024-0001
location: Hall 3 / Line 2
state: Executing / mode: Processing
component 12:HydraulicUnit
building block 13:Identification
building block 3:MachineryEquipment
building block 8:JobManagement
building block 3:Monitoring
building block 3:MachineryItemState
building block 3:Notifications
building block 13:OperationCounters
building block 3:MachineryOperationMode
building block 3:Components
device health: NORMAL
operation counters: power-on 12480.0 h, operating 9215.5 h
equipment UpperDie (urn:acme-machine-tools.example:equipment:die)
life: 118400 of 250000 left
process value HydraulicOilTemperature: 44.0 °C (setpoint 45.0, 45.7 % of range)
limits: 5.0 / 15.0 / 65.0 / 80.0
energy CompressedAir: Main = HydraPress-1/CompressedAir
NeEnergyImportHp = 0
NeEnergyExportHp = 0
Volume = 184320
VolumeFlowRate = 18.4
Pressure = 620000
Temperature = 294.65
energy Electricity: Main = HydraPress-1/Electricity
AcActivePowerTotal = 41250
Latest result: stroke-000019
downloaded 42 bytes:
resultId,stroke-000019
peakForceKN,4703.6
Observing MachineryItemState for 4s (OPC 40001-1 state changes always come from the server) …
22:33:52 -> Executing (via FromNotExecutingToExecuting)
22:33:52 -> NotExecuting (via FromExecutingToNotExecuting)
The namespace indices above are whatever the server happened to assign; only
the browse names are stable. 2:Identification and 2:OperationCounters show
up as 13: because OPC 40001-1 gives both the Device Integration namespace —
see Default instance browse names are
load-bearing.
The building blocks come from the OPC 40001-1 MachineryBuildingBlocks
organizer, which is the one place a client can discover what a machine
supports instead of probing for each block in turn. The oil temperature walks a
slow ramp across the datasheet limits, so the exclusive limit alarm on the
process value activates and clears while the sample runs; subscribe to
HydraulicOilTemperature to watch it.
What the sample does not need a second terminal for is the same walk as an
automated test: MachineryEndToEndTests runs this server and this client over
opc.tcp inside the test suite.
See the Machinery developer guide.