Skip to content

Server-Rendered Overview

Key Points

  • MVC: classic model-view-controller. Controller actions return views. Strong for complex page flows, multi-form pages.
  • Razor Pages: page-centric (each page = .cshtml + .cshtml.cs). Simpler for CRUD; less ceremony than MVC.
  • Static SSR Blazor (covered in Render Modes) is the modern alternative — same mental model with components.
  • For new projects: prefer Blazor Static SSR or Razor Pages. MVC for legacy maintenance / specific patterns.
  • See MVC & Razor Pages for detailed mechanics.

Concepts (deep dive)

MVC

public class HomeController : Controller
{
    public IActionResult Index() => View();
    public IActionResult About() => View();

    [HttpPost]
    public async Task<IActionResult> Submit(MyModel model)
    {
        if (!ModelState.IsValid) return View(model);
        await _svc.SaveAsync(model);
        return RedirectToAction(nameof(Index));
    }
}
Views/
├── Home/
│   ├── Index.cshtml
│   └── About.cshtml
└── Shared/
    └── _Layout.cshtml

Controller maps URLs to actions; actions return IActionResult.

Razor Pages

// Pages/Index.cshtml.cs
public class IndexModel : PageModel
{
    [BindProperty] public MyModel Input { get; set; } = new();

    public void OnGet() { /* ... */ }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid) return Page();
        await _svc.SaveAsync(Input);
        return RedirectToPage("/Confirm");
    }
}
<!-- Pages/Index.cshtml -->
@page
@model IndexModel
<form method="post">
    <input asp-for="Input.Name" />
    <button>Submit</button>
</form>

Page-centric: URL maps directly to file. Less navigation indirection.

When MVC vs Razor Pages

Need Pick
Complex multi-action flows MVC
API-style server actions MVC
Page-per-view CRUD Razor Pages
Beginners Razor Pages
Legacy WebForms migration Razor Pages

In practice many apps mix.

Static SSR Blazor as alternative

@page "/about"
<h1>About</h1>
<p>Server-rendered; no client interactivity.</p>

Same mental model as MVC but using component model. Future-aligned: Microsoft is investing here.

Routing

app.MapControllerRoute(name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.MapRazorPages();

Conventional MVC routing or attribute routing per action.

View resolution

HomeController.About() → Views/Home/About.cshtml
                       OR Views/Shared/About.cshtml

Layouts and partials

<!-- _Layout.cshtml -->
<!DOCTYPE html>
<html>
<head><title>@ViewData["Title"]</title></head>
<body>
    @RenderBody()
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>

<!-- View -->
@{ Layout = "_Layout"; }
<h1>@ViewData["Title"]</h1>

@section Scripts { <script>console.log('done');</script> }
<partial name="_Card" model="@Model.Card" />

View Components

public class CartViewComponent : ViewComponent
{
    public IViewComponentResult Invoke()
    {
        var cart = /* load */;
        return View(cart);
    }
}
<vc:cart />

Reusable rendered components. More logic-friendly than partials.

Tag Helpers

<form asp-action="Submit" method="post">
    <input asp-for="Email" class="form-control" />
    <span asp-validation-for="Email"></span>
    <button type="submit">Save</button>
</form>

C#-aware HTML helpers. Generate URLs, anti-forgery tokens, validation attributes.

ViewBag, ViewData, TempData

  • ViewBag / ViewData: per-request. Same data; different APIs.
  • TempData: across one redirect.
TempData["Message"] = "Saved";
return RedirectToAction(nameof(Index));

// Index view:
@TempData["Message"]   // "Saved"; cleared after read

Model binding

public IActionResult Submit(MyModel model) { /* model auto-bound */ }

Comes from form, route, query, body. Customize with [FromBody], [FromQuery], etc.

Validation

public class MyModel
{
    [Required, StringLength(100)]
    public string? Name { get; set; }
}

if (!ModelState.IsValid) return View(model);

Filters

Action filters, result filters, exception filters, authorization filters. See Filters & Action Pipeline.

Areas

Areas/
├── Admin/
│   ├── Controllers/
│   ├── Views/
│   └── Pages/
└── Public/

Sub-applications within. Routing: /Admin/Users/Index.

When server-rendered vs SPA

  • Server-rendered: SEO; quick first load; minimal JS; admin panels; CRUD.
  • SPA: highly interactive; offline; real-time; mobile-app-like UX.

For most apps: server-rendered with sprinkles of JS/HTMX is faster to ship.

htmx

For interactive server-rendered without SPA:

<button hx-post="/api/like" hx-target="#count">Like</button>
<span id="count">@Model.Likes</span>

POST returns HTML fragment; htmx swaps. Lightweight.

For .NET, htmx pairs well with Razor Pages or Static SSR Blazor.

Performance

  • Output caching for static or rarely-changing pages.
  • Response compression (gzip, brotli).
  • MapStaticAssets for hashed static assets.
  • Minimize DB calls per request.

Modern .NET MVC project

Boilerplate is minimal:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
var app = builder.Build();
app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
app.Run();

Code: correct vs wrong

❌ Wrong: business logic in controller

public IActionResult Save(MyModel m)
{
    using var conn = new SqlConnection(...);
    conn.Open();
    /* SQL, validation, mapping, all here */
    return View();
}

✅ Correct: thin controller, service for logic

public IActionResult Save(MyModel m)
{
    if (!ModelState.IsValid) return View(m);
    return _svc.SaveAsync(m).Result.Match(
        _ => RedirectToAction(nameof(Index)),
        err => { ModelState.AddModelError("", err.Message); return View(m); });
}

❌ Wrong: ViewBag with magic strings

ViewBag.UserCount = 42;
// View: @ViewBag.UserCount  ← typo-prone

✅ Correct: strongly-typed ViewModel

public class IndexVm { public int UserCount { get; set; } }
return View(new IndexVm { UserCount = 42 });

Design patterns for this topic

Pattern 1 — "Strongly-typed ViewModels"

  • Intent: compile-time safety in views.

Pattern 2 — "Razor Pages for CRUD; MVC for flows"

  • Intent: match style to pattern.

Pattern 3 — "Static SSR Blazor for new"

  • Intent: future-aligned.

Pattern 4 — "View Components for reusable logic+UI"

  • Intent: smarter than partials.

Pattern 5 — "PRG (Post/Redirect/Get) for forms"

  • Intent: avoid duplicate submits.

Pros & cons / trade-offs

Style Pros Cons
MVC Familiar; powerful More ceremony
Razor Pages Page-centric; less code Pattern mismatch for complex
Static SSR Blazor Component model Newer
htmx Server-driven interactivity Less SPA-like

When to use / when to avoid

  • Use Razor Pages for new CRUD apps.
  • Use Static SSR Blazor for new component-style.
  • Use MVC for complex flows / legacy.
  • Avoid mixing MVC and Razor Pages without need.

Interview Q&A

Q1. MVC vs Razor Pages? MVC: controller actions for many views. RP: each page is a self-contained file pair.

Q2. Static SSR Blazor advantage? Component model + server-rendered. Forward-looking.

Q3. PRG pattern? POST → redirect → GET. Avoid double submits on refresh.

Q4. ViewModel vs DTO? ViewModel: shape for view. DTO: data transfer. Often same; distinct concepts.

Q5. ViewBag vs ViewData vs TempData? ViewBag/ViewData: per-request. TempData: across one redirect.

Q6. Tag Helpers? HTML elements that bind to C# (asp-for, asp-action). Strongly typed.

Q7. View Component? Reusable widget with code + view. More than a partial.

Q8. Areas? Sub-app within. Used to organize large apps (Admin, Public).

Q9. htmx? HTML-driven interactivity. POST returns fragment; client swaps. Less JS than SPA.

Q10. Performance levers? Output cache, compression, minimal DB per req, MapStaticAssets.

Q11. Razor compilation? Compiled to code at runtime (default) or pre-compiled at build (faster startup).

Q12. When SPA over server-rendered? Real-time, offline, app-like UX. Otherwise server-rendered ships faster.


Gotchas / common mistakes

  • ⚠️ Logic in controllers.
  • ⚠️ ViewBag for typed data.
  • ⚠️ No PRG — duplicate POSTs on refresh.
  • ⚠️ Mixing MVC and Razor Pages chaos.
  • ⚠️ Direct DB calls in views.

Further reading