Skip to main content
Published / updated

State Management

Before you start

You need: the lifecycle (Article 02) and server controls (Article 03).

Time: about 45 minutes, plus the practice.

Learning objective

For any value a Web Forms page needs to remember, choose the correct store, and diagnose why an existing value is being lost.

Topics

  • Why state management exists
  • ViewState — what it holds, what it costs
  • Session — and why it disappears
  • Query string, cookies, hidden fields
  • Application state and Cache
  • Choosing a store
  • Diagnosing lost state

Terminology

TermMeaning
ViewStateControl state encoded into a hidden field, round-tripped on every postback
SessionPer-user server-side storage, keyed by a cookie
Application stateServer-side storage shared by every user
CacheServer-side storage that can expire or be evicted

HTTP is stateless. Each request arrives with no memory of the last one. Web Forms hides this, and every store below is part of the illusion.

ViewState

ViewState preserves control property values across postbacks. The framework serialises them, Base64-encodes the result, and writes it into a hidden field:

<input type="hidden" name="__VIEWSTATE" value="/wEPDwUKMTU4Nzk4NDkxNQ9kFgICAw9kFgICAQ8..." />

That field travels to the browser and back on every postback. This is why a GridView with 500 rows produces a page that is slow on a mobile connection: the data is being sent twice per interaction.

What ViewState is not

It is not encrypted. By default it is only Base64-encoded, which anyone can decode. Never put a SchoolId, a salary, or a permission flag in it and trust the value.

It is tamper-protected by a MAC when enableViewStateMac is on — which it is by default and must never be turned off — so a modified ViewState is rejected. Protection against reading requires ViewStateEncryptionMode="Always".

Using it deliberately

Private Property CurrentPage As Integer
Get
If ViewState("CurrentPage") Is Nothing Then
Return 1
End If

Return CInt(ViewState("CurrentPage"))
End Get
Set(value As Integer)
ViewState("CurrentPage") = value
End Set
End Property

Wrapping ViewState access in a property keeps the null check in one place. Values stored must be serialisable — primitives, strings, DataTable, and anything marked <Serializable>. Storing a SqlConnection or a custom class that is not serialisable throws at render time, and the error points at the page, not at your assignment.

Controlling the cost

<%@ Page EnableViewState="false" ... %>

<asp:GridView ID="grdReport" runat="server" EnableViewState="false" />

Turning ViewState off on a read-only grid is a large, safe win. Turning it off where controls need to remember values breaks them — a DropDownList populated in Not IsPostBack loses its items entirely, and SelectedValue then throws or reads empty.

The rule: read-only display can disable ViewState; anything the user edits or that must survive a postback cannot.

Check the real cost with page tracing (Trace="true") — the control tree lists ViewState size per control, which shows exactly what is expensive.

Session

Server-side, per user, keyed by an ASP.NET_SessionId cookie.

Session("SchoolId") = school.Id
Session("UserName") = user.Name

Dim schoolId As Integer = 0

If Session("SchoolId") IsNot Nothing Then
schoolId = CInt(Session("SchoolId"))
End If

Always test for Nothing. Session expires, and reading CInt(Session("SchoolId")) on an expired session throws InvalidCastException on Nothing — reported by users as "the site crashed", with a stack trace pointing at an unrelated line.

Why session disappears

This is a top support complaint on legacy applications, and there are five distinct causes.

CauseSymptom
Timeout reached (default 20 min)Logged out after a period of inactivity
App pool recycle with InProc modeEveryone logged out at once, at the same time each day
Deploying a file that touches the appEveryone logged out immediately after a release
Web farm without shared session stateLogged out at random, whenever the load balancer switches server
Cookie blocked or a domain changeNever stays logged in at all
<sessionState mode="InProc" timeout="20" />

InProc stores session in the worker process's memory. Anything that recycles the process — the default IIS recycle schedule, a Web.config edit, a deployment, memory pressure — destroys every session. "Everyone gets logged out at 3 a.m." is the IIS recycle schedule, not a bug in your code.

The fixes are StateServer or SQLServer mode, both of which require every stored object to be serialisable:

<sessionState mode="StateServer"
stateConnectionString="tcpip=127.0.0.1:42424"
timeout="20" />

Switching from InProc to StateServer will break code that stored a non-serialisable object in session. That failure appears only at run time, on the line that writes to session.

Session discipline

Session is per user and lives on the server, so a large object stored for a thousand concurrent users is a thousand copies. Store identifiers, not objects. A Student in session becomes stale the moment someone edits that student.

Query string

Response.Redirect("~/Students/Edit.aspx?publicId=" & student.PublicId.ToString())
Dim raw As String = Request.QueryString("publicId")
Dim publicId As Guid

If Not Guid.TryParse(raw, publicId) Then
Response.Redirect("~/Students/List.aspx")
Return
End If

Visible, bookmarkable, shareable — and entirely user-controlled. Always parse defensively with TryParse; never CInt(Request.QueryString("id")), which throws on any non-numeric value including a missing one.

Never trust a query-string id for authorisation. Edit.aspx?id=42 invites the user to try 43. Use the PublicId GUID, and still verify the record belongs to the signed-in user's school:

Dim student As Student = _repository.GetByPublicId(CInt(Session("SchoolId")), publicId)

If student Is Nothing Then
Response.Redirect("~/Students/NotFound.aspx")
Return
End If

Passing the SchoolId from session into the query is what makes the check real. A query that filters only by PublicId is a cross-tenant leak waiting for one shared URL.

Also URL-encode anything you append:

Response.Redirect("~/Students/List.aspx?term=" & Server.UrlEncode(txtSearch.Text))

A search for 10th & A breaks the query string without it.

Cookies

Dim preference As New HttpCookie("PreferredClass", ddlClass.SelectedValue)
preference.Expires = Date.Now.AddDays(30)
preference.HttpOnly = True
preference.Secure = True
Response.Cookies.Add(preference)
Dim cookie As HttpCookie = Request.Cookies("PreferredClass")
Dim value As String = String.Empty

If cookie IsNot Nothing Then
value = cookie.Value
End If

HttpOnly stops JavaScript reading it, which limits the damage of an XSS flaw. Secure stops it travelling over plain HTTP. Set both on anything that is not purely cosmetic.

Cookies are user-editable. A cookie is a preference, never a permission.

Hidden fields

<asp:HiddenField ID="hdnStudentPublicId" runat="server" />

Simpler than ViewState for a single value, and visible in the page source. Same rule: the user can change it, so re-validate server-side before acting on it.

Application state and Cache

' Application — shared by every user, lives for the app's lifetime
Application.Lock()
Application("ActiveSchoolCount") = count
Application.UnLock()

Application is shared mutable state across all requests and threads. Every write needs Lock/UnLock, and it dies on an app pool recycle just as InProc session does. In practice Cache is almost always the better choice.

Dim classes As List(Of String) = TryCast(Cache("ClassList"), List(Of String))

If classes Is Nothing Then
classes = _repository.GetClassNames()
Cache.Insert("ClassList", classes, Nothing,
Date.Now.AddMinutes(30), TimeSpan.Zero)
End If

Cache supports expiry and can be evicted under memory pressure — which means the null check is mandatory, every time. Code that reads Cache("ClassList") and uses it directly works in testing and throws in production when memory gets tight.

Choosing a store

NeedUse
A control's value across a postbackViewState (usually automatic)
Page-specific value across postbacksViewState
Signed-in user identitySession (or Forms auth ticket)
A value shared across pages, per userSession
A bookmarkable or shareable valueQuery string
A durable per-browser preferenceCookie
Reference data shared by all usersCache
Anything that must not be lostThe database

Two rules that resolve most arguments:

  • If losing it would matter, it goes in the database. Session and Cache both evaporate on a recycle.
  • If the user could benefit from changing it, it must be re-validated server-side. ViewState, query string, cookies, and hidden fields are all user-visible.

Diagnosing lost state

Work in this order.

1. Which store is it in? Search for the key name across the project. The same logical value is often written to two stores in different places, and one write wins.

2. Is it being overwritten in Page_Load? A missing IsPostBack guard resets values before the click handler runs. Check this before anything else.

3. For session loss, correlate with time. Everyone at once at a fixed hour is an app pool recycle. One user after inactivity is the timeout. Random and intermittent, across several users, is a web farm with InProc mode.

4. Check the cookie. In DevTools → Application → Cookies, confirm ASP.NET_SessionId exists and is not changing between requests. A changing id means a new session on every request — usually a cookie being blocked, or a domain or protocol mismatch.

5. For ViewState, check it is enabled. EnableViewState="false" on the page, the control, or in Web.config silently disables persistence with no error.

6. Check for a redirect. Response.Redirect starts a new request. Values set in page fields before the redirect are gone; only Session, query string, or cookies survive it.

Errors you will hit

What you seeCauseFix
Session is Nothing after a whileThe session timed out, or the app pool recycledDo not keep anything you cannot rebuild in Session
Users see each other's dataSomething user-specific stored in Application or a Shared fieldIt must be per-user — use Session
Page is very slow and the HTML is enormousLarge objects in ViewStateSet EnableViewState="False" on grids you rebind anyway
Validation of viewstate MAC failedWeb farm without a shared machine key, or the app pool recycledSet an explicit machineKey
A value from a query string breaks the pageTrusted user input from the URLValidate it; never trust it
Data is right on load and wrong after a clickRebound in Page_Load without IsPostBackAdd the check

Anything in a query string is user input. ?studentId=12 can be edited to ?studentId=13 in the address bar, so authorisation has to be checked on the server every time.

Common mistakes

  • CInt(Session("SchoolId")) with no Nothing check
  • Storing an object rather than an id in session, then serving stale data
  • Trusting ViewState, a hidden field, a cookie, or a query-string id for authorisation
  • Assuming ViewState is encrypted — by default it is only encoded
  • Disabling ViewState on a control that must remember a value
  • Leaving ViewState on for a large read-only grid
  • InProc session in a web farm
  • Storing a non-serialisable object in session, then switching to StateServer
  • Reading Cache(...) without a null check
  • Sequential ids in the query string
  • Not URL-encoding an appended query value

Practice

On an existing Web Forms application, inventory every use of Session(, ViewState(, Request.QueryString(, and Cache(. For each, note whether the read is null-checked and whether the value is trusted for a security decision. Fix one missing null check and one unvalidated query-string id.

Then run the course debugging exercise — fix lost state. Reproduce each cause deliberately:

  1. Set <sessionState timeout="1" />, wait two minutes, and click something. Record the exception and where it surfaces.
  2. Touch Web.config while logged in to force a recycle, and confirm the session is gone.
  3. Set EnableViewState="false" on a page with a populated dropdown and observe what SelectedValue returns after a postback.
  4. Remove an IsPostBack guard and watch a selection reset.

Finally, enable Trace="true" on your heaviest page and record which control holds the most ViewState. Disable ViewState on that control if it is read-only, and record the new page size.

You can now

  • Choose the right store for any value: ViewState, Session, Application, query string, cookie
  • Explain why a session disappeared from its timing pattern alone
  • Turn off ViewState where it only costs you
  • Say why a query-string value can never be trusted
  • Identify state a page is trusting that it should not

Review questions

  1. Why is ViewState not a safe place for a SchoolId?
  2. Why does InProc session mode log every user out at the same time each night?
  3. What breaks when you disable ViewState on a DropDownList populated in Not IsPostBack?
  4. Why must every read from Cache be null-checked?

Next: Master pages and navigation