# AyaNova 8 back end → .NET 10 The plan and running record for the port. Tick items and add a log line **in the same commit** as the change they record. Anything found along the way that is out of scope goes under "Deferred", not into the branch. | | | |---|---| | Starting point | AyaNova 8.2.4: ASP.NET Core 8, EF Core 8 | | Target | `net10.0`, every back-end package at its current release | | Front end | Minimal or no change. Rebuild only if needed (open question) | | Branch | `net10-port` in `raven` (and `raven-client` only if the client changes) | | Baseline tag | `net8-baseline`, on the last commit before the port | | Release version | Not yet decided (open question) | ## Where this plan comes from Sockeye, the internal licensing server, is a near-copy of this codebase. It was ported from .NET 6 to .NET 10 in September–October 2026 and released as `8.1.0`. This plan is that port's carry-forward list (Appendix A of the Sockeye migration runbook), arranged as a checklist. **AyaNova starts from .NET 8, not .NET 6.** So every item is "check whether this applies", not "this will apply". Several items will turn out to be already done or not present. Mark those `n/a` with a one-line reason rather than deleting them. When an item is unclear, look at how Sockeye did it: ```powershell git -C ..\sockeye log --oneline net6-baseline..8.1.0 git -C ..\sockeye show ``` **Where AyaNova differs from Sockeye:** customers choose their own database role names and passwords, every customer uses reports, and AyaNova *verifies* licences rather than issuing them. Items touching those three areas carry more weight here than they did in Sockeye. ## Scope - **In:** target framework; all NuGet packages in `server/AyaNova`; code changes those require; the Sockeye-found fixes in section 3, each as its own commit. - **Out:** front-end dependency changes; refactoring (Roadmap step D); new features; PostgreSQL major upgrades. - **Undecided:** see "Open questions". ## Working method - Version bump first. One concern per commit. - Prove each gate on the **unmodified** code before changing anything, so a later failure points at the port, not at the test. - Packages that must move together: framework-matched `Microsoft.*` packages all go to 10.x in one step. `BouncyCastle.Cryptography` 2.x goes with MailKit/MimeKit 4.x, because MimeKit 3.x pulls in `Portable.BouncyCastle` and the assemblies clash. - Publish `server\AyaNova\AyaNova.csproj` directly, never the solution, so no test project can reach the artifact. --- ## 0. Before changing anything - [ ] Record any port work already done in earlier sessions (branch, commits) here first, and reconcile it with this list - [ ] `raven` and `raven-client` clean on `main` and pushed - [ ] Annotated tag `net8-baseline` on the pre-port commit in `raven` and `raven-client`, pushed by name (`git push origin net8-baseline`), checked with `git ls-remote --tags origin` - [ ] Branch `net10-port` created and pushed with upstream - [ ] **First commit: version bump** in every place listed in `devdocs/build/BUILD.md` → "Version numbers" - [ ] Dev machine has the .NET 10 SDK and keeps the .NET 8 runtime for baseline comparisons ### Gates (prove on the unmodified code) - [ ] **Integration suite baseline.** Run `raven-test-integration` against the unmodified build and record the counts in the log below. Expected: everything passes except the three known server-side failures listed in that repo's CLAUDE.md - [ ] **Licence verification gate.** Find out exactly how AyaNova 8 verifies a v8 licence: library, public key, and when it re-checks. If it uses BouncyCastle, write a test that runs every v8 row of Sockeye's `alicense` fixture through AyaNova's own verification routine. The fixture is customer data: it stays outside the repo and is found through an environment variable, as Sockeye's re-sign harness does with `SOCKEYE_RESIGN_FIXTURE`. Must be green on unmodified code. A regression here would reject licences customers already have installed - [ ] **Report baseline.** Render the advanced reports on the unmodified build and keep the PDFs for comparison. Sockeye shares only one report with AyaNova, so Sockeye's testing says little about AyaNova's reports - [ ] **E2E baseline.** Cypress run against a devtest server on the unmodified build ## 1. Framework and packages - [ ] `net8.0` → `net10.0`, with every framework-matched `Microsoft.*` package to 10.x in the same step - [ ] **Hosting model.** `WebHost`, `IWebHost` and `WebHostBuilder` are obsolete in .NET 10 (ASPDEPR008, ASPDEPR004). `Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(web => web.UseStartup())` keeps the `Startup` class unchanged - [ ] **Asp.Versioning.** Older notes say AyaNova already uses `Asp.Versioning.Mvc` **[unverified]**. If not: rename from `Microsoft.AspNetCore.Mvc.Versioning`; `ApiVersion` moves to `Asp.Versioning`; wiring is `services.AddApiVersioning(...).AddMvc().AddApiExplorer(...)`. Leave AV0014 warnings: **do not add `[ApiController]`** - [ ] **Npgsql / EF Core 10.** - Check `Npgsql.EnableLegacyTimestampBehavior`. Sockeye's was commented out. If AyaNova sets it, `DateTime.Kind` is a real port item - Check for `ConfigureWarnings(w => w.Throw())`. If present, every new EF warning becomes an exception naming its ID: fix the query, or move that ID to `.Log(...)` - Exercise LINQ-based queries screen by screen. Data-list filters build raw SQL and are unaffected - [ ] **BouncyCastle.** If AyaNova references `BouncyCastle`, it becomes `BouncyCastle.Cryptography` 2.x, moved together with MailKit/MimeKit 4.x. In Sockeye, `PemReader` still yields an `AsymmetricCipherKeyPair` for a PKCS#1 key - [ ] **Swashbuckle 10 / OpenAPI.NET 2.** `Microsoft.OpenApi.Models` merged into `Microsoft.OpenApi`. Bearer requirement becomes `c.AddSecurityRequirement(document => new OpenApiSecurityRequirement { [new OpenApiSecuritySchemeReference("Bearer", document)] = [] })`. No explicit `Microsoft.OpenApi` reference - [ ] **NLog 6.** `ArchiveNumbering` is gone; `NLogBuilder.ConfigureNLog` is replaced by `NLog.LogManager.Configuration = logConfig`. Archive numbers keep incrementing and the un-numbered file is always current. If the log viewer in the UI sorts by file name, the order breaks past 99 - [ ] **Newtonsoft.Json.** `Mvc.NewtonsoftJson` 10.x sets a 13.0.3 floor (NU1605). Pin it as a direct reference - [ ] **JwtBearer 10.x**, which also retires the vulnerable IdentityModel 6.10.0 dependency if AyaNova still resolves it - [ ] **SMTP.** Any use of `System.Net.Mail.SmtpClient` (SYSLIB0014) moves to MailKit 4.x, as in Sockeye and contact - [ ] **PuppeteerSharp → 25.x.** Reporting is AyaNova's main Puppeteer surface - `BrowserFetcher.DefaultChromiumRevision` is gone: call `DownloadAsync()` with no argument - `IJSHandle.ExecutionContext` is gone: evaluate on the handle. Evaluate results are `JsonElement?` - The default download is Chrome for Testing, into `Chrome/` and `ChromeHeadlessShell/` beside the app. AyaNova 8.2.0 already moved from Chromium to Chrome to follow Puppeteer's default, so the browser choice is settled - Launch arguments must be separate array elements (`--no-sandbox` on its own). **This includes the report-render browser params setting** customers can edit - AppArmor restricts unprivileged user namespaces on Ubuntu 24.04+, which affects the Chrome sandbox - [ ] Remaining packages to current releases - [ ] Warnings: CA2022 → `ReadExactly`; ASP0019 → header indexer instead of `Headers.Append` - [ ] **MiniProfiler.** Confirm it is absent: `git grep -i -e miniprofiler -e mini-profiler -e StackExchange.Profiling` in **both** `raven` and `raven-client` - [ ] **BinaryFormatter.** Confirm it is unused, or that `EnableUnsafeBinaryFormatterSerialization` is already false - [ ] **`System.GC.Server`.** Keep the Web SDK default (`true`). On one vCPU the runtime uses workstation GC anyway, and DATAS keeps server GC lean since .NET 9 - [ ] **`InvariantGlobalization`.** Decide on EF Core string comparison and date-parsing grounds. Still open in Sockeye too ## 2. Configuration behaviour to check - [ ] **URL binding precedence.** An in-code `UseUrls` beats `ASPNETCORE_URLS`. Find where AyaNova takes its URLs from (Sockeye: `SOCKEYE_USE_URLS` in `config.json`) and make sure the documented setting is the one that wins - [ ] **Logging providers.** `appsettings.json` very likely sets the Debug and Console providers to `Trace`, as Sockeye's does. Check whether `Program.cs` calls `ClearProviders()` before adding NLog. If it does not, `Trace` floods the journal under systemd. Fix the levels in the repo either way - [ ] **Data Protection keys.** Only matters if AyaNova encrypts anything with Data Protection and runs under a service account with no home directory: then the key ring must be persisted explicitly. A JWT-only API is unaffected ## 3. Fixes found through Sockeye (each its own commit) These are bugs, not port mechanics, so each is a separate commit that can be reverted alone. Sockeye included its equivalents in the port branch. John decides per item whether they go in this branch or a follow-up release. - [ ] **Case 4676: data-list SQL injection — priority.** The numeric and role filters in `DataListSqlFilterCriteriaBuilder` built SQL from input. In AyaNova any authenticated user who can open a data list could reach them. Port the Sockeye fix - [ ] **`DbUtil.AdminConnectionString`.** If it is `_dbConnectionString.Replace(_dbName, "postgres")`, any customer whose role name, password or host contains the database name as text gets a mangled admin connection, and the boot check authenticates as the wrong user. Fix: `new NpgsqlConnectionStringBuilder(cs) { Database = "postgres" }.ConnectionString` - [ ] **Backup `pg_dump` call.** If it builds a `postgresql://user:password@host:port/db` URI with the raw password and runs it through a shell for a `> file` redirect, a password with spaces or `@ : / ? # %` breaks backups. Fix: password in `PGPASSWORD` in the process environment, connection parameters as separate arguments through `ProcessStartInfo.ArgumentList` (no shell), output with `-f` - [ ] **Drop-and-recreate database.** Without `CREATEDB` it drops the database and then fails to create it. Test the permission before dropping, or change the order - [ ] **Report browser download location.** Point it under the data directory in code. Beside the app it is read-only under a hardened systemd unit (`ProtectSystem=strict`), and a redeploy that replaces the app folder deletes it - [ ] **Report failures must not kill the server.** On the Sockeye destination a failed browser download was an unhandled exception that ended the whole process (systemd `core-dump`, then restart); the client saw only `TypeError … reading 'length'`. Catch download and launch failures, log them through NLog, and return an error to the client - [ ] **Data-list date filters, small fixes.** `>=` and `<` should use `ZeroSeconds`, not `MaxSeconds`; date `NotEqual` should exclude the whole minute (`< ZeroSeconds OR > MaxSeconds`). In DEBUG builds, `DataListField.UiFieldDataType` is read before `DataListField` is null-checked - [ ] **Relative date tokens use the server's time zone** (`*today*`, `*yesterday*` …), which is wrong for any user in a different zone from the server. The fix builds the range from `clientTimeStamp.Offset`, with care for ranges that cross a DST change. **This changes behaviour** and affects the integration tests' `TIME_ZONE_ADJUSTMENT`. Probably better as a follow-up release Not port items, listed so they are not forgotten: the three server bugs the integration suite is waiting on (case 4653, case 4654, the contract issue). ## 4. Test under realistic conditions - [ ] **Database role.** Run under a non-superuser role **named like the database**, with a password containing spaces and `@ : / ? # %`. Boot, on-demand backup, scheduled backup, restore. Sockeye ran as `postgres` for years and never exercised either bug in section 3 - [ ] **Restore with a dedicated role.** `createdb -O `, then `pg_restore --no-owner --no-privileges --role=`. Without `--role`, a superuser restore leaves every table owned by `postgres`, and the integrity check, which counts through `information_schema`, fails. Compare with the restore instructions in the customer manual (`docs/`) and note any change needed - [ ] **Reports, all advanced reports**, compared with the baseline PDFs. On Windows and on Linux. On Linux also: `ldd` on the downloaded browser binary for missing libraries (install exactly what is reported; several Ubuntu 24.04+ names carry a `t64` suffix); the sandbox under the service account; no orphaned `chrome` processes after a cancelled render - [ ] Integration suite: same result as the baseline - [ ] Licence verification gate: green - [ ] Cypress E2E against a devtest server running the .NET 10 build - [ ] `build-release.bat` produces all five packages; install and start each one: Windows standalone, Windows LAN, Linux server, Linux desktop, subscription Linux ## 5. Release - [ ] **Every host that runs a framework-dependent build needs the .NET 10 runtime** before it gets the release: - Hosted subscription droplets (Ubuntu 24.04). How `aspnetcore-runtime-10.0` is obtained on 24.04 needs checking **[unverified]**. On 26.04 it is in Ubuntu's own archive - Self-hosted Linux customers (server and desktop zips are framework-dependent) - Windows LAN: `lan.iss` prompts for the ASP.NET Core Hosting Bundle. Update its text and link to .NET 10 - Windows standalone and the launcher are self-contained: no runtime needed on the customer's machine - [ ] Customer manual (`docs/`): system requirements and upgrade instructions for self-hosted customers - [ ] Build VM: install the .NET 10 SDK; update `devdocs/build/DEV-VM-SETUP.md` and `devdocs/build/BUILD.md` - [ ] Final gate runs on the finished code - [ ] Merge, per repo: ```powershell git switch net10-port; git status; git push; git fetch origin git switch main; git pull git log --oneline net10-port..main # must print nothing: main untouched git merge --no-ff net10-port -m "Merge net10-port: .NET 10 port" git diff net10-port main # must print nothing: main is exactly what was tested git push origin main git tag -a -m "" git push origin git ls-remote --tags origin ``` `--no-ff` keeps a merge commit, so the whole port can be backed out with `git revert -m 1 ` - [ ] Rebuild the client from `main`, publish, and test **the published artifact itself** - [ ] Rollout: own test droplet first, then customer droplets by time zone, as in the weekly routine - [ ] After release: delete `net10-port` locally and on the remote --- ## Open questions - Release version number (Sockeye's port was released as 8.1.0) - `raven-launcher` (.NET 8, self-contained): move to .NET 10 in the same release? - `raven-test-integration`: move to `net10.0` (separate commit) or leave on `net8.0`? It only talks HTTP to the server - Does the client need a rebuild in this pass, or can its current output be carried across? - Section 3: which fixes go in this branch, which in a follow-up - How the .NET 10 runtime gets onto Ubuntu 24.04 hosts ## Deferred Found during the port, out of scope. One line each. - ## Log | Date | Step | Result | |---|---|---| | | | |