From c76796c136722b313ea40b7c75dc3465c1a868bc Mon Sep 17 00:00:00 2001 From: karluiz Date: Thu, 11 Jun 2026 09:10:25 -0500 Subject: [PATCH 1/5] docs: add .NET 10 migration plan --- .../plans/2026-06-11-migrate-to-net10.md | 313 ++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-11-migrate-to-net10.md diff --git a/docs/superpowers/plans/2026-06-11-migrate-to-net10.md b/docs/superpowers/plans/2026-06-11-migrate-to-net10.md new file mode 100644 index 0000000..6c8e599 --- /dev/null +++ b/docs/superpowers/plans/2026-06-11-migrate-to-net10.md @@ -0,0 +1,313 @@ +# .NET Core 1.1 → .NET 10 Migration Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate the three-project solution (GR.Repository, GR.Services, GR.Mvc) from .NET Core 1.1 to .NET 10 (LTS) with MongoDB.Driver 3.x and the minimal-hosting model. + +**Architecture:** Bottom-up migration following the project dependency graph: GR.Repository (class lib, MongoDB) → GR.Services (class lib) → GR.Mvc (ASP.NET Core MVC web app). Each project is retargeted, its packages updated, and the build verified before moving up. The web app's `Program`/`Startup` pair is replaced by a single minimal-hosting `Program.cs`. Obsolete tooling (bower, BrowserLink, BundlerMinifier, Application Insights 2.0, `DotNetCliToolReference`) is removed; the static files already committed under `wwwroot/lib` keep working as-is. + +**Tech Stack:** .NET 10 SDK (10.0.101 installed locally), ASP.NET Core 10 (framework reference, no Microsoft.AspNetCore.* packages), MongoDB.Driver 3.x. + +**Environment notes:** +- Verify build with `dotnet build GR.Mvc.sln` after every task. +- There are no existing tests, so verification is compile + run + manual smoke. MongoDB connection string in `appsettings.json` is a placeholder — runtime smoke test only confirms the app starts and serves `/Home/About` (no DB) unless a real MongoDB is available (`docker run -d -p 27017:27017 mongo` works for a full CRUD smoke test). +- Commit after each task. + +--- + +### Task 1: Pin the SDK and retarget GR.Repository to net10.0 with MongoDB.Driver 3.x + +**Files:** +- Create: `global.json` +- Modify: `GR.Repository/GR.Repository.csproj` + +- [ ] **Step 1: Create `global.json` at repo root** + +```json +{ + "sdk": { + "version": "10.0.101", + "rollForward": "latestFeature" + } +} +``` + +- [ ] **Step 2: Replace `GR.Repository/GR.Repository.csproj` contents** + +```xml + + + + net10.0 + disable + disable + + + + + + + +``` + +(`Nullable`/`ImplicitUsings` stay off to avoid touching every source file; existing explicit `using` directives keep working. Check `dotnet package search MongoDB.Driver --exact-match` for the actual latest 3.x and use that version.) + +- [ ] **Step 3: Build the project** + +Run: `dotnet build GR.Repository/GR.Repository.csproj` +Expected: likely SUCCESS. MongoDB.Driver 2.4→3.x is a major-version jump, but this repo only uses `MongoClient`, `GetDatabase`, `GetCollection`, `InsertOneAsync`, `ReplaceOneAsync`, `Find`, `FindOneAndDeleteAsync`, `Builders.Filter`, `BsonClassMap`, `ConventionPack`, `ObjectId` — all still present in 3.x. If a compile error appears, fix only the signature that broke (e.g. `ReplaceOneAsync` overload ambiguity: pass `(ReplaceOptions)null` instead of `null`). + +Known nullable-overload gotcha in `Repository.cs:102` — if `InsertOneAsync(entity, null, cancellationToken)` is ambiguous, change to: + +```csharp +await Collection.InsertOneAsync(entity, options: null, cancellationToken: cancellationToken); +``` + +and in `Repository.cs:125`, if `ReplaceOneAsync(idFilter, entity, null, cancellationToken)` is ambiguous: + +```csharp +var result = await Collection.ReplaceOneAsync(idFilter, entity, (ReplaceOptions)null, cancellationToken); +``` + +- [ ] **Step 4: Commit** + +```bash +git add global.json GR.Repository/ +git commit -m "chore: retarget GR.Repository to net10.0, MongoDB.Driver 3.x" +``` + +--- + +### Task 2: Retarget GR.Services to net10.0 + +**Files:** +- Modify: `GR.Services/GR.Services.csproj` + +- [ ] **Step 1: Replace `GR.Services/GR.Services.csproj` contents** + +```xml + + + + net10.0 + disable + disable + + + + + + + +``` + +- [ ] **Step 2: Build** + +Run: `dotnet build GR.Services/GR.Services.csproj` +Expected: SUCCESS (pure C# code, no framework-specific APIs). + +- [ ] **Step 3: Commit** + +```bash +git add GR.Services/GR.Services.csproj +git commit -m "chore: retarget GR.Services to net10.0" +``` + +--- + +### Task 3: Retarget GR.Mvc to net10.0 and adopt minimal hosting + +**Files:** +- Modify: `GR.Mvc/GR.Mvc.csproj` +- Modify: `GR.Mvc/Program.cs` +- Delete: `GR.Mvc/Startup.cs` +- Modify: `GR.Mvc/appsettings.json` +- Modify: `GR.Mvc/Properties/launchSettings.json` + +- [ ] **Step 1: Replace `GR.Mvc/GR.Mvc.csproj` contents** + +All `Microsoft.AspNetCore.*` and `Microsoft.Extensions.*` packages come from the `Microsoft.NET.Sdk.Web` framework reference now — no PackageReferences needed. BrowserLink, CodeGeneration.Tools (`DotNetCliToolReference` is dead since SDK 2.x), `PackageTargetFallback`, and ApplicationInsights 2.0 are dropped. + +```xml + + + + net10.0 + disable + disable + + + + + + + + +``` + +- [ ] **Step 2: Replace `GR.Mvc/Program.cs` with minimal hosting (absorbs Startup.cs)** + +```csharp +using GR.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllersWithViews(); +builder.Services.AddSingleton( + new TicketService(builder.Configuration.GetConnectionString("MongoConnectionString"))); + +var app = builder.Build(); + +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Home/Error"); +} + +app.UseStaticFiles(); +app.UseRouting(); + +app.MapControllerRoute( + name: "default", + pattern: "{controller=Ticket}/{action=Index}/{id?}"); + +app.Run(); +``` + +Notes: `WebApplication.CreateBuilder` already loads `appsettings.json`, `appsettings.{Env}.json`, environment variables, and console logging — the old `Startup` constructor and `loggerFactory.AddConsole/AddDebug` code is fully replaced. `UseDeveloperExceptionPage` is automatic in Development. + +- [ ] **Step 3: Delete `GR.Mvc/Startup.cs`** + +```bash +rm GR.Mvc/Startup.cs +``` + +- [ ] **Step 4: Update `GR.Mvc/appsettings.json` to the modern logging schema** + +```json +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConnectionStrings": { + "MongoConnectionString": "mongodb://localhost:27017/GR" + } +} +``` + +Also update `GR.Mvc/appsettings.Development.json` to the same schema (keep its existing LogLevel values, just drop `IncludeScopes` if present). + +- [ ] **Step 5: Replace `GR.Mvc/Properties/launchSettings.json`** + +```json +{ + "profiles": { + "GR.Mvc": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} +``` + +- [ ] **Step 6: Build the whole solution** + +Run: `dotnet build GR.Mvc.sln` +Expected: SUCCESS, 0 errors. Warnings about obsolete MVC patterns are acceptable. + +Likely error to fix: `GR.Mvc/Controllers/HomeController.cs` `Error()` action may reference old patterns — if `Error.cshtml` or the controller fails, the fix is in Task 4. + +- [ ] **Step 7: Commit** + +```bash +git add -A GR.Mvc/ GR.Mvc.sln +git commit -m "feat: migrate GR.Mvc to net10.0 with minimal hosting" +``` + +--- + +### Task 4: Clean out dead tooling (bower, BundlerMinifier) and verify views + +**Files:** +- Delete: `GR.Mvc/bower.json`, `GR.Mvc/.bowerrc`, `GR.Mvc/bundleconfig.json` +- Check: `GR.Mvc/Views/**/*.cshtml`, `GR.Mvc/Views/_ViewImports.cshtml` + +- [ ] **Step 1: Delete bower/bundler config (the built assets in `wwwroot/lib` and `*.min.*` are committed and keep working)** + +```bash +git rm GR.Mvc/bower.json GR.Mvc/.bowerrc GR.Mvc/bundleconfig.json +``` + +- [ ] **Step 2: Verify `_ViewImports.cshtml` contains the tag helper registration** + +It must contain (add if missing): + +```cshtml +@using GR.Mvc +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +``` + +- [ ] **Step 3: Grep views for removed 1.1-era constructs** + +Run: `grep -rn "asp-append-version\|environment names=\|Microsoft.ApplicationInsights" GR.Mvc/Views/` + +- `` → change attribute to `include="Development"` / `exclude="Development"` (old syntax still works but is deprecated; update if found). +- Any `@inject Microsoft.ApplicationInsights...` or `@Html.ApplicationInsightsJavaScript...` lines in `_Layout.cshtml` → delete them (the package was removed in Task 3). + +- [ ] **Step 4: Build solution again** + +Run: `dotnet build GR.Mvc.sln` +Expected: SUCCESS. Razor views compile at build time in .NET 10, so view errors surface here — fix any reported `.cshtml` line directly (typical fixes are the two patterns in Step 3). + +- [ ] **Step 5: Commit** + +```bash +git add -A +git commit -m "chore: remove bower/bundler tooling, fix views for net10.0" +``` + +--- + +### Task 5: Runtime smoke test + +**Files:** none (verification only) + +- [ ] **Step 1: Run the app** + +Run: `ASPNETCORE_ENVIRONMENT=Development dotnet run --project GR.Mvc/GR.Mvc.csproj --urls http://localhost:5000` (background) +Expected: `Now listening on: http://localhost:5000` + +- [ ] **Step 2: Hit a no-database page** + +Run: `curl -s -o /dev/null -w "%{http_code}" http://localhost:5000/Home/About` +Expected: `200` + +- [ ] **Step 3 (optional, requires MongoDB): full CRUD smoke** + +If Docker is available: `docker run -d --name gr-mongo -p 27017:27017 mongo`, then +`curl -s -o /dev/null -w "%{http_code}" http://localhost:5000/` — Expected `200` (Ticket Index hits MongoDB via `GetAll`). +Then `curl -s -X POST http://localhost:5000/Ticket/AddOrUpdate -d "Title=test&Version=0"` and confirm `/` shows the ticket. +Clean up: `docker rm -f gr-mongo`. + +If no Docker/MongoDB, note in the final report that DB paths were not runtime-verified. + +- [ ] **Step 4: Stop the app and commit nothing — report results** + +--- + +### Task 6 (optional hardening, do only if user wants it): fix latent bugs surfaced during migration + +These pre-exist the migration and are out of scope unless requested: +- `TicketService` creates **two** `Repository` instances (base ctor + `_ticketRepository` field, which is never used) — the field can be deleted. +- `TicketController.AddOrUpdate` increments `Version`/sets timestamps that `Repository.Insert`/`Update` set again (double increment). +- `Repository.Pagination` ignores its `orderBy` parameter. From 91eeac8a51ea0e9922cb7280984684d384150211 Mon Sep 17 00:00:00 2001 From: karluiz Date: Thu, 11 Jun 2026 09:12:15 -0500 Subject: [PATCH 2/5] chore: retarget GR.Repository to net10.0, MongoDB.Driver 3.x Co-Authored-By: Claude Fable 5 --- GR.Repository/GR.Repository.csproj | 10 ++++++---- GR.Repository/Repository.cs | 2 +- global.json | 6 ++++++ 3 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 global.json diff --git a/GR.Repository/GR.Repository.csproj b/GR.Repository/GR.Repository.csproj index 8feee41..a1047d9 100644 --- a/GR.Repository/GR.Repository.csproj +++ b/GR.Repository/GR.Repository.csproj @@ -1,11 +1,13 @@ - + - netcoreapp1.1 + net10.0 + disable + disable - + - \ No newline at end of file + diff --git a/GR.Repository/Repository.cs b/GR.Repository/Repository.cs index 7298479..2e36842 100644 --- a/GR.Repository/Repository.cs +++ b/GR.Repository/Repository.cs @@ -122,7 +122,7 @@ private static string BuildCollectionName() entity.Version++; var idFilter = Builders.Filter.Eq(e => e.Id, entity.Id); //Find entity with same Id - var result = await Collection.ReplaceOneAsync(idFilter, entity, null, cancellationToken); + var result = await Collection.ReplaceOneAsync(idFilter, entity, (ReplaceOptions)null, cancellationToken); if (result != null && ((result.IsAcknowledged && result.MatchedCount == 0) || (result.IsModifiedCountAvailable && !(result.ModifiedCount > 0)))) throw new EntityException(entity, "Entity does not exist."); diff --git a/global.json b/global.json new file mode 100644 index 0000000..9eeb428 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.101", + "rollForward": "latestFeature" + } +} From 9bcaf72ac43dd3822d40cbd5d5cb0818e1928d03 Mon Sep 17 00:00:00 2001 From: karluiz Date: Thu, 11 Jun 2026 09:14:32 -0500 Subject: [PATCH 3/5] chore: retarget GR.Services to net10.0 --- GR.Services/GR.Services.csproj | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/GR.Services/GR.Services.csproj b/GR.Services/GR.Services.csproj index 9489197..151137d 100644 --- a/GR.Services/GR.Services.csproj +++ b/GR.Services/GR.Services.csproj @@ -1,11 +1,13 @@ - + - netcoreapp1.1 + net10.0 + disable + disable - \ No newline at end of file + From 412e443027593f04e4f36e2a4188f83e51232824 Mon Sep 17 00:00:00 2001 From: karluiz Date: Thu, 11 Jun 2026 09:17:48 -0500 Subject: [PATCH 4/5] feat: migrate GR.Mvc to net10.0 with minimal hosting Co-Authored-By: Claude Fable 5 --- GR.Mvc/GR.Mvc.csproj | 20 ++------- GR.Mvc/Program.cs | 46 +++++++++++---------- GR.Mvc/Properties/launchSettings.json | 21 ++-------- GR.Mvc/Startup.cs | 58 --------------------------- GR.Mvc/Views/Shared/_Layout.cshtml | 2 - GR.Mvc/appsettings.Development.json | 3 +- GR.Mvc/appsettings.json | 9 ++--- 7 files changed, 36 insertions(+), 123 deletions(-) delete mode 100644 GR.Mvc/Startup.cs diff --git a/GR.Mvc/GR.Mvc.csproj b/GR.Mvc/GR.Mvc.csproj index fc750bb..e766788 100644 --- a/GR.Mvc/GR.Mvc.csproj +++ b/GR.Mvc/GR.Mvc.csproj @@ -1,23 +1,11 @@ - + - netcoreapp1.1 + net10.0 + disable + disable - - $(PackageTargetFallback);portable-net45+win8+wp8+wpa81; - - - - - - - - - - - - diff --git a/GR.Mvc/Program.cs b/GR.Mvc/Program.cs index 3509a7d..ecebef8 100644 --- a/GR.Mvc/Program.cs +++ b/GR.Mvc/Program.cs @@ -1,25 +1,27 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; +using GR.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; -namespace GR.Mvc -{ - public class Program - { - public static void Main(string[] args) - { - var host = new WebHostBuilder() - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseStartup() - .UseApplicationInsights() - .Build(); +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllersWithViews(); +builder.Services.AddSingleton( + new TicketService(builder.Configuration.GetConnectionString("MongoConnectionString"))); - host.Run(); - } - } +var app = builder.Build(); + +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Home/Error"); } + +app.UseStaticFiles(); +app.UseRouting(); + +app.MapControllerRoute( + name: "default", + pattern: "{controller=Ticket}/{action=Index}/{id?}"); + +app.Run(); diff --git a/GR.Mvc/Properties/launchSettings.json b/GR.Mvc/Properties/launchSettings.json index 6ea3db1..f5e2c5c 100644 --- a/GR.Mvc/Properties/launchSettings.json +++ b/GR.Mvc/Properties/launchSettings.json @@ -1,27 +1,12 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:45765/", - "sslPort": 0 - } - }, +{ "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, "GR.Mvc": { "commandName": "Project", "launchBrowser": true, + "applicationUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:45766" + } } } } diff --git a/GR.Mvc/Startup.cs b/GR.Mvc/Startup.cs deleted file mode 100644 index 70b8ddc..0000000 --- a/GR.Mvc/Startup.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using GR.Services; - -namespace GR.Mvc -{ - public class Startup - { - public Startup(IHostingEnvironment env) - { - var builder = new ConfigurationBuilder() - .SetBasePath(env.ContentRootPath) - .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) - .AddEnvironmentVariables(); - Configuration = builder.Build(); - } - - public IConfigurationRoot Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - // Add framework services. - services.AddMvc(); - services.AddSingleton(new TicketService(Configuration.GetSection("ConnectionStrings:MongoConnectionString").Value)); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) - { - loggerFactory.AddConsole(Configuration.GetSection("Logging")); - loggerFactory.AddDebug(); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - app.UseBrowserLink(); - } - else - { - app.UseExceptionHandler("/Home/Error"); - } - - app.UseStaticFiles(); - - app.UseMvc(routes => - { - routes.MapRoute( - name: "default", - template: "{controller=Ticket}/{action=Index}/{id?}"); - }); - } - } -} diff --git a/GR.Mvc/Views/Shared/_Layout.cshtml b/GR.Mvc/Views/Shared/_Layout.cshtml index 3189cca..7e8f918 100644 --- a/GR.Mvc/Views/Shared/_Layout.cshtml +++ b/GR.Mvc/Views/Shared/_Layout.cshtml @@ -1,4 +1,3 @@ -@inject Microsoft.ApplicationInsights.AspNetCore.JavaScriptSnippet JavaScriptSnippet @@ -16,7 +15,6 @@ asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" /> - @Html.Raw(JavaScriptSnippet.FullScript)