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

<PropertyGroup>
 <IncludeOpenAPIAnalyzers>true</IncludeOpenAPIAnalyzers>
</PropertyGroup>
  • 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 isn't a 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.

Kestrel

Configuration

Migrate Kestrel configuration to the web host builder provided by ConfigureWebHostDefaults (Program.cs):

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.ConfigureKestrel(serverOptions =>
            {
                // Set properties and call methods on options
            })
            .UseStartup<Startup>();
        });

If the app creates the host manually with HostBuilder, call UseKestrel on the web host builder in ConfigureWebHostDefaults:

public static void Main(string[] args)
{
    var host = new HostBuilder()
        .UseContentRoot(Directory.GetCurrentDirectory())
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseKestrel(serverOptions =>
            {
                // Set properties and call methods on options
            })
            .UseIISIntegration()
            .UseStartup<Startup>();
        })
        .Build();

    host.Run();
}

Connection Middleware replaces Connection Adapters

Connection Adapters (IConnectionAdapter) have been removed from Kestrel. Replace Connection Adapters with Connection Middleware. Connection Middleware is similar to HTTP Middleware in the ASP.NET Core pipeline but for lower-level connections. HTTPS and connection logging:

  • Have been moved from Connection Adapters to Connection Middleware.
  • These extension methods work as in previous versions of ASP.NET Core.

For more information, see the TlsFilterConnectionHandler example in the ListenOptions.Protocols section of the Kestrel article.

Transport abstractions moved and made public

The Kestrel transport layer has been exposed as a public interface in Connections.Abstractions. As part of these updates:

For more information, see the following GitHub resources:

Kestrel Request trailer headers

For apps that target earlier versions of ASP.NET Core:

  • Kestrel adds HTTP/1.1 chunked trailer headers into the request headers collection.
  • Trailers are available after the request body is read to the end.

This causes some concerns about ambiguity between headers and trailers, so the trailers have been moved to a new collection (RequestTrailerExtensions) in 3.0.

HTTP/2 request trailers are:

  • Not available in ASP.NET Core 2.2.
  • Available in 3.0 as RequestTrailerExtensions.

New request extension methods are present to access these trailers. As with HTTP/1.1, trailers are available after the request body is read to the end.

For the 3.0 release, the following RequestTrailerExtensions methods are available:

  • GetDeclaredTrailers – Gets the request Trailer header that lists which trailers to expect after the body.
  • SupportsTrailers – Indicates if the request supports receiving trailer headers.
  • CheckTrailersAvailable – Checks if the request supports trailers and if they're available to be read. This check doesn't assume that there are trailers to read. There might be no trailers to read even if true is returned by this method.
  • GetTrailer – Gets the requested trailing header from the response. Check SupportsTrailers before calling GetTrailer, or a NotSupportedException may occur if the request doesn't support trailing headers.

For more information, see Put request trailers in a separate collection (aspnet/AspNetCore #10410).

AllowSynchronousIO disabled

AllowSynchronousIO enables or disables synchronous IO APIs, such as HttpReqeuest.Body.Read, HttpResponse.Body.Write, and Stream.Flush. These APIs are a source of thread starvation leading to app crashes. In 3.0, AllowSynchronousIO is disabled by default. For more information, see the Synchronous IO section in the Kestrel article.

In addition to enabling AllowSynchronousIO with ConfigureKestrel's options, synchronous IO can also be overridden on a per-request basis as a temporary mitigation:

var syncIOFeature = HttpContext.Features.Get<IHttpBodyControlFeature>();

if (syncIOFeature != null)
{
    syncIOFeature.AllowSynchronousIO = true;
}

If you have trouble with TextWriter implementations or other streams that call synchronous APIs in Dispose, call the new DisposeAsync API instead.

For more information, see [Announcement] AllowSynchronousIO disabled in all servers (aspnet/AspNetCore #7644).

Microsoft.AspNetCore.Server.Kestrel.Https assembly removed

In ASP.NET Core 2.1, the contents of Microsoft.AspNetCore.Server.Kestrel.Https.dll were moved to Microsoft.AspNetCore.Server.Kestrel.Core.dll. This was a non-breaking update using TypeForwardedTo attributes. For 3.0, the empty Microsoft.AspNetCore.Server.Kestrel.Https.dll assembly (and the NuGet package) have been removed.

Libraries referencing Microsoft.AspNetCore.Server.Kestrel.Https should update ASP.NET Core dependencies to 2.1 or later.

Apps and libraries targeting ASP.NET Core 2.1 or later should remove any direct references to the Microsoft.AspNetCore.Server.Kestrel.Https package.

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. Your app may require this reference if it uses Newtonsoft.Json-specific feature such as JsonPatch or converters or if it formats Newtonsoft.Json-specific types.

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.AddControllersWithViews();
    services.AddRazorPages();
}

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 IRouteConstraint. 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 (and after UseCors if CORS Middleware is used).
  • Replace UseMvc or UseSignalR with UseEndpoints.
  • If the app uses CORS scenarios, such as [EnableCors], place the call to UseCors before any other middleware that use CORS (for example, place UseCors before UseAuthentication, UseAuthorization, and UseEndpoints).
  • Replace IHostingEnvironment with IWebHostEnvironment and add a using statement for the Microsoft.Extensions.Hosting namespace.
  • Replace IApplicationLifetime with IHostApplicationLifetime (Microsoft.Extensions.Hosting namespace).
  • Replace EnvironmentName with Environments (Microsoft.Extensions.Hosting namespace).

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.UseCors();

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

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

Health Checks

Health Checks use endpoint routing with the Generic Host. In Startup.Configure, call MapHealthChecks on the endpoint builder with the endpoint URL or relative path:

app.UseEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health");
});

Health Checks endpoints can:

  • Specify one or more permitted hosts/ports.
  • Require authorization.
  • Require CORS.

For more information, see Health checks in ASP.NET Core.

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 in the call to AddAuthorization.

The DefaultPolicy is initially configured to require authentication, so no additional configuration is required. In the following example, MVC endpoints are marked as RequireAuthorization so that all requests must be authorized based on the DefaultPolicy. However, the HomeController allows access without the user signing into the app due to [AllowAnonymous]:

public void Configure(IApplicationBuilder app)
{
    ...

    app.UseRouting();

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

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

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

Policies can also be customized. Building upon the previous example, the DefaultPolicy is configured to require authentication and a specific scope:

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddAuthorization(options =>
    {
        options.DefaultPolicy = new AuthorizationPolicyBuilder()
          .RequireAuthenticatedUser()
          .RequireScope("MyScope")
          .Build();
    });
}

public void Configure(IApplicationBuilder app)
{
    ...

    app.UseRouting();

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

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

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

Alternatively, all endpoints can be configured to require authorization without [Authorize] or RequireAuthorization by configuring a FallbackPolicy. The FallbackPolicy is different from the DefaultPolicy. The DefaultPolicy is triggered by [Authorize] or RequireAuthorization, while the FallbackPolicy is triggered when no other policy is set. FallbackPolicy is initially configured to allow requests without authorization.

The following example is the same as the preceding DefaultPolicy example but uses the FallbackPolicy to always require authentication on all endpoints except when [AllowAnonymous] is specified:

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddAuthorization(options =>
    {
        options.FallbackPolicy = new AuthorizationPolicyBuilder()
          .RequireAuthenticatedUser()
          .RequireScope("MyScope")
          .Build();
    });
}

public void Configure(IApplicationBuilder app)
{
    ...

    app.UseRouting();

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

    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.

Additionally, each endpoint can customize its authorization requirements. In the following example, UseAuthorization processes authorization with the DefaultPolicy, but the /healthz health check endpoint requires an admin user:

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.

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>();
    });
}

There is a new option for controlling message size limits from clients. For example, in Startup.ConfigureServices:

services.AddSignalR(hubOptions =>
{
    hubOptions.MaximumReceiveMessageSize = 32768;
});

In ASP.NET Core 2.2, you could set the TransportMaxBufferSize and that would effectively control the maximum message size. In ASP.NET Core 3.0, that option now only controls the maximum size before backpressure is observed.

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?}");
    });
}

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);

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.

AddAuthorization moved to a different assembly

The ASP.NET Core 2.2 and lower AddAuthorization methods in Microsoft.AspNetCore.Authorization.dll:

  • Have been renamed AddAuthorizationCore.
  • Have been moved to Microsoft.AspNetCore.Authorization.Policy.dll.

Apps that are using both Microsoft.AspNetCore.Authorization.dll and Microsoft.AspNetCore.Authorization.Policy.dll are not impacted.

Apps that are not using Microsoft.AspNetCore.Authorization.Policy.dll should do one of the following:

  • Switch to using AddAuthorizationCore
  • Add a reference to Microsoft.AspNetCore.Authorization.Policy.dll.

For more information, see Breaking change in AddAuthorization(o =>) overload lives in a different assembly #386.

SignalR code

The SignalR JavaScript client has changed from @aspnet/signalr to @microsoft/signalr. To react to this change, change the references in package.json files, require statements, and ECMAScript import statements.

System.Text.Json is the default protocol

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 Razor file compilation in ASP.NET Core.