ASP.NET CORE MVC - Models and Strongly Typed Razor Views

ASP.NET CORE MVC TUTORIAL SERIES · PART 3

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.

Objective

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.

Lab environment used in this series

This tutorial assumes a Xubuntu virtual machine with Linux username xubuntu. The project is located at /home/xubuntu/aspnet-mvc-tutorial/ProductManagement.

In this tutorial
  1. Review the current Product model
  2. Expand the model to five properties
  3. Understand C# property types
  4. Understand Razor syntax
  5. Use the @model directive
  6. Display a single product
  7. Render a list of products
  8. Use Razor code blocks and expressions
  9. Use conditional logic and loops
  10. Understand HTML Helpers
  11. Understand Tag Helpers
  12. Troubleshoot common Razor view problems
  13. 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
Checkpoint

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?

int Suitable for whole numbers such as IDs and quantities.
string Suitable for text such as a product name.
string? Allows the description to be absent or null.
decimal Suitable for monetary values where decimal precision matters.
Why 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.

Validation comes later

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.

Razor View ↓ Server-side processing ↓ Generated HTML ↓ Browser

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
Keep the existing MVC filenames

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);
}
No database yet

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.

Model ↓ Product 1 ──→ table row Product 2 ──→ table row Product 3 ──→ table row

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

GET /Products ↓ ProductsController.Index() ↓ List<Product> ↓ View(products) ↓ Index.cshtml ↓ foreach ↓ HTML table

For a details request:

GET /Products/Details/2 ↓ ProductsController.Details(2) ↓ Product object ↓ View(product) ↓ Details.cshtml ↓ HTML response

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.csproj

27. Important Distinction: Model versus Database

A model class does not automatically create or store database records.

Product class ↓ Represents data ↓ NOT YET persistent

At the moment, product objects are constructed directly inside the controller.

Temporary learning approach

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

Exercise: Add Another Product

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:

  1. Run the application.
  2. Open /Products.
  3. Confirm that four products appear.
  4. Confirm that Mechanical Keyboard displays Out of stock.
  5. Click a Details link.
  6. 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

  1. What is the purpose of a model class?
  2. Why is decimal appropriate for a product price?
  3. What does string? indicate?
  4. What does the Razor @model directive do?
  5. What is the difference between @model and @Model?
  6. How do you display a property such as the product name?
  7. How do you execute several C# statements in a Razor view?
  8. Which Razor construct is useful for rendering every item in a product collection?
  9. What does IEnumerable<Product> represent?
  10. What is a Tag Helper?
  11. What does asp-route-id help generate?
  12. What is an HTML Helper?
  13. Do the products created in this part persist after the application restarts?
Show suggested answers
  1. It represents structured application data.
  2. It is well suited to decimal financial values.
  3. The reference may contain a string or be null.
  4. It declares the type expected by a strongly typed Razor view.
  5. @model declares the type; @Model accesses the current model object.
  6. For example, @Model.Name.
  7. Use a block beginning with @{ ... }.
  8. @foreach.
  9. A sequence or collection of Product objects.
  10. Server-side Razor behavior attached to HTML-like elements and attributes.
  11. A route value named id in the generated URL.
  12. A Razor-accessible method that helps generate HTML or display model information.
  13. No. They are currently created in memory by the controller.

32. Part 3 Summary

In this tutorial, you:

  • expanded the Product model to five core properties;
  • reviewed int, string, string? and decimal;
  • learned basic Razor syntax;
  • used the @model directive;
  • 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.
Product Model ↓ Controller ↓ View(product / products) ↓ Strongly Typed Razor View ↓ HTML Response
Next: Part 4 — Entity Framework Core and SQLite

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.