Page Icon

Migrating the website to net core - 2025-03-20


Introduction

Some of our customers may have noticed that our website has changed a little over the last few weeks. Well, that was the culmination of our first project of the year - to migrate our website from old .net technology to new .net technology.

For quite a while we had stuck to the old 'if it ain't broke don't fix it' philosophy, but after about a decade of pretty smooth operation it was becoming clear that our old .net 4.8 website was starting to show its age. More to the point it was becoming more and more difficult to support, and some of the packages used in its operation had not been updated in years or were displaying warnings of vulnerabilities. To be clear these were assessed for risk, but still even the time taken to do that was mounting up.

Not to mention that, by not using the latest versions of asp.net and .net, that we were missing out on performance, and possibly cost, benefits.

Goals

Before starting any project of this size it is important to have a solid idea of where you would like to be at the end of it. Having written software for decades I was pragmatic enough to keep our goals quite high level and focus on the most important ones.

So here they are:

  • The site should provide the same functionality as it did before.
  • The site should broadly look and work as it did before.
  • All code should be updated to .net core 8, and asp.net core etc.
  • All packages should be updated to the latest and/or .net core versions.
  • 'Standard' architectural components should be used when available.
  • Only actively used code was to be migrated (over time redundant code had accumulated).

There were also some specific areas that had 'evolved' over time that needed rewriting:

  • The underlying CMS engine had been retro-fitted from various sources after the original site was written, and had not matured well.
  • Publishing product updates was more complicated and disruptive than it needed to be. A whole new feature was designed to solve this.

The Migration Approach

Luckily, when designing and building the original site, I had used a nice layered, service oriented, architecture that I usually use, and roughly followed this structure:

  • Web App
  • Console App
  • Etc.
    • Business logic
      • Services
      • Libraries
    • Product Shared
      • Services
      • Libraries
    • Shared
      • Services
      • Libraries

Phase One - Business Logic

So, to begin with, I installed the '.NET Upgrade Assistant', which is a Visual Studio extension intended to help upgrade libraries to the latest .net version. I then took a copy of the entire project, created an entirely new 'ASP.NET core Web App' solution. Now it this point I could also have decided to create a Blazor/Razor project but decided that I was happy to work with the 'lower level' that a basic web app provides because it fitted more closely to what I already had.

I then considered my starting point.

Bearing in mind that a lift and shift was not what I wanted to do, I decided to start with the most pivotal code in the project: the business logic. So I migrated those projects by right clicking on them and selecting 'Upgrade', and copying the result into the new project, then trying to build.

Ouch - that was painful. I then faced several challenges:

  • All the referenced packages were still their old .net 4.8 versions.
  • All references to previous shared libraries were broken (obviously).
  • Some of the old libraries were based on pre .net core 8 packages.
  • There were quite a few 'Obsolete' references in the code.
  • There were some platform specific references in the code, which .net core does not like.

So the work began by:

  • Deleting package references that were covered by existing functionality in ASP.NET.
  • Updating package references that had new .net core equivalents.
  • Moving referenced library code from the old code base. For the most part I decided to create two projects to contain this code, rather than have a more 'pure' multi-project approach, one in the business layer, and one in the shared layer.
  • In some cases it made more sense to migrate referenced library code as a whole project. Most of the time this was quite smooth, but in some cases upgrading the referenced packages to .net core was painful.

By the end of this phase we had a solution that looked like it contained the business logic and compiled. So naive. Testing would reveal a lot of code that needed further work.

Phase Two - ASP.NET

The basic philosophy of ASP.NET (controller, view, components etc.) has not changed since the earlier .net versions but the architecture and implementation have dramatically changed. To name a few differences:

  • The service/pipeline (middleware) architecture is a nice piece of engineering on which the entire ASP.NET core is based. This needs to be understood.
  • Functionality needs to be explicitly added to this architecture as required. A lot less is implemented as standard, which is great because your solution can be as lean as needed, but sometimes leads to a bit of searching.
  • Standard mechanisms like services, middleware, error handling, logging and routing now handled many aspects of the solution that previously needed to be explicitly implemented or configured.
  • There are many places where the code flows differently. For example, rather than throw an exception for HTTP responses that constitute an error, the new approach is to return a response with said error code set in the http code.
  • A massive jump forward in logical, pragmatic, design aligned with today's technologies and needs.

There are probably more, but the above list is enough to get the point across - this was not going to be a straight migration. After some thought I decided to copy across:

  • The Layout and shared View components.
  • The Areas.
  • The Controllers.
  • The Views.

There were breakages but after patching them up so they at least built. Many things were much easier to do, but many things needed to be re-thought.

However, the goal of this phase was to move past just building, and get something running, so that the testing phase could start.

Phase Three - The New Components

As mentioned above, one area, the CMS, was completely re-written, and a completely new set of functionality was added - the product publishing mechanism. Whilst both were identifiable peaces of work, they were probably not really 'phases', as they were really interleaved with the rest of the work.

I am mentioning them for completeness, but because it was new functionality, it's not really part of the migration, and hence this post.

Phase Four - Testing and Visuals

Oh boy.

So we have a building solution, with most of the components of the original site in place. We are happy, optimistic, and thinking that we can run the project, spend a few days debugging and 'hey presto' - a working site right? Wrong!

I think 'boom' is the best way to describe this phase; it was by far the most time consuming part.

Bare in mind that, at this point, the solution was compiling, maybe with a few warnings, so everything we found now was whilst debugging. As each section of the site was brought 'online' and executed, it would bring a slew of issues that needed to be fixed:

  • Differences in underlying referenced packages.
  • Finding how to do things in the new ASP.NET core architecture.
  • Entity framework differences.
  • Subtle async/await issues.
  • All Bootstrap related visuals were basically broken.

To make things more 'interesting' once an area had been fixed up, the changes would sometimes impact another areas, which would then need to be re-visited.

A couple of things that saved time are worth mentioning:

  • The 'search/replace' technique, where changes could literally be searched and replaced across the project, especially helpful for bootstrap.
  • The 'cross apply' technique, where changes made to shared code would generate compile errors so they could be cross-applied.
  • Using a website testing, or SEO, tool to scan the site locally picked up quite a few issues early on, and added assurance later as changes were made.

Gradually the project was coerced into one tightly integrated whole, with all the parts moving nicely together.

Additional Challenges

Aside from the challenges mentioned above, there were a few other items that are worth mentioning:

  • Entity framework code that worked before just stopped working. Various online articles did mention breaking changes, but finding those at run-time, rather than compile time, and even then the cause not being immediately obvious, was frustrating.
  • The new solution operated noticeably faster. That was great, but it caused parts of the code to crash with a memory exception because more operations were being processed simultaneously. Load throttling was introduced.
  • Related to the above, the new packages we were using to access Azure blob storage were allocating a huge buffer by default. We needed to trim that right down for multiple simultaneous downloads.
  • After deployment we noticed we had a slow memory leak that would take about 24 hours to kill the site. After a bit of investigation and searching we figured out that the culprit was the .net core Services IoC container. Or more accurately how we were using it. Having translated the code directly from our old IoC container to the new Services architecture (and possibly not having read the docs completely), we found that any services registered with 'Transient' scope needed to be created from their own service provider scope, or they would not be released at the end of a request cycle.
  • Having updated all the packages quite close to the beginning of the project, a version check towards the middle and end still brought a huge slug of updates. This is a positive thing that justifies the update all on its own, but it's a bit scary to think that the solution was out of date before I even completed it.

Conclusion

So, to conclude, our approach was:

  • Migrate the business logic.
  • Follow the breadcrumbs until that built.
  • Add the presentation code to a new ASP.NET project.
  • Follow the breadcrumbs until that built.
  • Test the bejesus out of it, until it ran.

It was worth it, but it went about twice over 'budget'.

I have been doing this for years, and a project that generates an excellent result almost always runs over time. If a project comes in on time it is either very straight forward, or it is loaded with technical debt

Note: A project loaded with technical debt is not complete - its just a way of lying to the management.

That just seems to be the way it is.