Incremental ASP.NET to ASP.NET Core Migration

Taylor Southwick

ASP.NET Core is the modern, unified, web framework for .NET that handles all your web dev needs. It is fully open source, cross-platform, and full of innovative features: Blazor, SignalR, gRPC, minimal APIs, etc. Over the past few years, we have seen many customers wanting to move their application from ASP.NET to ASP.NET Core. Customers that have migrated to ASP.NET Core have achieved huge cost savings, including Azure Cosmos DB, Microsoft Graph, and Azure Active Directory.

We have also seen many of the challenges that customers face as they go through this journey. This ranges from dependencies that are not available on ASP.NET Core as well as the time required for this migration. We have been on a journey ourselves to help ease this migration for you as customers, including work over the past year on the .NET Upgrade Assistant. Today we are announcing more tools, libraries and patterns that will allow applications to make the move to ASP.NET Core with less effort and in an incremental fashion. This work will be done on GitHub in a new repo where we are excited to engage with the community to ensure we build the right set of adapters and tooling.

Check out Mike Rousos’s BUILD session below to see the new incremental ASP.NET migration experience in action, or read on to learn more.

Current Challenges

There are a number of reasons why this process is slow and difficult, and we have taken a step back to look at the examples of external and internal partners as to what is the most pressing concerns for them. This boiled down to a couple key themes we wanted to address:

  • How can a large application incrementally move to ASP.NET Core while still innovating and providing value to its business?
  • How can libraries that were written against System.Web.HttpContext be modernized without duplication and a full rewrite?

Let me dive into each of these issues to share what we have seen and what work we are doing to help.

Incremental Migration

Many large applications are being used daily for business-critical applications. These need to continue to function and cannot be put on hold for a potentially long migration to ASP.NET Core. This means that while a migration is occurring, the application needs to still be production ready and new functionality can be added and deployed as usual.

A pattern that has proven to work for this kind of process is the Strangler Fig Pattern. The Strangler Fig Pattern is used to replace an existing legacy system a piece at a time until the whole system has been updated and the old system can be decommissioned. This pattern is one that is fairly easy to grasp in the abstract, but the question often arises how to actually use this in practice. This is part of the incremental migration journey that we want to provide concrete guidance around.

System.Web.dll usage in supporting libraries

ASP.NET apps depend on APIs in System.Web.dll for accessing things like cookies, headers, session, and other values from the current request. ASP.NET apps often depend on libraries that depends on these APIs, like HttpContext, HttpRequest, HttpResponse, etc. However, these types are not available in ASP.NET Core and refactoring this code in an existing code base is very difficult.

To simplify this part of the migration journey, we are introducing a set of adapters that implement the shape of System.Web.HttpContext against Microsoft.AspNetCore.Http.HttpContext. These adapters are contained in the new package Microsoft.AspNetCore.SystemWebAdapters. This package will allow you to continue using your existing logic and libraries while additionally targeting .NET Standard 2.0, .NET Core 3.1, or .NET 6.0 to support running on ASP.NET Core.

Example

Let’s walk through what an incremental migration might look like for an application. We’ll start with an ASP.NET application that has supporting libraries using System.Web based APIs:

This application is hosted on IIS and has a set of processes around it for deployment and maintenance. The migration process aims to move towards ASP.NET Core without compromising the current deployment.

The first step is to introduce a new application based on ASP.NET Core that will become the entry point. Traffic will enter the ASP.NET Core app and if the app cannot match a route, it will proxy the request to the ASP.NET application via YARP. The majority of code will continue to be in the ASP.NET application, but the ASP.NET Core app is now set up to start migrating routes to.

This new application can be deployed to any place that makes sense. With ASP.NET Core you have multiple options: IIS/Kestrel, Windows/Linux, etc. However, keeping deployment similar to the ASP.NET app will simplify the migration process.

In order to start moving over business logic that relies on HttpContext, the libraries need to be built against Microsoft.AspNetCore.SystemWebAdapters. This allows libraries using System.Web APIs to target .NET Framework, .NET Core, or .NET Standard 2.0. This will ensure that the libraries are using surface area that is available with both ASP.NET and ASP.NET Core:

Now we can start moving routes over one at a time to the ASP.NET Core app. These could be MVC or Web API controllers (or even a single method from a controller), Web Forms ASPX pages, HTTP handlers, or some other implementation of a route. Once the route is available in the ASP.NET Core app, it will then be matched and served from there.

During this process, additional services and infrastructure will be identified that must be moved to run on .NET Core. Some options include (listed in order of maintainability):

  1. Move the code to shared libraries
  2. Link the code in the new project
  3. Duplicate the code

Over time, the core app will start processing more of the routes served than the .NET Framework Application:

During this process, you may have the route in both the ASP.NET Core and the ASP.NET Framework applications. This could allow you to perform some A/B testing to ensure functionality is as expected.

Once the .NET Framework Application is no longer needed, it may be removed:

At this point, the application as a whole is running on the ASP.NET Core application stack, but it’s still using the adapters from this repo. At this point, the goal is to remove the use of the adapters until the application is relying solely on the ASP.NET Core application framework:

By getting your application directly on ASP.NET Core APIs, you can access the performance benefits of ASP.NET Core as well as take advantage of new functionality that may not be available behind the adapters.

Getting Started

Now that we’ve laid the groundwork of the Strangler Fig pattern and the System.Web adapters, let’s take an application and see what we need to do to apply this.

In order to work through this, we’ll make use of a preview Visual Studio extension that helps with some of the steps we’ll need to do. You’ll also need the latest Visual Studio Preview to use this extension.

We’re going to take an ASP.NET MVC app and start the migration process. To start, right click on the project and select Migrate Project, which opens a tool window with an option to start migration. The Migrations wizard opens:

Migrate project wizard, Incremental ASP.NET to ASP.NET Core Migration

You can set up your solution with a new project or you can select an existing ASP.NET Core project to use. This will do a few things to the ASP.NET Core project:

  • Add Yarp.ReverseProxy and add initial configuration for that to access the original framework app (referred to as the fallback)
  • Add an environment variable to launchSettings.json to set the environment variable needed to access the original framework app
  • Add Microsoft.AspNetCore.SystemWebAdapters, registers the service, and inserts the middleware for it

The startup code for the ASP.NET Core app will now look like this:

using Microsoft.AspNetCore.SystemWebAdapters;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSystemWebAdapters();
builder.Services.AddReverseProxy().LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

// Add services to the container.
builder.Services.AddControllersWithViews();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Home/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();
app.UseSystemWebAdapters();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapReverseProxy();

app.Run();

At this point, you may run the ASP.NET Core app and it will proxy all requests through to the framework app that do not match in the core app. If you use the default template, you’ll see the following behavior:

If you go to /, it will show you the /Home/Index of the ASP.NET Core app:

/

If you go to a page that doesn’t exist on the ASP.NET Core but does exist on the framework app, it will now surface:

/home/contact, Incremental ASP.NET to ASP.NET Core Migration

Notice that the URL is the same (https://localhost:7234) but will proxy to the request to the framework app when needed. You are now set up to start bringing routes over to the core app, including libraries referencing System.Web.dll.

Proxy support

The framework app will now be downstream of a reverse proxy. This means that some values, such as the request URL, will be different as it will not be using the public entry point. To update these values, turn on proxy support for the framework app with the following code in Global.asax.cs or Global.asax.vb:

protected void Application_Start()
{
  Application.AddSystemWebAdapters()
      .AddProxySupport(options => options.UseForwardedHeaders = true);
}

This change may cause additional impact on your application. Sorting that out will be part of the initial process of getting the Strangler Fig pattern implemented. Please file issues for challenges encountered here that are not already documented.

Session support

Session works very differently between ASP.NET and ASP.NET Core. In order to provide a bridge between the two, the session support in the adapters is extensible. To have the same session behavior (and shared session state) between ASP.NET Core and ASP.NET, we are providing a way to populate the session state on core with the values from the framework app.

This set up will add a handler into the framework app that will allow querying of session state by the core app. This has potential issues of performance and security that should be taken into account, but it allows for a shared session state between the two apps.

In order to set this up, add the Microsoft.AspNetCore.SystemWebAdapters.SessionState package to both applications.

Next, add the services on the ASP.NET Core app:

builder.Services.AddSystemWebAdapters()
    .AddJsonSessionSerializer(options =>
    {
        // Serialization/deserialization requires each session key to be registered to a type
        options.RegisterKey<int>("test-value");
        options.RegisterKey<SessionDemoModel>("SampleSessionItem");
    })
    .AddRemoteAppSession(options =>
    {
        // Provide the URL for the remote app that has enabled session querying
        options.RemoteApp = new(builder.Configuration["ReverseProxy:Clusters:fallbackCluster:Destinations:fallbackApp:Address"]);

        // Provide a strong API key that will be used to authenticate the request on the remote app for querying the session
        options.ApiKey = "strong-api-key";
    });

This will register keys so that the session manager will know how to serialize and deserialize a given key, as well as tell it where the remote app is. Note that this now requires that the object be serializable by System.Text.Json, which may require altering some of the objects that are being stored in session.

Identifying keys may be challenging for a code base that has been using session values ad hoc. The first preview requires you to register them but does not help identify them. In a future update, we plan to log helpful messages and optionally throw if keys are found that are not registered.

Finally, add the following code on the framework app in a similar way in Global.asax:

protected void Application_Start()
{
    Application.AddSystemWebAdapters()
        .AddRemoteAppSession(
            options => options.ApiKey = "some-strong-key-value",
            options => options.RegisterKey<int>("some-key"));
}

Once this is set up, you’ll be able to use (int)HttpContext.Session["some-key"] as you’d expect!

Summary

Today we are introducing some tooling and libraries to help address incremental migration and help you move your ASP.NET applications forward faster and more easily.

There is more to do in this space, and we hope you find this helpful. We are developing this on GitHub at https://github.com/dotnet/systemweb-adapters and welcome issues and PRs to move this forward.

32 comments

Comments are closed. Login to edit/delete your existing comments

  • John King 0

    will Azure Dev Ops Server upgrade to asp.net core as well , so I can try it in docker containers like GitLab does

  • MgSam 1

    “Incremental” migration only further increases complexity and generally should be avoided unless it’s absolutely critical to have both versions of the app live side-by-side, in my opinion. It means that now not only do you need to rewrite your app, you need to make it compatible and possibly share state with the new version.

    I strongly encourage you guys at Microsoft to all get on the same page and port WebForms to Core. From the recent blog post, it seems you guys are building a new Web Forms designer anyway. “Porting” a Web Forms app to a modern stack is a total rewrite and frankly a nonstarter for websites which already work perfectly well.

    It’s strange to me that WinForms and WPF were ported to .NET Core (and even WCF) but WebForms was not.

    • David FowlerMicrosoft employee 1

      Winforms and WPF were ported to .NET Core because there was no client stack at all (and the designer is a MASSIVE rewrite), ASP.NET Core existed and we had a new web stack, porting those client stacks were motivated by filling that gap. CoreWCF is a very different version of WCF that is somewhat compatible but isn’t the same product. It’s built on top of ASP.NET Core and anything using it deeply enough will see those differences. Fortunately, there’s enough usage of “WCF just for RPC between C# things” that it made sense to continue the smoothen the migration effort for some service side components that were very bound to the WCF service contract model or had to continue exposing the soap contract because of an inability to change clients.

      There are 2 things that are coupled but should discussed separately:
      1. System.Web – The hosting model
      2. WebForms – The UI framework on top

      System.Web is tightly coupled to IIS and lots of customers application code by now depends on every implicit behavior ever observed. System.Web has tentacles into the CLR hosting APIs, and is an entirely separate hosting model for a console or windows application (which is MUCH simpler in comparison). System.Web is also fundamentally built on AppDomains, which don’t exists in .NET Core and we’ve directionally moved away from them. The compilation system was designed based on some assumptions about machine wide installs and that’s incompatible with .NET 6+ and all of the different modes its designed to run in (installed into Windows, IIS has hard dependencies on the idea of a managed module, different Configuration system coupled to IIS etc etc etc). I could go on, this just the tip of the iceberg. Long story short, we have no plans to pulling System.Web or the existing infrastructure to support it out of windows in order to make it work on .NET Core 6+. We built ASP.NET Core specifically as a new hosting platform built for modern web applications.

      WebForms is a different can of worms. It would be possible to build something like CoreWCF, that would be incompatible with the any of the controls you consume today in your existing .NET Framework WebForms application that would run on top of ASP.NET Core. The designer in Visual Studio also wouldn’t work, we’d need to rewrite it to make it work with these new .NET Core based controls and “there be dragons”. The coupling to System.Web would need to be broken, managed IIS modules and module pipeline would no longer work, configuration would need to be re-done, the compilation system would need to be updated. IMHO it would be death by 1000 paper cuts (I am speaking from experience here).

      Ultimately though the bigger problem is that we have no plans to move webforms forward as a technology. We’ve built a modern hosting platform (ASP.NET Core) and modern page based models and component models (Razor pages and Blazor) that we want developers building new application or modifying existing ones (like this blog post describes), to be able to take advantage of the new tech from day 0.

      Webforms and .NET Framework will continue to exist as it is built into windows and will get security patches and other smaller updates (bug fixes etc).

      PS: If there’s interest in creating a CoreWebForms ™ equivalent, a good starting point would be the mono webforms implementation. That is currently decoupled from IIS and can run cross platform today. It’s still coupled to the System.Web abstractions, but the packages we’re shipping in this blog post adapt would help with adapting the HttpContext.

      • anonymous 0

        this comment has been deleted.

    • Jon Miller 1

      I agree with MgSam. I will stay on Web Forms with Telerik UI controls for my intranet apps. Microsoft has been reinventing the wheel and creating less functional ways of doing things for the past 15 years (starting with MVC which is just a rip off of Ruby On Rails). The whole approach they took to making .NET multi-platform was a fiasco. The new stuff isn’t any easier either. Way too many design patterns. Logging is a joke also. I’m pretty sure all the original .NET developers at Microsoft left for more money somewhere else. Then, the newbies came in with no knowledge of what was already there and decided to rewrite everything, with breaking changes everywhere. Not to mention, they just want to jam cloud down everyone’s throats to make more money and turn everyone into renters.

    • gambler4882@yahoo.com 0

      I highly appreciate the effort of .NET core team, and I completely understand the issues David Fowler talks about (thanks for detailed explanation). Still, as an old developer from early 2000s I have to say that development was more fun those days. WebForms and Winforms are the example of encapsulation done right. I know that many things changed since than, and I don’t want to sound grumpy but, even tho the new technology stack is the most advanced and flexible ever, programmers experience was better back then.

      • David FowlerMicrosoft employee 1

        WebForms is great and productive but is an artifact of the time it was made. I agree that web development is way too complicated today (but so are the requirements!). That said, Blazor gives a very close to webforms productivity experience and is modern. DotVVM is also a great alternative to webforms that runs on .NET.

        I hear you though, you know and love webforms and want to continue using it. I think that’s fine as long as you don’t expect any big changes to it.

  • Igor Nesterov 0

    Have an experience of migration of tens apps, including large bank corporate.
    I should say HttpContext usually is a smallest problem from possible.
    Usually problem is a legacy .net 4.0 dependencies which limits you to migrate on .net core version 2.2 maximum.
    Because this is the last one which allows to host asp.net core on .Net framework

    • Taylor SouthwickMicrosoft employee 0

      Thanks for your comment. It’s a common one we’ve seen internally and externally and also a basic building block of other compatibility efforts that people may want to do. We have also seen legacy dependencies being a challenge – please join us on Github to help us understand what the issues you’re facing are and if there’s things we can do there.

  • A M 0

    I’m trying to temper my excitement here.

    This has been my biggest gripe about the direction of .NET:
    Developers working on still supported technologies like Web Forms were basically told to rewrite the entire application from scratch or go fly a kite.
    Even something as simple as an adaptor/bridge for HttpContext was non-existent.

    So even just SystemWebAdapters is huge.

    I’m hoping this means that the .NET team is going to be doing more going forward to support teams who would like to use .NET 6+, but are chained to semi-abandoned frameworks.

    Maybe one day you’ll even let us share Razor views between Web Forms/MVC5 and Core/NET6, so we can migrate routes/pages that rely on global usercontrols/partials like header/footer etc.

    • David Keaveny 0

      This was definitely Microsoft’s biggest failing for me – 16 years after MVC was made part of ASP.NET Framework, there is still zero upgrade path from WebForms to MVC, whether in .NET Framework or .NET Core, and the only solution is a complete rewrite of the UI. That’s only the beginning of course, because back when WebForms came out, dependency injection etc was not a first-class citizen, so unless your developers were super switched-on and disciplined, then you’ll find so much business logic embedded in the code-behind pages, and disentangling that particular Gordian Knot is even harder and more error-prone.

      • David FowlerMicrosoft employee 1

        There’s no reasonable migration path from WebForms to MVC. I think you could make very shallow situations work but ultimately there’s a fundamental impedance mismatch between those programming models and there’s not something you can paper over. It’s not because we don’t like to solve hard problems or because we are intentionally trying to hurt WebForms users, the problem is hard to automate without there being a compatible approach on the other side.

        This is why it would be more possible to migrate to something like Blazor from webforms. It wouldn’t be the exact same thing but the component model and life cycle make it similar enough that it would be possible to migrate one to the other. It’s why things like this https://github.com/FritzAndFriends/BlazorWebFormsComponents could be built.

        The most reasonable approach I could think of would be more refactoring tools to move chunks of code around the project (extracting business logic for example), but there’s no big bang migration approach that’s going to make moving legacy code easy.

      • A M 0

        Couldn’t agree more.
        Between 406 aspx and 389 ascx files, there’s precisely zero chance we will ever migrate the entire site, which is already a technical debt nightmare that was originally migrated from Classic ASP 15 years ago. I don’t expect Microsoft to fix that, obviously.
        But what’s worse is that we have no choice but to continue to add new pages/controls in Web Forms instead of MVC, because of how non-existent the integration is between Razor and WebForms renderers.

        We dragged ourselves through the poorly documented and overly complicated process of converting the project from a Web Site to a Web Application Project, added MVC5, etc. We did everything we could to drag ourselves to the future, despite Microsoft’s abandonment.
        They “support” WebForms and .NET 4.8, but not developers who still have to develop those applications.

        And don’t even get me started on new C# versions.

  • Tom Wilson 1

    is there any work being done to share .net identity cookie authentication between .net framework and .net core?

    • Mike RousosMicrosoft employee 1

      Yes, we’re working on shared auth support (in-progress pull request is here) and hope to be able to release it soon in an update.

      • Tim B 1

        This sounds like what i have been needing for several years 🙂

        Just to be clear…
        i have a .net4.8 mvc5/webapi app which authenticates using the aspnet identity. Since Identity from MVC5 and aspnet core 6 are not compat I could use yarp from an aspnet core 6 app and proxy authentication requests to the .net app using the sytemweb adapter?
        Whilst i upgrade and move pages over to the core app? If so, do i have the ability to access the .NET identity objects on the core side?

        i.e. i hit NetCore/ProtectedContentController i need to login so the request is sent to .NET app NetFramework/Account/Login, then is the user property etc available to query on aspnet core side, when i am redirected back to /NetCore/ProtectedContentController ?

  • William Denton 0

    I am mid migration with a large solution. Everything runs net6, with as much code as possible moved to shared libraries that are multi-targeted net48;net6.0, except a large MVC5 app that relies heavily on MVC htmlhelpers. We’re working on ripping bits out, but a quality of life improvement would be to support SDK projects for MVC5 apps. This project goes a long way to showing it can be done, but I’m not sure I want to bet the business on this clever hack. https://github.com/CZEMacLeod/MSBuild.SDK.SystemWeb

    Also discussed in this issue:
    https://github.com/dotnet/project-system/issues/2670

  • Michael Ketting 1

    We (that is, our company RUBICON IT GmbH) face the same challenge with our WebForms-based applications: an incremental migration to another UI stack isn’t a good approach for us, but we still have an interest in using .NET 6 in our daily work. With the existing offerings from Microsoft, we can only choose between keeping some of our code base on .NET Framework for many years to come or start from scratch / do a big-bang migration. Since both of those options aren’t working for us, we needed a third approach, namely to be able to use WebForms inside a .NET 6 / ASP.NET Core application.

    So, for past couple of years, I’ve been involved in an effort to port WebForms to ASP.NET Core and we now have a working solution that allows us to integrate an existing ASP.NET WebForms code-base in an ASP.NET Core web application running on .NET 6.

    If you want, you can check out our demo application where I actually dual-target both ASP.NET Core and .NET Framework:
    The web application as a library: https://github.com/re-motion/Framework/tree/develop/Remotion/ObjectBinding/Web.Development.WebTesting.TestSite.Shared
    The web application running with .NET 6 / ASP.NET Core: https://github.com/re-motion/Framework/tree/develop/Remotion/ObjectBinding/Web.Development.WebTesting.TestSite
    The web application running with .NET Framework: https://github.com/re-motion/Framework/tree/develop/Remotion/ObjectBinding/Web.Development.WebTesting.TestSite.NetFramework
    (Please note we don’t actually have made the ported System.Web assemblies publicly available at this time so it’s more of a code-browsing exercise.)

    Since we believe this scales also to larger ASP.NET WebForms code bases outside our own company (including those that use 3rd party control libraries such as Telerik Controls), we’re looking into providing this as a product. If you’re interested in getting into contact with us, please write us at coreforms@rubicon.eu

    Best regards, Michael

    • David FowlerMicrosoft employee 0

      Amazing!

      Would you be willing to open source this port? Feels like it could be a great offering for the community!

      • Michael Ketting 0

        Hi David,

        thanks for the feedback! Daniel already contacted us via e-mail to discuss this further.

        Best regards, Michael

    • biroj nayak (.NET OSS Dev ~ AWS) 0

      This would be an amazing project for open source community.. we will be interested to contribute further…

  • Maurice De Castro 0

    This is something I have been waiting. Thanks for that effort.
    I have a big Asp.Net MVC 4.8 Framework app and want to start doing this migration process

    The reverse proxy is working well for me. Did the steps to enable session sharing but I am getting this error when accesing session from the asp.net core app:

    On this line:
    var Database = System.Web.HttpContext.Current?.Session?[“Database”] as string;

    I get nothing

    What I did:
    Made the configuration on the global.asax
    Made the configuration on the program.cs
    Ensured that the api key were the same
    Ensured that the session variable was registered on both sides with RegisterKey
    I tried adding and removing .RequireSystemWebAdapterSession(); on the net core side

    Any ideas?

    • Maurice De Castro 0

      Confirmed, System.Web.HttpContext.Current.Session is always null on the asp.net core project

  • Jonathan.Ayoub 0

    This is great – thank you for creating this. I think I could use this for a migration I am working on.

    How does this need to be deployed/hosted? Do I need to host both the asp.net core app and the .net framework app separately, and update the proxy fallback address somehow to point to the hosted .net framework app? Or can I deploy the .net core project and host it alongside the .net framework dll somehow?

    For reference, I am deploying to Azure to an App Service where my current .net framework app is hosted.

    • Taylor SouthwickMicrosoft employee 0

      In your case, you would want to deploy the new ASP.NET Core app as a separate AppService (but could be on the same plan if you want) and update the proxy fallback address to the current AppService endpoint.

  • Max Santos 0

    Me and a colleague migrated a big site like this a few months ago, with YARP still in beta, it worked great.
    First we migrated the pages that got more traffic.
    The new .NET core app is a lot less CPU intensive, so, with the same number of VMs, we are now able to handle more traffic.
    It’s a good solution for some cases.
    There are many horror stories of a new site introducing so many erros that it is basically a flop.
    This avoids that because the old .NET site is still all there, all you need to revert a page to the old temporarily is to remove the route on the new .NET core one.

Feedback usabilla icon