diff --git a/.planning/phases/01-architecture-foundation/01-RESEARCH.md b/.planning/phases/01-architecture-foundation/01-RESEARCH.md new file mode 100644 index 0000000..2dd2fa1 --- /dev/null +++ b/.planning/phases/01-architecture-foundation/01-RESEARCH.md @@ -0,0 +1,500 @@ +# Phase 1: Architecture Foundation - Research + +**Researched:** 2026-03-27 +**Domain:** .NET 9 Blazor WASM + ASP.NET Core Web API solution scaffolding +**Confidence:** HIGH + +## Summary + +Phase 1 creates the three-project solution structure (`ChatAgent.Client`, `ChatAgent.Api`, `ChatAgent.Shared`) with working CORS communication between WASM client and API server, and establishes the tutorial commenting convention that applies to all subsequent phases. No feature code is written -- this phase locks in the architectural boundaries (no API key in WASM, no file I/O in WASM, no direct OpenAI calls from WASM) so that later phases build on a verified foundation. + +The .NET 9 SDK is available on this machine (9.0.312), alongside .NET 10 (10.0.201). Per CLAUDE.md the project targets .NET 9. The `blazorwasm`, `webapi`, and `classlib` templates are all available. The `webapi` template supports `--use-controllers` for the MVC Controller pattern the user chose (D-05). The hosted Blazor WASM template was removed in .NET 8, so the three projects must be created separately and added to a solution manually. + +**Primary recommendation:** Create the solution using `dotnet new` templates targeting `net9.0`, add a shared class library for DTOs, wire up a minimal health-check endpoint, confirm CORS works from WASM to API, and verify `dotnet publish` completes cleanly. + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions +- **D-01:** Solution named `ChatAgent.sln` at repo root with three projects: `ChatAgent.Client` (Blazor WASM), `ChatAgent.Api` (ASP.NET Core backend), `ChatAgent.Shared` (shared models/DTOs) +- **D-02:** Projects live in `src/` subfolders: `src/ChatAgent.Client/`, `src/ChatAgent.Api/`, `src/ChatAgent.Shared/` +- **D-03:** Solution file at repo root for easy `dotnet build` from project root +- **D-04:** Typed HttpClient pattern -- a `ChatApiClient` class in the Client project wraps all backend API calls, registered via DI +- **D-05:** Backend uses traditional MVC Controllers (not Minimal API) -- more structure, familiar pattern for tutorial +- **D-06:** CORS configured on the API to allow the WASM client origin during development +- **D-07:** Full tutorial-style inline comments -- explain everything including basic patterns, treat every file as a teaching moment +- **D-08:** Comments go inline (XML doc comments and `//` comments right next to the code), no separate companion docs +- **D-09:** Every Blazor concept introduced must have a comment explaining WHAT it is and WHY it's used +- **D-10:** Start with plain HTML/CSS in Phase 1 -- no MudBlazor yet. Learn raw Blazor rendering first, add component library later +- **D-11:** Light theme as default look and feel + +### Claude's Discretion +- CORS configuration details (origins, headers, methods) +- Base URL configuration approach (appsettings.json vs environment variables) +- Project file (.csproj) configuration details +- Test project structure (if any placeholder tests needed) +- .NET version targeting (9 vs 10 based on current stability) + +### Deferred Ideas (OUT OF SCOPE) +None -- discussion stayed within phase scope + + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|------------------| +| CODE-01 | Every Blazor concept introduced has inline comments explaining what and why | Tutorial commenting convention (D-07/D-08/D-09); every `.cs`, `.razor`, and `.csproj` file gets comments explaining the Blazor/ASP.NET Core concept it demonstrates | +| CODE-02 | Each phase introduces one concept incrementally (tutorial-style progression) | Phase 1's concept is "solution structure and project boundaries" -- no feature code, no OpenAI calls, no persistence, no streaming. Just the scaffold + one health-check round-trip to prove communication works | + + +## Standard Stack + +### Core (Phase 1 Only) +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| .NET 9 SDK | 9.0.312 | Runtime and tooling | Installed on machine; stable; CLAUDE.md specifies .NET 9 | +| Blazor WebAssembly Standalone | net9.0 | Client SPA | Non-negotiable per constraints | +| ASP.NET Core Web API | net9.0 | Backend API server | Required for API key isolation | +| Class Library | net9.0 | Shared models/DTOs | Referenced by both Client and Api projects | + +### Not Needed in Phase 1 +| Library | Why Deferred | +|---------|-------------| +| `OpenAI` NuGet | No AI calls in Phase 1 -- introduced in Phase 3 | +| `Markdig` | No markdown rendering in Phase 1 -- introduced in Phase 4 | +| `MudBlazor` | D-10 explicitly defers this; plain HTML/CSS first | +| Any test framework | No business logic to test yet; placeholder tests are optional (Claude's discretion -- recommend skipping to keep Phase 1 focused) | + +**Installation:** +```bash +# From repo root +dotnet new sln -n ChatAgent + +# Create projects targeting .NET 9 +dotnet new blazorwasm -n ChatAgent.Client --framework net9.0 -o src/ChatAgent.Client +dotnet new webapi -n ChatAgent.Api --framework net9.0 --use-controllers -o src/ChatAgent.Api +dotnet new classlib -n ChatAgent.Shared --framework net9.0 -o src/ChatAgent.Shared + +# Add projects to solution +dotnet sln ChatAgent.sln add src/ChatAgent.Client/ChatAgent.Client.csproj +dotnet sln ChatAgent.sln add src/ChatAgent.Api/ChatAgent.Api.csproj +dotnet sln ChatAgent.sln add src/ChatAgent.Shared/ChatAgent.Shared.csproj + +# Add Shared reference to both projects +dotnet add src/ChatAgent.Client/ChatAgent.Client.csproj reference src/ChatAgent.Shared/ChatAgent.Shared.csproj +dotnet add src/ChatAgent.Api/ChatAgent.Api.csproj reference src/ChatAgent.Shared/ChatAgent.Shared.csproj +``` + +## Architecture Patterns + +### Recommended Project Structure (Phase 1) +``` +ChatAgent.sln # Solution file at repo root (D-03) +src/ + ChatAgent.Client/ # Blazor WASM standalone app + ChatAgent.Client.csproj + Program.cs # DI registration, HttpClient base URL config + App.razor # Root component + _Imports.razor # Global using directives + Layout/ + MainLayout.razor # App shell layout + Pages/ + Home.razor # Landing page (placeholder in Phase 1) + Services/ + ChatApiClient.cs # Typed HttpClient wrapper (D-04) + wwwroot/ + index.html # HTML host page + css/app.css # Basic styling (plain CSS, D-10) + + ChatAgent.Api/ # ASP.NET Core Web API + ChatAgent.Api.csproj + Program.cs # DI, CORS, controller mapping + Controllers/ + HealthController.cs # GET /api/health -- proves CORS works + appsettings.json # API config (base URLs, future API key ref) + appsettings.Development.json # Dev-specific config + + ChatAgent.Shared/ # Shared class library + ChatAgent.Shared.csproj + Models/ + HealthResponse.cs # Simple DTO for health-check response +``` + +### Pattern 1: Typed HttpClient in Blazor WASM (D-04) +**What:** A `ChatApiClient` class wraps `HttpClient` for all API calls. Registered in DI as a typed client. +**When to use:** Always -- components never use `HttpClient` directly. +**Example:** +```csharp +// Source: ASP.NET Core docs + CONTEXT.md D-04 +// ChatApiClient.cs -- Typed HttpClient wrapper +// In Blazor WASM, HttpClient is backed by the browser's Fetch API. +// We wrap it in a typed client so components don't depend on HttpClient directly. +// This makes testing easier and centralizes API URL management. +public class ChatApiClient +{ + private readonly HttpClient _httpClient; + + public ChatApiClient(HttpClient httpClient) + { + _httpClient = httpClient; + } + + /// + /// Calls the health endpoint to verify the API server is reachable. + /// This is the first HTTP call from WASM to the backend -- proves CORS works. + /// + public async Task GetHealthAsync() + { + return await _httpClient.GetFromJsonAsync("api/health"); + } +} + +// Registration in Program.cs: +// AddHttpClient registers a typed HttpClient with its own configuration. +// The base address points to the API server (different port during local dev). +builder.Services.AddHttpClient(client => +{ + client.BaseAddress = new Uri("https://localhost:7100"); +}); +``` + +### Pattern 2: MVC Controller on API (D-05) +**What:** Traditional `[ApiController]` with `[Route("api/[controller]")]` attributes instead of Minimal API `app.MapGet()`. +**When to use:** All API endpoints in this project. +**Example:** +```csharp +// HealthController.cs -- ASP.NET Core MVC Controller +// We use Controllers (not Minimal API) for more explicit structure (D-05). +// Each controller is a class with methods for each HTTP verb. +// [ApiController] enables automatic model validation and 400 responses. +[ApiController] +[Route("api/[controller]")] +public class HealthController : ControllerBase +{ + /// + /// Health check endpoint. Returns server status and timestamp. + /// Used by the WASM client to verify the API is reachable and CORS is working. + /// + [HttpGet] + public IActionResult Get() + { + return Ok(new HealthResponse + { + Status = "healthy", + Timestamp = DateTime.UtcNow + }); + } +} +``` + +### Pattern 3: CORS Configuration (D-06) +**What:** Explicit CORS policy on the API allowing the WASM client origin. +**When to use:** Required for any cross-origin HTTP call from WASM to API during development. +**Example:** +```csharp +// Program.cs (API project) -- CORS setup +// CORS (Cross-Origin Resource Sharing) is required because the Blazor WASM client +// runs on a different port (e.g., localhost:5200) than the API (e.g., localhost:7100). +// Browsers block cross-origin requests by default for security. +// We define a named policy that allows only our client origin. +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowBlazorClient", policy => + { + policy.WithOrigins("https://localhost:5200") // WASM client origin + .AllowAnyHeader() + .AllowAnyMethod(); + }); +}); + +// ... after builder.Build() +app.UseCors("AllowBlazorClient"); +``` + +### Pattern 4: Base URL Configuration (Claude's Discretion) +**Recommendation:** Use `appsettings.json` in the WASM client for the API base URL. This is standard for Blazor WASM and the file is in `wwwroot/` (public, no secrets). Environment variables are harder to configure for WASM since it runs in the browser. + +```csharp +// wwwroot/appsettings.json (Client project) -- PUBLIC config only +// In Blazor WASM, appsettings.json is a static file served to the browser. +// NEVER put secrets here. Only public configuration like API URLs. +{ + "ApiBaseUrl": "https://localhost:7100" +} + +// Program.cs -- reading the config +var apiBaseUrl = builder.Configuration["ApiBaseUrl"] + ?? throw new InvalidOperationException("ApiBaseUrl not configured"); +builder.Services.AddHttpClient(client => +{ + client.BaseAddress = new Uri(apiBaseUrl); +}); +``` + +### Anti-Patterns to Avoid +- **Any `System.IO.File` usage in the Client project:** WASM runs in a browser sandbox; file I/O does not persist. All persistence is server-side. +- **Any API key or secret in `wwwroot/appsettings.json`:** This file is publicly accessible. Secrets belong in the API project only. +- **OpenAI SDK registration in the Client `Program.cs`:** All OpenAI calls go through the backend API. The Client never talks to OpenAI directly. +- **CORS wildcard (`AllowAnyOrigin`):** Even for local dev, scope to the actual client origin. Prevents accidental exposure. +- **Logic in `.razor` code blocks:** Components call services; services own logic (D-04 enforces this via typed client pattern). + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| HTTP client management | Manual `HttpClient` instantiation in components | `IHttpClientFactory` via `AddHttpClient` | Proper socket management, DI lifecycle, testability | +| JSON serialization | Manual string building | `System.Text.Json` (built-in) with `GetFromJsonAsync`/`PostAsJsonAsync` | Type-safe, trim-compatible, zero dependencies | +| CORS handling | Custom middleware or response headers | `builder.Services.AddCors()` + `app.UseCors()` | Handles preflight OPTIONS requests, header management automatically | +| Project references | Copy-paste DTOs between projects | `dotnet add reference` to Shared project | Single source of truth; compile-time verification | + +## Common Pitfalls + +### Pitfall 1: Wrong Port in CORS or Client Config +**What goes wrong:** WASM client gets CORS errors because the API CORS policy specifies a different port than the client actually runs on. +**Why it happens:** `dotnet new blazorwasm` and `dotnet new webapi` assign random ports in `launchSettings.json`. Developer hardcodes one set of ports but the templates use different ones. +**How to avoid:** After project creation, check `Properties/launchSettings.json` in BOTH projects. Align the CORS origin in the API with the actual client URL. Or set explicit ports in `launchSettings.json`. +**Warning signs:** Browser console shows `Access-Control-Allow-Origin` errors. + +### Pitfall 2: Missing `UseCors()` Middleware Order +**What goes wrong:** CORS policy is registered in DI but `app.UseCors()` is called after `app.MapControllers()`, causing CORS headers to not be applied. +**Why it happens:** ASP.NET Core middleware order matters. CORS must run before routing/endpoints. +**How to avoid:** Call `app.UseCors("PolicyName")` before `app.MapControllers()` and before `app.UseAuthorization()`. +**Warning signs:** CORS errors despite correct policy configuration. + +### Pitfall 3: IL Trimming Warnings on Publish +**What goes wrong:** `dotnet publish` for the WASM project produces IL trimmer warnings about types that may be removed. +**Why it happens:** Blazor WASM uses IL trimming in Release mode. Types used only via reflection (JSON serialization) can be trimmed away. +**How to avoid:** For Phase 1, the surface area is tiny (one DTO). Verify `dotnet publish` completes without warnings. In later phases, use `[JsonSerializable]` source generators. +**Warning signs:** `dotnet publish` output contains lines with `IL2xxx` warning codes. + +### Pitfall 4: Solution File Path Issues +**What goes wrong:** `dotnet build` from repo root fails because `ChatAgent.sln` references projects with wrong relative paths. +**Why it happens:** Solution was created in a subdirectory, or projects were moved after being added. +**How to avoid:** Create the solution at repo root, add projects using their paths relative to the sln file location. +**Warning signs:** `The project file could not be found` errors. + +### Pitfall 5: WASM Project Serves on HTTPS But Certificate Not Trusted +**What goes wrong:** Browser shows security warning when accessing WASM app, blocking API calls. +**Why it happens:** Dev HTTPS certificate not installed or not trusted. +**How to avoid:** Run `dotnet dev-certs https --trust` once before starting development. Alternatively, use `--no-https` flag during project creation for Phase 1 simplicity and add HTTPS later. +**Warning signs:** Browser shows "Your connection is not private" page. + +## Code Examples + +### Shared DTO (ChatAgent.Shared) +```csharp +// Models/HealthResponse.cs +// This is a shared model (DTO - Data Transfer Object). +// It lives in the Shared project so both the Client and Api can use it +// without duplicating the class definition. When the API returns this object, +// the Client can deserialize it into the same type. +namespace ChatAgent.Shared.Models; + +public class HealthResponse +{ + /// + /// Server health status (e.g., "healthy"). + /// + public string Status { get; set; } = string.Empty; + + /// + /// Server timestamp -- proves we're getting a live response, not a cached one. + /// + public DateTime Timestamp { get; set; } +} +``` + +### WASM Program.cs (Client) +```csharp +// Program.cs -- Blazor WASM application entry point +// This is where the WebAssembly application starts. +// WebAssemblyHostBuilder configures: +// 1. Root components (what Razor components to render) +// 2. Services (dependency injection container) +// 3. Configuration (reads from wwwroot/appsettings.json) +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using ChatAgent.Client; +using ChatAgent.Client.Services; + +var builder = WebAssemblyHostBuilder.CreateDefault(args); + +// Add("#app") tells Blazor to render the App component +// inside the
element in wwwroot/index.html. +builder.RootComponents.Add("#app"); + +// HeadOutlet manages elements (title, meta tags) from Razor components. +builder.RootComponents.Add("head::after"); + +// Read the API base URL from configuration (wwwroot/appsettings.json). +// This file is PUBLIC -- never put secrets here. +var apiBaseUrl = builder.Configuration["ApiBaseUrl"] + ?? "https://localhost:7100"; + +// Register ChatApiClient as a typed HttpClient. +// AddHttpClient uses IHttpClientFactory internally, which: +// - Manages HttpClient lifetimes properly (avoids socket exhaustion) +// - Allows per-client configuration (base address, headers) +// - Makes the service injectable via constructor DI +builder.Services.AddHttpClient(client => +{ + client.BaseAddress = new Uri(apiBaseUrl); +}); + +await builder.Build().RunAsync(); +``` + +### API Program.cs (Server) +```csharp +// Program.cs -- ASP.NET Core Web API entry point +// This is the backend server that the Blazor WASM client talks to. +// It will eventually proxy OpenAI calls and manage JSON file storage. +// In Phase 1, it only serves a health check endpoint. +var builder = WebApplication.CreateBuilder(args); + +// AddControllers() registers MVC controller services. +// We use Controllers (not Minimal API) for explicit structure (D-05). +builder.Services.AddControllers(); + +// CORS (Cross-Origin Resource Sharing) configuration. +// The WASM client runs on a different origin (different port), +// so the browser blocks requests unless the server explicitly allows them. +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowBlazorClient", policy => + { + // TODO: Read from configuration instead of hardcoding + policy.WithOrigins("https://localhost:5200") + .AllowAnyHeader() + .AllowAnyMethod(); + }); +}); + +var app = builder.Build(); + +// Middleware order matters in ASP.NET Core. +// CORS must be applied before routing and authorization. +app.UseCors("AllowBlazorClient"); + +app.UseAuthorization(); + +// MapControllers() discovers all [ApiController] classes +// and maps their [HttpGet], [HttpPost], etc. attributes to routes. +app.MapControllers(); + +app.Run(); +``` + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| Hosted Blazor WASM template (Client+Server+Shared in one template) | Separate projects added to a manual solution | .NET 8 (Nov 2023) | Must create three projects manually; no `--hosted` flag | +| `Newtonsoft.Json` for serialization | `System.Text.Json` built into .NET | .NET Core 3.0+ | No extra dependency; source generators for trim safety | +| `HttpClient` registered directly as Singleton/Scoped | `IHttpClientFactory` via `AddHttpClient` | .NET Core 2.1+ | Proper socket management; typed clients for clean architecture | + +## .NET Version Decision (Claude's Discretion) + +**Recommendation: Target .NET 9 (`net9.0`)** + +Rationale: +- CLAUDE.md explicitly specifies .NET 9 as the target +- .NET 9.0.312 SDK is installed on this machine +- .NET 10 SDK (10.0.201) is also available but is not yet at LTS; using it in a tutorial project risks encountering preview-stage breaking changes +- All recommended packages (OpenAI 2.9.1, Markdig 1.1.1, MudBlazor 9.2.0) are confirmed compatible with .NET 9 +- The `blazorwasm` template with `--framework net9.0` is verified working + +## Validation Architecture + +### Test Framework +| Property | Value | +|----------|-------| +| Framework | None yet (greenfield) | +| Config file | None -- see Wave 0 | +| Quick run command | `dotnet build ChatAgent.sln` | +| Full suite command | `dotnet build ChatAgent.sln && dotnet publish src/ChatAgent.Client/ChatAgent.Client.csproj -c Release --nologo` | + +### Phase Requirements -> Test Map +| Req ID | Behavior | Test Type | Automated Command | File Exists? | +|--------|----------|-----------|-------------------|-------------| +| CODE-01 | Every file has inline comments explaining Blazor concepts | manual | Manual review of all `.cs` and `.razor` files | N/A | +| CODE-02 | Phase introduces one concept incrementally | manual | Manual review -- Phase 1 concept = "solution structure and project boundaries" | N/A | +| (SC-1) | `dotnet run` starts both projects without errors | smoke | `dotnet build ChatAgent.sln` (build verification) | N/A -- Wave 0 | +| (SC-2) | WASM client reaches API server (CORS working) | integration | Manual -- start both projects, verify health endpoint from client UI | N/A | +| (SC-3) | Shared models referenced by both projects | build | `dotnet build ChatAgent.sln` (compile-time check) | N/A -- Wave 0 | +| (SC-4) | `dotnet publish` completes with no IL trim warnings | smoke | `dotnet publish src/ChatAgent.Client/ChatAgent.Client.csproj -c Release --nologo 2>&1 \| grep -c "warning IL"` | N/A -- Wave 0 | +| (SC-5) | Every file contains inline comments | manual | Manual review | N/A | + +### Sampling Rate +- **Per task commit:** `dotnet build ChatAgent.sln` +- **Per wave merge:** `dotnet build ChatAgent.sln && dotnet publish src/ChatAgent.Client/ChatAgent.Client.csproj -c Release --nologo` +- **Phase gate:** Full build + publish clean + manual CORS verification + +### Wave 0 Gaps +- No formal test project needed in Phase 1 -- the verification is build success + publish success + manual CORS check +- The `dotnet build` and `dotnet publish` commands serve as the automated validation +- CODE-01 and CODE-02 are inherently manual-review requirements + +## Environment Availability + +| Dependency | Required By | Available | Version | Fallback | +|------------|------------|-----------|---------|----------| +| .NET 9 SDK | All projects | Yes | 9.0.312 | -- | +| .NET 10 SDK | Not required | Yes | 10.0.201 | -- | +| `dotnet new blazorwasm` template | Client project | Yes | Included in SDK | -- | +| `dotnet new webapi` template | API project | Yes | Included in SDK | -- | +| `dotnet new classlib` template | Shared project | Yes | Included in SDK | -- | +| HTTPS dev certificate | Local HTTPS | Unknown | -- | Use `--no-https` flag or run `dotnet dev-certs https --trust` | + +**Missing dependencies with no fallback:** None. + +**Missing dependencies with fallback:** +- HTTPS dev certificate status is unknown -- if untrusted, can use HTTP for Phase 1 local dev or run the trust command. + +## Open Questions + +1. **Port numbers for local development** + - What we know: Templates assign ports in `launchSettings.json` which vary per creation + - What's unclear: Exact ports until projects are actually created + - Recommendation: After project creation, inspect both `launchSettings.json` files and align CORS config with actual client URL. Consider setting explicit predictable ports (e.g., Client: 5200, API: 7100). + +2. **HTTPS vs HTTP for Phase 1 local dev** + - What we know: HTTPS requires a trusted dev certificate + - What's unclear: Whether the dev cert is already trusted on this machine + - Recommendation: Attempt HTTPS first. If certificate issues arise, fall back to HTTP for Phase 1 (add `--no-https` and use `http://` URLs). HTTPS can be re-enabled in later phases. + +## Project Constraints (from CLAUDE.md) + +- **Tech stack**: .NET / C# / Blazor WebAssembly -- non-negotiable +- **LLM provider**: OpenAI GPT API (not used in Phase 1, but architecture must support it) +- **Storage**: JSON files on local disk (not used in Phase 1, but architecture must support it) +- **Architecture**: WASM client + backend API (API key stays server-side) +- **Code style**: Every Blazor concept introduced must have inline comments explaining what it does and why +- **GSD Workflow**: Use GSD entry points for all file changes + +## Sources + +### Primary (HIGH confidence) +- .NET 9 SDK verified installed: `dotnet --list-sdks` shows 9.0.312 +- `blazorwasm` template verified: `dotnet new list blazorwasm` confirms availability +- `webapi --use-controllers` flag verified: `dotnet new webapi --help` confirms option exists +- Template output verified: Created temporary projects to confirm `Program.cs` structure, `.csproj` contents, and default file layout + +### Secondary (MEDIUM confidence) +- `.planning/research/ARCHITECTURE.md` -- pre-existing project research on WASM/API split patterns +- `.planning/research/STACK.md` -- pre-existing stack research with NuGet-verified versions +- `.planning/research/PITFALLS.md` -- pre-existing pitfalls research from official GitHub issues + +### Tertiary (LOW confidence) +- None -- all findings verified against installed SDK and official templates + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH -- verified against installed SDK and templates +- Architecture: HIGH -- three-project structure is well-documented Microsoft pattern; template output confirmed +- Pitfalls: HIGH -- CORS ordering, port mismatch, and IL trimming are well-known ASP.NET Core issues documented in official sources + +**Research date:** 2026-03-27 +**Valid until:** 2026-04-27 (stable -- .NET 9 is mature, templates are not changing)