Executive Summary
In this article, I'll discuss the state of software upgrade and modernization projects, and how these activities are evolving. Then I'll dive into the tools currently available from Microsoft to help with these efforts. I'll cover the Upgrade Assistant extension for Visual Studio, the brand new, AI-powered GitHub Copilot app modernization: upgrade for .NET, which works with VS, VS Code, and in the CLI. Then I'll talk about how to take advantage of new tooling for modernization like .NET Aspire once your applications are up to date. I'll explain when and how each tool is useful. I'll walk through examples using each tool and I'll show how to use them together to erase technical debt and make all your software up to date and modern in a surprisingly short time.
Introduction
Software upgrade and modernization projects bring up many emotions and visceral reactions, and not the good kind. But that's changed for me recently. I've done an unusually large number of upgrade and modernization projects in my career, and I want to share the story of my most recent, and most gratifying. These types of projects spring from an old mindset that software development is project based with a start and an end, but they also offer an opportunity. Once upgraded, technical debt is reduced or eliminated, and the apps can be modernized and maintained on an equal footing with new applications.
When it comes to philosophies of software development, the companies I've worked with fall into 1 of 3 categories:
- Software development is a project.
- Software is re-imagined and re-created on a regular basis.
- Software development is a never-ending cycle.
The project-based mindset is old-school. Define a project, implement it. Release patches when necessary and updates if, and when we can. This is where modernization projects come from. Thankfully, this category is shrinking, but in my observation at a custom software consulting business, it's still the largest category in the enterprise software world.
This approach comes with frustration with the traditional project-based approach. The technologies, libraries and frameworks used are patched and updated constantly. Running old, insecure versions is a risk, but implementing, testing and rolling out new versions is difficult and expensive. Users expect all software to have the convenient features they get in their consumer apps. The users' work evolves constantly, and they need changes and new features constantly to get their work done. Frameworks and tools change all the time too. A telling example is that it's often faster and cheaper to re-write code using a new framework or a new major version of the same framework, than to upgrade. Big upgrade projects are difficult, slow and expensive, so they tend to not happen.
About a decade ago I started working with clients in the second category. These forward-thinking clients, instead of living with frustration, embraced their situation and began building software with the expectation that it would last 2-5 years and then be re-imagined, re-written and replaced, lock, stock, and barrel. They gained the competitive advantage of having the best software, the happiest development staff and the least technical debt.
More recently, I'm working with companies in the third category, who implement their ideas in software and then put them on a never-ending development cycle. I see that some enterprises are adopting this approach, pioneered by consumer-facing mobile app companies. To illustrate this point, I have some of the same apps on my iPhone today that I installed on my iPhone 4 when I got it 15 years ago. I doubt there is a single asset or line of code that still exists in any of those apps from 2010, but I've used these apps continuously since then, and I expect to keep using them for another 15 years. These apps are constantly updated, many of them weekly, to provide bug fixes, security patches, new features, and sometimes even complete tech stack changes or user experience overhauls. These companies enjoy many of the advantages I mentioned above but also don't have to contend with massive budget outlays since they've converted to a model that's continuous, steady and predictable.
Unfortunately, most of my enterprise clients still fall into the old-school category, which means I spend a lot of time upgrading and modernizing old systems, trying to get them into one of the newer categories, which is not the most fun, rewarding, or the cheapest work one can do. Fortunately, this work has been getting easier, and I want to share some of the tools and techniques I use. In this article, I'm going to walk you through some modernization projects I recently completed. One is based on an ASP.NET 4.6 application that initially, I couldn't even load onto my machine. Over a week, the application was updated to ASP.NET 9, with all the latest dependencies. It's more manageable, faster, and more secure than ever. In another upgrade, I was able to use AI to do almost all the work on a slightly newer ASP.NET Core app. This is the future of upgrades and modernization.
Upgrade Assistant
Before they can be modernized, applications need to be upgraded to a current version of .NET. When I retrieved the source code for the first upgrade, I found it in an old TFS repo on an on-premises server. As it turns out there is no TFS source control plug-in for Visual Studio 2022 for ARM64, so I downloaded the source as a .ZIP file from the TFS website, created a new Git repo and uploaded the code. First problem solved. Next, I opened the solution to ensure I could build and run it, so I had a good starting point for updating. Visual Studio warned me the .NET Framework 4.6.2 dev package wasn't installed and that I would need to either download it or upgrade to 4.8. I decided to download 4.6.2 as it seemed safer. I could now open the solution. Second problem solved. But it didn't run. I traced the problem back to a NuGet package that only ran on x86, so I created an x86 build profile. Third problem solved. After some fiddling with configuration files, I was able to get the site to run on my development machine. I created a new branch and pushed, preserving a clean starting point. I was now ready to START the upgrade process.
In the past, I would have created a new ASP.NET 9 project with a corresponding new Git repo and slowly started copying bits from the legacy site into a new branch, updating NuGet packages and other references and namespaces until I could get it to function. I would then merge each new branch as I completed and tested it. Every upgrade project, whether it was ASP.NET, Windows Forms, WCF Services, or some other project type, was an adventure (not the fun kind), and I would tackle each challenge as it appeared. I had little insight into what was coming, and I found it extremely difficult to estimate the effort each upgrade would require. I would base estimates on what I did know, such as the number of files and lines of source code as well as previous experiences where I'd learned some things to look out for in past upgrades.
This time, however, I was armed with the Upgrade Assistant extension for Visual Studio.
Originally released in February 2023, the tool has evolved to support ASP.NET (except WebForms), Azure Functions, WPF, WinForms, Class Libraries, Console, Xamarin.Forms, .NET MAUI, and UWP. WCF support is expected, but not baked in yet. I loaded the solution in Visual Studio, right-clicked on it in the Solution Explorer, chose “Upgrade”, and chose to create a new report. Note that choosing a project instead of a solution has a different effect. More on that later. Since I can't publish my clients' actual code, I created a new ASP.NET 4.6.2 MVC project for this article. See Figure 1.

I chose all the projects I wanted to report on as shown in Figure 2. In this case, there was only a single project, but in my experience, some solutions have had dozens of projects.

I was asked to choose a target framework which can be as early as .NET 6. I chose .NET 9, which is the current STS version shown in Figure 3.

I chose to analyze source code and settings as well as Binary dependencies and started the analysis as shown in Figure 4.

The tool churned for a while and created something I didn't have when I was doing updates by hand—a report outlining nearly all the work I would have to do. This feature alone helped make my upgrade estimates both easier and more accurate. Figure 5 shows the generated report.

By drilling into the Aggregate issues, I could see all the work the tool had identified, how critical it was to fix or upgrade, and how many instances I was up against. It even gave a pretty good estimation of Story Points, though in this simple example, none of the incidents was worth more than one story point, as shown in Figure 6.

Drilling into an issue (Figure 7), shows a very nice explanation of the issue and how I could approach mitigating it. There is even a link to take me directly to every instance of the issue in source code. Plus, there's an Ask Copilot button which further explained the issue, each of my options, and let me ask questions about them until I had an idea of the best way to move forward.

How I wish I'd had this tool years ago! But wait, there's more! So far, the Upgrade Assistant has been a passive tool, analyzing and reporting and presenting information so I could do the work. Remember when I said you get a different result if you right-clicked on a project instead of the solution? Figure 8 shows what that looks like.

This part of the Upgrade Assistant does some of the work for you. The In-place project upgrade option is great for small projects, but I typically don't use it for projects of any size as it's too easy to break the project and not be able to build and run it at all for extended periods. For most projects (like this ASP.NET Framework upgrade), I use the Side-by-side incremental upgrade. It creates a brand new, mostly empty project with the new SDK-style project file which runs right away and can be put into its own source control repo. Optionally, you can create a new target project, customize it, and use that as the new target. I find that useful when I'm converting a lot of applications for the same client and they all need to target a similar base project. I chose the project type (MVC) and target framework (.NET 9) and ran the upgrade.
It's important to point out that this does not actually update the old source code. It creates a new ASP.NET 9 project in the solution, sets both the original project and the new project as Startup projects, and directs the legacy project to not open a browser. The new ASP.NET 9 project opens a browser and then passes all calls through to the legacy application via a YARP proxy. If you're unfamiliar with YARP (Yet Another Reverse Proxy), it's a .NET library that sits in front of your web sites and routes request between them. It can also do load balancing, caching, SSL/TLS termination, and handle security. When you run the solution, the legacy site runs as it always has, but calls can selectively be passed to the new site instead. Now you can convert the legacy site a bit at a time, putting the new code into the new project and turning off the redirection for each piece as they're completed and tested. There is even some interop available to help you share things like session state between the two sites during the transition and some shims to imitate obsolete, legacy calls during the transition.
Once you have the new setup running, the tool provides more help. There are upgrade wizards for Controllers, Views and Classes. If this tab is closed and you need to get back to it, select the project in the Solution Explorer and right-click to run the Upgrade tool again. It's not an all at once conversion, you upgrade them one at a time, but it does take a lot of the grunt work out of the process for you. You'll still need to do some work on each after conversion. In my experience, almost none of the converted files run right off the bat, but I've still saved a lot of time, and the issues are usually not very difficult to address. Many of the changes are familiar as I've done this process manually for years, but I've also found the GitHub Copilot Chat window to be quite helpful when I encounter a new challenge. I just have to ask a question or two and it helps.
One thing I've found particularly helpful is to export the upgrade report to HTML, CSV, and/or JSON so it can be searched easily. Though you can search the files in your solution, there is no search feature in the VS IDE for the upgrade report. If I'm having an issue with the Microsoft.AspNet.Identity
namespace after conversion for instance, I can Open the HTML report in the browser and <Ctrl><F>
in the Aggregate Issues page (with the issues expanded) to search, and it takes me right to the instances in the nicely formatted report where that's mentioned. This lets you see the notes, suggestions and other instances of the issue in case you want to fix them all at once. Of course, you can also search the exported files directly tool.
GitHub Copilot App Modernization: Upgrade for .NET
If you're like me, you're probably wondering why Microsoft hasn't infused the Upgrade Assistant tool with AI like it's done with virtually everything else. The answer is it has! Released during BUILD 2025, the new AI powered extension for VS 2022 can be found at https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.GitHubCopilotUpgradeAgent. Aside from the questionable name, the new tool is quite good, but there are a few caveats, which you can learn about here. Some of the interesting caveats are that you must be running VS 2022 v17.14 or later with the .NET Desktop workload installed, you have to enable “agent mode” for GitHub copilot in (Tools Options GitHub), it only works with C# projects and Git repos, and you may have to restart VS a couple of times to get it all to work.
Also note that while you can upgrade any version of .NET Core or .NET to a newer version, it does not (yet) support converting full framework applications as of this writing. It does support all ASP.NET flavors such as MVC, Razor Pages, and Web API, Blazor, Azure Functions, WPF, Windows Forms, Class Libraries, and Console Apps.
For all the caveats in this v1 release, what it does do is simply astounding. I'm using a simple ASP.NET Core 3.1 MVC application as my second example for this article, but it worked very much the same on a real ASP.NET Core MVC application I recently upgraded using the tool. First, open your Git connected solution and make sure you have the latest code in the branch you've chosen. Next, go to your Copilot window and make sure Agent mode is selected, then make sure the .NET Upgrade tool is enabled as shown in Figure 9.

If these are not set in Copilot, it will still work to some extent, but Copilot will not be able to take any action on your behalf. You'll have to do all the changes yourself. I've used OpenAI's GPT4.1 in this article, but I've noticed what seems to be even better results using the latest Claude model as of this writing.
You can now right-click on the solution file and choose Upgrade with GitHub Copilot from the context menu, which types a prompt into the Copilot window that says, “Help me upgrade this solution to a newer version of .NET”, or you can skip the menu and just type in your own prompt. Copilot churns for a moment and then starts a dialog with you. If your prompt didn't already specify, it will ask you which version of .NET you want to upgrade to and will list all the versions currently supported as links so you can click on one to enter your selection in the chat window and continue the conversation. I chose .NET 9.0, then it asked me which branch I wanted to use, and I responded with master. It then suggested upgrade-to-NET9 as the name of the new branch and asked me to confirm. I responded, “sounds good”. It then recapped that I wanted to upgrade to .NET 9 and listed the NuGet packages it would upgrade and told me it was going to generate an upgrade plan, which is a markdown file that lists all the steps it's suggesting it takes next. But after telling me about the planned upgrade including information about the project, it erroneously responded that there were no projects in the solution. Because it's a natural language conversation, I typed in the name of the project and asked it to generate the plan. It was a minor hiccup that was easily solved.
The plan generation step is important, because the assistant will follow the upgrade plan when you proceed. What may not be obvious is that you can edit the plan and tweak it to your preferences. In my case, the original developer had used a freeware NuGet package which, at some point, became a paid package. It supported a minor feature, and the program owner didn't want to upgrade to the paid version, so I modified the plan to only upgrade to the newest freeware version. It's important to know that you can do a lot of customization to the plan and the assistant will follow the plan as you approve it, not as Copilot originally wrote it. You can see the plan Copilot generated for me in Figure 10.

After my minor tweak to the plan, I typed “continue” and watched as the magic happened. First, Copilot realized that I didn't have the .NET 9 SDK installed on the machine, so it installed the .NET 9 SDK for me. Then it proceeded with the upgrade plan and logged everything it did, as well as every change it made, as seen in Figure 11 and Figure 12.


For this sample ASP.NET Core project, the upgrade was straightforward, but my real-world project was a bit more complex, and Copilot stopped a couple of times to chat with me about how I wanted to handle certain changes. That solution also had some unit and integration test projects, so it converted those too and ran all the tests for me after the upgrade. I did have to do some sleuthing on that more complex project to get it all running, but I found it easy work because Copilot did a lot of the research, made a lot of recommendations and did most of the code fixes for me. Compared to the old days, it was bliss. I found that once I fixed an issue once or twice, Copilot took the hint and took it upon itself to make the same change in other places for me. Wow!
I found myself in my new upgrade-to-NET9 branch with a fully working website in a few short hours. As I inspected the code, I found that it had skipped updating a few deprecated features and missed a couple of other modernizations I wish it had done for me, but I had those cleaned up quickly. Unfortunately, the tool doesn't yet remember all the tweaks and changes I'd made for this conversion when I started the next one, but the GitHub Copilot app modernization - upgrade for .NET team has already confirmed to the community that's on the roadmap. I expect this tool will improve both quickly and dramatically as people like me start using it and the team at Microsoft starts getting our feedback. All in all, the upgrade using GitHub Copilot Chat happened much faster and required much less work from me than the Upgrade Assistant.
I'm really looking forward to Copilot supporting upgrades from the full .NET Framework. I've even put some of those projects on hold, knowing how much easier they'll be when that happens. For now, I'm finding the Copilot Chat window in Agent mode already handles a lot of the changes when moving from Framework to .NET, but I have to ask one issue at a time. I recommend installing GitHub Copilot app modernization - upgrade for .NET if you're planning on using the Upgrade Assistant to upgrade a .NET Framework project.
After the initial upgrades of these projects, I was left incrementally migrating my 4.6.2 framework project to Core 9. That work took time, but it was approachable. I also had a functional solution running on Core 9 for the ASP.NET Core 3.1 project, but there was some technical debt and some things that could be improved.
.NET Aspire
Now that I had everything running in .NET 9 I could start using new tools like .NET Aspire https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview#project-templates-and-tooling for centralized logging and telemetry and a better debugging experience. I added a project reference to an existing ASP.NET 9 Web API project that the legacy application calls so I could run and troubleshoot them in the same solution. Because .NET Aspire project templates are part of Visual Studio, VS Code, and the .NET CLI, I simply right-clicked on my LegacyASPNetCoreWebApplication
and chose Add .NET Aspire Orchestrator Support… as shown in Figure 13.

VS notified me that it would create two projects, a project with the suffix .AppHost
and another with .ServiceDefaults, as shown in Figure 14.

It set the .AppHost
project as the startup project, but I got an error message (Figure 15) that said, “Adding the .NET Aspire orchestration code failed. Failed to insert code for project LegacyASPNetCoreWebApplication. Can't find a call to method(s) Build on type Microsoft.AspNetCore.Builder.WebApplicationBuilder
in the program main”.

When I looked at the project more closely, I saw that it was still using the old IHostBuilder
method of starting up, including a separate Startup.cs
file, which is a bit too old for Aspire to support. I made a mental note to edit the upgrade plan next time to have Copilot update the code to the more modern method. Then I asked copilot, "Can you change the startup code to use the modern WebApplication.CreateBuilder()
method used in .NET 9?", which it did after asking my permission! I re-ran the .NET Aspire orchestrator support tool with the updated code. I then ran the tool on the Web API project I added a reference to and pressed <F5>
. The Aspire dashboard came up with both projects running, telemetry and everything. I noticed that Aspire was not the newest version released at BUILD, so I updated the Aspire NuGet package and ran it again. The newest version has Copilot support built right in, so I really wanted the update. When I later encountered a runtime error, I asked Copilot about it and Copilot gave me a great summary of the problem based on the logs and telemetry and made a suggestion to fix the issue.
Summary
Until (and if) everyone embraces the reimagine and recreate and continuous cycle approaches to software development, there will be legacy upgrade and modernization projects, but every day, they're getting a little bit easier. Tools like the Upgrade Assistant, GitHub Copilot Upgrade Assistant for .NET, and .NET Aspire are making it easier than ever to upgrade and modernize legacy code. They can get you on the path to a new approach to development and shed that technical dept. In this article, I showed you how a pair of modernization projects I was not looking forward to turned into a pleasant and gratifying learning experience, how I saved a lot of time, and how I ended up with a radically modernized, up-to-date, and secure application that eliminated technical debt. The upgraded systems are a pleasure to work on with AI-assisted monitoring and troubleshooting. If you're looking at taking on similar projects, or are just interested in the technology, I highly recommend installing the VS or VS Code Upgrade Assistant extension for .NET Framework upgrades, GitHub Copilot Upgrade Assistant for .NET for every upgrade and, especially if you're working on distributed systems, I recommend .NET Aspire.