Migration from .NET Core 2.2 to .NET Core 3.1 on the example of a real project

ivkadp profile image ivkadp ・9 min read


This article is a logical continuation of the upgrading the nopCommerce project - a free open-source CMS for creating online stores. Last time we talked about our experience in migrating nopCommerce from ASP.NET MVC to ASP.NET Core 2.2. In this article we will look at the migration to .NET Core 3.1.

Net Core 3.1 will be officially supported until December 2022, so migration is a hot topic right now.
If you want to take full advantage of the updated framework, keep up with technological innovations and the growing global trends then it's time to start migration.

Challenges to be solved in the process of migration to .Net Core 3.1

For the fast growing eCommerce project, it is imperative to pay great attention to the system performance and security. In the first .NET Core 3.0 review, it was announced that the new version of the framework will be much more fast and productive. Implementing the following .Net Core 3.1 features push nopCommerce to the new level of scalability, performance and security:

  1. Tiered compilation allows us to decrease the startup time.
  2. The new built-in high-performance and low-memory support for JSON.
  3. The endpoint routing introduced in .NET Core 2.2 has been improved. The main advantage is that the route is now determined before running the middleware.

At the time of the release, our expectations were confirmed in practice.
Next let’s see what exactly affected the nopCommerce performance and how .NET Core evolved since the release of version 2.2.

What's new in .NET Core 3.1

Let's take a closer look at the .NET Core 3.1 innovations that we use in nopCommerce. You can find the detailed instructions on how to migrate from .NET Core 2.2 to .NET Core 3.1 on the official Microsoft website. In this article we will look at the benefits we receive using these innovations.

Generic Host

In .NET Core 2.1 Generic Host is an addition to the Web Host. It allows you to use tools such as dependency injection (DI) and logging abstractions. .NET Core 3 emphasized the greater compatibility with Generic Host, so you can now use the updated Generic Host Builder instead of Web Host Builder. This allows to create any kind of application, from console applications and WPF to web applications, on the same basic hosting paradigm with the same common abstractions.

public class Program
        public static void Main(string[] args)

        public static IHostBuilder CreateHostBuilder(string[] args)
            return Host.CreateDefaultBuilder(args)
                .UseServiceProviderFactory(new AutofacServiceProviderFactory())
                .ConfigureWebHostDefaults(webBuilder =>

You can still continue to use WebHostBuilder but you need to understand that some types are deprecated in ASP.NET Core 3.1, and may be replaced in the next version.

Alt Text


The idea and possibility of using this feature already existed in the .NET Core 2.0 version, but its capabilities were incomplete. It was only possible to specify the version of the SDK that your application is using. More flexible SDK version control became available only in .NET Core 3.0 with the introduction of policies such as allowPrerelease and rollForward. The code below illustrates the use of the .NET Core SDK roll-forward policies.

  "sdk": {
    "version": "3.1.201",
    "rollForward": "latestFeature",
    "allowPrerelease": false

You can see the full version of the application code in our repository on GitHub.

Now you can specify the version from which you can easily build the application, without having to edit global.json every time after the next patch is released. This way you can define the range of functionality that you need based on your requirements. It will also give you a guarantee that users of your application will run it exactly on those SDK assemblies that you have defined, and not the latest version installed on their server.

ASP.NET Core Module V2

Prior to .NET Core 2.2, IIS hosted a .NET Core app by executing a Kestrel instance and forwarding requests from IIS to Kestrel by default. Basically IIS acted like a proxy. This works, but is slow because there is a double hop from IIS to Kestrel while processing the request. This hosting method is called «OutOfProcess».

Alt Text

.NET Core 2.2 introduced the new "InProcess" hosting model. Instead of forwarding requests to Kestrel, IIS serves requests inside itself. This makes the requests processing much faster because you don't need to forward the request to Kestrel. However, this was an optional feature and was not used by default.

Alt Text

In .NET Core 3.1, the in-process hosting model is already configured by default, which significantly improves the throughput of ASP.NET Core requests in IIS. During testing, we recorded a significant increase of the system performance with a large number of requests.

    <Copyright>Copyright (c) Nop Solutions, Ltd</Copyright>
    <Company>Nop Solutions, Ltd</Company>
    <Authors>Nop Solutions, Ltd</Authors>
    <Description>Nop.Web is also an MVC web application project, a presentation layer for public store and admin area.</Description>
    <!--Set this parameter to true to get the dlls copied from the NuGet cache to the output of your project-->
    <!--When true, compiles and emits the Razor assembly as part of publishing the project-->

Note that running the application on Linux will still use the out-of-process web server hosting model. Also, if you host multiple in-process applications on your server, you must ensure that each such application has its own pool.

Endpoint Routing

In .NET Core 2.1, routing was done in Middleware (ASP.NET Core MVC middleware) at the end of the HTTP request pipeline. This means that information about the route, such as what controller action will be taken, was not available for the middleware that processed the request before the MVC middleware in the request pipeline. Starting with .NET Core 2.2, a new endpoint-based routing system was introduced. This routing concept addresses the aforementioned issues.

Endpoint Routing is now built differently in .NET Core 3.1. The routing phase is separated from the call to the endpoint. This way we have two intermediate middlewares:

  • EndpointRoutingMiddleware - this determines which endpoint will be called for each path of the URL request and essentially acts as a routing
  • EndpointMiddleware - calls the endpoint

Alt Text

The application defines the endpoint to be shipped early in the middleware pipeline. The following middleware can use this information to provide functionality which is not available in the current pipeline configuration.

/// <summary>
/// Configure Endpoints routing
/// </summary>
/// <param name="application">Builder for configuring an application's request pipeline</param>
public static void UseNopEndpoints(this IApplicationBuilder application)
    //Add the EndpointRoutingMiddleware

    //Execute the endpoint selected by the routing middleware
    application.UseEndpoints(endpoints =>
        //register all routes

You can learn more about the code in our repository on GitHub.

С# 8.0 syntactic sugar

In addition to updating the .NET Core itself, a new version of C # 8.0 was also released. There are a lot of updates. Some of them are fairly global, others involve cosmetic improvements, giving developers "syntactic sugar".

Alt Text

You can find the detailed description in the documentation.

Performance evaluation

After we upgraded our nopCommerce application, it was necessary to check how performance increased in our case. For users, this is one of the most important criteria when choosing an eCommerce platform.
Tests were run on Windows 10 (10.0.19041.388), where IIS 10 (10.0.19041.1) acted as a proxy for the Kestrel web server on the same machine. To simulate the high load, we used Apache JMeter, which allows us to simulate a very serious load with many parallel requests. For the test, we have specially prepared a database typical for an average online store. The number of products was about 50,000 items, the number of product categories was 516 with random nesting, the number of registered users was about 50,000, and the number of orders was about 80,000 with a random inclusion of 1 to 5 products. All this was running by MS SQL Server 2017 (14.0.2014.14).

JMeter ran multiple load tests generating a summary measurement report for each request each time. The averaged results after several runs are presented below.

Test time is about 20% faster (less is better)

Alt Text

Average response time is about 13.7% faster (less is better)

Alt Text

The number of requests per unit of time (throughput) increased by 12.7% (more is better)

Alt Text

We also measured actual memory usage of the application after launch and after a short-term load test. The test was carried out in a few consecutive series. Our goal was to find out how efficiently the application frees the memory. And how the ASP.NET Core module update affected the starting memory consumption. Let me remind you that we started using the new “InProcess” model.

Alt Text

The results are presented on the diagram above. In-process model shows more than 1.5 times better efficiency after passing the first load cycle. Further, more efficient use of resources is observed. By the end of the third cycle of load tests, the total memory consumption is more than 2 times more efficient in comparison with out-of-process allocation. This is a very good result.

For comparison, we provide a table with test results including the AspNetCoreModule which we used with .NET Core 2.2. The new module undoubtedly outperforms its predecessor.

Alt Text

The starting memory consumption remained almost unchanged.

Alt Text


The .NET Core platform migration process is still cumbersome and time-consuming. And if you make updates on time and consistently, this will help you to avoid a lot of mistakes and save your time. Many aspects of .NET Core have been improved and you get better performance as a bonus. In our particular case, we got an average performance increase of 13%. Now requests to the application run faster. This allows you to send more data in less time which makes the application more comfortable to use. Therefore, this is quite a significant increase, considering that you do not actually need to do performance refactoring of your application. You simply update the framework and get an overall improvement of the platform performance.

It is also important to note that, as always, .NET Core pays a lot of attention to security issues. By the way, since the release of .NET Core 3.1.0, a number of updates have already been released (the latest version is currently 3.5.1) including security updates.

A very important point is that .NET Core 3.1 is the next LTS version after 2.1, which guarantees us and our customers support and receiving the latest fixes, including security patches. Therefore, it is important for us to move forward with the release of the LTS versions of .NET Core. .NET 5 will be the next global release, and moving your application to .NET Core 3.1 is the best way to prepare for that.

In the future, we plan to further update the nopCommerce, adding more and more new features that the .NET Core platform provides. One of them is using System.Text.Json instead of Newtonsoft.Json. This is a more performant, safe, and more standardized approach to handling JSON objects. We also plan to implement and use as many features as possible that C # 8.0 provides.

You can learn more about our project on nopcommerce.com or by visiting our repository on GitHub.

Posted on by:


Editor guide