Upgrading My ASP.NET Core Projects to 3.1

.NET Core 3.1 is LTS meaning it's a great time to upgrade projects to the newest version. This article goes through the process of converting one of my existing .NET Core 1.1 projects to 3.1. I chose this project because updating from .NET Core 1.1 is a bit more complicated than 2.0. Also, the source code for this project is available, so anyone interested can view the commit log.

The first step is installing the .NET Core 3.1 SDK. If you're using Visual Studio, it's as simple as updating to the latest version. If you aren't using Visual Studio, go download the SDK.

Project files

Once the dependancies are in order, edit the .csproj for the application.

<PropertyGroup>
  <TargetFramework>netcoreapp1.1</TargetFramework>
</PropertyGroup>

Under the TargetFramework section netcoreapp1.1 turns to netcoreapp3.1. This step is the same as all the .NET Core versions.

<PropertyGroup>
  <TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>

Next up is the project references. NET Core consolidated several packages, so a few PackageReference elements get removed. The full details of consolidated packages are available in the upgrade documentation.

<ItemGroup>
  <PackageReference Include="BuildBundlerMinifier" Version="2.4.337" />
  <PackageReference Include="Microsoft.AspNetCore" Version="1.1.1" />
  <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.2" />
  <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="1.1.1" />
  <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.1" />
  <PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink" Version="1.1.0" />
</ItemGroup>

In my case, I was able to remove all but one of the packages.

<ItemGroup>
  <PackageReference Include="BuildBundlerMinifier" Version="2.4.337" />
</ItemGroup>

Next, I upgraded the remaining NuGet packages to their latest versions. The old versions are below.

<ItemGroup>
  <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
  <PackageReference Include="xunit" Version="2.2.0" />
  <PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
</ItemGroup>

I did this part with the JetBrains Rider Nuget Package Manager. You can use Visual Studio, or do it manually. The project results are below.

<ItemGroup>
  <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
  <PackageReference Include="xunit" Version="2.4.1" />
  <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
</ItemGroup>

Code updates

At this point, nothing's going to work. There are a few differences between .NET Core 1.1 and 3.1, so I started at the top. Open the Program.cs.

public static void Main(string[] args)
{
    var host = new WebHostBuilder()
        .UseKestrel()
        .UseContentRoot(Directory.GetCurrentDirectory())
        .UseIISIntegration()
        .UseStartup<Startup>()
        .Build();

    host.Run();
}

.NET Core 1.1 explicitly calls UseKestrel, and .Net 2.2 uses the WebHostBuilder. .NET Core 3.0 uses IHostBuilder. This is a quick fix.

public static void Main(string[] args)
{
    CreateHostBuilder(args).Build().Run();
}

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

The next update is in Startup.cs. The first update is in the Startup method.

public Startup(IHostingEnvironment env)
{
    var builder = new ConfigurationBuilder()
        .SetBasePath(env.ContentRootPath)
        .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
        .AddEnvironmentVariables();
    Configuration = builder.Build();
}

.NET Core 1.1 requires the JSON files to be added with code. Settings get inferred when injecting IConfiguration.

public Startup(IConfiguration configuration)
{
    Configuration = configuration;
}

Now the ConfigureService method.

public void ConfigureService(IServiceCollection services)
{
    services.AddMvc();
}

AddControllersWithViews replaces AddMvc.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
}

The Configure method's next. Here's the original.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseBrowserLink();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseStaticFiles();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

Logging's configured via appSettings.json, so adding a logger factory isn't necessary. Also, endpoint management isn't done with UseMvc anymore.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        //app.UseBrowserLink();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseStaticFiles();
    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}

Configuration

Now the project builds, but it isn't going to run. The appsettings.json file must be updated.

{
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Warning"
    }
  }
}

The logging section has a couple of changes for hosting lifetime.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

These changes must be propogated to the appsettings.Development.json file as well.

{
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  }
}

Update to the same configuration specified previously.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}

Finally, my .NET Core 1.0 project didn't have a launchSettings.json file in it.

{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:43023"
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "Cyberkruz.PublishProfile.Web": {
      "commandName": "Project",
      "launchBrowser": true,
      "applicationUrl": "http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

Conclusion

At this point the project ran like a champ. I'll be upgrading all my projects, and I'll post any gotchas I happen to find. Upgrading these projects isn't terribly difficult, but it does take a bit of precision work. Unit tests help. Hopefully, this can help someone out.