Upgrade from ASP.NET Framework to ASP.NET Core

Why upgrade to the latest .NET

ASP.NET Core is the modern web framework for .NET. While ASP.NET Core has many similarities to ASP.NET in the .NET Framework, it's a new framework that's completely rewritten. ASP.NET apps updated to ASP.NET Core can benefit from improved performance and access to the latest web development features and capabilities.

ASP.NET Framework update approaches

Most non-trivial ASP.NET Framework apps should consider using the incremental upgrade approach. For more information, see Incremental ASP.NET to ASP.NET Core upgrade.

For ASP.NET MVC and Web API apps, see Learn to upgrade from ASP.NET MVC and Web API to ASP.NET Core MVC. For ASP.NET Framework Web Forms apps, see Learn to upgrade from ASP.NET Web Forms to ASP.NET Core.

Reliable web app patterns

See The Reliable Web App Pattern for.NET YouTube videos and article for guidance on creating a modern, reliable, performant, testable, cost-efficient, and scalable ASP.NET Core app, whether from scratch or refactoring an existing app.

URI decoding differences between ASP.NET to ASP.NET Core

ASP.NET Core has the following URI decoding differences with ASP.NET Framework:

ASCII Encoded ASP.NET Core ASP.NET Framework
\ %5C \ /
/ %2F %2F /

When decoding %2F on ASP.NET Core:

  • The entire path gets unescaped except %2F because converting it to / would change the path structure. It can’t be decoded until the path is split into segments.

To generate the value for HttpRequest.Url, use new Uri(this.AspNetCoreHttpRequest.GetEncodedUrl()); to avoid Uri misinterpreting the values.

Migrating User Secrets from ASP.NET Framework to ASP.NET Core

See this GitHub issue.

This article serves as a reference guide for migrating ASP.NET apps to ASP.NET Core.

Visual Studio has tooling to help migrate ASP.NET apps to ASP.NET Core. For more information, see Migrating from ASP.NET to ASP.NET Core in Visual Studio.

The .NET Upgrade Assistant is a command-line tool that can help migrate ASP.NET to ASP.NET Core. For more information, see Overview of the .NET Upgrade Assistant and Upgrade an ASP.NET MVC app to .NET 6 with the .NET Upgrade Assistant.

See the ebook Porting existing ASP.NET apps to .NET Core for a comprehensive porting guide.

Prerequisites

.NET Core SDK 2.2 or later

Target frameworks

ASP.NET Core projects offer developers the flexibility of targeting .NET Core, .NET Framework, or both. See Choosing between .NET Core and .NET Framework for server apps to determine which target framework is most appropriate.

When targeting .NET Framework, projects need to reference individual NuGet packages.

Targeting .NET Core allows you to eliminate numerous explicit package references, thanks to the ASP.NET Core metapackage. Install the Microsoft.AspNetCore.App metapackage in your project:

<ItemGroup>
   <PackageReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

When the metapackage is used, no packages referenced in the metapackage are deployed with the app. The .NET Core Runtime Store includes these assets, and they're precompiled to improve performance. See Microsoft.AspNetCore.App metapackage for ASP.NET Core for more detail.

Project structure differences

The .csproj file format has been simplified in ASP.NET Core. Some notable changes include:

  • Explicit inclusion of files isn't necessary for them to be considered part of the project. This reduces the risk of XML merge conflicts when working on large teams.

  • There are no GUID-based references to other projects, which improves file readability.

  • The file can be edited without unloading it in Visual Studio:

    Edit CSPROJ context menu option in Visual Studio 2017]

Global.asax file replacement

ASP.NET Core introduced a new mechanism for bootstrapping an app. The entry point for ASP.NET applications is the Global.asax file. Tasks such as route configuration and filter and area registrations are handled in the Global.asax file.

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
    }
}

This approach couples the application and the server to which it's deployed in a way that interferes with the implementation. In an effort to decouple, OWIN was introduced to provide a cleaner way to use multiple frameworks together. OWIN provides a pipeline to add only the modules needed. The hosting environment takes a Startup function to configure services and the app's request pipeline. Startup registers a set of middleware with the application. For each request, the application calls each of the middleware components with the head pointer of a linked list to an existing set of handlers. Each middleware component can add one or more handlers to the request handling pipeline. This is accomplished by returning a reference to the handler that's the new head of the list. Each handler is responsible for remembering and invoking the next handler in the list. With ASP.NET Core, the entry point to an application is Startup, and you no longer have a dependency on Global.asax. When using OWIN with .NET Framework, use something like the following as a pipeline:

using Owin;
using System.Web.Http;

namespace WebApi
{
    // Note: By default all requests go through this OWIN pipeline. Alternatively you can turn this off by adding an appSetting owin:AutomaticAppStartup with value “false”. 
    // With this turned off you can still have OWIN apps listening on specific routes by adding routes in global.asax file using MapOwinPath or MapOwinRoute extensions on RouteTable.Routes
    public class Startup
    {
        // Invoked once at startup to configure your application.
        public void Configuration(IAppBuilder builder)
        {
            HttpConfiguration config = new HttpConfiguration();
            config.Routes.MapHttpRoute("Default", "{controller}/{customerID}", new { controller = "Customer", customerID = RouteParameter.Optional });

            config.Formatters.XmlFormatter.UseXmlSerializer = true;
            config.Formatters.Remove(config.Formatters.JsonFormatter);
            // config.Formatters.JsonFormatter.UseDataContractJsonSerializer = true;

            builder.UseWebApi(config);
        }
    }
}

This configures your default routes, and defaults to XmlSerialization over Json. Add other Middleware to this pipeline as needed (loading services, configuration settings, static files, etc.).

ASP.NET Core uses a similar approach, but doesn't rely on OWIN to handle the entry. Instead, that's done through the Program.cs Main method (similar to console applications) and Startup is loaded through there.

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;

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

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

Startup must include a Configure method. In Configure, add the necessary middleware to the pipeline. In the following example (from the default web site template), extension methods configure the pipeline with support for:

  • Error pages
  • HTTP Strict Transport Security
  • HTTP redirection to HTTPS
  • ASP.NET Core MVC
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseMvc();
}

The host and application have been decoupled, which provides the flexibility of moving to a different platform in the future.

Note

For a more in-depth reference to ASP.NET Core Startup and Middleware, see Startup in ASP.NET Core

Store configurations

ASP.NET supports storing settings. These setting are used, for example, to support the environment to which the applications were deployed. A common practice was to store all custom key-value pairs in the <appSettings> section of the Web.config file:

<appSettings>
  <add key="UserName" value="User" />
  <add key="Password" value="Password" />
</appSettings>

Applications read these settings using the ConfigurationManager.AppSettings collection in the System.Configuration namespace:

string userName = System.Web.Configuration.ConfigurationManager.AppSettings["UserName"];
string password = System.Web.Configuration.ConfigurationManager.AppSettings["Password"];

ASP.NET Core can store configuration data for the application in any file and load them as part of middleware bootstrapping. The default file used in the project templates is appsettings.json:

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

Loading this file into an instance of IConfiguration inside your application is done in Startup.cs:

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

public IConfiguration Configuration { get; }

The app reads from Configuration to get the settings:

string userName = Configuration.GetSection("AppConfiguration")["UserName"];
string password = Configuration.GetSection("AppConfiguration")["Password"];

There are extensions to this approach to make the process more robust, such as using Dependency Injection (DI) to load a service with these values. The DI approach provides a strongly-typed set of configuration objects.

// Assume AppConfiguration is a class representing a strongly-typed version of AppConfiguration section
services.Configure<AppConfiguration>(Configuration.GetSection("AppConfiguration"));

Note

For a more in-depth reference to ASP.NET Core configuration, see Configuration in ASP.NET Core.

Native dependency injection

An important goal when building large, scalable applications is the loose coupling of components and services. Dependency Injection is a popular technique for achieving this, and it's a native component of ASP.NET Core.

In ASP.NET apps, developers rely on a third-party library to implement Dependency Injection. One such library is Unity, provided by Microsoft Patterns & Practices.

An example of setting up Dependency Injection with Unity is implementing IDependencyResolver that wraps a UnityContainer:

using Microsoft.Practices.Unity;
using System;
using System.Collections.Generic;
using System.Web.Http.Dependencies;

public class UnityResolver : IDependencyResolver
{
    protected IUnityContainer container;

    public UnityResolver(IUnityContainer container)
    {
        if (container == null)
        {
            throw new ArgumentNullException("container");
        }
        this.container = container;
    }

    public object GetService(Type serviceType)
    {
        try
        {
            return container.Resolve(serviceType);
        }
        catch (ResolutionFailedException)
        {
            return null;
        }
    }

    public IEnumerable<object> GetServices(Type serviceType)
    {
        try
        {
            return container.ResolveAll(serviceType);
        }
        catch (ResolutionFailedException)
        {
            return new List<object>();
        }
    }

    public IDependencyScope BeginScope()
    {
        var child = container.CreateChildContainer();
        return new UnityResolver(child);
    }

    public void Dispose()
    {
        Dispose(true);
    }

    protected virtual void Dispose(bool disposing)
    {
        container.Dispose();
    }
}

Create an instance of your UnityContainer, register your service, and set the dependency resolver of HttpConfiguration to the new instance of UnityResolver for your container:

public static void Register(HttpConfiguration config)
{
    var container = new UnityContainer();
    container.RegisterType<IProductRepository, ProductRepository>(new HierarchicalLifetimeManager());
    config.DependencyResolver = new UnityResolver(container);

    // Other Web API configuration not shown.
}

Inject IProductRepository where needed:

public class ProductsController : ApiController
{
    private IProductRepository _repository;

    public ProductsController(IProductRepository repository)  
    {
        _repository = repository;
    }

    // Other controller methods not shown.
}

Because Dependency Injection is part of ASP.NET Core, you can add your service in the ConfigureServices method of Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    // Add application services.
    services.AddTransient<IProductRepository, ProductRepository>();
}

The repository can be injected anywhere, as was true with Unity.

Note

For more information on dependency injection, see Dependency injection.

Serve static files

An important part of web development is the ability to serve static, client-side assets. The most common examples of static files are HTML, CSS, Javascript, and images. These files need to be saved in the published location of the app (or CDN) and referenced so they can be loaded by a request. This process has changed in ASP.NET Core.

In ASP.NET, static files are stored in various directories and referenced in the views.

In ASP.NET Core, static files are stored in the "web root" (<content root>/wwwroot), unless configured otherwise. The files are loaded into the request pipeline by invoking the UseStaticFiles extension method from Startup.Configure:

Note

If targeting .NET Framework, install the NuGet package Microsoft.AspNetCore.StaticFiles.

For example, an image asset in the wwwroot/images folder is accessible to the browser at a location such as http://<app>/images/<imageFileName>.

Note

For a more in-depth reference to serving static files in ASP.NET Core, see Static files.

Multi-value cookies

Multi-value cookies aren't supported in ASP.NET Core. Create one cookie per value.

Authentication cookies are not compressed in ASP.NET Core

For security reasons, authentication cookies are not compressed in ASP.NET Core. When using authentication cookies, developers should minimize the number of claim information included to just that necessary for their needs.

Partial app migration

One approach to partial app migration is to create an IIS sub-application and only move certain routes from ASP.NET 4.x to ASP.NET Core while preserving the URL structure the app. For example, consider the URL structure of the app from the applicationHost.config file:

<sites>
    <site name="Default Web Site" id="1" serverAutoStart="true">
        <application path="/">
            <virtualDirectory path="/" physicalPath="D:\sites\MainSite\" />
        </application>
        <application path="/api" applicationPool="DefaultAppPool">
            <virtualDirectory path="/" physicalPath="D:\sites\netcoreapi" />
        </application>
        <bindings>
            <binding protocol="http" bindingInformation="*:80:" />
            <binding protocol="https" bindingInformation="*:443:" sslFlags="0" />
        </bindings>
    </site>
	...
</sites>

Directory structure:

.
├── MainSite
│   ├── ...
│   └── Web.config
└── NetCoreApi
    ├── ...
    └── web.config

[BIND] and Input Formatters

Previous versions of ASP.NET used the [Bind] attribute to protect against overposting attacks. Input formatters work differently in ASP.NET Core. The [Bind] attribute is no longer designed to prevent overposting when used with input formatters to parse JSON or XML. These attributes affect model binding when the source of data is form data posted with the x-www-form-urlencoded content type.

For apps that post JSON information to controllers and use JSON Input Formatters to parse the data, we recommend replacing the [Bind] attribute with a view model that matches the properties defined by the [Bind] attribute.

Additional resources