The most formidable barrier to migrating legacy ASP.NET MVC applications to .NET 8 is not the syntax changes or the project file format; it is the architectural dependency on System.Web.dll.
Specifically, enterprise applications built between 2010 and 2018 often treat HttpContext.Current as a global singleton, accessing request data, session state, and user identity deep within business logic layers, static helper methods, and repositories. When you target .NET 8, System.Web disappears. The modern Microsoft.AspNetCore.Http.HttpContext is designed to be injected via Dependency Injection (DI), not accessed statically.
Rewriting hundreds of thousands of lines of code to thread IHttpContextAccessor through every method signature is rarely feasible for an initial migration. This post details how to implement the System.Web Adapters, a library maintained by Microsoft that creates a compatibility layer, allowing you to run legacy code accessing HttpContext.Current inside a .NET 8 environment.
The Root Cause: ThreadStatic vs. AsyncLocal
To understand why HttpContext.Current was removed, you must understand how the hosting model changed.
In ASP.NET 4.x (IIS), the request context was tied to the execution thread using thread-local storage (specifically CallContext in later versions). This worked because IIS controlled the threading model tightly.
In .NET 8 (ASP.NET Core), the request pipeline is asynchronous and decoupled from the web server implementation (Kestrel, HTTP.sys, IIS). A single request might jump across multiple threads as it awaits I/O operations. If you used thread-local storage, you would lose the context immediately after the first await.
To solve this, .NET 8 flows context using AsyncLocal<T>, which persists data across asynchronous control flow changes. However, for architectural purity and testability, the ASP.NET Core team removed the static accessor. They enforced explicit dependencies via IHttpContextAccessor.
The System.Web Adapters bridge this gap by implementing a static shim that looks like System.Web.HttpContext but delegates logic to the modern IHttpContextAccessor backed by AsyncLocal.
The Fix: Implementing the System.Web Adapter Shim
We will configure a .NET 8 Web API/MVC project to support a legacy service that calls HttpContext.Current.
1. Project Setup and Dependencies
Assume you have a standard .NET 8 ASP.NET Core Web API project. Install the core services package. This package provides the APIs that mimic System.Web.
dotnet add package Microsoft.AspNetCore.SystemWebAdapters
2. The Legacy Scenario
Here is a typical legacy service. In a strict .NET 8 environment, this code would not compile because System.Web does not exist. With the adapter, we can keep this code almost exactly as is.
LegacyService.cs
using System.Web; // This namespace is provided by the Adapter package
namespace EnterpriseApp.Legacy.Services;
public class LegacyUserContext
{
// This method relies on the static HttpContext.Current
// commonly found in older DAL or Business Logic layers.
public string GetCurrentUserIp()
{
// In standard .NET 8, HttpContext.Current is null or non-existent.
if (HttpContext.Current == null)
{
throw new InvalidOperationException("Legacy HttpContext is not available.");
}
// Accessing the Request property statically
return HttpContext.Current.Request.UserHostAddress
?? "0.0.0.0";
}
public void SetLegacySessionData(string key, string value)
{
// Heavily reliant on Session["key"]
if (HttpContext.Current?.Session != null)
{
HttpContext.Current.Session[key] = value;
}
}
}
3. Middleware Configuration
You must register the services and the middleware pipeline in Program.cs. The order of operations is critical. The UseSystemWebAdapters middleware must run early in the pipeline to ensure the context is populated before any legacy logic executes.
Program.cs
using EnterpriseApp.Legacy.Services;
var builder = WebApplication.CreateBuilder(args);
// 1. Register standard MVC/API services
builder.Services.AddControllers();
// 2. Add System.Web Adapters
// This registers the IHttpContextAccessor and the logic required
// to map the modern context to the legacy shim.
builder.Services.AddSystemWebAdapters()
.AddJsonSessionSerializer(options =>
{
// Optional: Configure session serialization if you use Session state
// Keys strictly required for System.Web compatibility
options.KnownKeys.Add("UserId");
options.KnownKeys.Add("CartId");
})
// Pre-loads session logic to mimic blocking synchronous session access
.WrapAspNetCoreSession();
// Register the legacy service for DI injection into modern controllers
builder.Services.AddScoped<LegacyUserContext>();
// Required for session support in .NET 8
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession();
var app = builder.Build();
// 3. Configure the HTTP Request Pipeline
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
// 4. Enable .NET 8 Session (Must be before SystemWebAdapters)
app.UseSession();
// 5. Enable System.Web Adapters
// This populates HttpContext.Current for the duration of the request.
app.UseSystemWebAdapters();
app.MapControllers();
app.Run();
4. Integration
Now, you can invoke the legacy code from a modern .NET 8 Controller. The adapter ensures that when the legacy code reaches out to the static HttpContext.Current, it finds a valid object populated with data from the current request.
Controllers/MigrationController.cs
using Microsoft.AspNetCore.Mvc;
using EnterpriseApp.Legacy.Services;
namespace EnterpriseApp.Controllers;
[ApiController]
[Route("api/[controller]")]
public class MigrationController(LegacyUserContext legacyService) : ControllerBase
{
[HttpGet("check-ip")]
public IActionResult GetLegacyIp()
{
// When this executes, the SystemWebAdapter middleware has already
// assigned the AsyncLocal context to the static shim.
try
{
var ip = legacyService.GetCurrentUserIp();
// Prove that session write works via the shim
legacyService.SetLegacySessionData("UserId", "USER_12345");
return Ok(new
{
Source = "Legacy System.Web Adapter",
IpAddress = ip,
SessionStored = true
});
}
catch (Exception ex)
{
return StatusCode(500, new { Error = ex.Message });
}
}
}
Why This Works
The Microsoft.AspNetCore.SystemWebAdapters library performs two distinct operations:
- Type Forwarding & Shimming: It provides classes named
System.Web.HttpContext,HttpRequest, andHttpResponsethat align with the old .NET Framework API surface. However, these are wrappers. - Context Injection: When
app.UseSystemWebAdapters()executes, it retrieves the currentMicrosoft.AspNetCore.Http.HttpContext(the modern context). It then wraps this context in the legacy shim and assigns it to a static property backed byAsyncLocal.
When LegacyUserContext calls HttpContext.Current, it accesses that thread-safe AsyncLocal value.
A Critical Warning on Session
In the example above, we used .WrapAspNetCoreSession(). Legacy ASP.NET Session was synchronous and blocking. ASP.NET Core Session is asynchronous (await session.LoadAsync()).
If your legacy code accesses HttpContext.Current.Session["Key"], it expects the data to be there synchronously. The adapter middleware handles this by pre-loading the session asynchronously at the start of the pipeline so that it is available in memory when the legacy code accesses the indexer.
Conclusion
The "Lift and Shift" strategy is often criticized, but for large monoliths, it is a necessary first step. The System.Web Adapters allow you to migrate the hosting platform to .NET 8 without immediately refactoring the internal architecture of your application.
Once the application is running on .NET 8, you should prioritize a phased refactoring strategy: locate usages of HttpContext.Current, inject IHttpContextAccessor (or better yet, specific data objects) directly, and remove the adapter dependency.