All topics
Backend · Learning hub

ASP.NET notes for developers

Master ASP.NET with a curated set of 1 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallTest yourself — ASP.NET quizMore Backend notes
ASP.NET

ASP.NET (Framework) Essentials

ASP.NET (Framework) Essentials "ASP.NET" here means the classic .NET Framework web stack (ASP.NET Web Forms, MVC 5, Web API 2) that runs on the full .NET Framew

ASP.NET (Framework) Essentials

"ASP.NET" here means the classic .NET Framework web stack (ASP.NET Web Forms, MVC 5, Web API 2) that runs on the full .NET Framework (4.x) and, traditionally, IIS on Windows. It predates and is architecturally distinct from ASP.NET Core — Core is a ground-up rewrite that's cross-platform, faster, and open source. A huge amount of enterprise .NET still runs on classic ASP.NET, so recognizing its patterns (System.Web, the HttpContext-per-request model tied to IIS, web.config) matters even on new projects that have to interoperate with or migrate off it.

Three flavors exist under the ASP.NET umbrella: Web Forms (event-driven, page-lifecycle, ViewState — largely legacy today), MVC 5 (controllers/views/models, the pattern most new classic ASP.NET code still uses), and Web API 2 (HTTP/REST-focused, no views). This page focuses on MVC 5 and Web API 2, since that's what you'll encounter in any actively maintained classic ASP.NET codebase.

MVC 5 controllers and routing

Routing in classic ASP.NET MVC is configured centrally in RouteConfig.cs (registered from Global.asax.cs on application start) rather than declared per-route through minimal-API-style lambdas. Convention-based routing maps {controller}/{action}/{id} by default; attribute routing (added in MVC 5) lets you decorate actions directly, which is the more common approach in modern classic-ASP.NET code.

// App_Start/RouteConfig.cs
public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
        routes.MapMvcAttributeRoutes(); // enables [Route] attributes below

        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
    }
}

// Controllers/OrdersController.cs
[RoutePrefix("api/orders")]
public class OrdersController : ApiController
{
    private readonly IOrderRepository _repository;

    public OrdersController(IOrderRepository repository)
    {
        _repository = repository;
    }

    [HttpGet]
    [Route("")]
    public IHttpActionResult GetAll(int page = 1, int pageSize = 20)
    {
        var orders = _repository.GetPage(page, pageSize);
        return Ok(orders);
    }

    [HttpGet]
    [Route("{id:int}")]
    public IHttpActionResult GetById(int id)
    {
        var order = _repository.GetById(id);
        if (order == null) return NotFound();
        return Ok(order);
    }

    [HttpPost]
    [Route("")]
    public IHttpActionResult Create(OrderDto dto)
    {
        if (!ModelState.IsValid) return BadRequest(ModelState);
        var created = _repository.Create(dto);
        return Created(Request.RequestUri + "/" + created.Id, created);
    }
}

Note the split: Controller (for MVC, returning ActionResult / views) versus ApiController (for Web API, returning IHttpActionResult / JSON). They live in different base classes and, historically, different pipelines — a frequent source of confusion when a project mixes both, which most real classic-ASP.NET apps do.

Razor views and model binding

MVC views use the Razor syntax (.cshtml) with strongly-typed models, HTML helpers, and server-side partials. Model binding maps posted form fields (or query string values) onto action parameters or a bound model automatically, driven by parameter/property name matching.

// Controllers/AccountController.cs
public class AccountController : Controller
{
    [HttpGet]
    public ActionResult Register()
    {
        return View(new RegisterViewModel());
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Register(RegisterViewModel model)
    {
        if (!ModelState.IsValid)
        {
            return View(model); // redisplay with validation errors
        }

        _userService.CreateUser(model.Email, model.Password);
        return RedirectToAction("Index", "Home");
    }
}
@* Views/Account/Register.cshtml *@
@model RegisterViewModel

@using (Html.BeginForm("Register", "Account", FormMethod.Post))
{
    @Html.AntiForgeryToken()

    <div class="form-group">
        @Html.LabelFor(m => m.Email)
        @Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
        @Html.ValidationMessageFor(m => m.Email)
    </div>

    <div class="form-group">
        @Html.LabelFor(m => m.Password)
        @Html.PasswordFor(m => m.Password, new { @class = "form-control" })
        @Html.ValidationMessageFor(m => m.Password)
    </div>

    <button type="submit" class="btn btn-primary">Register</button>
}

`@Html.AntiForgeryToken()` paired with `[ValidateAntiForgeryToken]` is how classic ASP.NET MVC defends against CSRF on POST forms — it's opt-in per action, not automatic, so it's easy to forget on a new form and leave it vulnerable.

Configuration, IIS, and the request lifecycle

Classic ASP.NET configuration lives in web.config (XML), not appsettings.json — connection strings, app settings, custom HTTP modules/handlers, and IIS-specific settings (authentication mode, custom errors, request limits) are all declared there. The application boots through Global.asax.cs, whose Application_Start fires once per app-domain and is where routes, filters, bundles, and DI containers get wired up.

<!-- Web.config -->
<configuration>
  <connectionStrings>
    <add name="DefaultConnection"
         connectionString="Data Source=.;Initial Catalog=AppDb;Integrated Security=True"
         providerName="System.Data.SqlClient" />
  </connectionStrings>

  <appSettings>
    <add key="Environment" value="Production" />
    <add key="ApiBaseUrl" value="https://api.example.com" />
  </appSettings>

  <system.web>
    <compilation debug="false" targetFramework="4.8" />
    <httpRuntime targetFramework="4.8" maxRequestLength="10240" />
    <customErrors mode="On" defaultRedirect="~/Error" />
  </system.web>
</configuration>

Under IIS, each request runs inside an application pool worker process (w3wp.exe); a pool recycle (scheduled, on config change, or after a crash) tears down and rebuilds the whole app domain, wiping any in-memory static state — a common source of "it worked until IIS recycled" bugs when someone caches something in a static field instead of a proper cache provider.

Common pitfalls and gotchas

  • Classic ASP.NET is synchronous-by-default in older codebases; blocking calls on IIS threads under load exhaust the thread pool. Prefer async controller actions (`async Task<ActionResult>`) and `await` for I/O — MVC 5 and Web API 2 both support it, but a lot of legacy code predates that convention.

  • ViewState (Web Forms only) silently bloats page size and can leak state across postbacks if not disabled where it isn't needed — a frequent, invisible performance drag on old Web Forms pages.

  • web.config transforms (Web.Release.config, Web.Debug.config) apply at publish/build time, not runtime — a common gotcha when someone expects environment-specific values to switch just by changing an environment variable, which is the ASP.NET Core mental model, not this one.

  • IIS is required (or an IIS-compatible host like IIS Express) — classic ASP.NET has no self-hosted Kestrel-style option, which is one of the biggest reasons teams migrate to ASP.NET Core for containerized/cross-platform deployment.

  • Dependency injection isn't built in the way it is in ASP.NET Core — you wire up a container (Unity, Autofac, Ninject) yourself in Global.asax.cs or a dedicated DI config class; nothing resolves automatically out of the box.

  • `Server.Transfer` vs `Response.Redirect`: Transfer keeps the request server-side (same URL in the browser, no round trip) while Redirect issues a real 302 and a new client request — mixing them up breaks bookmarking and back-button behavior.

Keep your ASP.NET knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever