Migrating Classic ASP (VBScript) to ASP.NET Core 10 — Yes, It's Possible

A Technology Older Than Me

I'm currently working on a project to migrate a Classic ASP application — written in VBScript — to ASP.NET Core 10. For context, Classic ASP was released in 1996 and effectively replaced by ASP.NET in 2002. This technology was deprecated before I was born.

When I first got assigned to this project, my initial reaction was: "Can I even open this?" The codebase is a collection of .asp files with inline VBScript, #include directives, and ADO database calls. No solution file. No project structure in any modern sense. Just folders full of .asp pages, some shared include files, and a global.asa.

I'd worked on ASP.NET MVC 5 to ASP.NET Core migrations before — that's a well-documented path with clear patterns. But Classic ASP to .NET Core? That's jumping across 25+ years of web development evolution. There's no migration tool for this. No automated converter. Every page has to be understood and rewritten.

Step 1: Can We Even Run It?

The first challenge was simply getting the existing application running locally so I could understand what it does. Classic ASP runs on IIS, and I wasn't sure if Visual Studio 2026 would have any idea what to do with a folder full of .asp files.

It turns out — it works. Here's how:

Opening the Project

Since there's no .sln or .csproj file, you can't use "Open Project." Instead:

  1. File → Open → Web Site... (not "Open Project")
  2. Select the folder containing the .asp files
  3. Visual Studio opens it as a "Web Site" project

This is a feature that's been in Visual Studio for years, originally designed for ASP.NET Web Forms "Web Site" projects (as opposed to "Web Application" projects). It also works for Classic ASP.

Running on IIS Express

Once opened, the site runs on Visual Studio's built-in IIS Express. Classic ASP support is included in IIS Express — no extra configuration needed. Hit F5 (or Ctrl+F5 for without debugging) and the .asp pages load in the browser.

You might need to make sure the IIS Express applicationhost.config has Classic ASP enabled:

<section name="asp" overrideModeDefault="Allow" />

And in the site's web.config or IIS configuration, ensure the ASP module is loaded:

<system.webServer>
    <handlers>
        <add name="ASPClassic" path="*.asp" verb="*"
             modules="IsapiModule"
             scriptProcessor="%windir%\system32\inetsrv\asp.dll"
             resourceType="File" />
    </handlers>
</system.webServer>

In most cases, IIS Express handles this automatically — but if you get 404 errors on .asp pages, check these settings.

Debugging

This was the biggest surprise. You can actually debug Classic ASP — but it requires a specific setup through IIS, not just IIS Express.

Step 1: Enable Remote Debugging on IIS

First, you need to enable server-side debugging in IIS for your Classic ASP site:

  1. Open IIS Manager (inetmgr)
  2. Select your site, then open ASP under the IIS section
  3. Expand Compilation and set Debugging Properties:
    • Enable Server-side DebuggingTrue
    • Enable Client-side DebuggingTrue (optional, helps with client script issues)
  4. Click Apply

Important: Server-side debugging significantly impacts performance. Only enable it on your development machine, never on a production server.

Step 2: Attach Visual Studio to the IIS Worker Process

Once debugging is enabled on IIS, you attach Visual Studio to the running IIS process:

  1. Make sure your site is running in IIS (browse to it first so the worker process starts)
  2. In Visual Studio, go to Debug → Attach to Process... (Ctrl+Alt+P)
  3. In the Attach to field, click Select... and check Script
  4. Find w3wp.exe in the process list (this is the IIS worker process)
    • If you don't see it, check Show processes from all users
    • If there are multiple w3wp.exe processes, identify the correct one by its application pool (check the User Name column or use appcmd list wp in a command prompt)
  5. Select the process and click Attach

Now you can set breakpoints directly in .asp files in the VBScript code. Visual Studio will break on your breakpoints, show local variables, and let you step through VBScript line by line. It's not as polished as debugging C# — the variable inspection is more limited, and IntelliSense doesn't exist for VBScript — but it works. Being able to step through the legacy code while figuring out what it does is invaluable.

Understanding the Classic ASP Codebase

Before rewriting anything, I spent time reading and mapping the existing application. Classic ASP codebases tend to share certain patterns:

Inline Everything

There's no separation of concerns. HTML, VBScript logic, SQL queries, and presentation are all mixed together in the same .asp file:

<%
Dim conn, rs, sql
Set conn = Server.CreateObject("ADODB.Connection")
conn.Open Application("ConnectionString")

sql = "SELECT * FROM Products WHERE CategoryId = " & Request.QueryString("catId")
Set rs = conn.Execute(sql)
%>

<html>
<body>
<table>
<% Do While Not rs.EOF %>
    <tr>
        <td><%= rs("ProductName") %></td>
        <td><%= FormatCurrency(rs("Price")) %></td>
    </tr>
<%
    rs.MoveNext
Loop
rs.Close
conn.Close
%>
</table>
</body>
</html>

Yes, that's a SQL injection vulnerability on line 5. Classic ASP code is full of them. This alone is a compelling reason to migrate.

Include Files as "Components"

Classic ASP doesn't have real components or classes (well, VBScript has Class, but it's rarely used well). Instead, shared code lives in include files:

<!-- #include file="includes/db.asp" -->
<!-- #include file="includes/auth.asp" -->
<!-- #include file="includes/functions.asp" -->

These includes are essentially copy-pasted into the page at runtime. There's no namespace, no scope isolation. A variable defined in functions.asp is visible in every page that includes it. This makes it tricky to understand where a variable or function comes from.

Session and Application State

Classic ASP uses Session and Application objects heavily — for user authentication, shopping carts, configuration, cached data. The global.asa file (similar to Global.asax) handles application and session start/end events:

<SCRIPT LANGUAGE="VBScript" RUNAT="Server">
Sub Application_OnStart
    Application("ConnectionString") = "Provider=SQLOLEDB;Data Source=..."
    Application("SiteName") = "My Application"
End Sub

Sub Session_OnStart
    Session("IsLoggedIn") = False
End Sub
</SCRIPT>

The Migration Strategy

You can't automate this migration. There's no tool that converts VBScript to C# or .asp pages to Razor Pages. Every page needs to be rewritten. But you can be systematic about it.

1. Map the Application

Before writing any C# code, I document every .asp page:

  • What URL does it serve?
  • What database queries does it run?
  • What does it display?
  • Does it handle form submissions?
  • What session/application state does it depend on?
  • What include files does it use?

This gives you a complete picture of the application's surface area. For a medium-sized app, this might be 50-100 pages, but many of them follow the same patterns.

2. Identify Shared Patterns

Most Classic ASP apps have a handful of patterns repeated across every page:

  • Database connection — open connection, execute query, loop through results, close connection
  • Authentication checkIf Session("IsLoggedIn") = False Then Response.Redirect "login.asp"
  • Form handlingRequest.Form("fieldName") → validate → INSERT/UPDATE → redirect
  • Pagination — manual record counting and OFFSET/TOP logic

Each of these becomes a clean abstraction in ASP.NET Core: EF Core or Dapper for data access, ASP.NET Core Identity or cookie authentication middleware, model binding with validation, and built-in pagination helpers.

3. Build the ASP.NET Core Shell First

Set up the new ASP.NET Core 10 project with:

  • The data access layer (EF Core + Dapper for performance-critical queries)
  • Authentication and authorization
  • The shared layout (master page equivalent)
  • A CSS framework or custom styles that match the existing site's look

Don't try to improve the UI during migration — match the existing look first, redesign later. Users should see the same interface after migration, even though the technology underneath is completely different.

4. Migrate Page by Page

For each .asp page, create the equivalent Razor Page (or Controller + View):

  1. Read the .asp file and understand what it does
  2. Extract the SQL queries → create a repository method or stored procedure call
  3. Create the Razor Page with the same URL route
  4. Implement the same logic in C# with proper parameterized queries
  5. Build the view to produce the same HTML output
  6. Test against the same database

The key is to run both applications side by side during migration — the Classic ASP site on one port and the new ASP.NET Core site on another — so you can compare the output page by page.

5. Fix Security Issues Along the Way

Classic ASP applications almost always have security vulnerabilities:

  • SQL injection — inline string concatenation for queries → parameterized queries in Dapper/EF Core
  • XSS<%= variable %> doesn't encode by default → Razor encodes by default with @variable
  • No CSRF protection — Classic ASP has no anti-forgery tokens → ASP.NET Core includes them automatically
  • Weak authentication — often custom session-based auth with plain-text password comparison → ASP.NET Core Identity with proper password hashing

Don't just port the vulnerabilities. Use the migration as an opportunity to fix every security issue. This is one of the strongest arguments for doing the migration in the first place.

Classic ASP to ASP.NET Core — A Translation Guide

Here's a quick reference for translating common Classic ASP patterns:

Classic ASP (VBScript) ASP.NET Core (C# / Razor)
Request.QueryString("id") Request.Query["id"] or model binding
Request.Form("name") Request.Form["name"] or [BindProperty]
Response.Redirect "page.asp" return RedirectToPage("Page")
Response.Write "text" @text in Razor
Server.HTMLEncode(str) Razor auto-encodes; @Html.Raw() for unencoded
Session("key") = value HttpContext.Session.SetString("key", value)
Server.CreateObject("ADODB.Connection") Dependency injection + EF Core / Dapper
<!-- #include file="..." --> Partial views, Tag Helpers, DI services
Server.MapPath("/uploads") IWebHostEnvironment.ContentRootPath
FormatCurrency(val) val.ToString("C")
global.asa Program.cs (service registration, middleware)

Lessons So Far

A few things I've learned working on this migration:

Don't underestimate the domain knowledge embedded in the code. The VBScript might be ugly, but it encodes years of business rules and edge cases. Read it carefully. That weird If statement on line 47 is probably handling a real scenario that someone discovered the hard way.

Classic ASP's simplicity is also its documentation. Because everything is inline — SQL, HTML, logic — you can read a single .asp file and understand the entire request lifecycle for that page. There are no layers of abstraction to trace through. Use this to your advantage during the mapping phase.

Run both versions in parallel. Having the old and new apps side by side, pointing at the same database, is the best way to verify that the migration is correct. Open the same page in both browsers and compare the output.

Visual Studio 2026 is surprisingly capable with Classic ASP. I expected the experience to be painful, but being able to open, run, and debug the legacy codebase in the same IDE where I'm building the replacement makes the whole process much smoother.

The migration is ongoing, but the approach is working. One page at a time, the VBScript is becoming C#, the inline SQL is becoming parameterized queries, and the security holes are getting closed. It's not glamorous work, but it's satisfying to see a 25-year-old codebase get a modern foundation.

← Back to all posts