Files
UA-.NETStandard/docs/migrate
Marc SchierandCopilot 4cbcec6e23 Update NuGet dependencies to latest and version ASP.NET Core per target framework (#4223)
# Description

Refreshes the centrally-managed NuGet dependencies in
`Directory.Packages.props` to their latest stable versions, and changes
how the ASP.NET Core packages are pinned so their version follows the
target framework.

## ASP.NET Core packages are now versioned per TFM

`Microsoft.AspNetCore.Authentication.Certificate`,
`Microsoft.AspNetCore.Authentication.JwtBearer`,
`Microsoft.AspNetCore.Mvc.Testing` and `Microsoft.AspNetCore.TestHost`
ship one band per .NET major, carry no `netstandard2.0` asset, and do
not roll forward across majors — a `net8.0` project simply cannot
consume the `10.0.x` band. A single central pin therefore had to sit on
the oldest supported band and hold *every* consumer there, which is why
these sat at `8.0.28` while `10.0.10` was current.

They are now selected from `$(TargetFramework)`, mirroring the existing
MQTTnet split:

| TFM | Version |
|---|---|
| `net8.0` | `8.0.29` |
| `net9.0` | `9.0.18` |
| everything else | `10.0.10` |

The last group is the fallback rather than an explicit `net10.0` match,
so a TFM the file does not enumerate — a future `net11.0`, or the
`net10.0` shell that legacy `netstandard2.0`/`netstandard2.1`
`$(CustomTestTarget)` builds fall back to — still resolves a version.
Verified against the generated `project.assets.json` that each TFM of
`Opc.Ua.Bindings.Https` really does resolve its own band.

## Version changes

`Crdt`/`Crdt.Transport` and the `RaftCs` family 1.1.2 · `Dekaf` 1.11.2 ·
`JsonSchema.Net` 9.4.0 · `Microsoft.Bcl.TimeProvider`, the
`Microsoft.Extensions.*` set and
`System.Composition.AttributedModel`/`System.Formats.Asn1`/`System.Text.Json`/`System.Threading.Channels`/`System.Threading.RateLimiting`
10.0.10 · `Microsoft.Extensions.Http.Resilience` and
`Microsoft.Extensions.TimeProvider.Testing` 10.8.0 ·
`Microsoft.NET.Test.Sdk` 18.8.1 · `Microsoft.SourceLink.*` 10.0.301 ·
`Nerdbank.GitVersioning` 3.10.91 · `Serilog` 4.4.0 ·
`System.CommandLine` 2.0.10 · `Avalonia`/`Avalonia.Desktop` 12.1.1 ·
`MQTTnet`/`MQTTnet.Server` 5.2.0.1603 (modern band only) ·
`Roslynator.*` 4.16.0 · `TUnit` 1.64.6.

The two `System.Collections.Immutable` `VersionOverride`s in the schema
test projects move to 10.0.10 to stay ahead of what `JsonSchema.Net`
requires.

## Removed: the DotNext pin

`DotNext` 5.26.3 was pinned centrally but no project references it. The
`docs/migrate/2.0.x/packages.md` row claiming `src/Opc.Ua.Lds.Server`
consumes it was also wrong — that project references only
`Makaretu.Dns.Multicast` and
`Microsoft.Extensions.Hosting.Abstractions`. Both are corrected.

## BouncyCastle held at 2.6.2 — CI caught a real regression

The first push of this branch took `BouncyCastle.Cryptography` to 2.7.0.
CI failed `Opc.Ua.Core.Tests` on **net48 only**
(`VerifyPEMSupportDirectoryStoreAsync` expecting 3 certificates and
getting 0), and the cause turned out to be a genuine behavioural break
rather than a flaky test.

BouncyCastle is referenced **only on net472/net48**, where it backs
`PEMReader`/`PEMWriter` in `Opc.Ua.Security.Certificates`. 2.7.0
tightened `Asn1.X509.TbsCertificateStructure` to throw:

```
certificate issuer is an empty distinguished name
```

`OpenSsl.PemReader.ReadObject()` therefore aborts on the *first* entry
of a chain whose issuer DN is empty. `ImportPublicKeysFromPEM` has no
per-object recovery, so the entire file is discarded and the store
enumerates nothing.

This is a **runtime** behaviour change, not a test artefact: .NET's own
PEM support on net8.0+ still accepts such a certificate, so taking 2.7.0
would let the same trusted-peer store load fine on net8.0+ and come back
**empty** on .NET Framework. Whether the stack *should* reject these
certificates on every TFM is a fair question, but it is a
security-behaviour decision that does not belong in a dependency refresh
— so the pin is documented in place with that context for a follow-up.

Confirmed by bisecting the package version against the net48 tests
(2.6.2 passes, 2.7.0 fails) and by reading the actual PEM asset with
both versions to capture the exact throw site.

## Deliberately left alone

Every existing pin with a documented reason is preserved, and each was
re-checked rather than assumed:

- **Roslyn compat band** (`roslyn.props`) — `Microsoft.CodeAnalysis.*`
stays on the pinned band and
`System.Collections.Immutable`/`System.Reflection.Metadata` stay on
`$(RoslynRuntimeVersion)`. Raising these is what re-breaks the analyzer
`ImmutableArray<T>` identity the file warns about, and it would raise
the minimum compiler that will load the shipped analyzers.
`Microsoft.CodeAnalysis.Analyzers` stays at 4.14.0 for the same reason.
- **`Microsoft.AspNetCore.*` 2.3.11** — last band with a
`netstandard2.0` asset; already latest.
- **`PacketDotNet` / `SharpPcap`** — SDL native-code pins; already
latest stable.
- **`System.Net.Http` 4.3.4 / `System.Text.RegularExpressions` 4.3.1 /
`System.Private.Uri` 4.3.2** — transitive CVE remediation pins; already
latest.
- **`MQTTnet` 4.3.7.1207** for `net472`/`net48`/`netstandard2.1` —
MQTTnet 5.x dropped those TFMs.
- **`BouncyCastle.Cryptography` 2.6.2** — see the section above.
- **`Moq`** — pinned.
- **Prerelease-only upgrades skipped** — `BenchmarkDotNet`
0.16.0-preview, `NUnit` 5.0.0-beta, `NUnit.Console` 4.0.0-beta,
`EmbedIO` 4.0.0-rc1, `Newtonsoft.Json` 13.0.5-beta, `PacketDotNet`
1.4.9-pre, and others.

## Overlap with #4195 — please note

**OpenUSD and ModelContextProtocol are intentionally not touched here.**
#4195 already moves OpenUSD to `0.6.0-alpha` together with the restored
render smoke job and the `OpenUsd.Rendering.Silk.Vulkan` package that
the bump actually depends on, and separately moves MCP to `2.1.0`.
Rev'ing either in this PR would duplicate that work, and would risk
silently downgrading MCP if the two landed in the wrong order. They are
left to #4195.

There is still a small expected overlap in `Directory.Packages.props`:
#4195 also bumps `Microsoft.Extensions.Configuration.Abstractions`,
`Microsoft.Extensions.Hosting.Abstractions` and `System.Text.Json` to
10.0.10 — the same values as here, so whichever merges second resolves
to an identical line.

## The one code change

Roslynator 4.16 adds **RCS1258**, which flags `TypeAttributes.Public |
TypeAttributes.Class` in `ComplexTypeBuilder`. It is correct:
`TypeAttributes.Class` is `0`, so the flag is a no-op, and the
`DefineEnum` call directly above already passes `TypeAttributes.Public`
alone. Dropping it is a verified semantic no-op (`Public | Class ==
Public`).

## Related Issues

_No tracking issue — routine dependency maintenance, in the same spirit
as the weekly Dependabot NuGet updates configured in
`.github/dependabot.yml`._

## Checklist

- [x] 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.<br/>_No new tests: this
changes dependency versions, not behaviour. The existing suite is the
proof, and the one code change is a verified no-op._
- [x] I have added all necessary
documentation.<br/>_`docs/migrate/2.0.x/packages.md` — corrected version
rows, removed the wrong `DotNext` row, added a section explaining the
per-TFM ASP.NET Core scheme._
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.<br/>_Full `UA.slnx` build across all six TFMs
(`net472`, `net48`, `netstandard2.1`, `net8.0`, `net9.0`, `net10.0`):
**0 warnings, 0 errors**. `dotnet restore`: 0 warnings. `dotnet pack`: 0
warnings, 0 errors, package validation passes._
- [x] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.<br/>_See
the verification note below._
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

### Update after the first CI run

The initial push failed one CI job — `Opc.Ua.Core.Tests` on net48, 2 PEM
tests. That was **not** flakiness: it was a real break from BouncyCastle
2.7.0, root-caused and fixed by holding that package at 2.6.2 (see the
section above). `Opc.Ua.Core.Tests` on net48 now runs **4161 passed / 0
failed** locally, and the full six-TFM build is still 0 warnings / 0
errors.

### Review feedback

Both review threads are addressed in `78bdd0ea3`:

- The **package table's third column was stale** in more rows than the
ones flagged. Every entry is now checked against the actual
`PackageReference` items — `System.Threading.Channels` belongs to
`Opc.Ua.Core`/`Core.Diagnostics`/`PubSub.Diagnostics` (not the LDS
server), the `Microsoft.CodeAnalysis.*` rows come from the
generator/analyzer variant targets under `tools/`, and
`Microsoft.CodeAnalysis.Analyzers` plus `System.Reflection.Metadata`
have no direct reference at all and are labelled as central pins. The
column is renamed **"Referenced by"**, which is what it actually records
and what makes it verifiable.
- The **BouncyCastle pin now references
[#4224](https://github.com/OPCFoundation/UA-.NETStandard/issues/4224)**,
which carries the decision on whether the stack should reject
empty-issuer certificates on every TFM, whether
`ImportPublicKeysFromPEM` should recover per entry, and whether the
expired `Test_chain.pem` asset should be regenerated.

## Verification note on the test run

The full net10.0 run covered ~48,000 tests. 15 failed, and each was
individually shown **not** to be caused by this change:

- **4 in `Opc.Ua.Core.Diagnostics.Tests`** (3×
`ReadChunkDecryptsStackEncryptedSymmetricChunk`,
`PrivateRequestSummaryFormatsKnownRequestKinds`) — reproduced
**identically at `HEAD` with the changes stashed**. Pre-existing.
- **8 across `Client`, `Di`, `WotCon.Samples`, `Server`** — all pass
when their project is run in isolation; they only fail under the
parallelism of a full-solution run. The machine also had ~45 orphaned
test processes holding sockets, which were cleared.
- **3 in `Gds`/`Stress`** — the log shows socket port exhaustion (`Only
one usage of each socket address...`). The `CertRotationLiveTests` pair
passes when run standalone, and that project's net10.0 package closure
contains **none** of the bumped packages (`BouncyCastle` is
`net472`/`net48`-only, so it cannot reach a net10.0 test at all).

The AOT/TUnit suite (`Opc.Ua.Aot.Tests`) was A/B tested: TUnit **1.58**
produced the same intermittent 0–2 failures as 1.64, confirming
pre-existing flakiness rather than a regression. On **1.64.6** it ran
**129/129 twice**.

`net48` was built and exercised via `CustomTestTarget=net48`; the
`MSB3277`/`NU1702` warnings it surfaces in `Opc.Ua.Stress.Tests` are
structural to that mode (the project builds `net8.0` while consuming
`net48` outputs) and pre-date this change — the `net462` asset's
assembly version simply tracks the patch number, so `10.0.0.9` became
`10.0.0.10`.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2be367ab-e62d-4141-a4df-e5b2ca3cd126
2026-08-10 18:18:27 +00:00
..