In this episode, we’ll go through the main changes required to upgrade our currently ASP.NET Core 2.2 applications to ASP.NET Core 3.0.

For the walk-through you can check out the next video, but if you prefer reading, skip to the written synthesis.

The playlist for the whole series is here.

Intro

I guess it’s time for a boring episode, as .NET Core 3.0 came out recently, we need to get our projects updated to the latest version.

There aren’t too many required changes, but being a new major version, there are a couple of breaking changes and some new recommended practices.

I probably won’t go through every change in this post, so if you want to check out all the details, take a look at the diff in the GitHub repositories.

Basics and other quick stuff

Let’s begin with the simplest bits.

The most obvious is of course going through all the csproj files and bumping up the .NET Core version, by replacing the target framework, currently netcoreapp2.2 with netcoreapp3.0.

The second most obvious change is upgrading the NuGet packages, namely the ones from Microsoft that have their versions tied to the framework version, but even others (e.g. IdentityServer4) sometimes keep their versions paired with the framework’s. In this case, for simplicity, I just used the IDE and updated all the packages to their latest stable version.

Other quick changes were:

  • SetCompatibilityVersion method, which we called when setting up MVC is no longer needed, at least with 3.0, maybe it’ll be back in 3.1.
  • In GroupEntityConfiguration, when setting the auto increment column, UseNpgsqlIdentityAlwaysColumn is deprecated in favor of UseIdentityAlwaysColumn.
  • IHostingEnvironment, used for instance in the Startup class is deprecated, now replaced by IHostEnvironment. If specific web applications bits are needed, the alternative is IWebHostEnvironment (which inherits from IHostEnvironment).
  • We can remove the reference to the metapackage Microsoft.AspNetCore.App, because it’s implicit when the project SDK is Microsoft.NET.Sdk.Web, as is the case. If we weren’t using this SDK, we should move this reference to a FrameworkReference instead of a PackageReference. For more info, go here.
  • Entity Framework Core 3.0 now targets .NET Standard 2.1 instead of 2.0 (though this will be reverted in 3.1).

Packages extracted from the ASP.NET Core shared framework

With this release, some packages that were part of ASP.NET Core shared framework (the things we got when referencing Microsoft.AspNetCore.App), so we need to reference the ones we’re using.

The main package in this situation is Entity Framework Core. The main reasons for this is change seem to be to make the process of targeting EF Core the same whether it’s an ASP.NET Core app or not, as well as not forcing the upgrade of EF Core version when upgrading the framework. More info in the migration docs here.

Other extracted packages that we must now explicitly reference are Microsoft.AspNetCore.Authentication.OpenIdConnect and Microsoft.AspNetCore.Authentication.JwtBearer.

IWebHost → IHost

Previously we were using IWebHost (along with IWebHostBuilder) to setup the host of our web applications. When we wanted to build other kinds of services, for instance a message queue consumer, we would use the more generic IHost and its accompanying IHostBuilder (we’ll eventually implement such services in this series).

With ASP.NET Core 3.0, everything was consolidated under a single interface, the generic IHost.

The main class which looked like the following:

Program.cs

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

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

Has been adapted to:

Program.cs

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

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

Endpoint routing

Endpoint routing isn’t something new in ASP.NET Core 3.0, being introduced in 2.2, but it gains more visibility with this latest release.

The goal of endpoint routing is to make routing usable in ASP.NET Core in general, not just be a feature of MVC.

As an example of the changes this introduces, let’s see the Configure method of our group management API Startup class.

GroupManagement\src\CodingMilitia.PlayBall.GroupManagement.Web\Startup.cs

public void Configure(IApplicationBuilder app, IHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.Use(async (context, next) =>
    {
        context.Response.OnStarting(() =>
        {
            context.Response.Headers.Add("X-Powered-By", "ASP.NET Core: From 0 to overkill");
            return Task.CompletedTask;
        });

        await next.Invoke();
    });

    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        // we'll take a another look at this in the next section
        endpoints
            .MapControllers()
            .RequireAuthorization();
    });

    app.Run(async (context) =>
    {
        await context.Response.WriteAsync("No middlewares could handle the request");
    });
}

The new bits are the calls to UseRouting and UseEndpoints:

  • UseRouting marks the point in the pipeline in which the route will be resolved, so any middleware that comes after it knows what endpoint will eventually run.
  • UseEndpoints is where we configure the “routable” components, in this specific case we’re configuring our controllers (more on why no MVC later). If we take a look at the options autocomplete gives us when we dot on endpoints, we’ll see things like MapHub (for SignalR) or MapBlazorHub (for Blazor), which are taking advantage of the new routing capabilities.

We’ll certainly make more use of these new routing features in the future, but for now we’ll stick to adapting our existing code to the new recommendations - UseMvc still works, but we might as well start using the new bits.

One thing that we can immediately see, that’s taking advantage of this new feature is the call to RequireAuthorization inside UseEndpoints. Previously we would enforce that the controllers required authorization through an MVC configuration, as we can see below.

GroupManagement\src\CodingMilitia.PlayBall.GroupManagement.Web\IoC\ServiceCollectionExtensions.cs

// ...

var mvcBuilder = services.AddMvcCore(options =>
{
    var policy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .RequireClaim("scope", "GroupManagement")
        .Build();

    options.Filters.Add(new AuthorizeFilter(policy));
});

// ...

While this still works, so we could have left it, now having routing information available outside of MVC allows to do some things in a more generic way. In this case we’re not getting any particular advantage out of it, but if we wanted to use the same authorization infrastructure we’re used to in MVC with other kinds of endpoints, we could.

RequireAuthorization has some overloads to further configure the authorization requirements of the target endpoint, but we don’t really need it here as we can just setup a default policy for the group management API. This can be done when calling AddAuthorization in ConfigureServices.

services.AddAuthorization(options =>
{
    var policy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .RequireClaim("scope", "GroupManagement")
        .Build();

    options.DefaultPolicy = policy;
});

Segregation of MVC components

From the previous section you might have already noticed that MVC components are now better segregated when configuring the application in the Startup class, as we can see in both ConfigureServices and Configure methods.

On the ConfigureServices side, we can still call AddMvc to get everything setup, but we have some new options if we don’t want to use everything: AddControllers, AddControllersWithViews and AddRazorPages.

Similarly, on the Configure side, inside UseEndpoints we can map things individually, by calling methods such as MapControllers or MapRazorPages.

Now we can use what we need in our applications. In the group management API and the BFF we only need the controller bits, in the auth service we’ll use everything.

Breaking change in route resolution of async actions

Quick but important shout-out to a breaking change in route resolution of async methods, is that the Async suffix is removed from the action names.

As an example, the group management API’s GroupsController.AddAsync action returns a 201 with a link to the GetByIdAsync route. Previously we would do this with:

return CreatedAtAction("GetByIdAsync", new {id = group.Id}, group.ToModel());

With ASP.NET Core 3.0, we need to get rid of the Async suffix, like so:

return CreatedAtAction("GetById", new {id = group.Id}, group.ToModel());

Outro

That should be it for our upgrade to .NET and ASP.NET Core 3.0. There are a lot more things going on, so be sure to check out the docs, we just went through the main things that affected what we developed so far in the series.

For the full diff be sure to look at the repositories on GitHub, and feel free to ask any question. I took the opportunity to do some unrelated changes, as I was touching some code I wanted to improve. but most of the diff should be upgrade related.

Links in the post:

The source code for this post is spread across the repositories in the “Coding Militia: ASP.NET Core - From 0 to overkill” organization, tagged as episode032.

Sharing and feedback always appreciated!

Thanks for stopping by, cyaz!