Migrate from ASP.NET Core 2.2 to 3.0

By Scott Addie and Rick Anderson

This article explains how to update an existing ASP.NET Core 2.2 project to ASP.NET Core 3.0.

Prerequisites

Update the project file

  • Set the Target Framework Moniker (TFM) to netcoreapp3.0:

    <TargetFramework>netcoreapp3.0</TargetFramework>
    
  • Remove any <PackageReference> to the Microsoft.AspNetCore.All or Microsoft.AspNetCore.App metapackage.

  • Update the Version attribute on remaining <PackageReference> elements for Microsoft.AspNetCore.* packages to the current preview (for example, 3.0.0-preview5-19227-01).

    If there's no 3.0 version of a package, the package might have been deprecated in 3.0. Many of these packages are part of Microsoft.AspNetCore.App and shouldn't be referenced individually. For a preliminary list of packages no longer produced in 3.0, see Stop producing packages for shared framework assemblies in 3.0 (aspnet/AspNetCore #3756). The shared framework is the set of assemblies (.dll files) that are installed on the machine and referenced by Microsoft.AspNetCore.App. For more information, see The shared framework.

  • The assemblies for several notable components were removed from Microsoft.AspNetCore.App in 3.0. Add <PackageReference> elements if you're using APIs from packages listed in Assemblies being removed from Microsoft.AspNetCore.App 3.0 (aspnet/AspNetCore #3755).

    Examples of removed components include:

    • Microsoft.AspNet.WebApi.Client
    • Microsoft.EntityFrameworkCore
    • System.Data.SqlClient

    The list of assemblies shipping in Microsoft.AspNetCore.App hasn't been finalized and will change before 3.0 RTM.

    Consider the following code:

    var branches = await response.Content.ReadAsAsync<IEnumerable<GitHubBranch>>();
    

    The ReadAsAsync method called in the preceding code is included in Microsoft.AspNet.WebApi.Client. Install the Microsoft.AspNet.WebApi.Client NuGet package to resolve the compilation issue in 3.0.

  • Add Json.NET support.

  • Projects default to the in-process hosting model in ASP.NET Core 3.0 or later. You may optionally remove the <AspNetCoreHostingModel> property in the project file if its value is InProcess.

Json.NET support

As part of the work to improve the ASP.NET Core shared framework, Json.NET has been removed from the ASP.NET Core shared framework.

To use Json.NET in an ASP.NET Core 3.0 project:

  • Add a package reference to Microsoft.AspNetCore.Mvc.NewtonsoftJson.

  • Update Startup.ConfigureServices to call AddNewtonsoftJson.

    services.AddMvc()
        .AddNewtonsoftJson();
    

    AddNewtonsoftJson is compatible with the new MVC service registration methods:

    • AddRazorPages
    • AddControllersWithViews
    • AddControllers
    services.AddControllers()
        .AddNewtonsoftJson();
    

    Json.NET settings can be set in the call to AddNewtonsoftJson:

    services.AddMvc()
        .AddNewtonsoftJson(options =>
               options.SerializerSettings.ContractResolver =
                  new CamelCasePropertyNamesContractResolver());
    

MVC service registration

ASP.NET Core 3.0 adds new options for registering MVC scenarios inside Startup.ConfigureServices.

Three new top-level extension methods related to MVC scenarios on IServiceCollection are available. Templates use these new methods instead of UseMvc. However, AddMvc continues to behave as it has in previous releases.

The following example adds support for controllers and API-related features, but not views or pages. The API template uses this code:

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

The following example adds support for controllers, API-related features, and views, but not pages. The Web Application (MVC) template uses this code:

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

The following example adds support for Razor Pages and minimal controller support. The Web Application template uses this code:

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

The new methods can also be combined. The following example is equivalent to calling AddMvc in ASP.NET Core 2.2:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddRazorPages();
}

Update routing startup code

If an app calls UseMvc or UseSignalR, migrate the app to Endpoint Routing if possible. To improve Endpoint Routing compatibility with previous versions of MVC, we've reverted some of the changes in URL generation introduced in ASP.NET Core 2.2. If you experienced problems using Endpoint Routing in 2.2, expect improvements in ASP.NET Core 3.0 with the following exceptions:

Endpoint Routing supports the same route pattern syntax and route pattern authoring features as IRouter. Endpoint Routing supports IRouteContraint. Endpoint routing supports [Route], [HttpGet], and the other MVC routing attributes.

For most applications, only Startup requires changes.

Migrate Startup.Configure

General advice:

  • Add UseRouting.
  • If the app calls UseStaticFiles, place UseStaticFiles before UseRouting.
  • If the app uses authentication/authorization features such as AuthorizePage or [Authorize], place the call to UseAuthentication and UseAuthorization after UseRouting.
  • If the app uses CORS features, such as [EnableCors], place UseCors next.
  • Replace UseMvc or UseSignalR with UseEndpoints.

The following is an example of Startup.Configure in a typical ASP.NET Core 2.2 app:

public void Configure(IApplicationBuilder app)
{
    ...

    app.UseStaticFiles();
    
    app.UseAuthentication();

    app.UseSignalR(hubs =>
    {
        hubs.MapHub<ChatHub>("/chat");
    });

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

After updating the previous Startup.Configure code:

public void Configure(IApplicationBuilder app)
{
    ...

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

    app.UseAuthentication();
    app.UseAuthorization();
    app.UseCors();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<ChatHub>("/chat");
        endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
    });
}

Security middleware guidance

Support for authorization and CORS is unified around the middleware approach. This allows use of the same middleware and functionality across these scenarios. An updated authorization middleware is provided in this release, and CORS Middleware is enhanced so that it can understand the attributes used by MVC controllers.

CORS

Previously, CORS could be difficult to configure. Middleware was provided for use in some use cases, but MVC filters were intended to be used without the middleware in other use cases. With ASP.NET Core 3.0, we recommend that all apps that require CORS use the CORS Middleware in tandem with Endpoint Routing. UseCors can be provided with a default policy, and [EnableCors] and [DisableCors] attributes can be used to override the default policy where required.

In the following example:

  • CORS is enabled for all endpoints with the default named policy.
  • The MyController class disables CORS with the [DisableCors] attribute.
public void Configure(IApplicationBuilder app)
{
    ...

    app.UseRouting();

    app.UseCors("default"); 

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}

[DisableCors]
public class MyController : ControllerBase
{
    ...
}

Authorization

In earlier versions of ASP.NET Core, authorization support was provided via the [Authorize] attribute. Authorization middleware wasn't available. In ASP.NET Core 3.0, authorization middleware is required. We recommend placing the ASP.NET Core Authorization Middleware (UseAuthorization) immediately after UseAuthentication. The Authorization Middleware can also be configured with a default policy, which can be overridden.

In ASP.NET Core 3.0 or later, UseAuthorization is called in Startup.Configure, and the following HomeController requires a signed in user:

public void Configure(IApplicationBuilder app)
{
    ...

    app.UseRouting();

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

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}

public class HomeController : ControllerBase
{
    [Authorize]
    public IActionResult BuyWidgets()
    {
        ...
    }
}

If the app uses an AuthorizeFilter as a global filter in MVC, we recommend refactoring the code to provide a policy to the UseAuthorization middleware.

In the following example, a custom policy to be applied to all requests when UseAuthorization is called, and the HomeController allows access without the user signing into the app:

public void Configure(IApplicationBuilder app)
{
    ...

    app.UseRouting();

    app.UseAuthentication();
    app.UseAuthorization(new AuthorizationPolicyBuilder().Build()));

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}

[AllowAnonymous]
public class HomeController : ControllerBase
{
    ...
}

Authorization by middleware works without the framework having any specific knowledge of authorization. For instance, health checks has no specific knowledge of authorization, but health checks can have a configurable authorization policy applied by the middleware.

In the following example, UseAuthorization processes authorization without a default policy, but the /healthz health check endpoint requires the user to be in the admin role:

public void Configure(IApplicationBuilder app)
{
    ...

    app.UseRouting();

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

    app.UseEndpoints(endpoints =>
    {
        endpoints
            .MapHealthChecks("/healthz")
            .RequireAuthorization(new AuthorizeAttribute(){ Roles = "admin", });
    });
}

Protection is implemented for some scenarios. UseEndpoint middleware throws an exception if an authorization or CORS policy is skipped due to missing middleware. Analyzer support to provide additional feedback about misconfiguration is in progress.

Migrate SignalR

Mapping of SignalR hubs now takes place inside UseEndpoints.

Map each hub with MapHub. As in previous versions, each hub is explicitly listed.

In the following example, support for the ChatHub SignalR hub is added:

public void Configure(IApplicationBuilder app)
{
    ...

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<ChatHub>();
    });
}

Migrate MVC controllers

Mapping of controllers now takes place inside UseEndpoints.

Add MapControllers if the app uses attribute routing. Since routing includes support for many frameworks in ASP.NET Core 3.0 or later, adding attribute-routed controllers is opt-in.

Replace the following:

  • MapRoute with MapControllerRoute
  • MapAreaRoute with MapAreaControllerRoute

Since routing now includes support for more than just MVC, the terminology has changed to make these methods clearly state what they do. Conventional routes such as MapControllerRoute/MapAreaControllerRoute/MapDefaultControllerRoute are applied in the order that they're added. Place more specific routes (such as routes for an area) first.

In the following example:

  • MapControllers adds support for attribute-routed controllers.
  • MapAreaControllerRoute adds a conventional route for controllers in an area.
  • MapControllerRoute adds a conventional route for controllers.
public void Configure(IApplicationBuilder app)
{
    ...

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
        endpoints.MapAreaControllerRoute(
            "admin", 
            "admin", 
            "Admin/{controller=Home}/{action=Index}/{id?}");
        endpoints.MapControllerRoute(
            "default", "{controller=Home}/{action=Index}/{id?}");
    });
}

Migrate Razor Pages

Mapping Razor Pages now takes place inside UseEndpoints.

Add MapRazorPages if the app uses Razor Pages. Since Endpoint Routing includes support for many frameworks, adding Razor Pages is now opt-in.

In the following example, MapRazorPages adds support for Razor Pages:

public void Configure(IApplicationBuilder app)
{
    ...

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

Use MVC without Endpoint Routing

Using MVC via UseMvc or UseMvcWithDefaultRoute in ASP.NET Core 3.0 requires an explicit opt-in inside Startup.ConfigureServices. This is required because MVC must know whether it can rely on the authorization and CORS Middleware during initialization. An analyzer is provided that warns if the app attempts to use an unsupported configuration.

If the app requires legacy IRouter support, disable EnableEndpointRouting using any of the following approaches in Startup.ConfigureServices:

services.AddMvc(options => options.EnableEndpointRouting = false);
services.AddControllers(options => options.EnableEndpointRouting = false);
services.AddControllersWithViews(options => options.EnableEndpointRouting = false);

services.AddRazorPages().AddMvcOptions(options => options.EnableEndpointRouting = false);

Migrate health checks

Health checks can be used as a router-ware with Endpoint Routing.

Add MapHealthChecks to use health checks with Endpoint Routing. The MapHealthChecks method accepts arguments similar to UseHealthChecks. The advantage of using MapHealthChecks over UseHealthChecks is the ability to apply authorization and to have greater fine-grained control over the matching policy.

In the following example, MapHealthChecks is called for a health check endpoint at /healthz:

public void Configure(IApplicationBuilder app)
{
    ...

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHealthChecks("/healthz", new HealthCheckOptions() { });
    });
}

HostBuilder replaces WebHostBuilder

The ASP.NET Core 3.0 templates use Generic Host. Previous versions used Web Host. The following code shows the ASP.NET Core 3.0 template generated Program class:

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

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

The following code shows the ASP.NET Core 2.2 template-generated Program class:

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

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

IWebHostBuilder remains in 3.0 and is the type of the webBuilder seen in the preceding code sample. WebHostBuilder will be deprecated in a future release and replaced by HostBuilder.

The most significant change from WebHostBuilder to HostBuilder is in dependency injection (DI). When using HostBuilder, you can only inject IConfiguration and IHostingEnvironment into Startup's constructor. The HostBuilder DI constraints:

  • Enable the DI container to be built only one time.
  • Avoids the resulting object lifetime issues like resolving multiple instances of singletons.

Update SignalR code

System.Text.Json is now the default Hub Protocol used by both the client and server.

In Startup.ConfigureServices, call AddJsonProtocol to set serializer options.

Server:

services.AddSignalR(...)
        .AddJsonProtocol(options =>
        {
            options.WriteIndented = false;
        })

Client:

new HubConnectionBuilder()
    .WithUrl("/chatHub")
    .AddJsonProtocol(options =>
    {
        options.WriteIndented = false;
    })
    .Build();

Switch to Newtonsoft.Json

If you're using features of Newtonsoft.Json that aren't supported in System.Text.Json, you can switch back to Newtonsoft.Json:

  1. Install the Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson NuGet package.

  2. On the client, chain an AddNewtonsoftJsonProtocol method call to the HubConnectionBuilder instance:

    new HubConnectionBuilder()
        .WithUrl("/chatHub")
        .AddNewtonsoftJsonProtocol(...)
        .Build();
    
  3. On the server, chain an AddNewtonsoftJsonProtocol method call to the AddSignalR method call in Startup.ConfigureServices:

    services.AddSignalR()
        .AddNewtonsoftJsonProtocol(...);
    

Opt in to runtime compilation

In 3.0, runtime compilation is an opt-in scenario. To enable runtime compilation, see https://docs.microsoft.com/aspnet/core/mvc/views/view-compilation?view=aspnetcore-3.0#runtime-compilation.