ASP.NET HTTP Pipeline Migration — Modules, Handlers, and Global.asax
Overview
Migrate the ASP.NET Framework HTTP pipeline (HttpModules, HttpHandlers, Global.asax) to ASP.NET Core middleware and endpoints. Pipeline ordering is the critical concern — module execution order directly affects authentication, logging, and error handling behavior, and must be reconstructed exactly in the Core middleware pipeline.
Adapter precedence: If the
migrating-mvc-system-web-adaptersskill is loaded, its guidance takes precedence over HttpModule and HttpHandler sections during scaffold and migrate task phases. Global.asax migration is NOT covered by adapters — always migrate directly using this skill.
Workflow
Track progress across these steps:
Migration Progress:
- [ ] Step 1: Inventory pipeline components
- [ ] Step 2: Map pipeline ordering
- [ ] Step 3: Migrate Global.asax events
- [ ] Step 4: Convert HttpModules to middleware
- [ ] Step 5: Convert HttpHandlers to endpoints
- [ ] Step 6: Register middleware in correct order
- [ ] Step 7: Remove legacy pipeline references
Step 1: Inventory Pipeline Components
Scan the project for all HTTP pipeline components:
web.config—<httpModules>,<httpHandlers>, and<system.webServer><modules>/<handlers>sections- Classes implementing
IHttpModuleorIHttpHandler/IHttpAsyncHandler Global.asax/Global.asax.cs— allApplication_*andSession_*event methods.ashxfiles (generic handlers)
Record each component's purpose and the pipeline event it hooks into. This inventory drives all subsequent steps.
Step 2: Map Pipeline Ordering
Reconstruct the pipeline execution order from web.config registration order and Global.asax event sequence. Modules execute in registration order for each event, and the order directly affects behavior.
ASP.NET Framework pipeline event order:
BeginRequestAuthenticateRequest/PostAuthenticateRequestAuthorizeRequest/PostAuthorizeRequestResolveRequestCacheMapRequestHandlerAcquireRequestStatePreRequestHandlerExecute- Handler executes
PostRequestHandlerExecuteReleaseRequestStateUpdateRequestCacheEndRequest
Map each module's events to this sequence. Record the intended middleware order — this becomes the app.Use*() registration order in Program.cs.
Step 3: Migrate Global.asax Events
Convert each Global.asax event method to its ASP.NET Core equivalent:
| Global.asax Event | ASP.NET Core Equivalent |
|---|---|
Application_Start |
Program.cs — code before app.Run() |
Application_End |
IHostApplicationLifetime.ApplicationStopping |
Application_Error |
app.UseExceptionHandler() middleware |
Application_BeginRequest |
Custom middleware (before next()) |
Application_EndRequest |
Custom middleware (after next()) |
Application_AuthenticateRequest |
Authentication middleware |
Session_Start / Session_End |
Removed — no equivalent in Core |
Application_Start — move initialization logic to Program.cs:
Before (Global.asax.cs):
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
Database.SetInitializer(new MigrateDatabaseToLatestVersion<AppDbContext, Configuration>());
}
After (Program.cs):
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
// DB initializer does not move to startup. If a .NET Framework host — or any other
// application, job, or pipeline — still writes this database, this is a side-by-side
// window: load managing-shared-database-schema first and follow its ownership
// decision. A second schema writer breaks it regardless of instance count.
// If you cannot tell, assume it is shared and take that branch.
// Otherwise, apply schema from one deployment step; only call Database.Migrate()
// here if this deployable is the single designated migration runner — a host scaled
// to N instances runs N migrators concurrently.
var app = builder.Build();
Application_End — register a shutdown callback:
var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
lifetime.ApplicationStopping.Register(() =>
{
// Cleanup logic from Application_End
});
Session_Start / Session_End — these events have no ASP.NET Core equivalent. Session state in Core is a simple key-value store without lifecycle events. Remove these methods and relocate any initialization logic to middleware that checks session state on each request.
Step 4: Convert HttpModules to Middleware
Each IHttpModule becomes a middleware class. The module's Init method subscribed to pipeline events; the middleware's InvokeAsync replaces those subscriptions with code that runs before and/or after calling next().
Before (LoggingModule.cs):
public class LoggingModule : IHttpModule
{
public void Init(HttpApplication context)
{
context.BeginRequest += OnBeginRequest;
context.EndRequest += OnEndRequest;
}
private void OnBeginRequest(object sender, EventArgs e)
{
var app = (HttpApplication)sender;
app.Context.Items["RequestStart"] = DateTime.UtcNow;
}
private void OnEndRequest(object sender, EventArgs e)
{
var app = (HttpApplication)sender;
var start = (DateTime)app.Context.Items["RequestStart"];
var elapsed = DateTime.UtcNow - start;
Debug.WriteLine($"Request took {elapsed.TotalMilliseconds}ms");
}
public void Dispose() { }
}
After (LoggingMiddleware.cs):
public class LoggingMiddleware
{
private readonly RequestDelegate _next;
public LoggingMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// BeginRequest equivalent — runs before the rest of the pipeline
context.Items["RequestStart"] = DateTime.UtcNow;
await _next(context);
// EndRequest equivalent — runs after the rest of the pipeline
var start = (DateTime)context.Items["RequestStart"]!;
var elapsed = DateTime.UtcNow - start;
Debug.WriteLine($"Request took {elapsed.TotalMilliseconds}ms");
}
}
Key conversion rules:
- BeginRequest → code before
await _next(context) - EndRequest → code after
await _next(context) - AuthenticateRequest / AuthorizeRequest → authentication/authorization middleware; position relative to
UseAuthentication()/UseAuthorization()is critical - PostAuthorizeRequest → middleware registered immediately after
UseAuthorization() - Error →
app.UseExceptionHandler()or a custom exception middleware wrappingnext()in try/catch - Module state via
HttpContext.Items→HttpContext.Itemsexists in Core and works identically - Inject services via constructor — Core middleware supports DI natively
Step 5: Convert HttpHandlers to Endpoints
Each IHttpHandler maps to either a minimal API endpoint or a terminal middleware, depending on complexity.
Simple handler → minimal API endpoint:
Before (StatusHandler.cs):
public class StatusHandler : IHttpHandler
{
public bool IsReusable => true;
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "application/json";
context.Response.Write("{\"status\":\"ok\"}");
}
}
After (Program.cs):
app.MapGet("/status", () => Results.Json(new { status = "ok" }));
Async handler → async minimal API or middleware:
Before (ReportHandler.cs):
public class ReportHandler : IHttpAsyncHandler
{
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object state)
{
// Async report generation
}
public void EndProcessRequest(IAsyncResult result) { }
public void ProcessRequest(HttpContext context) { }
public bool IsReusable => false;
}
After (Program.cs):
app.MapGet("/report", async (ReportService reportService) =>
{
var report = await reportService.GenerateAsync();
return Results.File(report, "application/pdf");
});
Generic handlers (.ashx) — convert to a controller action or minimal API endpoint. There is no .ashx equivalent in Core. Move the ProcessRequest logic to the new endpoint method.
Handler factory pattern — if a custom IHttpHandlerFactory dispatches to different handlers based on the request, replace with route-based dispatch using app.MapGet / app.MapPost with distinct route patterns.
Step 6: Register Middleware in Correct Order
Register all converted middleware in Program.cs in the exact order determined in Step 2. Middleware order in Core is the registration order — there is no event-based system.
Standard pipeline order for a typical migration:
var app = builder.Build();
app.UseExceptionHandler("/Home/Error"); // Application_Error
app.UseHsts();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// Custom middleware from modules (BeginRequest-phase modules)
app.UseMiddleware<LoggingMiddleware>();
app.UseAuthentication(); // AuthenticateRequest
app.UseAuthorization(); // AuthorizeRequest
// Custom middleware from PostAuthorizeRequest modules
app.UseMiddleware<PostAuthMiddleware>();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
// Minimal API endpoints from converted handlers
app.MapGet("/status", () => Results.Json(new { status = "ok" }));
Pipeline ordering is the most common source of migration bugs. Verify that:
- Exception middleware is registered first (catches errors from all subsequent middleware)
- Authentication runs before authorization
- Custom modules that depend on auth state are registered after
UseAuthorization() - Logging/telemetry middleware wraps as much of the pipeline as the original module did
Step 7: Remove Legacy Pipeline References
Remove all legacy pipeline artifacts:
- Delete
Global.asaxandGlobal.asax.cs - Delete original
IHttpModuleimplementation files - Delete original
IHttpHandler/IHttpAsyncHandlerfiles - Delete
.ashxand.ashx.csfiles - Remove
<httpModules>,<httpHandlers>,<modules>, and<handlers>sections fromweb.config(ifweb.configis still present) - Remove
using System.Webstatements that are no longer needed - Search for remaining references to
HttpApplication,IHttpModule,IHttpHandler, andHttpContext.Current— these indicate incomplete migration
Success Criteria
- All
IHttpModuleimplementations converted to middleware classes - All
IHttpHandler/IHttpAsyncHandlerimplementations converted to endpoints or terminal middleware - All
Global.asaxevents migrated toProgram.cs, lifetime hooks, or middleware Session_Start/Session_Endremoved with logic relocated or dropped- Middleware registration order in
Program.csmatches the original module execution order - No references remain to
Global.asax,IHttpModule,IHttpHandler, orHttpContext.Current - No
.ashxfiles remain in the project - Project builds without errors