.NET anti-pattern
WebForms ViewState and code-behind logic
ASP.NET WebForms pages keep server values in ViewState, round-tripped through the browser, and put business rules in code-behind event handlers tied to the page lifecycle.
- FAMILY
- .NET
- CUTOVER RISK
- High
- Hides business logic or coupling that a rewrite will drop unless it is extracted explicitly.
- MODERN MAPPING
- SPA front end over a stateless API that rereads authoritative data
- GENERATED ARTIFACT
- APPROVAL_LIMIT.dmn.json
Before and after
legacy source → modern mapping
Legacy: exhibits the pattern
1protected void Page_Load(object sender, EventArgs e)2{3 if (!IsPostBack)4 {5 var claim = _repo.Get(Request.QueryString["id"]);6 ViewState["ClaimId"] = claim.Id;7 ViewState["Allowed"] = claim.AllowedAmount;8 ViewState["Tier"] = claim.Member.Tier;9 BindLines(claim);10 }11}1213protected void btnApprove_Click(object sender, EventArgs e)14{15 decimal allowed = (decimal)ViewState["Allowed"];16 string tier = (string)ViewState["Tier"];17 decimal limit = tier == "GOLD" ? m : m;18 if (allowed > limit && !User.IsInRole("SeniorAdjuster"))19 {20 lblMsg.Text = "Requires senior adjuster";21 return;22 }23 _repo.Approve((string)ViewState["ClaimId"], User.Identity.Name);24}
Modern: SPA front end over a stateless API that rereads authoritative data
1[HttpPost("claims/{id}/approve")]2public async Task<IActionResult> Approve(3 string id, [FromHeader(Name = "If-Match")] string etag)4{5 var claim = await _claims.GetAsync(id); // fresh read6 if (claim.ETag != etag)7 return StatusCode(412);89 var needsSenior = _limits.RequiresSenior(10 claim.Member.Tier, claim.AllowedAmount);11 if (needsSenior && !User.IsInRole("SeniorAdjuster"))12 return Problem(title: "Requires senior adjuster",13 statusCode: 403);1415 await _claims.ApproveAsync(claim, User.Identity!.Name!);16 return NoContent();17}
Why it is dangerous in modernization
WebForms made the web feel like VB6. Values that a page needs on postback are stored in ViewState["key"], serialized into the hidden __VIEWSTATE field, sent to the browser and posted back. Event handlers such as btnApprove_Click run after Page_Load, and the code inside them reads those ViewState values instead of rereading the database. ViewState is integrity-protected by its MAC in current ASP.NET versions, so it cannot be forged easily, but it is a snapshot from when the page was first rendered. A claim approved twenty minutes after the page loaded is approved against the allowed amount of twenty minutes ago. Rules also hide in validator controls, in GridView RowDataBound handlers that compute displayed values, and in the order of lifecycle events. Moving the UI to a single-page app and writing APIs for each button loses the rules that only exist in the page.
How PYD ENTERPRISES detects it
PYD ENTERPRISES parses .aspx and .ascx markup together with code-behind classes. Controls, validators (RequiredFieldValidator, RangeValidator, RegularExpressionValidator, CompareValidator, CustomValidator with its ServerValidate handler) and data-binding expressions become part of the page model. Event wiring from markup and from AutoEventWireup is resolved to handlers, and handlers are ordered by the page lifecycle. Each ViewState and Session key is tracked from the handler that writes it to every handler that reads it, which exposes stale-snapshot reads. Decisions in handlers are then extracted as rules with the page action that triggers them.
Maps to: SPA front end over a stateless API that rereads authoritative data
Each button handler becomes an API operation that loads current data from the system of record instead of trusting a page snapshot. Validator controls become JSON Schema constraints on the request plus the same checks in the front end. Display-only calculations in RowDataBound become either view-model fields computed by the API or front-end formatting, decided explicitly.
Generated specification fragment
1{2 "decision": "Requires senior adjuster",3 "hitPolicy": "UNIQUE",4 "inputs": ["memberTier", "allowedAmount"],5 "rules": [6 { "id": "R1", "memberTier": "\"GOLD\"",7 "allowedAmount": "> 5000", "output": true },8 { "id": "R2", "memberTier": "\"GOLD\"",9 "allowedAmount": "<= 5000", "output": false },10 { "id": "R3", "memberTier": "not(\"GOLD\")",11 "allowedAmount": "> 2500", "output": true },12 { "id": "R4", "memberTier": "not(\"GOLD\")",13 "allowedAmount": "<= 2500", "output": false }14 ],15 "source": "ClaimReview.aspx.cs:17-22"16}
Every generated element links back to the source file and line range shown in the legacy pane. Review and sign off before building against it.
Stale snapshots, not forged ones
Since ASP.NET 4.5.2 the framework enforces the ViewState MAC regardless of the EnableViewStateMac setting, so tampering is not the main risk in a supported application. The risk is time. The approval rule above uses the allowed amount and member tier captured when the page first loaded. If an adjuster in another office reprices the claim, or the member's tier changes, the approval still runs against the old values. PYD ENTERPRISES flags every rule input that comes from ViewState rather than from a fresh read, and the generated API always rereads, with an ETag check so concurrent changes are reported instead of overwritten.
Lifecycle order is part of the logic
WebForms runs Init, Load, control events, PreRender and Render in a fixed order, and postback event handlers run after Page_Load. Code that sets a value in Page_Load and overwrites it in PreRender, or a handler that depends on a dropdown's SelectedIndexChanged having already fired, encodes behavior in the lifecycle. The engine orders handlers accordingly when it slices a rule, so the extracted logic reflects what actually runs on a postback, not the order of methods in the file.
What to extract from the markup
- Validator controls: required fields, ranges, regular expressions and comparisons, mapped to JSON Schema.
- CustomValidator ServerValidate handlers: real business validations, extracted as rules.
- Visible and Enabled bindings that depend on roles or data: authorization rules expressed as UI state.
- GridView and Repeater binding handlers that compute displayed totals or flags.
All four are commonly lost when the UI is rebuilt from screenshots.
Related patterns
Usually found in the same codebase.
- CICSCritical
CICS pseudo-conversational state
Maps to Stateless REST resource with explicit versioning
- Java EEHigh
Struts god Action
Maps to Thin controller per command, domain service underneath
- VB6High
VB6 global module state
Maps to Explicit request context and injected services
- .NETMedium
WCF DataContract versioning
Maps to Versioned OpenAPI 3.1 schemas with discriminated oneOf
Find every WebForms ViewState and code-behind logic in your codebase.
Run PYD ENTERPRISES in your private VPC and get each occurrence with file, line range, modern mapping and equivalence fixtures, ready for your architects to review.