Few years ago, early in doing ASP.NET Core migration work, I picked up a project that looked straightforward: move a handful of WebForms pages to the new stack, one at a time, no rush. I opened the first page's code-behind, saw a data grid, a couple of buttons, a few dropdowns, and figured: a weekend, two at most. I was off by close to three weeks, and the entire gap had one cause: ViewState.

I had budgeted time to rebuild the markup, restyle it, maybe untangle an old event handler or two. I hadn't budgeted for the fact that the page's actual behavior — what happened to all that data between one click and the next — depended on a mechanism that simply does not exist anywhere in modern .NET. Every assumption I'd made about "porting" the page fell apart once I looked past the HTML and into how the page behaved across requests.

This is the piece almost nobody accounts for when they ask for a quick WebForms upgrade estimate. It's worth explaining properly, because once you understand it, you understand why these migrations are rebuilds, not conversions.

What ViewState actually did

In classic WebForms, every control on a page — a textbox, a dropdown, a grid with its sort order and selected row — carried its own state silently across postbacks. You didn't write code to remember that a user had typed something into a field, or that a grid was sorted by date instead of name. The framework did it for you: it serialized the state of the whole control tree into a hidden field, __VIEWSTATE, sent it down with the page, and read it back in on the next request to restore everything exactly as it was.

It felt like magic at the time, and for a certain kind of form-heavy intranet app, it was genuinely productive. You built a page, dropped controls on it, wired up a few event handlers, and the framework handled the plumbing of what the user already did for you.

Why that model has no home in ASP.NET Core

ASP.NET Core — whether you're in MVC, Razor Pages, or Blazor Server — doesn't have a page lifecycle in the WebForms sense. There's no Init, Load, PreRender sequence running behind the scenes. There are no server controls that track their own state between requests. Each request is handled on its own terms: the framework doesn't remember what a dropdown had selected unless something explicitly tells it to.

This isn't a missing feature waiting to be added back. It's a different model entirely, and a deliberate one: ASP.NET Core is built around requests being stateless by default, because that's what scales, what's testable, and what works cleanly across a load-balanced web farm without sticky sessions. Microsoft doesn't offer a ViewState equivalent in modern .NET because the whole architecture is built to not need one.

What replaces it, conceptually

The replacement isn't a single feature — it's a mindset: explicit state management instead of implicit. You decide, for each piece of state, where it actually needs to live:

  • View models carry what the current request needs to render the page, built fresh each time.
  • Form fields and query strings carry what needs to survive exactly one round trip, the same job hidden fields did in WebForms, just declared on purpose instead of auto-generated.
  • Session or a database carries what needs to survive across multiple pages or requests — a shopping cart, a multi-step wizard, a user's in-progress filter selections.
  • Client-side state (a bit of JavaScript, local storage) carries what's purely a UI convenience and never needed to touch the server at all.

None of this is harder than ViewState, once you see it clearly. It's actually simpler to reason about, because nothing is happening invisibly. But it does mean someone has to look at the old page and decide, deliberately, what each piece of "remembered" behavior actually was, and where it belongs now.

Why this is the real reason quick-upgrade quotes go sideways

From the outside, a WebForms page looks portable: a table here, a couple of buttons there, a dropdown filter. It's easy to estimate "rebuild this UI" and move on. The surprise shows up once you open the code-behind and find that a chunk of what looks like business logic is actually state-plumbing logic — code that only exists because ViewState made a certain kind of interaction possible in the first place. A grid that remembers its selected row after a postback, a wizard that keeps its step state across page loads, a filter panel that works when you come back to the page — all of that was riding on ViewState, invisibly, and all of it needs a new, explicit home.

That's the gap between "convert" and "rebuild." You can't translate ViewState-dependent behavior line by line into ASP.NET Core, because there's no destination for a literal translation. The UI has to be rebuilt around a different state model, even when the end result looks and behaves identically to the user.

A practical way to approach it

When I audit a WebForms page before migration, I don't start by asking what the page looks like. I ask what it remembers between requests, and why. For every control that seems to hold onto something — a selected row, a typed value, a filter, a step in a flow — I note its lifetime (one request? the whole session? forever, in a database?) and whether it's essential business state or just convenience. Only once that list exists do I start deciding the modern equivalent for each item.

That one exercise turns a guessing-based estimate into a planned one. It's also usually where a "WebForms migration" quote changes from a number pulled out of the air to a number someone can actually defend.

What this means for your timeline

None of this means a WebForms upgrade is a bad idea or an impossible one — it means it's a different kind of project than pointing the new framework at the old pages. The business logic underneath is often perfectly salvageable; it's the state model wrapped around it that has to change. Knowing that going in is the difference between a plan and a surprise three weeks into the work.

Let's talk through your situation.