The Migration Was Not a Rewrite: What Web Forms Teaches Us About Modernisation

What an old Web Forms application teaches you about modernisation

When an application began in Web Forms and later acquired MVC code, the obvious migration goal was usually “move it to ASP.NET Core”. That description was too small. The real work was deciding which behaviour was essential, which behaviour was accidental, and where each responsibility should live after the move.

The old application was a set of contracts

The legacy system contained more than pages and code-behind. It contained contracts with users and operators:

  • familiar workflows;
  • database table and column names;
  • identity and role assumptions;
  • audit and logging expectations;
  • file-storage conventions;
  • deployment and support procedures;
  • reports and integrations that were not visible in the UI.

A successful migration had to preserve the important contracts while making the implementation easier to reason about.

Why a direct translation fails

Web Forms encourages event handlers, postbacks, server controls, and implicit page state. ASP.NET Core MVC encourages explicit routes, actions, model binding, dependency injection, middleware, and views.

Moving code line by line can produce an application that looks modern but still behaves like a collection of hidden page events. The better approach is to translate the behaviour into the new request model:

legacy page event
  -> explicit route and controller action
  -> validated request model
  -> service or database operation
  -> full view, partial view, or JSON response

The translation is about responsibility and flow, not syntax.

The database was part of the migration boundary

Replacing the application layer did not mean replacing the database in one step. Existing tables, identity data, audit fields, and reference data still had operational value.

The practical strategy was to make the new application explicit about its database contexts and configuration while preserving compatibility where it was necessary. That allowed the application and database to evolve at different speeds.

This is not an argument for keeping every legacy quirk forever. It is an argument for identifying the quirks that are actually external contracts before removing them.

The composition root became a map

One of the most useful improvements was making startup readable. The entry point now orchestrates configuration, database settings, service registration, middleware, routes, and initialisation through focused extension classes.

The implementation lives in:

  • Program.cs
  • Configuration/ApplicationConfigurationExtensions.cs
  • Configuration/DatabaseSettings.cs
  • DependencyInjection/ApplicationServiceExtensions.cs
  • Startup/ApplicationPipelineExtensions.cs

The goal is not fewer files. The goal is that a developer can answer “where is this concern configured?” without searching through a giant startup method.

Migration decisions worth documenting

For each feature, classify the work before coding:

  1. Preserve the workflow and replace the implementation.
  2. Wrap the legacy dependency behind a service.
  3. Move the rule into the new application layer.
  4. Retire the behaviour because it was only a workaround.

That classification creates a record of intent and prevents the migration from quietly becoming a permanent copy of the old system.

The durable lesson

Modernisation succeeds when the team migrates understanding as well as code. The new framework helps, but the lasting improvement comes from making hidden contracts, request flows, configuration, security decisions, and operational assumptions visible.

Leave a Reply

Your email address will not be published. Required fields are marked *