|
| 1 | +# dotnet Port — TODO |
| 2 | + |
| 3 | +This file tracks the integration work for the dotnet implementation and serves as the |
| 4 | +canonical checklist for adding future dotnet versions. |
| 5 | + |
| 6 | +## Quarkus → dotnet10 Concept Mapping |
| 7 | + |
| 8 | +| Concept | Quarkus 3 | dotnet10 | |
| 9 | +|---|---|---| |
| 10 | +| **HTTP framework** | RESTEasy (JAX-RS `@Path`, `@GET`) | ASP.NET Core Minimal API (`app.MapGet`) | |
| 11 | +| **DI container** | CDI (`@ApplicationScoped`, `@Inject`) | `IServiceCollection` (`AddScoped`, constructor injection) | |
| 12 | +| **ORM** | Hibernate ORM Panache (`PanacheEntityBase`) | EF Core (`DbContext`, `DbSet<T>`) | |
| 13 | +| **DB driver** | `quarkus-jdbc-postgresql` | Npgsql (`Npgsql.EntityFrameworkCore.PostgreSQL`) | |
| 14 | +| **Connection pool** | Agroal (built into Quarkus datasource) | EF Core connection pool (Npgsql default) | |
| 15 | +| **Health checks** | SmallRye Health (`/health/live`, `/health/ready`) | `AddHealthChecks().AddDbContextCheck<T>()` | |
| 16 | +| **Metrics** | Micrometer → OTel OTLP | OTel `WithMetrics` → OTLP (ASP.NET Core + runtime meters) | |
| 17 | +| **Traces** | SmallRye OTel, `traceidratio 0.1` | OTel `TraceIdRatioBasedSampler(0.1)` + `AddAspNetCoreInstrumentation()` | |
| 18 | +| **Logs** | OTel OTLP (`otel.logs.enabled: true`) | *(not yet wired — `UseOtlpExporter()` API gap in 1.11)* | |
| 19 | +| **OTel exporter** | OTLP gRPC `localhost:4317` | OTLP gRPC `localhost:4317` (same default) | |
| 20 | +| **JSON serialization** | Jackson (via `quarkus-rest-jackson`) | `System.Text.Json` with source-generated `FruitJsonContext` | |
| 21 | +| **Serialization optimization** | Reflection-free serializers (`enable-reflection-free-serializers: true`) | Source-generated context (`TypeInfoResolverChain`) — AOT-safe by design | |
| 22 | +| **Config format** | `application.yml` | `appsettings.json` / env vars / `builder.Configuration` | |
| 23 | +| **HTTP port** | 8080 (Quarkus default) | 8080 (explicit `UseUrls("http://0.0.0.0:8080")`) | |
| 24 | +| **Build output** | `quarkus-run.jar` (fast-jar) | Self-contained binary via `dotnet publish -c Release` | |
| 25 | +| **Native image** | GraalVM (`-Pnative`) — production-ready | `PublishAot` — blocked (see Native AOT section) | |
| 26 | +| **Test framework** | JUnit 5 + REST Assured + Quarkus DevServices | xUnit + `WebApplicationFactory` + Testcontainers | |
| 27 | +| **DB for tests** | Quarkus DevServices (auto-spins PostgreSQL) | Testcontainers (E2E) / mocked repos (unit) | |
| 28 | +| **Startup log** | `quarkus3 started in Xs` (built-in) | Custom `ApplicationStarted` callback emitting same pattern | |
| 29 | +| **CPU pinning** | `taskset --cpu-list 0-3` via `APP_CMD_PREFIX` | Same `APP_CMD_PREFIX` prepended to `runCmd` in `main.yml` | |
| 30 | +| **Memory limit** | `-Xmx512m` via `--jvm-memory` → `config.jvm.memory` | `DOTNET_GCHeapHardLimit` computed from `--jvm-memory` by `xmx_to_dotnet_gc_limit()` in `run-benchmarks.sh` → `config.dotnet.gcHeapHardLimit` | |
| 31 | +| **Processor count** | JVM infers from `taskset` cores | `DOTNET_ProcessorCount` set to `config.resources.app_cpus` (count derived from `--cpus-app`) | |
| 32 | +| **GC mode** | Server GC (JVM default for server workloads) | `DOTNET_gcServer=1` | |
| 33 | + |
| 34 | +## dotnet10 — Status |
| 35 | + |
| 36 | +### Done ✓ |
| 37 | + |
| 38 | +- [x] ASP.NET Core 10 implementation equivalent to the plain Quarkus version |
| 39 | +- [x] `scripts/stress.sh` detects dotnet binaries and sets correct runtime env vars |
| 40 | +- [x] `scripts/1strequest.sh` detects dotnet binaries and sets correct runtime env vars |
| 41 | +- [x] `scripts/perf-lab/main.yml` — `dotnet10` entry in `RUNTIMES` and `RUNTIMECMDS`; `updateScript: skip-version-update` prevents abort when qDup's `update-runtime-version` runs |
| 42 | +- [x] `scripts/perf-lab/run-benchmarks.sh` — `dotnet10` in `ALLOWED_RUNTIMES` |
| 43 | +- [x] `scripts/perf-lab/helpers/dotnet.yml` — `ensure-dotnet` requirement check |
| 44 | +- [x] `scripts/perf-lab/helpers/requirements.yml` — `ensure-dotnet` wired into `ensure-requirements` |
| 45 | +- [x] `dotnet10/README.md` documents how to run the dotnet app locally |
| 46 | +- [x] `Program.cs` emits a startup log matching the perf-lab `logFileStartedRegex` |
| 47 | +- [x] `scripts/remote-setup.sh` — one-command remote host preparation (SSH key install, passwordless sudo, environment verification) |
| 48 | +- [x] `README.md` — "Better: Run on a dedicated remote Linux box" section with 3-step workflow and `--repo-url` guidance for local changes |
| 49 | +- [x] perf-lab runtime constraints made fair — `main.yml` `runCmd` adds CPU pinning (`taskset` via `APP_CMD_PREFIX`), `DOTNET_gcServer=1`, and `DOTNET_ProcessorCount` from `config.resources.app_cpus`; `DOTNET_GCHeapHardLimit` is computed from `--jvm-memory` via `xmx_to_dotnet_gc_limit()` in `run-benchmarks.sh` so memory limits always match the JVM constraint |
| 50 | + |
| 51 | +### Remaining / Open |
| 52 | + |
| 53 | +- [ ] **E2E tests need a running database in CI.** |
| 54 | + The unit tests (`Dotnet10.Tests.Service.*`) run fine without infrastructure. |
| 55 | + The E2E tests (`Dotnet10.Tests.E2e.*`) require PostgreSQL with seed data. |
| 56 | + Current CI job excludes E2E tests with `--filter "FullyQualifiedName!~Dotnet10.Tests.E2e"`. |
| 57 | + Options: |
| 58 | + - Add Testcontainers (`Testcontainers.PostgreSql`) to `Dotnet10.Tests` so E2E tests |
| 59 | + spin up their own database, matching how Quarkus DevServices works for the JVM jobs. |
| 60 | + - Or start `ghcr.io/quarkusio/postgres-17-perf:main` as a GitHub Actions service |
| 61 | + container and remove the filter. |
| 62 | + |
| 63 | +- [ ] **Dependabot NuGet monitoring.** |
| 64 | + `.github/dependabot.yml` only covers Maven (`package-ecosystem: maven`). |
| 65 | + Add a second entry for `nuget` pointing at `dotnet10/`. |
| 66 | + |
| 67 | +- [ ] **perf-lab: `java -version` calls are misleading for dotnet runtimes.** |
| 68 | + `measure-build-times`, `measure-time-to-first-request`, and `measure-rss` |
| 69 | + all call `sh: java -version` unconditionally. For dotnet this is harmless noise (Java is |
| 70 | + always present because qDup itself is a Java tool), but it clutters the output. |
| 71 | + Consider making the call conditional on `${{RUNTIME.type}} != dotnet`. |
| 72 | + (`test-build` was removed from the allowed tests in the upstream refactor, so it is no |
| 73 | + longer a concern.) |
| 74 | + |
| 75 | +- [ ] **perf-lab: no `--dotnet-version` selection support.** ⚠️ Hard |
| 76 | + `run-benchmarks.sh` lets you pin `--quarkus-version` and `--springboot3/4-version` because |
| 77 | + those are Maven dependency versions overridden at build time with a single property. The .NET |
| 78 | + SDK version is fundamentally different: it is an OS-level installation, not a project |
| 79 | + dependency. Whatever `dotnet` binary is on the `PATH` is what gets used, and `ensure-dotnet` |
| 80 | + in `helpers/dotnet.yml` merely logs it without any selection logic. |
| 81 | + To add proper version selection the following would all be needed: |
| 82 | + - Teach `helpers/dotnet.yml` to download and invoke Microsoft's `dotnet-install.sh` for a |
| 83 | + specific SDK version (mirroring how `ensure-java` / `ensure-graalvm` delegate to SDKMAN). |
| 84 | + - Add a `--dotnet-version` flag to `run-benchmarks.sh` and wire it through to a new qDup |
| 85 | + state variable (e.g. `config.dotnet.version`). |
| 86 | + - Handle SDK activation on the remote host — unlike SDKMAN's `sdk use`, the .NET SDK uses |
| 87 | + `global.json` or environment variables (`DOTNET_ROOT`, `PATH` prepending) to select a |
| 88 | + version, which must persist across the separate SSH commands qDup issues. |
| 89 | + - Decide whether to install into a shared location or a per-run sandbox to avoid races if |
| 90 | + benchmarks are ever run in parallel. |
| 91 | + |
| 92 | +- [ ] **perf-lab: no dotnet profiling support.** |
| 93 | + The JVM runtimes can use async-profiler (JFR / flamegraph). dotnet has equivalent tools: |
| 94 | + `dotnet-trace` (CPU samples → speedscope / chromium), `dotnet-counters`, and Linux `perf`. |
| 95 | + A `scripts/perf-lab/helpers/dotnet-trace.yml` helper could mirror `async-profiler.yml`. |
| 96 | + |
| 97 | +- [ ] **dotnet-native: not feasible yet.** ⛔ Blocked by Microsoft |
| 98 | + A `dotnet10-native` runtime (analogous to `quarkus3-native` / `spring3-native`) cannot be |
| 99 | + added in a like-for-like way with the current application stack. The blockers are fundamental, |
| 100 | + not configuration issues: |
| 101 | + |
| 102 | + **Why it doesn't work today:** |
| 103 | + - **EF Core**: Query compilation relies on expression trees and reflection that are built and |
| 104 | + evaluated at runtime. Native AOT trims this code away. As of .NET 10, EF Core's Native AOT |
| 105 | + support is still marked experimental and requires switching to interceptors (compile-time |
| 106 | + query pre-compilation via `dotnet ef dbcontext optimize`), which is a significant |
| 107 | + architectural change not present in the Quarkus or Spring implementations. |
| 108 | + - **Npgsql**: Requires explicit compile-time registration of all mapped CLR types |
| 109 | + (`NpgsqlDataSourceBuilder.EnableDynamicJson()` is not AOT-safe). Entity mappings, enum |
| 110 | + converters, and range types must all be declared upfront. |
| 111 | + - **Serialization edge cases**: Even with the source-generated `FruitJsonContext` already in |
| 112 | + place, generic collections and nested DTOs can still fail at runtime because the AOT |
| 113 | + trimmer's static analysis misses types that are only referenced through EF Core's |
| 114 | + internal materializer. |
| 115 | + - **No equivalent of Quarkus build-time extensions**: Quarkus has spent years solving these |
| 116 | + exact problems for Hibernate and Jackson via build-time bytecode generation and |
| 117 | + `@RegisterForReflection`. The .NET ecosystem has no equivalent tooling depth yet. |
| 118 | + |
| 119 | + **What would need to be true before adding `dotnet10-native`:** |
| 120 | + - EF Core Native AOT support reaches stable/non-experimental status (tracked in |
| 121 | + [dotnet/efcore#29840](https://github.com/dotnet/efcore/issues/29840)) |
| 122 | + - `dotnet ef dbcontext optimize` (compiled query interceptors) works correctly with Npgsql |
| 123 | + without requiring application code changes beyond what Quarkus needs |
| 124 | + - The published binary produces identical HTTP behaviour to the JIT version under load |
| 125 | + (no trimming-related `NullReferenceException` or `MissingMethodException` at runtime) |
| 126 | + - Verified by running the full perf-lab test suite without errors across all 3 iterations |
| 127 | + |
| 128 | + **Checklist for when the above is met:** |
| 129 | + - [ ] Run `dotnet ef dbcontext optimize` and commit the generated interceptors |
| 130 | + - [ ] Add `<PublishAot>true</PublishAot>` and `<TrimmerRootDescriptor>` to `Dotnet10.csproj` |
| 131 | + - [ ] Register all Npgsql type mappings explicitly in `Program.cs` |
| 132 | + - [ ] Add `dotnet10-native` RUNTIMECMD to `main.yml` (same pattern as `quarkus3-native` / |
| 133 | + `spring3-native`, with `buildCmd: "dotnet publish Dotnet10 -c Release -r linux-x64 -o publish"`) |
| 134 | + - [ ] Add `dotnet10-native` to `ALLOWED_RUNTIMES` and `DEFAULT_RUNTIMES` in `run-benchmarks.sh` |
| 135 | + - [ ] Add a `dotnet10-native-build-test` CI job |
| 136 | + - [ ] Run perf-lab end-to-end and confirm zero errors across all iterations before merging |
| 137 | + |
| 138 | +## Adding a Future dotnet Version (e.g., dotnet11) |
| 139 | + |
| 140 | +Every new major dotnet version needs changes in exactly these places. |
| 141 | +Work through the checklist top-to-bottom. |
| 142 | + |
| 143 | +### 1. New application directory |
| 144 | + |
| 145 | +``` |
| 146 | +cp -r dotnet10 dotnet11 |
| 147 | +``` |
| 148 | + |
| 149 | +Inside `dotnet11/`: |
| 150 | +- [ ] Rename solution and project files: |
| 151 | + `Dotnet10.sln` → `Dotnet11.sln`, `Dotnet10/` → `Dotnet11/`, `Dotnet10.Tests/` → `Dotnet11.Tests/` |
| 152 | +- [ ] Update `<TargetFramework>net10.0</TargetFramework>` → `net11.0` in both `.csproj` files |
| 153 | +- [ ] Update all NuGet package versions to their dotnet 11–compatible releases |
| 154 | +- [ ] Replace every occurrence of `dotnet10` / `Dotnet10` in source files and namespaces |
| 155 | + with `dotnet11` / `Dotnet11` (including the startup log message in `Program.cs`) |
| 156 | +- [ ] Update `dotnet11/README.md` |
| 157 | + |
| 158 | +### 2. `scripts/perf-lab/main.yml` |
| 159 | + |
| 160 | +- [ ] Add `dotnet11` to the `RUNTIMES` list (line ~62) |
| 161 | +- [ ] Add a `RUNTIMECMDS` entry (copy from `dotnet10`, update name, dir, and regex): |
| 162 | + ```yaml |
| 163 | + - name: dotnet11 |
| 164 | + type: dotnet |
| 165 | + dir: ${{DOTNET11_DIR}} |
| 166 | + updateScript: skip-version-update |
| 167 | + buildCmd: "dotnet publish Dotnet11 -c Release -o publish" |
| 168 | + runCmd: "./publish/dotnet11" |
| 169 | + logFileStartedRegex: ".*dotnet11.+started in.*" |
| 170 | + ``` |
| 171 | +- [ ] Add `update-state` entry (note: use `${{PROJ_REPO_DIR}}`, not `${{REPO_DIR}}/${{PROJ_REPO_NAME}}`): |
| 172 | + ```yaml |
| 173 | + - set-state: RUN.DOTNET11_DIR ${{PROJ_REPO_DIR}}/dotnet11 |
| 174 | + ``` |
| 175 | +- [ ] Add `output-vars` log line for `DOTNET11_DIR` |
| 176 | + |
| 177 | +### 3. `scripts/perf-lab/run-benchmarks.sh` |
| 178 | + |
| 179 | +- [ ] Add `dotnet11` to `ALLOWED_RUNTIMES` array (line ~338) and `DEFAULT_RUNTIMES` array (line ~339) |
| 180 | +- [ ] Add `dotnet11` to the `--runtimes` help text (line ~79) |
| 181 | + |
| 182 | +### 4. `.github/workflows/main.yml` |
| 183 | + |
| 184 | +- [ ] Add a `dotnet11-build-test` job (copy `dotnet-build-test`, change SDK version to |
| 185 | + `11.x` and working-directory to `dotnet11`) |
| 186 | + |
| 187 | +### 5. `README.md` |
| 188 | + |
| 189 | +- [ ] Add `dotnet11` entry to "What's in the repo" |
| 190 | +- [ ] Add `stress.sh` / `1strequest.sh` examples for dotnet11 |
| 191 | + |
| 192 | +### 6. `scripts/perf-lab/helpers/dotnet.yml` |
| 193 | + |
| 194 | +No change needed — `ensure-dotnet` already checks the generic `dotnet --version`, |
| 195 | +which satisfies any installed SDK version. |
| 196 | + |
| 197 | +If the benchmark environment must pin to a specific SDK version (e.g. to prevent a newer |
| 198 | +SDK from silently changing compiler behaviour), add a version-pinned variant: |
| 199 | +```yaml |
| 200 | +ensure-dotnet11: |
| 201 | + - log: Checking for .NET 11 SDK |
| 202 | + - sh: dotnet --version | grep -E '^11\.' |
| 203 | +``` |
| 204 | +and wire it into `requirements.yml` instead of (or alongside) `ensure-dotnet`. |
| 205 | + |
| 206 | +### 7. Dependabot |
| 207 | + |
| 208 | +- [ ] Add a third `updates` entry in `.github/dependabot.yml` for the new directory. |
0 commit comments