At work, I manage a .NET 8 codebase that I recently restructured to use .NET Aspire. The project consists of multiple interconnected websites and web APIs housed in a single monorepo. Aspire helps simplify launching and connecting everything on a developer’s local machine.
Additionally, Aspire made the codebase portable in a way. It is mostly self-contained. Aspire manages the SQL Server which is running a docker container, the databases, the Vite dev servers, and the ASP.NET backend applications.
Aspire is absolutely fantastic for this use case. While I would also love to use to deploy this project, we have other infrastructure constraints that make this an extremely difficult task that is simply not worth the effort. The simplicity of launching everything locally with Aspire is enough value for us… for now.
We did, however, take a hit to productivity recently when the front-end developers noticed that .NET watch was no longer working reliably.
The behavior was weird. Making any change to the code in any ASP.NET project made .NET Watch rebuild and restart the project. Initially, I thought this was due to the devs making “rude edits”. These are certain edits to the code that cannot be hot-reloaded into the already running process. There was one problem here. The edits were nowhere near being rude edits. Instead, they were simple HTML changes in razor or cshtml files.
With Trace log levels enabled, I saw that Aspire was starting child .NET Watch processes for each ASP.NET project it was managing. This should be fine but then I saw that the process was started with --no-hot-reload --non-interactive options. These options, combined, will cause .NET Watch to disable the hot-reload capability and always fallback to rebuild & restart.
This was very weird to me. Why is Aspire doing this? And, is there a way I can influence or change this?
I ended up coming across this Github issue on the .NET SDK repo. Essentially, since the release of .NET 8 in 2023, it was known that .NET Watch has big limitations working with Aspire. .NET Watch needed a number of updates and improvements to be made to allow is to understand Aspire and its concept of resources.
.NET 9 seems to have fixed this. However, our team’s policy is we move from LTS to LTS. Completely skipping over STS releases. Updating to .NET 9 is not possible at this time.
.NET 10 is out, it’s LTS, and according to the policy above we could upgrade and get the fixed .NET Watch version.
There is one very small problem with that. One of things we depend on in this codebase is a CMS called Optimizely (formerly known as EPiServer).
Optimizely are yet to communicate with developers when CMS 12 will support .NET 10. The word on the street now is that .NET 10 support will land with CMS 13 which is shaping up to be a massive upgrade (forced use of GraphQL and external cloud services from Optimizely. Topic for another day.)
When .NET 8 came out, within the week of its release Optimizely had a post up announcing support for it. This time around, silence. My theory is they are so busy Enshitifying their products, including CMS, with AI slop that they can’t spend a bit of time to test if their assemblies are compatible with .NET 10. Hell, they don’t even have to recompile anything. But whatever. You get the point.
Alright, so what can I do then? I tried a few ideas but ultimately nothing worked. I did, however, come across a fact I did not know about .NET.
See, .NET SDKs are backward compatible. An SDK of version X can compile assemblies that are compatible with runtime versions <= X.
In other words, I can use the .NET SDK to build my project for .NET runtime 8. This guarantees compatibility with the CMS dependency and the infrastructure we have in place now.
I have a global.json file that pinned .NET 8 as the SDK. I updated it to pin the latest .NET 10 SDK available at the time of writing this post:
{
"sdk": {
"version": "10.0.200",
"rollForward": "latestFeature"
}
}
I, then, installed the 10.0.200 SDK and built the project. Everything built without any errors or warnings (Treat Warnings as Errors is a must btw).
Then came the time to test .NET Watch. I ran it with this command: dotnet watch run --launch-profile https --verbose -- --configuration Debug. Once everything started up, I made some changes to some cshtml/razor files and some C# code and I could see hot-reload kicking in and updating the running the process without a full rebuild and restart. Success!!!
The next step was to test this on the hosting infrastructure to make sure nothing broke.
We are using Bitbucket Pipelines. I changed the image we are using in the build step to mcr.microsoft.com/dotnet/sdk:10.0. Kicked off a build and watched as everything compiled correctly to .NET 8 runtime compatible binaries. No issues on the hosting infrastructure.
After the move to Aspire and this fix, the developer experience improved drastically on this project which led to real productivity gains. Front-end developers benefit more from this just by the nature of the changes they typically make. On the backend, debugging sessions and some experimentation work has gotten a bit faster as we don’t have to wait for a rebuild and restart when changing some small amounts of code.
I hope the .NET team continues to improve the hot-reload functionality in the SDK so devs don’t hit rude edits as frequently.