HTTP Pipeline, Handlers, and Configuration

This section documents ASP.NET Web Forms on .NET Framework 4.8.1, the last and permanent version of Web Forms — the page life cycle and postback model, ViewState and control state, server controls, validation controls, master pages and themes, data-bound controls, and the provider-based security model — as described by the official documentation at Microsoft Learn and the ASP.NET previous-versions archive, which are the reference these pages are written and verified against.

Web Forms receives security fixes only and has no forward path onto modern .NET (.NET Framework 4.8.1 is Microsoft’s last version of .NET Framework; Web Forms itself never shipped on .NET Core/.NET 5+). It remains supported for existing applications running on Windows but is not recommended for new development — see Choosing an ASP.NET Framework and Migrating to Modern ASP.NET for what that means in practice.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Every ASP.NET Web Forms request, before it ever reaches a Page, passes through the same HttpApplication-driven pipeline that also serves .ashx handlers and any custom module — the same pipeline the page life cycle sits inside as one particular kind of handler.

HttpApplication and global.asax

global.asax (compiled into a class deriving from HttpApplication) is where application- and request-wide events are handled, by naming convention exactly like AutoEventWireup on a page:

// Global.asax.cs
public class Global : System.Web.HttpApplication
{
    protected void Application_Start(object sender, EventArgs e)
    {
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
    }

    protected void Application_BeginRequest(object sender, EventArgs e) { /* runs for every request, incl. .ashx/.axd */ }

    protected void Application_AuthenticateRequest(object sender, EventArgs e) { /* after auth module runs */ }

    protected void Application_Error(object sender, EventArgs e)
    {
        Exception ex = Server.GetLastError();
        Server.ClearError(); // suppress the default yellow-screen-of-death if handled here
        LogError(ex);
    }

    protected void Application_End(object sender, EventArgs e) { /* app domain shutting down */ }
}

Application_Start/Application_End fire once per application-domain lifetime (recycles count as a new lifetime); the *Request events fire on every request, in the fixed order documented at ASP.NET Application Life Cycle Overview for IIS.

HttpContext, HttpRequest, HttpResponse, Server

HttpContext.Current (or Page.Context inside a page) is the ambient, per-request object graph:

HttpRequest request = HttpContext.Current.Request;
string userAgent = request.UserAgent;
string queryValue = request.QueryString["id"];

HttpResponse response = HttpContext.Current.Response;
response.ContentType = "application/json";
response.Write("{\"ok\":true}");

HttpServerUtility server = HttpContext.Current.Server;
string safe = server.HtmlEncode(userInput);
server.Transfer("~/Error.aspx"); // server-side redirect, no round trip; Response.Redirect is client-side

HttpContext.Current is thread-static-like ambient state tied to the request thread, which is exactly why it does not survive naively across async/await continuations scheduled on a different thread in older code — a common source of null-reference surprises when retrofitting async onto legacy handlers.

Writing IHttpHandler/IHttpHandlerFactory and .ashx

A .ashx generic handler is the lightest way to answer an HTTP request with no page life cycle at all — appropriate for a raw image, JSON, or file-download endpoint:

// Thumbnail.ashx.cs
public class Thumbnail : IHttpHandler
{
    public bool IsReusable => true;

    public void ProcessRequest(HttpContext context)
    {
        int id = int.Parse(context.Request.QueryString["id"]);
        byte[] bytes = _imageService.GetThumbnail(id);
        context.Response.ContentType = "image/jpeg";
        context.Response.BinaryWrite(bytes);
    }
}
<%-- Thumbnail.ashx --%>
<%@ WebHandler Language="C#" CodeBehind="Thumbnail.ashx.cs" Class="WebFormsApp.Thumbnail" %>

An IHttpHandlerFactory sits one level above — it decides which handler instance to hand back for a given request, useful for routing a whole extension (or a custom-registered path) to different handler types based on request content:

public class ReportHandlerFactory : IHttpHandlerFactory
{
    public IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated)
        => context.Request.QueryString["format"] == "csv" ? new CsvReportHandler() : new PdfReportHandler();

    public void ReleaseHandler(IHttpHandler handler) { }
}

Both are registered in web.config under <httpHandlers> (IIS 6/classic pipeline) or <handlers> (IIS 7+ integrated pipeline, see below).

Writing IHttpModule

A module runs on every request through the pipeline, regardless of handler, by subscribing to HttpApplication events — the mechanism FormsAuthenticationModule, SessionStateModule, and similar built-in features are themselves implemented with:

public class RequestTimingModule : IHttpModule
{
    public void Init(HttpApplication app)
    {
        app.BeginRequest += (s, e) => ((HttpApplication)s).Context.Items["StartedAt"] = DateTime.UtcNow;
        app.EndRequest += (s, e) =>
        {
            var started = (DateTime)((HttpApplication)s).Context.Items["StartedAt"];
            Trace.WriteLine("Request took " + (DateTime.UtcNow - started).TotalMilliseconds + "ms");
        };
    }

    public void Dispose() { }
}
<system.webServer>
  <modules>
    <add name="RequestTimingModule" type="WebFormsApp.RequestTimingModule" />
  </modules>
</system.webServer>

IIS classic vs. integrated pipeline

flowchart LR subgraph Classic["IIS 6 classic mode"] direction TB C1[IIS native pipeline\nhandles static files, auth, logging] --> C2{Extension mapped\nto aspnet_isapi.dll?} C2 -->|.aspx/.ashx/.asmx| C3[ASP.NET pipeline\nmodules + handler run] C2 -->|.html/.css/.jpg| C4[Served by IIS directly\nASP.NET modules never run] end subgraph Integrated["IIS 7+ integrated mode"] direction TB I1[Unified IIS + ASP.NET pipeline] --> I2[ASP.NET modules can\nrun for EVERY request,\nany extension] I2 --> I3[Handler selected by\nconfigured mapping] end

Classic mode only ran the ASP.NET pipeline (and therefore custom IHttpModule`s) for requests IIS had mapped to the ASP.NET ISAPI extension — typically `.aspx/.ashx/.asmx only, so a module could not, for example, inspect a static .html request. Integrated mode (the default and effectively the only mode still used since IIS 7) merges the two pipelines so ASP.NET modules participate in every request IIS handles, static files included, which is what enables things like Forms-authentication-protecting static content. See IIS 6.0 vs. IIS 7 Integrated Pipeline (previous-versions archive) and ASP.NET Application Life Cycle Overview for IIS.

web.config hierarchy and machine.config

Configuration is hierarchical and inherited: machine.config (one per .NET Framework install) is the outermost layer, then the site-root web.config, then a web.config in each subfolder, each layer able to add to or override the one above it — exactly like nested CSS cascade:

<!-- web.config at the application root -->
<configuration>
  <system.web>
    <authorization>
      <deny users="?" /> <!-- deny anonymous site-wide by default -->
    </authorization>
  </system.web>
  <location path="Public">
    <system.web>
      <authorization>
        <allow users="*" /> <!-- override: this one subfolder is public -->
      </authorization>
    </system.web>
  </location>
</configuration>

<location> (used inline above, or as a path= targeting a specific file/folder from the root web.config) applies settings to a specific path without needing a physical nested web.config file — the two approaches are equivalent; <location> is preferred when the settings for many paths need to live in one reviewable place.

web.config transforms

Web Application Projects support per-configuration transform files (Web.Release.config, Web.Debug.config) applied during publish via XDT (XML Document Transform) syntax:

<!-- Web.Release.config -->
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <system.web>
    <compilation xdt:Transform="RemoveAttributes(debug)" />
    <customErrors xdt:Transform="Replace" mode="On" />
  </system.web>
</configuration>

Encrypting configuration sections

Connection strings and other secrets in web.config can be encrypted at rest on a given machine using aspnet_regiis:

%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_regiis.exe -pe "connectionStrings" -app "/MyApp"

-pe ("protect element") encrypts the named section in place; ASP.NET decrypts it transparently at runtime using the configured provider (RSA machine key container by default, so the encrypted file is only readable on the machine/key container that encrypted it — relevant when planning to sync web.config across a farm).

Compilation models and precompilation

By default, Web Forms compiles each page/control on first request (or eagerly if pre-JIT-warmed) into per-page assemblies under the ASP.NET temporary files folder. aspnet_compiler precompiles the whole site ahead of deployment:

aspnet_compiler.exe -v /MyApp -p C:\src\MyApp C:\out\MyApp -u

-u produces an updatable precompiled site (markup files are still deployed and can be tweaked without a full recompile; code-behind is precompiled); omitting it produces a fully precompiled, non-updatable site where even markup changes require recompilation. See Deployment and Diagnostics for the deployment-time trade-offs between the two, and ASP.NET Compilation Overview.