State Management and Caching
|
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. |
Web Forms offers several distinct ways to keep data alive across requests, each with a different lifetime, scope, and cost — picking the wrong one is a common source of both bugs (data that should have persisted did not) and performance problems (data that did not need to persist was carried anyway).
Client-side vs. server-side state
| Mechanism | Scope | Notes |
|---|---|---|
ViewState / control state |
One page, one user, round-trips every postback |
See ViewState and Control State — opaque to the client, carried in the response/request body. |
Hidden fields |
One page, one user |
Like ViewState but explicit and app-defined; no automatic serialization/tree-matching. |
Cookies |
One user (or session), across pages, size-limited (~4KB) |
Sent on every request to the matching domain/path regardless of whether the page needs them. |
Query string |
One user, one URL, visible/bookmarkable |
No server storage; easily tampered with, so never a substitute for authorization. |
|
Whole app, all users, in-process only |
See below. |
|
One user, across pages, server- or out-of-process |
See below. |
|
Whole app, expiration/eviction-aware |
See below. |
|
One (typically authenticated) user, persisted to storage |
Strongly typed, configured in
|
Application, Session, Cache, and Profile
Application is a single, process-wide IDictionary shared by every user and every request — rarely the
right choice today (no expiration, no locking safety by default, lost on app-pool recycle) but still seen as a
crude read-mostly cache:
protected void Application_Start(object sender, EventArgs e)
{
Application["StartedAtUtc"] = DateTime.UtcNow;
}
protected void Page_Load(object sender, EventArgs e)
{
Application.Lock();
Application["HitCount"] = (int)(Application["HitCount"] ?? 0) + 1;
Application.UnLock();
}
Session is per-user and is where the great majority of "remember this across pages" state actually belongs
(shopping cart contents, wizard-in-progress data, small caches of the current user’s own data).
Session modes
sessionState mode determines where session data physically lives:
| Mode | Behavior |
|---|---|
|
Stored in the worker process’s memory. Fastest; lost on app-pool recycle/restart and does not work across a farm without sticky sessions. |
|
Stored by a separate Windows service ( |
|
Stored in a SQL Server database ( |
|
A custom |
<system.web>
<sessionState mode="SQLServer"
sqlConnectionString="Data Source=sqlserver;Initial Catalog=ASPState;Integrated Security=true"
cookieless="false" timeout="20" />
</system.web>
Session-less pages (<%@ Page EnableSessionState="false" %> or "ReadOnly") skip the session-state
locking overhead entirely (EnableSessionState="True" serializes concurrent requests from the same session
against each other) or read-only access, which is worth setting explicitly on any page that does not touch
Session. Cookieless sessions (cookieless="true" or "AutoDetect") embed the session ID in the URL
instead of a cookie, at the cost of session-fixation risk and unwieldy URLs — generally avoided in favor of
requiring cookies.
The Cache API
System.Web.Caching.Cache (HttpContext.Cache/Page.Cache) is an application-wide, expiration- and
memory-pressure-aware store distinct from Application — entries can expire on a timer, on a dependency
(file, another cache key, a SQL change), or be evicted under memory pressure, and can notify code when removed:
public Product GetProduct(int id)
{
string key = "Product_" + id;
if (Cache[key] is Product cached) return cached;
var product = _repository.Find(id);
Cache.Insert(key, product,
dependencies: null,
absoluteExpiration: DateTime.UtcNow.AddMinutes(10),
slidingExpiration: Cache.NoSlidingExpiration,
priority: CacheItemPriority.Normal,
onRemoveCallback: (k, value, reason) => Trace.WriteLine($"Evicted {k}: {reason}"));
return product;
}
Dependencies (CacheDependency) invalidate an entry when a file changes on disk, or when another cache key
is removed (chained invalidation); priority (CacheItemPriority) influences which entries are evicted
first under memory pressure; the callback lets code react to eviction (e.g. to re-warm the entry).
SQL cache dependency
SqlCacheDependency invalidates a cache entry when the underlying database rows actually change, using either
polling (SQL Server 7/2000-compatible, a background poll of a tracked table) or the newer query-notification
mechanism (SqlDependency, SQL Server 2005+):
<system.web>
<caching>
<sqlCacheDependency enabled="true" pollTime="60000">
<databases>
<add name="CatalogDb" connectionStringName="CatalogDb" />
</databases>
</sqlCacheDependency>
</caching>
</system.web>
var dependency = new SqlCacheDependency("CatalogDb", "Products");
Cache.Insert("AllProducts", products, dependency);
Output caching
@OutputCache caches an entire page’s (or user control’s — "fragment caching") rendered response, keyed by
its VaryBy* attributes:
<%@ OutputCache Duration="120" VaryByParam="category;page" VaryByHeader="Accept-Language" %>
-
VaryByParam— a separate cached entry per distinct query-string/form value combination ("none"for a single shared entry,"*"for every parameter). -
VaryByControl— vary by a named control’s value (typically on a user control being fragment-cached). -
VaryByCustom— a free-form string resolved by overridingHttpApplication.GetVaryByCustomString(e.g. vary by device type or by authenticated-vs-anonymous). -
VaryByHeader— vary by one or more request header values.
Fragment caching applies @OutputCache to a .ascx user control instead of the whole page, so a mostly
static sidebar can be cached independently of a dynamic main content area on the same page.
Post-cache substitution
<asp:Substitution> punches a hole in an otherwise output-cached page for content that must always be
per-request fresh (e.g. "Welcome, `<username>`" on an otherwise static page):
<%@ OutputCache Duration="300" VaryByParam="none" %>
...
<asp:Substitution ID="Greeting" runat="server" MethodName="GetGreeting" />
public static string GetGreeting(HttpContext context)
{
// Static method; runs on every request even while the rest of the page is served from cache.
return "Welcome, " + (context.User.Identity.Name ?? "Guest");
}
Distributed output-cache providers (System.Web.Caching.OutputCacheProvider, pluggable since ASP.NET 4)
let output-cache entries live outside a single worker process — e.g. backed by a distributed cache such as
Redis — so a farm shares one cache instead of each server caching independently.