Commit Graph

3 Commits

Author SHA1 Message Date
romanett 9b034f8291 Enable nullable reference types centrally; migrate all unset and annotations-only projects (#4700)
# 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>
2026-10-08 08:48:13 +02:00
Marc Schier 5bc8a9d705 Reduce ISession: type the continuation points, delete the obsolete validate overload (#4199)
## Stacked on #4197

Base is `marcschier/server-bind-phase`. The full stack is **#4188 →
#4193 → #4197 → this**.
GitHub retargets each base automatically as the stack merges. **Review
the earlier three
first.**

**#4217 has been merged into this branch** (squash `53dd4b618`), so the
last remaining
item from the issue — `ISession.SessionDiagnostics` — is now part of
this PR.

Closes #4187.

Refs #4203 — **does not close it.** #4217 swapped `OnUpdateDiagnostics`
onto
`m_diagnosticsLock`, but it still calls
`Variant.FromStructure(ServerDiagnostics)` with the
default `copy: false`, which wraps the *live* structure in the
`ExtensionObject`. The caller
reads its fields after the lock is released, so the lock currently
excludes nothing. #4222
adds `copy: true` and is what actually closes #4203.

## The finding that changed the approach

`ISession` has **exactly one production adapter**:
`src/Opc.Ua.Server/Session/Session.cs`.
The other `ISession` implementations in the tree are the unrelated
**client** interface
(`src/Opc.Ua.Client/Session/Session.cs`, `RedundantClientSession`) and a
migration-analyzer test stub.

One adapter means the seam is hypothetical; two means it is real. The
issue proposed
splitting `ISession` into five concern-interfaces — that would turn one
hypothetical seam
into five. The interface earns its keep for testability and because node
managers receive
it through `OperationContext`, but that argues for making it
**smaller**, not for
multiplying it.

**So: reduce, don't split.** Reasoning posted to the issue as
[comment
5226712936](https://github.com/OPCFoundation/UA-.NETStandard/issues/4187#issuecomment-5226712936)
so the five-way framing does not get re-proposed later.

## Measurement

38 members, 368 references. External production consumers are **4 files
using 5 members**:

| Consumer | Members used |
|---|---|
| `Opc.Ua.Redundancy.Server/Sessions/DistributedSessionManager.cs` |
`Id`, `InitializeAsync`, `Dispose` |
| `Opc.Ua.Di.Server/Locking/DefaultLockService.cs` | `Id` |
| `samples/ConsoleReferenceServer/UAServer.cs` | `Id`, `Identity`,
`ReadDiagnostics` |
| `samples/Quickstarts.Servers/.../DataChangeMonitoredItem.cs` | type
reference only |

Every production caller of the five continuation-point members is inside
`Opc.Ua.Server`
(`MasterNodeManager`, `NodeManagerLifecycle`, `HistorianDispatcher`).

**`ISession` is 38 → 32 members.**

## Commits

### `882b2937` Type the history continuation point

`ISession` leaked `object` twice, against the repository rule:

```csharp
void SaveHistoryContinuationPoint(Guid id, object continuationPoint);
object? RestoreHistoryContinuationPoint(ByteString continuationPoint);
```

The erasure was **not** hiding a genuinely opaque plugin token.
`HistorianDispatcher` is the
only production caller, it always passes a `HistorianContinuationState`,
and it casts
straight back to that type on restore. The `object` was a round trip
through the type system
that bought nothing.

`HistorianContinuationState` is `internal`, so it cannot appear on a
public interface.
`IHistoryContinuationPoint` names the concept instead: it carries an
`Id` and extends
`IDisposable`.

Two latent bugs fell out of typing it:

- The session disposed only those points that **happened** to implement
`IDisposable` and
**silently leaked the rest**. Every point is now disposable by
construction, and three
  `as IDisposable` probes are gone.
- Carrying the `Id` on the point removes the separate `Guid` parameter,
which could disagree
with the payload it was filed under. The mock stores in the historian
tests had the same
  latent inconsistency.

### `0d020b77` Delete the obsolete synchronous `ValidateBeforeActivate`

**Zero production callers** — the nine references were all tests written
for it. `[Obsolete]`
since 1.5.378, which the repository rules allow removing in master.

It is also the divergent-signature problem the runtime analysis flagged:
the async form
returns the validated handler and policy as a tuple, while the sync one
took the same two
values as trailing `out` parameters, so a caller had to learn two shapes
for one operation.

The overload could not verify a user token requiring decryption, so on a
secure endpoint it
failed closed and told the caller to use the async path anyway. The one
test asserting that
fail-closed behaviour goes with the method it described; the path it
pointed at is already
covered by `ActivationValidationDecodesBinaryEncodedUserIdentityToken`.

The other eight tests are **converted, not deleted** — the channel,
signature,
security-policy and token-policy validation they cover is still
asserted, now against the
path the server actually uses.

### `821e8302` Route the continuation points behind one member

Five `ISession` members were one-line pass-throughs to
`SessionContinuationPoints`, a
419-line module that already owns the behaviour and already has its own
`IContinuationPointStore` seam. The interface restated the module's
surface without adding
anything.

```csharp
// was
session.SaveContinuationPoint(cp);
session.RestoreHistoryContinuationPoint(bytes);

// now
session.ContinuationPoints.SaveBrowse(cp);
session.ContinuationPoints.RestoreHistory(bytes);
```

The holder is `internal sealed`, so `ISessionContinuationPoints` is what
makes it
addressable.

**This is composition, not the service locator `IServerContext` refused
to be.** A subsystem
with an independent lifetime should be injected; continuation points
belong to the session
and cannot exist without it.

`InvalidateContinuationPoints` called `RemoveBrowseForManager` and
`RemoveHistoryForManager`
in sequence, so the pair becomes `RemoveForManager` — the caller's
intent is that a node
manager is going away, not that two collections need visiting.

## Tests

Three harnesses had hand-rolled a `Dictionary<Guid, object>` to stand in
for the holder. They
now use the **real** one, so the dispatcher and `BrowseNext` meet the
eviction and disposal
rules a live session actually applies.

**One hazard worth recording for future interface work.** A Moq member
that is never stubbed
returns `null`. Where the old code called a method straight on
`ISession`, that `null` was
harmless. Reaching *through* a property means an unstubbed
`ContinuationPoints` null-derefs
instead. Three tests failed exactly that way; they are wired to a real
holder, not restubbed.

| Suite | net10.0 | net48 |
|---|---|---|
| `Opc.Ua.Server.Tests` | 4032 passed, 1 failed | builds clean |
| Touched areas (continuation points, historian, master node manager,
session) | 898 passed | — |

The single failure is
`ConfigureApplicationBuildsSharedClientAndServerConfigurationAsync`, a
local certificate-store problem verified to reproduce unchanged at HEAD
with these changes
stashed. Not caused by this PR.

`Opc.Ua.Redundancy.Server`, `Opc.Ua.Di.Server`, `Quickstarts.Servers`
and
`ConsoleReferenceServer` all build clean.

## Docs

`docs/MigrationGuide.md` gains a "Removed members on ISession" section
covering the
`ValidateBeforeActivate` removal with the async replacement, and the
`IHistoryContinuationPoint` change with before/after.

## The SessionDiagnostics removal (folded in from #4217)

`ISession.SessionDiagnostics` handed out the whole mutable
`SessionDiagnosticsDataType` —
the structure the session's diagnostics lock protects — so a caller
could read a field while
the owner was writing it. Every server-side reader wanted exactly one
value out of it, so the
interface now exposes those two values (`SessionName`,
`ClientApplicationUri`) directly and
the structure stays behind the lock.

That work landed here as #4217 rather than as a separate PR, and it also
fixes #4203:
`OnUpdateDiagnostics` now takes `m_diagnosticsLock` instead of locking
the diagnostics
payload it was handed.

## Reviewer questions

- `ISessionContinuationPoints` exposes `MaxBrowse` as read-only while
the concrete holder
keeps a setter (the test framework adjusts it to exercise limits).
Should the setter be on
the interface, or is test-only mutation better left to the concrete
type?
- `RemoveForManager` collapses two removal methods. The concrete
`RemoveBrowseForManager`/`RemoveHistoryForManager` remain public on the
holder — worth
  making them private now that nothing outside calls them individually?

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e9c13dd0-a6e3-4ad2-8ad6-1c3fe6323041
2026-08-11 06:57:40 +02:00
Marc Schier 7cec2d614d Move repository files to the consolidated layout 2026-07-16 06:25:54 +02:00