Application upgrade
.NET .NET Core ASP.NET Core C# Visual Studio Web API

Upgrading a Web API from .NET Core 3.1 to .NET 7

Welcome to today’s post.

In today’s post I will show how I upgraded a Web API from version 3.1 (.NET Core) to 7.0 (.NET).

As I mentioned in a previous post (Blog 366 – Upgrading to .NET 5 and the Future of .NET Core), upgrading from the latest .NET Core framework (3.1) to the earliest .NET framework (5.0) doesn’t mean that most of your code has to be re-written or thrown away. Aside from re-installing new packages and a few minor changes to the bootstrap code, it should work the same as before.

There isn’t much difference in the application structure between .NET Core versions 3.1, 5.0 and upwards with Web API applications.

The main differences we have between the .NET applications are:

  1. Development IDE
  2. The target framework
  3. NuGet package versions
  4. Program bootstrapping

Aside from the above, everything else including the Startup class you use to load up the service collection instances, and application middleware is the same (apart from one or two obscure pieces of middleware like OData, which I will explain in a later post or update to this post).

In the first section, I will which show which development environment we can use to help us upgrade to the .NET 7.0 framework.

The Development IDE and Upgrading the Target Framework

With .NET Core 3.1 Web API applications, we can still run them in Visual Studio 2019 and 2022, however, to be able to upgrade them to a .NET 7.0 compliant Web API application, we will need to use Visual Studio 2022, version 15.7.3 as it comes installed with the .NET 7.0 framework.

The target framework is the first part of the application project configuration that we will upgrade.

If we have a look as the project configuration (CSPROJ) file within the solution, you will notice the current target framework is located between the <TargetFramework>..</TargetFramework> tags as shown:

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
    <UserSecretsId>…</UserSecretsId>
    <DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
  </PropertyGroup>
  …

To apply an upgrade of the target framework, first open the application solution (SLN) file in Visual Studio 2022. Once the solution project(s) are loaded, open the project properties from the IDE. Select the Application | General option. You will then see the application type and below it, the Target framework, with choices from .NET Core 1.0 up to the latest available, which is .NET 7.0. Select version .NET 7.0 as shown below:

Below the target framework is the Target OS option. You will be given the choices of Android, iOS, Mac Catalyst, macOS, tvOS and Windows. As I am using the Windows operating system for development, I choose Windows as shown:

Following from that, I see the Target OS version option below the Target OS. There are options from 7.0, 10 and other versions of 10. I choose 7.0. For the Supported OS version option, I choose 7.0 as the minimum OS version the project will run. If you wish to target Windows 10 as the target OS, then choose a version of 10 instead.

After saving the above settings, you will see the project configuration file will show the target framework amends as shown:

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net7.0-windows</TargetFramework>
    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
    <UserSecretsId>…</UserSecretsId>
    <DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
  </PropertyGroup>
   …

You will notice the target OS is concatenated to the target framework.

In the next section, I will look at upgrading NuGet packages that are .NET 7.0 compatible.

NuGet Package Versions

If we look at the <ItemGroup> section of the project configuration file, we will notice most of the NuGet packages are version 3.1.0 as shown:

<ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.0" />
    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="3.1.0" />
    <PackageReference Include="Serilog.AspNetCore" Version="4.0.0" />
    <PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="5.0.0" />
    <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="5.0.0" />
    <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUi" Version="5.0.0" />
</ItemGroup>

Next, open the NuGet package manager and upgrade the following packages one by one to version 7.0.4:

Microsoft.AspNetCore.*
Microsoft.EntityFrameworkCore.*
Microsoft.Extensions.Diagnostics.*
Microsoft.Extensions.Configuration.Binder

Upgrade the following (if referenced) to 7.0.0:

Microsoft.Extensions.Caching.*
Microsoft.Extensions.Configuration.* 
Microsoft.Extensions.DependencyInjection.*
Microsoft.Extensions.FileProviders.*
Microsoft.Extensions.Hosting.*
Microsoft.Extensions.Logging.*
Microsoft.Extensions.Options.ConfigurationExtensions
Microsoft.Extensions.Primitives

Upgrade the following (if referenced) to 6.27.0:

Microsoft.IdentityModel.*

Upgrade the following (if referenced) to 5.0.17:

Microsoft.AspNetCore.Http.Features

Upgrade the following (if referenced) to 5.1.0:

Microsoft.Data.SqlClient

Upgrade the following (if referenced) to 7.0.1:

Microsoft.Extensions.Options

Upgrade the following (if referenced) to 4.51.0:

Microsoft.Identity.Client

Upgrade the following (if referenced) to 1.63.0:

Microsoft.OpenApi

Upgrade the following (if referenced) to 6.1.0:

Serilog.AspNetCore

Upgrade the following (if referenced) to 6.5.0:

Swashbuckle.AspNetCore.*

You will notice that many other packages are transient dependency packages of the original packages and were also required to be upgraded. The entire list of upgraded package versions is shown below:

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.4" />
    <PackageReference Include="Microsoft.AspNetCore.Http.Features" Version="5.0.17" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.4" />
    <PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.4" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="7.0.4" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Analyzers" Version="7.0.4" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.4" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.4" />
    <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.DependencyModel" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.4" />
    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="7.0.4" />
    <PackageReference Include="Microsoft.Extensions.FileProviders.Abstractions" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1" />
    <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0" />
    <PackageReference Include="Microsoft.Extensions.Primitives" Version="7.0.0" />
    <PackageReference Include="Microsoft.Identity.Client" Version="4.51.0" />
    <PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="6.27.0" />
    <PackageReference Include="Microsoft.IdentityModel.Logging" Version="6.27.0" />
    <PackageReference Include="Microsoft.IdentityModel.Protocols" Version="6.27.0" />
    <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.27.0" />
    <PackageReference Include="Microsoft.OpenApi" Version="1.6.3" />
    <PackageReference Include="Serilog.AspNetCore" Version="6.1.0" />
    <PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="6.5.0" />
    <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" />
    <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUi" Version="6.5.0" />
  </ItemGroup>

In the next section, I will show how to upgrade the application startup code to work with .NET 7.0.

Program bootstrapping

When we started a .NET Core 3.1 Web API application from the static Main() method, we were required to use the IWebHost service with the CreateDefaultBuilder() method. 

To configure logging into the application pipeline, we create the logger configuration with LoggerConfiguration() before we build the web host.

To use the SeriLog logging library, we chained the creation of the SeriLog library instance to the WebHost.CreateDefaultBuilder(..) instance with UseSeriLog().

We then chained the startup class to the above WebHost.CreateDefaultBuilder(..) instance with UseStartup<Startup>().

The above chained sequence is shown in the static method BuildWebHost() below:

using System;
using Serilog;
using Serilog.Events;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

namespace BookLoan.Catalog.API
{
    public class Program
    {
        public static int Main(string[] args)
        {
            Log.Logger = new LoggerConfiguration()
                .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
                .Enrich.FromLogContext()
                .WriteTo.Console()
                .WriteTo.File("logs/log.txt", rollingInterval: RollingInterval.Day)
                .CreateLogger();

            try
            {
                Log.Information("Starting the Web Host...");
                BuildWebHost(args).Run();
                return 0;
            }
            catch (Exception ex)
            {
                Log.Fatal(ex, "Host terminated unexpectedly");
                return 1;
            }
            finally
            {
                Log.CloseAndFlush();
            }            
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseSerilog()
                .UseStartup<Startup>()
                .Build();
    }
}

With .NET 7.0, there are similarities, but the main one to remember, is that since .NET 5.0, Web applications are bootstrapped using the Generic Host Service. This is done using the IHostBuilder service with the Host.CreateDefaultBuilder(..) method used to chain the Startup class and application configurations through a lambda delegate. The Logging is configured before the host builder is created, configured, and run.

The Serilog library is chained to the host builder after Host.CreateDefaultBuilder(..) and host configuration ConfigureWebHostDefaults() is chained. 

The differences are shown below with the .NET 7 application startup sequence:

using System;
using Serilog;
using Serilog.Events;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

namespace BookLoan.Catalog.API
{
    public class Program
    {
        public static void Main(string[] args)
        {
            Log.Logger = new LoggerConfiguration()
                .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
                .Enrich.FromLogContext()
                .WriteTo.Console()
                .WriteTo.File("logs/log.txt", rollingInterval: RollingInterval.Day)
                .CreateLogger();

            try
            {
                Log.Information("Starting the Web Host...");
                CreateHostBuilder(args).UseSerilog().Build().Run();
                return;
            }
            catch (Exception ex)
            {
                Log.Fatal(ex, "Host terminated unexpectedly");
                return;
            }
            finally
            {
                Log.CloseAndFlush();
            }
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

In the next section, I will review the startup class and explain why no changes are required with it.

The Startup Class

There are no differences with the Startup class, with the familiar methods ConfigureServices(IServiceCollection) and Configure(IApplicationBuilder, IHostEnvironment, ILogger<Startup>) both containing the same service collections and application middleware setups as before:

namespace BookLoan.Catalog.API
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            ...
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostEnvironment env, ILogger<Startup> logger)
        {
            ...
        }
    }
}

In the next section, I will review some common errors that can occur during the upgrade and how to overcome them.

Common issues that can prevent the startup from completing successfully

In this section, I will be reviewing some common errors that can occur when initially upgrading an application to .NET 7.0.

Database connection issues

One other thing to note is with the Entity Framework SQL Server packages, when you upgrade them from 3.1.0 to 7.0.4, the connection strings require a slight change for them to avoid a certificate error when you run the EnsureCreated() method in the following scoped service instantiation in the configuration of application middleware within Configure().

try
{
    using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
    {
        var context = serviceScope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
        context.Database.EnsureCreated(); // create database if not already created.
    }
}
catch (Exception ex)
{
    Console.WriteLine("Error: Initialising the database.");
}

To avoid the certificate error when connecting to the SQL database in a development environment, add the following key-value property:

Trust Server Certificate=true; 

to the connection string as shown:

"ConnectionStrings": {
    "AppDbContext": "Server=localhost;Database=aspnet-BookCatalog;User Id=????;Password=????;Trust Server Certificate=true;",
},

In the next section, I will explain how to resolve a common error when running ASP.NET Core applications following the upgrade.

Timeout of the ASP.NET Core Module

The timeout of the ASP.NET Core Module, also known as the ANCM occurs when the application services, configured within your startup ConfigureServices() and application middleware, configured within Configure() exceeds the startup timeout limit, which is 120 seconds.

When you view the error from the console, you will see an error stack trace that shows like this:

System.Runtime.InteropServices.COMException (0x80004005): Error HRESULT E_FAIL has been returned from a call to a COM component.
   at Microsoft.AspNetCore.Hosting.WebHostBuilderIISExtensions.UseIIS(IWebHostBuilder hostBuilder) …

The startup sequence has failed before IIS even started. This shows up as a 500.37 error and the full error message from the asp.net web server console log is shown below:

The startup timeout limit can be specified within the web.config configuration file. When your ASP.NET (Core) application is created initially, the <aspNetCore> element has the processPath and hostingModel options set (I recall the option stdoutLogEnabled is set to false initially) as shown:

<system.webServer>
      <handlers>
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
      </handlers>
      <aspNetCore processPath="%LAUNCHER_PATH%" 
           arguments="%LAUNCHER_ARGS%" 
           stdoutLogEnabled="true" 
           stdoutLogFile=".\logs\stdout" 
           hostingModel="InProcess">
        <environmentVariables />
    </aspNetCore>
</system.webServer>

Below is the startupTimeLimit option set to an overridden value of 240 seconds.

<system.webServer>
      <handlers>
        <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
      </handlers>
      <aspNetCore processPath="%LAUNCHER_PATH%" 
          arguments="%LAUNCHER_ARGS%" 
          stdoutLogEnabled="true" 
          stdoutLogFile=".\logs\stdout" 
          hostingModel="InProcess" 
          startupTimeLimit="240">
          <environmentVariables />
      </aspNetCore>
</system.webServer>

When you re-run the Web API application, the startup sequence can take longer to allow the configurations to be applied and to allow you to add breakpoints at various points in the startup and configuration code when diagnosing startup exceptions and avoiding the application to timeout unexpectedly.

Another error that occurs during the resolving of classes during dependency injection binding is explained in the next section.

Exceptions when Configuring or Loading of Services

There are other times when the configuration or loading of services in the service collection can lead to exceptions including argument exceptions such as the one below for console logging:

The above error was caused after I converted the original logging services in .NET Core 3.1 below:

services.AddLogging(config =>
{
    config.AddConsole(opts =>
    {
        opts.IncludeScopes = true;
    });
    config.AddDebug();
});

To this version below in .NET 7.0 after the build during compilation complained about not allowing IncludeScopes option as a console configuration:

services.AddLogging(config =>
{
    config.AddConsole().AddDebug();
 	                 
    config.AddConsoleFormatter<ConsoleFormatter,ConsoleFormatterOptions>
    (
        opts => { 
            opts.IncludeScopes = true; 
        }
    );
});

The above change using console formatter then gives the invalid argument exception.

I then stripped away the excess configuration code and left the code below, which worked without errors:

services.AddLogging(config =>
{
    config.AddConsole().AddDebug();
});

The above is the minimum you will be required to upgrade a .NET Core 3.1 Web API to a .Net 7.0 Web API. I haven’t dealt with some other third-party packages you may have, so the above should be a good starting point. After the above setups, I recommend building and running the API application to ensure no errors during the execution. 

That is all for today’s post.

I hope you have found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial