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:
web.configis read with full<location>, inheritance, and<httpModules>/<httpHandlers>semantics.Global.asax,HttpApplicationevents, custom modules, and handlers run unchanged.Postbacks,
__VIEWSTATE, validators, master pages, user controls,GridView,Repeater,UpdatePanel, andScriptManagerall work.App_Codeis still compiled at runtime byBuildManager.
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
};
});
Join the conversation! Your thoughts help the community grow.