Parts 1 and 2 covered WPF and MVVM, a desktop application built and then properly structured. Part 3 pivots to the web, genuinely new territory. Razor is the syntax Blazor, covered in Part 4, is built directly on top of, so this post covers Razor on its own first, deliberately, before any framework gets layered in, the same Task Tracker data from the previous two posts, now rendered as a simple, read-only web page.
What Razor Actually Is
Razor is a syntax for mixing C# directly into HTML, letting a single file describe both the visual structure of a page and the logic that decides what actually gets rendered, no separate templating language to learn, since the logic portions are just genuine C#.
Think of a mail-merge letter template. Most of the letter is fixed, ordinary text, but certain spots contain a placeholder, "Dear [Name]", that gets filled in with real data before the letter is actually printed. Razor works the same way: most of a .cshtml file is plain HTML, but specific spots marked with @ get replaced with real, computed values, or entire blocks of logic, before the page is actually sent to a browser.
The @ Symbol: How Razor Knows Where C# Starts
Everything in a Razor file is treated as plain HTML by default. The @ symbol is the signal that switches into C# mode for whatever comes immediately after it.
<h2>Task Tracker</h2>
<p>You have @Model.Tasks.Count tasks.</p>
<!-- Everything here is plain HTML, rendered exactly
as written, EXCEPT @Model.Tasks.Count - that
specific expression is evaluated in C#, and the
RESULT (a number) is what actually appears in
the rendered page -->
Razor is genuinely smart about where C# ends and HTML resumes again, it doesn't require an explicit closing symbol for a simple expression like this one, since it can tell from the syntax itself where the C# expression naturally ends.
Mixing C# and HTML Directly: A Small First Example
@{
var completedCount = Model.Tasks.Count(t => t.IsCompleted);
}
<h2>Task Tracker</h2>
<p>@completedCount of @Model.Tasks.Count tasks completed</p>
<!-- The @{ } block above is a CODE BLOCK - pure C#,
computing a value, but not directly rendering
anything itself. The variable it creates
(completedCount) is then USED further down,
inside the HTML, with a simple @ expression -->
@foreach: Rendering a List
Razor supports genuine C# control structures directly inside HTML, a @foreach loop generates one copy of whatever HTML sits inside it, once per item.
<ul>
@foreach (var task in Model.Tasks)
{
<li>@task.Description</li>
}
</ul>
<!-- This is a REAL C# foreach loop - the exact same
syntax used anywhere else in C#. The <li> element
inside the loop body gets rendered once per task
in Model.Tasks, with @task.Description substituted
in each time -->
@if: Conditional Rendering
The same applies to @if and else, genuine C# conditional logic, deciding which HTML actually gets rendered.
<ul>
@foreach (var task in Model.Tasks)
{
<li>
@if (task.IsCompleted)
{
<s>@task.Description</s>
}
else
{
<span>@task.Description</span>
}
</li>
}
</ul>
<!-- Completed tasks render inside a <s> tag
(strikethrough) - matching visually what checking
a box did in the WPF version from Parts 1 and 2.
Incomplete tasks render in a plain <span> instead -->
Rendering the Complete Task Tracker as a Read-Only Page
@model TaskTrackerViewModel
<h2>Task Tracker</h2>
@{
var completedCount = Model.Tasks.Count(t => t.IsCompleted);
}
<p>@completedCount of @Model.Tasks.Count tasks completed</p>
<ul>
@foreach (var task in Model.Tasks)
{
<li>
@if (task.IsCompleted)
{
<s>@task.Description</s>
}
else
{
<span>@task.Description</span>
}
</li>
}
</ul>
@if (!Model.Tasks.Any())
{
<p><em>No tasks yet.</em></p>
}
<!-- @model at the very top declares WHAT TYPE of data
this specific Razor file expects to receive - here,
a TaskTrackerViewModel containing a list of tasks.
This is what makes Model.Tasks, used throughout the
rest of the file, actually valid and known at
compile time -->
// TaskTrackerViewModel.cs - the plain C# class this
// Razor file expects, supplied by whatever is rendering
// the page (a Razor Page or an MVC controller action)
public class TaskTrackerViewModel
{
public List<TaskItem> Tasks { get; set; }
}
What's Deliberately Not Here Yet
This page is genuinely read-only, there's no way to add a task, check a box, or delete anything from it. That's intentional: Razor by itself describes how to render HTML from data, nothing more. The interactivity, a button that actually does something without a full page reload, a checkbox that actually updates state, is exactly what Blazor, covered in the next post, adds on top of this same Razor syntax.
Key Lessons
Razor mixes real C# directly into HTML using the @ symbol, there's no separate templating language, the logic portions are genuine, ordinary C#.
A code block computes values without directly rendering anything; a simple expression renders a value directly at that exact spot.
@foreach and @if are the exact same C# control structures used anywhere else, just usable directly inside HTML markup.
@model at the top of a file declares the exact type of data that file expects, giving compile-time knowledge of what Model actually contains throughout the rest of the file.
Razor on its own only describes how to render HTML from data, it has no interactivity by itself, which is exactly the gap Blazor fills next.
What's Next
Part 4 covers Blazor, taking this exact Razor syntax and adding real interactivity on top of it, turning the read-only Task Tracker page built here into one where adding, checking, and deleting tasks actually works, without a full page reload.
Summary
Razor lets C# and HTML live in the same file, using @ as the signal for where C# begins, a code block for computing values, a simple expression for rendering one directly, and genuine control structures like @foreach and @if for loops and conditionals. The Task Tracker rendered here is intentionally read-only, since Razor by itself only describes how to turn data into HTML, it has no concept of interactivity on its own. That gap is exactly what the next post's subject, Blazor, exists to fill, building directly on top of everything covered in this one.
More from TechStack Blog: C# / .NET: https://www.techstackblog.com/category.html?cat=csharp
CS Fundamentals: https://www.techstackblog.com/category.html?cat=cs-fundamentals
Top comments (0)