Incrementally Migrating an ASP.NET Framework application to Razor Pages

Do you have a huge .NET framework application (Web forms, MVC) that relies on authentication and/or System.Web types that you would ideally like to migrate to .NET Core, but just don't have the bandwidth to put everything on hold while you rewrite the entire application on a new framework? If so, you might be interested in an exciting new project from Microsoft - SystemWebAdapters for ASP.NET Core - that enables you to incrementally migrate your old application, endpoint by endpoint.

The first release candidate of these adapters is now available, and as with most projects in active development, some package names and APIs described below have changed. For more details on the changes, check the project team's blog announcement.

The basics of the project are relatively simple. It consists of a number of adapters that enable collaboration and sharing of state between a System.Web-based ASP.NET application and one built using ASP.NET Core. This allows, for example, an ASP.NET application to share cookies, session and authentication state with an ASP.NET Core application, despite the fact that the nature of these elements have no implicit compatibility between frameworks. The adapters also act a a drop-in replacement for System.Web in business logic libraries that depend on types within System.Web - HttpContext, Session and so on, so that they "just work" in a .NET Core application, which means that you don't have to rewrite such libraries to get your .NET Core application up and running.

The adapters enable a migration based on the "strangler fig" pattern, whereby the original legacy application is slowly replaced, route by route, by the new application. You create an ASP.NET Core application that acts as the entry point. It will proxy all requests that it does not cater for to the legacy application. Over time, as you implement equivalent endpoints in the new application, the old application is slowly replaced, or strangled by the new application.

Strangler Fig Proxy

As part of the project, a Visual Studio extension has been made available that helps with the initial configuration. At the moment, it caters for migrating  from .NET MVC or Web Forms to a .NET Core MVC application, and it also includes some support for migrating controllers and views to their .NET Core equivalents. The tool is in experimental stage at the moment and doesn't offer Razor Pages as a migration target, despite the fact that Razor Pages is the recommended go-to for server-side HTML generation on .NET Core. However, the SystemWebAdapters package supports this option, although it (currently?) requires manual configuration. Note: there is currently no support for migration to Blazor.

In the following walkthrough, I show the steps required to manually configure the starting point for a migration from a Web Forms application that has Identity enabled (Individual accounts) to a Razor Pages application using the adapters. The steps would be the same if you wanted to migrate from .NET Framework MVC to Razor Pages. I will refer to the .NET Framework app as the "legacy" app.

  1. Add the Microsoft.AspNetCore.SystemWebAdapters package to legacy app. This is in preview at the moment (currently preview 3), so you will need to check Include Preview option if you are using the Visual Studio UI for managing Nuget packages, or the -pre switch when executing install-package. As part of the installation process, it will add SystemWebAdapterModule to the modules node in web.config.
  2. Add the following to Application_Start in Global.asax. This step configures the adapters and support for proxied requests in the legacy app. You will be sharing authentication state between two applications, so the ApiKey should be a strong one, and you should ensure that both applications run under HTTPS. Authentication will be handled remotely by the legacy application :
    SystemWebAdapterConfiguration.AddSystemWebAdapters(this)
        .AddProxySupport(options => options.UseForwardedHeaders = true)
        .AddRemoteApp(options =>
        {
            options.ApiKey = "test-key";
        })
        .AddRemoteAppAuthentication();
  3. Create a new Razor Pages app and install the same Microsoft.AspNetCore.SystemWebAdapters package. Then install Yarp.ReverseProxy.
  4. Add the following section to the appSettings.config file. This provides configuration for the YARP proxy server. For now, the value that you assign to the fallbackApp:Address property should be the URL that the local version of your legacy app runs under:
    "ReverseProxy": {
      "Routes": {
        "fallbackRoute": {
          "ClusterId": "fallbackCluster",
          "Order": "1",
          "Match": {
            "Path": "{**catch-all}"
          }
        }
      },
      "Clusters": {
        "fallbackCluster": {
          "Destinations": {
            "fallbackApp": {
              "Address": "https://localhost:xxxx"
            }
          }
        }
      }
    }
  5. Open Program.cs and add the following to the WebApplicationBuilder configuration. This configures SystemWebAdapters and YARP in the .NET Core app:
    builder.Services.AddSystemWebAdapters().AddRemoteAppAuthentication(true).AddRemoteApp(options =>
    {
        options.RemoteAppUrl = new(builder.Configuration["ReverseProxy:Clusters:fallbackCluster:Destinations:fallbackApp:Address"]);
        options.ApiKey = "test-key";
    });
    builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
  6. Add authentication to the ASP.NET Core app after routing but before authorization:
    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();
  7. Then add SystemWebAdpaters and reverse proxy middleware to the pipeline using these extension methods. This goes after routing but before app.MapRazorPages:
    app.UseSystemWebAdapters();
    app.MapReverseProxy();

Make sure that the legacy app is running and then fire up the ASP.NET Core app. Initially, you should get the Razor Pages application's default home page:

In my example, I've added a link to the legacy app login page: /account/login, which has not been implemented in the new application. If I navigate to it, I reach the old Web forms app:

Once logged in, I am redirected to the home page, which is served by the new app again:

Notice that my identity, which was created by the Web Forms app is being shared with the Razor Pages app. This is the remote authentication at work.

Deployment

Deployment is pretty straightforward. You need two web sites; one for the legacy app, and one for the ASP.NET Core app. You configure the proxy server fallbackApp Address to the URL of the legacy app. You probably already have the legacy app running, so it will most likely just be a case of changing the binding for the legacy app to a different address, while configuring the new .NET Core app to run under the legacy app's current address.

Summary

The SystemWebAdapters project should be of great interest if you have a large .NET Framework app that you would like to migrate to .NET Core but cannot afford to put everything on hold while undertaking the significant work that is required. It is currently in preview and supports migration to ASP.NET MVC and Razor Pages. Experimental tooling is available that facilitates configuration and porting to MVC, but as demonstrated in this article, it is relatively straightforward to manually configure a Razor Pages application as a migration target.

It should be stressed that this project is still in preview and is subject to change. You can keep up with progress by following the project at Github.