A category groups products so customers can browse a smaller part of an online-store catalog. In a simple catalog, each Product has a Category value. A richer data model represents Category as a related object with a name, an ID, and associated products; Product holds both a CategoryId and a Category reference. section 8.1.1 section 18.2.2
To make categories work in a paginated storefront:
- Accept an optional category in the HomeController.Index action. Filter repository products where category is null or the product’s Category matches, then order and paginate them. Put the selection in ProductsListViewModel.CurrentCategory so the view can use it. section 8.1.1
- Build sidebar links from the distinct, ordered category values. A navigation view component can render them from the shared layout, read the selected category from route data, highlight it, and send category links to page 1. section 8.1.3
- Calculate PagingInfo.TotalItems from products in the selected category, not the whole repository. Otherwise a link can lead to an empty page; the Chess category exposes exactly that bug. section 8.1.4
Try and test the filter
Request http://localhost:5000/?category=soccer to inspect a filtered listing. In controller tests, pass null to request all products, or use a named category and assert that only matching products appear. Test page counts both with and without a selection. section 8.1.1 section 8.1.4
When categories are database objects, a product form can bind a nested name using Category.Name; an input tag helper generates that field name from asp-for="Category.Name". For a new category, the product editor can offer a “Create New Category” choice, collect its name in a partial view, save the category, assign its generated ID to Product.CategoryId, and revalidate before saving the product. This same-form approach suits a related object with few properties. section 28.4.2 section 31.4.1
Listing 18.7 The contents of the Product.cs file in the Models folder
using System.ComponentModel.DataAnnotations.Schema;
namespace WebApp.Models {
public class Product {
public long ProductId { get; set; }
public required string Name { get; set; }
[Column(TypeName = "decimal(8, 2)")]
public decimal Price { get; set; }
public long CategoryId { get; set; }
public Category? Category { get; set; }
public long SupplierId { get; set; }
public Supplier? Supplier { get; set; }
}
}
The Product model shows how CategoryId and the Category navigation property represent a category relationship, rather than the storefront’s simple category string. section 18.2.2


