I recently upgraded one of my projects from .NET 6 to .NET 10, and while the process was smoother than expected, there were several important steps and breaking changes worth documenting. This post summarizes what I did.
With .NET 6 no longer supported and the move to .NET 10 now overdue, it was finally time to bring my project up to date. This was a great opportunity to test the new GitHub Copilot modernization feature.
I git cloned the repo and dropped the “Upgrade my solution to .NET 10” prompt in the copilot and there it was, copilot removed the hassle of upgrading the .net SDK references, the docker file build stages and even upgraded the packages to have a fully buildable solution. This worked exquisitely well, given that copilot explicitly mentions that they support migration from .net 8 onwards.
A couple of "Time Of Your Life" (TOYL) units that I would have expected copilot to save were:
- The upgrade of sln to slnx file which is a simple `dotnet sln migrate` command
- Update the reference from .net 6 in the Readme file like references to the `Release\net6.0` folder and in the URLs there were some links with reference to aspnetcore-3.1 which were updated manually to aspnetcore-10.0. This is one of those housekeeping tasks that’s easy to forget but important for future contributors.
Since the project had already been migrated from .NET Core 3 to .NET 6, I needed to make another significant change: transitioning from the Generic Host model (used widely in .NET Core 3.1 and .NET 5) to the Minimal Hosting model introduced in .NET 6. This update combines the previously separate Program and Startup classes into a single, simplified Program.cs file. Now, service registration and configuration are managed by WebApplicationBuilder, which replaces the older IHostBuilder and IWebHostBuilder abstractions and creates a more straightforward initialization process. As a result, I guided Copilot to update dependency injection and middleware registration to establish a clear starting point. While I plan to refactor and remove the startup file in the future, all orchestration currently occurs within Program.cs, following the modern pattern.
The main breaking change I had to fix manually, it was introduced in .NET 8 (and remains in .NET 10): it now requires explicit configuration of known networks or known proxies for forwarded headers like X-Forwarded-For, X-Forwarded-Host, and X-Forwarded-Proto to work. Without this, these headers are ignored for security and this means that your login redirects or the return urls may be ending up to plain http which will break your oauth flows if you are using nginx to terminate the ssl for you. I added the necessary known network entries to keep my reverse‑proxy setup working properly.
Previously, I depended on the .NET CLI secret manager for sensitive configuration, but lately I've found environment variables, especially using .env files, to be much simpler for project management. So, I switched from hardcoding `UserSecretsId` to using the DotNetEnv package. All it took was adding `DotNetEnv.Env.TraversePath().Load();` in the Main method, which automatically loads the .env file from the solution root. The .env file is ignored by Git, while a `.env.sample` is included in the repository to help developers quickly see what environment variables are supported. This approach keeps configuration organized and tidy.
Finally, I needed to revise my CI/CD process. I implemented a 'combine PR' GitHub workflow that merges multiple Dependabot pull requests into one. Additionally, I updated Dependabot to handle not just NuGet dependencies, but also Docker images, the .NET SDK, and even GitHub Actions that need periodic updates. Hopefully this will save some TOYL units for me and keep my pet project secure.
Hope this helps!
No comments:
Post a Comment