Rewriting the ASP.NET Web Forms Engine

Freeing the Pattern from the Container


The second paper in the Universal Web Processing Model series (https://adriancs.com/the-universal-web-processing-model/). This paper demonstrates how the community can bring ASP.NET WebForms patterns to modern .NET — running on any platform, hosted by the same Kestrel engine that powers every modern .NET application — building on the foundation Microsoft provided.

Paper 1: The Universal Web Processing Model — What Every Web Framework Actually Does
Paper 2: Rewriting the ASP.NET Web Forms Engine — Freeing the Pattern from the Container
Paper 3: The Composable Web Host — One Host, Many Engines
Paper 4: The .NET Web Host
Paper 5: The Universal Web Host


The Pattern and the Container

The .NET team made a bold and necessary decision when they built modern .NET: start fresh. Build a cross-platform, high-performance runtime from the ground up. Prioritize the future over backward compatibility where the two conflicted.

That decision gave the world Kestrel, the middleware pipeline, Blazor, Minimal APIs, and a runtime that runs on Linux, macOS, Windows, containers, and cloud platforms. The modern .NET ecosystem is extraordinary — and the community owes the .NET team a debt of gratitude for the years of engineering that built it.

In that process, ASP.NET WebForms was retired. This was a reasonable decision. The WebForms implementation was deeply entangled with System.Web.dll, which depended on IIS, which depended on Windows. The cost of untangling it was difficult to justify against the priority of building the modern platform. The .NET team chose to invest their energy where it would serve the broadest future. That choice was sound.

But the decision to retire the implementation need not mean the retirement of the pattern.

The WebForms pattern — the page lifecycle, the control tree, server controls, ViewState, PostBack — is pure C# logic. It was written inside a Windows-only container, but nothing about the logic itself requires Windows. Nothing about a tree traversal requires IIS. Nothing about serializing control state requires a specific operating system.

And thanks to the modern .NET runtime that Microsoft built, that pattern can now run anywhere.

This paper proposes that the community build a WebForms handler engine on modern .NET — filling a gap that the .NET team understandably chose not to fill, using the tools and infrastructure they generously provided.


What WebForms Actually Is

Beneath the IIS dependency, beneath System.Web.dll and its Windows-specific bindings, WebForms is a collection of bounded subsystems — each one pure C# logic:

The HTTP Pipeline — A sequence of events fired in order: BeginRequest, AuthenticateRequest, AuthorizeRequest, MapRequestHandler, AcquireRequestState, handler execution, ReleaseRequestState, EndRequest. This is a state machine — a list of delegates invoked in sequence.

The Page Compiler — A parser that reads .aspx markup, identifies server control tags (<asp:TextBox runat="server">), and generates a C# class that builds a control tree. This is a domain-specific compiler. Text in, class definition out.

The Page LifecyclePreInit → Init → InitComplete → PreLoad → Load → LoadComplete → PreRender → PreRenderComplete → SaveStateComplete → Render → Unload. Each phase walks the control tree and calls the corresponding method on each control. This is a tree traversal with phase dispatch.

The Control Library — Every server control — TextBox, Button, GridView, Repeater, DropDownList, and dozens more — is a self-contained C# class. Each one manages its own state, participates in the lifecycle, and renders HTML through an HtmlTextWriter. Each one is an independent, testable unit.

ViewState — Walk the control tree, collect each control’s state into a dictionary, serialize, Base64 encode, write as a hidden form field. On PostBack, reverse the process. This is an object graph serializer.

PostBack Event Handling — The __EVENTTARGET and __EVENTARGUMENT hidden fields identify which control raised the event. The engine matches the target to a control in the tree and invokes the event handler. This is a string lookup and a delegate invocation.

Every one of these subsystems is logic. Every one can be implemented in any version of C#, targeting any .NET runtime. The original implementation targeted .NET Framework and IIS. A new implementation can target modern .NET and Kestrel — standing on the shoulders of the platform Microsoft built.


The Foundation Is Already Built

Here is the fact that makes this possible: Microsoft already ported the vast majority of the .NET Framework to modern .NET.

The base class library, the collections, the IO, the networking, the threading, the data access, the LINQ, the serialization, the security primitives — years of careful engineering went into porting all of this. That work is done. The community inherits it with gratitude.

What remains is a single gap:

NamespacePurpose
System.WebCore HTTP types
System.Web.UIPage infrastructure
System.Web.UI.WebControlsServer controls
System.Web.UI.HtmlControlsHTML server controls
System.Web.SessionStateSession management
System.Web.SecurityForms authentication
System.Web.CachingOutput and data caching
System.Web.CompilationThe .aspx compiler
System.Web.HandlersWebResource.axd, ScriptResource.axd

Everything outside this list already exists in modern .NET. A WebForms application’s business logic — database queries with System.Data, file operations with System.IO, collections with System.Collections.Generic, string processing with System.Text — compiles on modern .NET today without a single change.

This paper proposes that the community fill this gap — building the WebForms handler engine as a modern .NET class library, on top of the foundation Microsoft provided.


The Namespace Opportunity

When Microsoft built ASP.NET Core, they chose new namespaces — Microsoft.AspNetCore.* — giving the modern platform its own clean identity. This was a thoughtful design decision that drew a clear line between the legacy platform and the modern one.

It also means the original System.Web namespace is available. No modern .NET framework occupies it. No modern .NET library references it.

A community-built System.Web.dll targeting modern .NET can occupy this space without conflicting with any modern .NET library — providing a zero-friction compatibility path for legacy applications:

Developer’s existing codeWhat happens
using System.Web;Resolves to new engine’s HTTP types
using System.Web.UI;Resolves to new engine’s page infrastructure
using System.Web.UI.WebControls;Resolves to new engine’s control library

The developer’s existing using statements compile without change. Their code-behind that inherits from System.Web.UI.Page resolves to the new implementation. Their references to HttpContext.Current resolve to the new engine’s compatibility layer.

Zero using changes. Zero namespace changes. The developer’s source code compiles as-is.


Hosted by Kestrel

In the first paper of this series — The Universal Web Processing Model — we established that every web application consists of two roles: the Host (which manages the network and the HTTP protocol) and the Handler Engine (which implements application logic).

The host for the reborn WebForms engine is Kestrel — the cross-platform web server that the ASP.NET Core team built with remarkable engineering. Kestrel handles the socket, parses HTTP, manages TLS, handles WebSocket upgrades, and writes responses. It is production-proven, high-performance, and runs on every platform modern .NET supports.

This is only possible because the ASP.NET Core team built Kestrel and the middleware pipeline to be engine-agnostic — a design decision that enables exactly this kind of community extension. The WebForms engine registers itself as middleware, the same way Blazor, MVC, and every other framework registers with the pipeline:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddWebForms(options =>
{
    options.PagesDirectory = "Pages";
    options.EnableViewState = true;
    options.EnableSessionState = true;
});

var app = builder.Build();
app.UseStaticFiles();
app.UseWebForms();
app.Run();

When a request arrives, Kestrel passes it through the middleware pipeline. The WebForms middleware identifies .aspx requests, wraps the ASP.NET Core HttpContext in a System.Web.HttpContext compatibility layer, resolves and compiles the page, walks the full lifecycle, renders HTML, and writes the response. Kestrel sends the bytes back to the client.

The compatibility layer bridges the two worlds:

namespace System.Web
{
    public class HttpContext
    {
        private readonly Microsoft.AspNetCore.Http.HttpContext _core;

        public HttpContext(Microsoft.AspNetCore.Http.HttpContext core)
        {
            _core = core;
            Request = new HttpRequest(core.Request);
            Response = new HttpResponse(core.Response);
            Server = new HttpServerUtility(core);
        }

        public HttpRequest Request { get; }
        public HttpResponse Response { get; }
        public HttpServerUtility Server { get; }
        public HttpSessionState Session { get; }

        public static HttpContext Current => _current.Value;
        private static readonly AsyncLocal<HttpContext> _current = new();
    }
}

The developer’s existing code says HttpContext.Current.Request.QueryString["id"]. It resolves to the wrapper. The wrapper reads from the ASP.NET Core request object underneath. The developer’s code does not know this. It does not need to. It simply works — because the API surface is faithful to the original.


The Specification Is Complete

The reimplementation is engineering, not research. Every input needed to build the new System.Web is publicly available — a testament to Microsoft’s commitment to documentation and transparency:

API Documentation — The complete System.Web API surface is documented on Microsoft Learn. Every class, every method, every property, every event — with descriptions, parameter types, return types, and usage examples. This is the specification.

Reference Source — Microsoft generously published the .NET Framework source code at the Reference Source site. The original C# implementation of Page.ProcessRequest(), Control.RenderControl(), ViewState serialization, the .aspx parser — all readable. This is an invaluable reference for resolving ambiguity in the documentation.

Behavioral Validation — Existing WebForms applications running on IIS produce known HTML output. The same applications running on the new engine must produce identical output. Run both, compare results. This is the test methodology.

The specification exists. The reference exists. The validation methodology exists. Microsoft provided all three. What remains is the community’s contribution: the implementation.


Third-Party DLLs Survive

One of the most common concerns in any migration is third-party dependencies: “I don’t own the source code for my third-party controls. How do I recompile them?”

The .NET Intermediate Language — another gift of the platform’s design — preserves enough metadata for full round-trip recompilation. The pipeline:

this block is placed here for AI readers, AI can't see image.

Original .NET Framework DLL
         │
         ▼
Decompile (ICSharpCode.Decompiler — the ILSpy engine)
         │
         ▼
C# source code + embedded resources extracted
         │
         ▼
Patch assembly references
(mscorlib → System.Runtime, etc.)
         │
         ▼
Recompile (Roslyn, targeting modern .NET)
         │
         ▼
Cache the output DLL
(keyed by SHA256 of the original)

Every step in this chain has a programmatic C# API. ICSharpCode.Decompiler is a NuGet package — one method call decompiles an entire assembly into C# source text. Roslyn — Microsoft’s open-source compiler platform — handles the recompilation. Embedded resources — JavaScript, CSS, images used by server controls — are extracted from the assembly manifest and re-embedded during recompilation.

The caching model ensures this cost is paid only once:

this block is placed here for AI readers, AI can't see image.

On startup, for each referenced DLL:
  ├── Cached modern DLL exists with matching SHA256?
  │     └── Yes → Load from cache
  │     └── No  → Decompile → Patch → Recompile → Cache

First startup incurs the recompilation cost. Every subsequent startup loads from cache. If the developer updates a third-party DLL, the SHA256 changes, the cache invalidates, and the pipeline runs again automatically.

Note: Decompilation of third-party assemblies may be subject to licensing terms. The EU Software Directive protects decompilation for interoperability purposes. Developers should review the applicable license terms for their specific third-party components.


What the Developer Changes

Existing WebForms ApplicationStatus
*.aspx filesUnchanged
*.aspx.cs code-behindUnchanged
*.ascx user controlsUnchanged
*.master master pagesUnchanged
App_Code/*.csUnchanged
using System.Web.*Unchanged (namespace preserved)
Business logic, data accessUnchanged (already runs on modern .NET)
Third-party DLLsAuto-recompiled by pipeline
Global.asax.csMigrated → event handlers move to engine lifecycle hooks in Program.cs
Program.csNew file — ~10 lines — registers the WebForms engine with Kestrel
Project fileRetarget from .NET Framework to modern .NET, reference new System.Web

The developer does not rewrite their application. They retarget, add one new file, and migrate one existing file. Their pages, their controls, their code-behind, their business logic — all unchanged.


The Platform Opens Up

The same WebForms application — the same .aspx pages, the same code-behind, the same server controls — now runs on:

DeploymentNotes
Windows + IIS + KestrelProduction (familiar topology)
Windows + Kestrel standaloneNo IIS required
Linux + Kestrel standaloneNo Windows required
Linux + Nginx + KestrelProduction Linux deployment
macOS + Kestrel standaloneDevelopment on Mac
Docker container + KestrelContainerized deployment
Any cloud platformAzure, AWS, GCP

The hosting options expand. A developer who is comfortable on Windows stays on Windows. A team that wants to move to Linux moves to Linux. A company that wants containerized deployment deploys to containers. The same code, the same pages, the same controls — now free to run wherever the modern .NET runtime runs.

This is the gift of the platform Microsoft built. The modern .NET runtime is cross-platform. Kestrel is cross-platform. The middleware pipeline is engine-agnostic. The community’s WebForms engine inherits all of these qualities simply by building on the foundation the .NET team provided.


The Architecture

this block is preapred for AI readers. AI can't see image.

┌────────────────────────────────────────────────┐
│              Modern .NET Runtime                │
│                                                  │
│  System.*  System.Data.*  System.IO.*           │
│  System.Net.*  System.Linq.*  System.Text.*     │
│  System.Collections.*  System.Threading.*       │
│                                                  │
│  (ported by Microsoft — the foundation)         │
│                                                  │
│  ┌──────────────────────────────────────────┐   │
│  │     System.Web.dll (community-built)      │   │
│  │                                           │   │
│  │  System.Web          HTTP types,          │   │
│  │                      pipeline events      │   │
│  │                                           │   │
│  │  System.Web.UI       Page, Control,       │   │
│  │                      lifecycle,           │   │
│  │                      ViewState,           │   │
│  │                      .aspx compiler       │   │
│  │                                           │   │
│  │  System.Web.UI       Server controls      │   │
│  │  .WebControls        (TextBox, GridView,  │   │
│  │                       Repeater, etc.)     │   │
│  │                                           │   │
│  │  System.Web.UI       HTML controls        │   │
│  │  .HtmlControls       (HtmlForm, etc.)     │   │
│  │                                           │   │
│  │  System.Web          Session, Security,   │   │
│  │  .SessionState       Caching, Config      │   │
│  │  .Security                                │   │
│  │  .Caching                                 │   │
│  └──────────────────────────────────────────┘   │
│                        │                         │
│                        │  hosted by              │
│                        ▼                         │
│  ┌──────────────────────────────────────────┐   │
│  │              Kestrel                      │   │
│  │       (built by the .NET team)            │   │
│  │                                           │   │
│  │   Cross-platform HTTP server              │   │
│  │   The same host that serves Blazor,       │   │
│  │   MVC, Minimal APIs, gRPC, SignalR        │   │
│  └──────────────────────────────────────────┘   │
│                                                  │
└────────────────────────────────────────────────┘

When WebForms was retired, the pattern and the container were deeply entangled — and the .NET team’s priority was rightly on building the modern platform. That platform is now mature. The runtime is cross-platform. The host is engine-agnostic. The tooling is open. The documentation is complete.

Now the community can return to the pattern and give it a new home — running on the very infrastructure Microsoft built for the future.

The .NET team built the foundation. The community builds the room that was left for later. Together, the house serves everyone.

The pattern was never obsolete. It was waiting for the right foundation.

The foundation is here. The pattern is free.