If you maintain a classic ASP.NET application—WebForms pages, MVC controllers, .asmx web services, maybe a couple of .svc WCF endpoints—you have heard the same advice for years: rewrite it. System.Web never made the jump to .NET Core, Microsoft has said it never will, and the official migration story is a gradual rewrite behind a YARP proxy.

This article presents a different option: a port of the ASP.NET stack itself—WebForms, MVC 4, Razor, Web Pages, Web API, .asmx, and .svc hosting—to .NET 10, running under Kestrel instead of IIS. Your .aspx, .ascx, .master, .cshtml, code-behind, controllers, and web.config stay exactly as they are. System.Web.UI.Page is still System.Web.UI.Page; System.Web.Mvc.Controller is still System.Web.Mvc.Controller. What changes is the host and the project file—not your application code.

The port is built from Mono's open-source implementation of System.Web and Microsoft's open-source aspnetwebstack (MVC, Razor, Web API), compiled in place against .NET 10, with the IIS/AppDomain-specific machinery replaced by an ASP.NET Core middleware.

The source code is available on GitHub: GitHub - pieroviano/Core.Windows.Forms · GitHub

The Problem: Why System.Web Never Came Along

Three hard blockers kept System.Web off .NET Core, and each needed a real answer.

1. The Assembly Identity Is Taken

Modern .NET ships an empty System.Web.dll facade inside the shared framework, and the runtime gives the shared framework precedence. An app-local System.Web.dll compiles fine and then fails at startup with FileNotFoundException.

The port therefore ships its assemblies under new names (Core.Web, Core.Configuration, and so on) while keeping the namespaces untouched, so your code and your generated page classes never notice.

2. Runtime Page Compilation Used CodeDOM

On .NET Core, CodeDomProvider's compile functionality throws PlatformNotSupportedException.

The port keeps CodeDOM for code generation (which still works) and hands the generated code to Roslyn for compilation.

Both C# and VB.NET pages compile at runtime, just as they did under IIS.

3. BinaryFormatter Is Gone

View state and out-of-process session state used BinaryFormatter for arbitrary object graphs.

Rather than silently replacing it with something insecure, the port refuses a type with no native encoding and reports which type caused the issue. You then opt in to a serializer (a JSON-based serializer is included, or you can provide your own).

Hosting: One Extension Method

The entire hosting model is a single ASP.NET Core extension method: UseWebForms.

Here is the complete Program.cs of a working WebForms application:

using Microsoft.AspNetCore.Builder;
using System.Web.Hosting.Kestrel;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Static files first: let Kestrel serve .css/.js/images directly rather than
// routing them through the System.Web pipeline.
app.UseStaticFiles();

app.UseWebForms(options =>
{
    options.PhysicalPath = app.Environment.ContentRootPath;
    options.VirtualPath = "/";
    options.SiteName = "WebFormsSample";

    // Opt in to a serializer for state objects with no native encoding.
    options.StateSerializer = new System.Web.JsonStateObjectSerializer();
});

app.Run();

That is the entire host.

Behind UseWebForms, the port initializes the System.Web runtime (HttpRuntime, BuildManager, the module and handler pipeline, configuration) inside a single process. Since there are no AppDomains anymore, an adapter maps each incoming ASP.NET Core request onto an HttpWorkerRequest, exactly the abstraction IIS previously supplied.

Everything downstream remains the ASP.NET you already know:

Your Pages Don't Change

To make the point concrete, here is code-behind from the sample application. There is nothing unusual to see—and that is the point.

This is ordinary WebForms code running on .NET 10.

public class DefaultPage : Page
{
    protected Label message;
    protected Label counter;
    protected Repeater items;
    protected GridView grid;

    // Survives postbacks through __VIEWSTATE, not a field.
    int PostbackCount
    {
        get
        {
            object v = ViewState["postbacks"];
            return v == null ? 0 : (int)v;
        }
        set
        {
            ViewState["postbacks"] = value;
        }
    }

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);

        message.Text = IsPostBack
            ? "hello again (postback)"
            : "hello from Page_Load";

        if (IsPostBack)
            PostbackCount = PostbackCount + 1;

        counter.Text = PostbackCount.ToString();

        if (!IsPostBack)
        {
            items.DataSource = new List<string>
            {
                "alpha",
                "beta",
                "gamma"
            };

            items.DataBind();
        }
    }
}

MVC, Web API, and the Rest of the Stack

MVC is not a separate host. Under classic ASP.NET, it was always just an HttpModule and a handler registered inside System.Web, and the same is true here.

The MVC sample host is almost identical to the WebForms host.

app.UseWebForms(options =>
{
    options.PhysicalPath = app.Environment.ContentRootPath;
    options.VirtualPath = "/";
    options.SiteName = "MvcSample";

    // Controllers are discovered by scanning loaded assemblies.
    options.ApplicationAssemblies =
        new[]
        {
            typeof(MvcSample.Controllers.HomeController).Assembly
        };
});

Supported Framework Components

StackStatus on .NET 10
WebForms (.aspx, .ascx, .master)Works, C# and VB.NET, runtime compiled by Roslyn
ASP.NET MVCMVC 4 with Razor v2, attribute routing ([Route]), bundling, and @await in views
Web Pages (.cshtml)Works
Web APIWorks with routing, content negotiation, and model binding
.asmx SOAP servicesWorks
.svc WCF servicesServed by CoreWCF at the same URLs; [ServiceContract] moves from System.ServiceModel to CoreWCF
Session StateInProc, StateServer (reimplemented over IDistributedCache), and SQLServer using ASPState database with unchanged web.config
Dynamic Data / LinqDataSourceWorks using IQueryable and EF Core
Script and Style BundlingSystem.Web.Optimization works
System.Web.MailWorks unchanged

Getting Started

A single package reference is enough for a WebForms application.

<ItemGroup>
  <PackageReference Include="AspNetCore.Web.Hosting.Kestrel" Version="1.0.0" />
</ItemGroup>

It transitively brings in the core of the port (AspNetCore.Web.Base, which contains the Core.Web assembly, along with configuration, services, and extensions).

Its build assets also perform important project setup automatically.

Facade Removal

The empty .NET System.Web.dll and System.Configuration.dll facades are removed from the reference set.

Without this, you would encounter CS0433 type conflicts during compilation.

The build targets also verify that Core.Web.dll is copied to the output directory, turning what would otherwise be a runtime TypeLoadException into a clear build-time error.

Content Handling

The build process automatically copies:

and related content beside your application assembly.

App_Code is copied as content because it is compiled by the runtime rather than MSBuild.

The bin and obj folders are excluded.

Afterward, simply add one package for each additional framework you use:

Notice the deliberate naming split:

The assembly name is only exposed when explicitly referenced in web.config.

Honest Limitations

A port is only trustworthy if it clearly explains what it does not support.

How Well Is It Tested?

The port includes 445 tests across fifteen projects.

These are not isolated unit tests.

The functional test suites:

Additional test suites verify:

Conclusion

"Rewrite it" is good advice when time and budget allow.

However, many organizations maintain large, stable WebForms and MVC applications where the only real limitation is the platform they run on: .NET Framework, IIS, and Windows.

This port removes that dependency.

The same pages, controllers, and web.config run under Kestrel on .NET 10 across any platform supported by .NET 10 while benefiting from modern tooling and a supported runtime.

The migration becomes a host swap instead of a rewrite—and a host swap is measured in days rather than months.

The full source code, sample applications, tests, and a step-by-step porting guide are available in the repository: GitHub - pieroviano/Core.Windows.Forms · GitHub

Happy coding!