ASP.NET CORE MVC - Models and Strongly Typed Razor Views
Building Product Models and Strongly Typed Razor Views
Expand the Product model, understand C# property types,
work with Razor syntax, display model values using @model and
@Model, render a product list, and compare HTML Helpers with Tag Helpers.
By the end of this tutorial, you will be able to represent product information with a C# model and display both single objects and collections through strongly typed Razor views.
This tutorial assumes a Xubuntu virtual machine with Linux username
xubuntu. The project is located at
/home/xubuntu/aspnet-mvc-tutorial/ProductManagement.
- Review the current
Productmodel - Expand the model to five properties
- Understand C# property types
- Understand Razor syntax
- Use the
@modeldirective - Display a single product
- Render a list of products
- Use Razor code blocks and expressions
- Use conditional logic and loops
- Understand HTML Helpers
- Understand Tag Helpers
- Troubleshoot common Razor view problems
- Complete a hands-on exercise
1. Starting Point
In Part 2, you created ProductsController,
Views/Products/Index.cshtml, Views/Products/Details.cshtml,
and a small Product model containing only Id and Name.
We will now expand that working MVC structure into a richer product model and more useful Razor views.
Open the project:
cd ~/aspnet-mvc-tutorial/ProductManagement
pwd
code .
For the xubuntu user, the expected path is:
/home/xubuntu/aspnet-mvc-tutorial/ProductManagement
Before continuing, confirm that the files created in Part 2 exist:
ls Controllers/ProductsController.cs
ls Models/Product.cs
ls Views/Products/Index.cshtml
ls Views/Products/Details.cshtml
If all four commands display their file paths without an error, your project is ready for Part 3.
Open:
Models/Product.cs
The previous version looked like:
namespace ProductManagement.Models;
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
2. Expand the Product Model
Replace the class with:
namespace ProductManagement.Models;
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
}
The model now represents the first five pieces of product information used throughout the series.
| Property | Type | Example |
|---|---|---|
Id |
int |
1 |
Name |
string |
Laptop |
Description |
string? |
14-inch business laptop |
Price |
decimal |
3299.90 |
Quantity |
int |
12 |
3. Why These C# Types?
null.
decimal for price?
For business and financial values, decimal is generally preferred over
binary floating-point types such as double when exact decimal-style arithmetic matters.
4. Nullable Reference Types
The project created in Part 1 enables nullable reference-type analysis:
<Nullable>enable</Nullable>
This makes these two declarations meaningfully different:
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
Name is intended to contain a string value, while
Description may be absent.
The C# type expresses intent, but it does not yet provide all business validation rules.
In Part 7 we will add data annotations such as [Required],
[StringLength] and [Range].
5. Create a Complete Product in the Controller
Open:
Controllers/ProductsController.cs
Change the Details() action to:
public IActionResult Details(int id)
{
var product = new Product
{
Id = id,
Name = $"Product {id}",
Description = "Sample product used for learning ASP.NET Core MVC.",
Price = 99.90m,
Quantity = 10
};
return View(product);
}
The m suffix on 99.90m tells C# that the literal is a
decimal value.
6. What Is a Razor View?
Razor views are files ending in .cshtml. They combine HTML with Razor syntax
so server-side C# values can participate in generating the final HTML response.
ASP.NET Core MVC supports strongly typed views based on a model object.
For example:
<h1>Product Details</h1>
<p>@Model.Name</p>
The browser never receives @Model.Name. Razor evaluates the expression on the server
and produces ordinary HTML.
7. The @model Directive
Open:
Views/Products/Details.cshtml
Place this at the top:
@model ProductManagement.Models.Product
The @model directive declares the type expected by the view.
Microsoft documents that it determines the type of the view's Model property.
After declaring the model type, the view can access:
@Model.Id
@Model.Name
@Model.Description
@Model.Price
@Model.Quantity
Do not rename Views/Products/Index.cshtml or
Views/Products/Details.cshtml. The controller actions rely on MVC view-discovery
conventions to find these files when they call View() or View(product).
8. Build the Product Details View
Replace the contents of Details.cshtml with:
@model ProductManagement.Models.Product
@{
ViewData["Title"] = "Product Details";
}
<h1>Product Details</h1>
<table class="table">
<tbody>
<tr>
<th>ID</th>
<td>@Model.Id</td>
</tr>
<tr>
<th>Name</th>
<td>@Model.Name</td>
</tr>
<tr>
<th>Description</th>
<td>@Model.Description</td>
</tr>
<tr>
<th>Price</th>
<td>RM @Model.Price.ToString("N2")</td>
</tr>
<tr>
<th>Quantity</th>
<td>@Model.Quantity</td>
</tr>
</tbody>
</table>
Run the application:
dotnet run
Then browse to:
/Products/Details/3
You should see all five properties rendered by the Razor view.
9. Razor Expressions
Razor uses the @ character to transition from HTML into C#.
Simple expressions:
@Model.Name
@Model.Price
@Model.Quantity
Method call:
@Model.Price.ToString("N2")
Combined with HTML:
<p>Price: RM @Model.Price.ToString("N2")</p>
10. Razor Code Blocks
Multiple C# statements can be placed inside a Razor code block:
@{
ViewData["Title"] = "Product Details";
var stockStatus = Model.Quantity > 0 ? "In stock" : "Out of stock";
}
The variable can then be rendered:
<p>Status: @stockStatus</p>
11. Conditional Logic in Razor
Razor can contain C# if statements:
@if (Model.Quantity > 0)
{
<p>This product is in stock.</p>
}
else
{
<p>This product is out of stock.</p>
}
Razor understands when markup is HTML and when code is C#, allowing both to appear naturally in the same view.
12. Update the Index Action to Return Multiple Products
Until now, Index() returned an empty view plus a message.
We will now send a collection of products.
Replace the Index() action with:
public IActionResult Index()
{
var products = new List<Product>
{
new Product
{
Id = 1,
Name = "Laptop",
Description = "14-inch business laptop",
Price = 3299.90m,
Quantity = 8
},
new Product
{
Id = 2,
Name = "Wireless Mouse",
Description = "Bluetooth wireless mouse",
Price = 89.90m,
Quantity = 25
},
new Product
{
Id = 3,
Name = "USB-C Hub",
Description = "Multi-port USB-C hub",
Price = 149.00m,
Quantity = 15
}
};
return View(products);
}
These objects exist only in memory. Restarting the application recreates them. Persistent storage begins in Part 4 with Entity Framework Core and SQLite.
13. Strongly Typed Collection View
Because Index() now sends a collection, the view must expect a collection.
Open:
Views/Products/Index.cshtml
Start with:
@model IEnumerable<ProductManagement.Models.Product>
The view's model is now a sequence of Product objects rather than one product.
14. Render the Product List
Replace Index.cshtml with:
@model IEnumerable<ProductManagement.Models.Product>
@{
ViewData["Title"] = "Products";
}
<h1>Products</h1>
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Price</th>
<th>Quantity</th>
</tr>
</thead>
<tbody>
@foreach (var product in Model)
{
<tr>
<td>@product.Id</td>
<td>@product.Name</td>
<td>RM @product.Price.ToString("N2")</td>
<td>@product.Quantity</td>
</tr>
}
</tbody>
</table>
Browse to:
/Products
You should see three rows.
15. How foreach Works in a Razor View
The loop:
@foreach (var product in Model)
{
...
}
executes once for each item in the collection.
16. Add a Details Link
Add another table heading:
<th>Action</th>
Then add this cell inside each product row:
<td>
<a asp-controller="Products"
asp-action="Details"
asp-route-id="@product.Id">Details</a>
</td>
This is a Tag Helper. It looks like HTML but contains server-side attributes
beginning with asp-.
17. What Are Tag Helpers?
Tag Helpers attach server-side behavior to HTML elements in Razor views. Microsoft notes that they allow Razor markup to remain closer to normal HTML than equivalent method-based helper syntax.
This markup:
<a asp-controller="Products"
asp-action="Details"
asp-route-id="@product.Id">Details</a>
generates a URL appropriate for the configured MVC route, such as:
/Products/Details/2
You should generally let MVC generate application URLs rather than manually constructing every route string.
18. Where Are Tag Helpers Enabled?
Open:
Views/_ViewImports.cshtml
You should see content similar to:
@using ProductManagement
@using ProductManagement.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
The @addTagHelper directive makes ASP.NET Core MVC Tag Helpers available to views.
19. Simplify @model with _ViewImports
Because ProductManagement.Models is imported by
_ViewImports.cshtml, you can normally simplify:
@model ProductManagement.Models.Product
to:
@model Product
Similarly:
@model IEnumerable<Product>
This makes Razor views shorter and easier to read.
20. What Are HTML Helpers?
HTML Helpers are methods called from Razor to generate HTML. Tag Helpers do not completely replace HTML Helpers; both mechanisms exist in ASP.NET Core MVC.
An HTML Helper example is:
@Html.DisplayFor(model => model.Name)
A direct Razor expression is:
@Model.Name
And a Tag Helper example is:
<a asp-action="Details" asp-route-id="@Model.Id">Details</a>
| Approach | Style | Example |
|---|---|---|
| Razor expression | Direct C# value | @Model.Name |
| HTML Helper | C# method call | @Html.DisplayFor(...) |
| Tag Helper | HTML-like attributes | asp-action="Details" |
21. Use DisplayFor
In Details.cshtml, you can replace:
@Model.Name
with:
@Html.DisplayFor(model => model.Name)
Both display the property value here, although HTML Helpers become more useful once metadata, formatting and validation attributes are introduced.
22. Add a Stock Status Column
Add a Status heading to the Index table.
Inside each row:
<td>
@if (product.Quantity > 0)
{
<span>In stock</span>
}
else
{
<span>Out of stock</span>
}
</td>
This demonstrates that a Razor view can combine loops, conditions and HTML.
23. Completed Index View
@model IEnumerable<Product>
@{
ViewData["Title"] = "Products";
}
<h1>Products</h1>
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Price</th>
<th>Quantity</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
@foreach (var product in Model)
{
<tr>
<td>@product.Id</td>
<td>@product.Name</td>
<td>RM @product.Price.ToString("N2")</td>
<td>@product.Quantity</td>
<td>
@if (product.Quantity > 0)
{
<span>In stock</span>
}
else
{
<span>Out of stock</span>
}
</td>
<td>
<a asp-controller="Products"
asp-action="Details"
asp-route-id="@product.Id">Details</a>
</td>
</tr>
}
</tbody>
</table>
24. Completed Details View
@model Product
@{
ViewData["Title"] = "Product Details";
}
<h1>Product Details</h1>
<table class="table">
<tbody>
<tr>
<th>ID</th>
<td>@Model.Id</td>
</tr>
<tr>
<th>Name</th>
<td>@Html.DisplayFor(model => model.Name)</td>
</tr>
<tr>
<th>Description</th>
<td>@Model.Description</td>
</tr>
<tr>
<th>Price</th>
<td>RM @Model.Price.ToString("N2")</td>
</tr>
<tr>
<th>Quantity</th>
<td>@Model.Quantity</td>
</tr>
</tbody>
</table>
<a asp-action="Index">Back to Products</a>
25. Current Request Flow
For a details request:
26. Current Project Structure
ProductManagement/
├── Controllers/
│ ├── HomeController.cs
│ └── ProductsController.cs
├── Models/
│ ├── ErrorViewModel.cs
│ └── Product.cs
├── Views/
│ ├── Home/
│ ├── Products/
│ │ ├── Index.cshtml
│ │ └── Details.cshtml
│ ├── Shared/
│ ├── _ViewImports.cshtml
│ └── _ViewStart.cshtml
├── wwwroot/
├── Program.cs
└── ProductManagement.csproj27. Important Distinction: Model versus Database
A model class does not automatically create or store database records.
At the moment, product objects are constructed directly inside the controller.
Hard-coded products are appropriate here because the purpose of Part 3 is to understand models and views. In Part 4, these in-memory examples will be replaced by Entity Framework Core and SQLite.
28. Troubleshooting
The view 'Index' or 'Details' was not found
Confirm that the files still exist with the exact Linux case-sensitive paths:
ls -l Views/Products/Index.cshtml
ls -l Views/Products/Details.cshtml
The folder must be Products with a capital P, and the files must be
Index.cshtml and Details.cshtml with capital first letters.
Product could not be found in the Razor view
Check Views/_ViewImports.cshtml. It should normally contain:
@using ProductManagement
@using ProductManagement.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
If ProductManagement.Models is not imported, use the full type name in the
@model directive or restore the import.
The application does not show recent changes
Save the modified files. If you are using dotnet run, stop the application with
Ctrl+C and run it again:
dotnet run
29. Hands-On Exercise
Add a fourth product to the list returned by Index():
new Product
{
Id = 4,
Name = "Mechanical Keyboard",
Description = "USB mechanical keyboard",
Price = 249.90m,
Quantity = 0
}
Then:
- Run the application.
- Open
/Products. - Confirm that four products appear.
- Confirm that Mechanical Keyboard displays Out of stock.
- Click a Details link.
- Use the Back to Products link to return to the list.
30. Challenge Exercise
Add another property to the model:
public string? Sku { get; set; }
Populate an SKU for each sample product and display it in both the Index and Details views.
Suggested example
Sku = "LAP-001"
Then render:
@product.Sku
31. Knowledge Check
- What is the purpose of a model class?
- Why is
decimalappropriate for a product price? - What does
string?indicate? - What does the Razor
@modeldirective do? - What is the difference between
@modeland@Model? - How do you display a property such as the product name?
- How do you execute several C# statements in a Razor view?
- Which Razor construct is useful for rendering every item in a product collection?
- What does
IEnumerable<Product>represent? - What is a Tag Helper?
- What does
asp-route-idhelp generate? - What is an HTML Helper?
- Do the products created in this part persist after the application restarts?
Show suggested answers
- It represents structured application data.
- It is well suited to decimal financial values.
- The reference may contain a string or be
null. - It declares the type expected by a strongly typed Razor view.
@modeldeclares the type;@Modelaccesses the current model object.- For example,
@Model.Name. - Use a block beginning with
@{ ... }. @foreach.- A sequence or collection of Product objects.
- Server-side Razor behavior attached to HTML-like elements and attributes.
- A route value named
idin the generated URL. - A Razor-accessible method that helps generate HTML or display model information.
- No. They are currently created in memory by the controller.
32. Part 3 Summary
In this tutorial, you:
- expanded the
Productmodel to five core properties; - reviewed
int,string,string?anddecimal; - learned basic Razor syntax;
- used the
@modeldirective; - displayed properties with
@Model; - used Razor code blocks and conditional logic;
- returned a collection of products from a controller;
- rendered the collection with
foreach; - used a Tag Helper to generate Details links;
- examined
_ViewImports.cshtml; and - used an HTML Helper with
DisplayFor.
In Part 4, we will install Entity Framework Core packages, create an application
DbContext, configure SQLite, register the context with dependency injection,
create the first migration, and generate the Products database table.