ASP.NET CORE MVC - Database One-to-Many Relationships
Adding Categories and One-to-Many Relationships
Create a Category model, connect Categories to Products with a one-to-many relationship,
update the database with a migration, load related data with Include(),
and add Category dropdowns to the Product Create and Edit forms.
By the end of this tutorial, Products can be associated with Categories and the application can display and edit those relationships through Entity Framework Core and Razor views.
Xubuntu VM, username xubuntu, .NET 8, Visual Studio Code,
and the ProductManagement project completed through Part 7.
- Verify the existing project
- Understand one-to-many relationships
- Create
Category.cs - Update
Product.cs - Update
ApplicationDbContext.cs - Create and apply the migration
- Update
ProductsController.cs - Update
Create.cshtml - Update
Edit.cshtml - Update
Index.cshtml - Update
Details.cshtml - Test and verify the relationship
1. Open and Verify the Existing Project
Open the Xubuntu Terminal and go to the project:
cd ~/aspnet-mvc-tutorial/ProductManagement
pwd
The expected path is:
/home/xubuntu/aspnet-mvc-tutorial/ProductManagement
Confirm the important files from Part 7:
ls Models/Product.cs
ls Data/ApplicationDbContext.cs
ls Controllers/ProductsController.cs
ls Views/Products/Create.cshtml
ls Views/Products/Edit.cshtml
ls Views/Products/Index.cshtml
ls Views/Products/Details.cshtml
Open the project in Visual Studio Code:
code .
Build the existing project before making changes:
dotnet build
Continue only when the project builds successfully.
2. Understand the One-to-Many Relationship
One Category can contain many Products. Each Product can reference one Category.
The database relationship will use:
Category.Id
Product.CategoryId
3. Create the Category Model
Step 3.1 — Create the file
From the project directory, create:
touch Models/Category.cs
Step 3.2 — Open the file
code Models/Category.cs
Step 3.3 — Add the Category class
Paste the following into the new file:
using System.ComponentModel.DataAnnotations;
namespace ProductManagement.Models;
public class Category
{
public int Id { get; set; }
[Required]
[StringLength(100)]
public string Name { get; set; } = string.Empty;
public ICollection<Product> Products { get; set; }
= new List<Product>();
}
Step 3.4 — Save and verify
ls -l Models/Category.cs
Id is the Category primary key, Name stores the Category name,
and Products is the collection navigation property representing the many related Products.
4. Update Product.cs
Step 4.1 — Open the file
code Models/Product.cs
Step 4.2 — Find the end of the Product class
Near the bottom, you should already have:
[Range(
0,
1000000,
ErrorMessage = "Quantity cannot be negative.")]
public int Quantity { get; set; }
Step 4.3 — Add the relationship properties
Immediately after Quantity and before the closing brace of the class, add:
public int? CategoryId { get; set; }
public Category? Category { get; set; }
Step 4.4 — Verify the completed lower section
The end of Product.cs should now look like:
[Range(
0,
1000000,
ErrorMessage = "Quantity cannot be negative.")]
public int Quantity { get; set; }
public int? CategoryId { get; set; }
public Category? Category { get; set; }
Existing Products from Parts 5–7 do not yet have Categories. Using int?
allows those rows to remain valid while the relationship is introduced.
5. Update ApplicationDbContext.cs
Step 5.1 — Open the file
code Data/ApplicationDbContext.cs
Step 5.2 — Find the existing Products DbSet
You should already have:
public DbSet<Product> Products { get; set; } = null!;
Step 5.3 — Add Categories DbSet
Immediately below the Products DbSet, add:
public DbSet<Category> Categories { get; set; } = null!;
Step 5.4 — Add OnModelCreating()
Still inside the ApplicationDbContext class, place the following method
after the DbSet properties and before the final closing brace:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Category>().HasData(
new Category { Id = 1, Name = "Computers" },
new Category { Id = 2, Name = "Accessories" },
new Category { Id = 3, Name = "Storage" }
);
}
Step 5.5 — Verify the completed file
using Microsoft.EntityFrameworkCore;
using ProductManagement.Models;
namespace ProductManagement.Data;
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(
DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<Product> Products { get; set; } = null!;
public DbSet<Category> Categories { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Category>().HasData(
new Category { Id = 1, Name = "Computers" },
new Category { Id = 2, Name = "Accessories" },
new Category { Id = 3, Name = "Storage" }
);
}
}
6. Build Before Creating the Migration
Save all modified files, then run:
dotnet build
Continue only when the build succeeds.
7. Create and Apply the AddCategories Migration
Step 7.1 — Create the migration
dotnet ef migrations add AddCategories
Step 7.2 — Verify the migration exists
dotnet ef migrations list
You should see:
InitialCreate
AddCategories
Step 7.3 — Apply the migration
dotnet ef database update
Step 7.4 — Inspect the database
sqlite3 ProductManagement.db
At the SQLite prompt:
.tables
.schema Categories
.schema Products
SELECT * FROM Categories;
.quit
You should see Categories such as:
1|Computers
2|Accessories
3|Storage
8. Update ProductsController.cs to Load Related Categories
Step 8.1 — Open the controller
code Controllers/ProductsController.cs
Step 8.2 — Update the Index action
Find the existing Index action:
public async Task<IActionResult> Index()
{
var products = await _context.Products.ToListAsync();
return View(products);
}
Replace it with:
public async Task<IActionResult> Index()
{
var products = await _context.Products
.Include(p => p.Category)
.ToListAsync();
return View(products);
}
Step 8.3 — Update the Details action
Find this existing query inside Details():
var product = await _context.Products
.FirstOrDefaultAsync(p => p.Id == id);
Replace only that query with:
var product = await _context.Products
.Include(p => p.Category)
.FirstOrDefaultAsync(p => p.Id == id);
Step 8.4 — Verify the completed Details action
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
var product = await _context.Products
.Include(p => p.Category)
.FirstOrDefaultAsync(p => p.Id == id);
if (product == null)
{
return NotFound();
}
return View(product);
}
9. Add SelectList Support to ProductsController.cs
Step 9.1 — Keep ProductsController.cs open
At the top of the controller you should already have several using statements.
Step 9.2 — Add the SelectList namespace
Add:
using Microsoft.AspNetCore.Mvc.Rendering;
The top of the controller should now look similar to:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using ProductManagement.Data;
using ProductManagement.Models;
10. Update the Create GET Action
Step 10.1 — Find the existing Create GET action
Inside Controllers/ProductsController.cs, find:
[HttpGet]
public IActionResult Create()
{
return View();
}
Step 10.2 — Replace it
[HttpGet]
public IActionResult Create()
{
ViewData["CategoryId"] = new SelectList(
_context.Categories,
"Id",
"Name");
return View();
}
The controller loads Category IDs and names into a SelectList
that the Create view can use for a dropdown.
11. Update Create.cshtml
Step 11.1 — Open the Create view
code Views/Products/Create.cshtml
Step 11.2 — Find the Quantity form group
You should already have a block similar to:
<div class="mb-3">
<label asp-for="Quantity" class="form-label"></label>
<input asp-for="Quantity" class="form-control" />
<span asp-validation-for="Quantity"
class="text-danger"></span>
</div>
Step 11.3 — Add the Category dropdown
Immediately after the Quantity block and before the Create button, add:
<div class="mb-3">
<label asp-for="CategoryId"
class="form-label">Category</label>
<select asp-for="CategoryId"
class="form-select"
asp-items="ViewBag.CategoryId">
<option value="">-- Select Category --</option>
</select>
<span asp-validation-for="CategoryId"
class="text-danger"></span>
</div>
Step 11.4 — Verify the lower part of the form
<div class="mb-3">
<label asp-for="Quantity" class="form-label"></label>
<input asp-for="Quantity" class="form-control" />
<span asp-validation-for="Quantity"
class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="CategoryId"
class="form-label">Category</label>
<select asp-for="CategoryId"
class="form-select"
asp-items="ViewBag.CategoryId">
<option value="">-- Select Category --</option>
</select>
<span asp-validation-for="CategoryId"
class="text-danger"></span>
</div>
<button type="submit"
class="btn btn-primary">Create</button>
12. Update the Create POST Action
Step 12.1 — Reopen the controller
code Controllers/ProductsController.cs
Step 12.2 — Find the existing Create POST action
Find:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(Product product)
{
if (ModelState.IsValid)
{
_context.Add(product);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(product);
}
Step 12.3 — Replace it
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(Product product)
{
if (ModelState.IsValid)
{
_context.Add(product);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
ViewData["CategoryId"] = new SelectList(
_context.Categories,
"Id",
"Name",
product.CategoryId);
return View(product);
}
The dropdown must be recreated when validation fails because the SelectList is not automatically rebuilt.
13. Update the Edit GET Action
Step 13.1 — Keep ProductsController.cs open
Find the existing Edit GET action.
Step 13.2 — Find this section
var product = await _context.Products.FindAsync(id);
if (product == null)
{
return NotFound();
}
return View(product);
Step 13.3 — Add the SelectList before return View(product)
var product = await _context.Products.FindAsync(id);
if (product == null)
{
return NotFound();
}
ViewData["CategoryId"] = new SelectList(
_context.Categories,
"Id",
"Name",
product.CategoryId);
return View(product);
14. Update Edit.cshtml
Step 14.1 — Open the Edit view
code Views/Products/Edit.cshtml
Step 14.2 — Find the Quantity form group
Find the same Quantity block used in the Create form.
Step 14.3 — Add the Category dropdown
Immediately after Quantity and before the Save Changes button, add:
<div class="mb-3">
<label asp-for="CategoryId"
class="form-label">Category</label>
<select asp-for="CategoryId"
class="form-select"
asp-items="ViewBag.CategoryId">
<option value="">-- Select Category --</option>
</select>
<span asp-validation-for="CategoryId"
class="text-danger"></span>
</div>
15. Update the Edit POST Action
Step 15.1 — Reopen ProductsController.cs
code Controllers/ProductsController.cs
Step 15.2 — Find the end of the Edit POST action
Near the end, you currently have:
return View(product);
Step 15.3 — Add the SelectList immediately before it
ViewData["CategoryId"] = new SelectList(
_context.Categories,
"Id",
"Name",
product.CategoryId);
return View(product);
16. Update Index.cshtml to Display Category
Step 16.1 — Open the Index view
code Views/Products/Index.cshtml
Step 16.2 — Find the table headings
You should already have headings such as:
<th>ID</th>
<th>Name</th>
<th>Price</th>
<th>Quantity</th>
<th>Action</th>
Step 16.3 — Add the Category heading
Add:
<th>Category</th>
Place it before Action.
Step 16.4 — Add Category to each Product row
Inside the foreach row, add:
<td>@(product.Category?.Name ?? "Unassigned")</td>
Place it before the Action cell.
17. Update Details.cshtml to Display Category
Step 17.1 — Open the Details view
code Views/Products/Details.cshtml
Step 17.2 — Find the Product details table
You should already have rows for ID, Name, Description, Price and Quantity.
Step 17.3 — Add the Category row
Add this row after Quantity:
<tr>
<th>Category</th>
<td>@(Model.Category?.Name ?? "Unassigned")</td>
</tr>
18. Build the Completed Part 8 Project
Save all files, then run:
dotnet build
Fix any build errors before running the application.
19. Test Create with a Category
dotnet run
Open:
/Products/Create
The Category dropdown should contain:
Computers
Accessories
Storage
Create a Product and select one Category.
20. Test Index and Details
Open:
/Products
Confirm that the Category name appears in the table.
Open a Product Details page and confirm that the same Category is displayed there.
21. Test Edit
Edit an existing Product and choose a different Category.
Save the changes and confirm the new Category appears in Index and Details.
22. Verify the Relationship in SQLite
sqlite3 ProductManagement.db
Run:
SELECT Id, Name, CategoryId
FROM Products;
Then:
SELECT
Products.Name,
Categories.Name
FROM Products
LEFT JOIN Categories
ON Products.CategoryId = Categories.Id;
Exit:
.quit
23. Final Relationship Architecture
24. Troubleshooting
Category dropdown is empty
Check the Categories table:
sqlite3 ProductManagement.db
SELECT * FROM Categories;
.quit
Confirm the AddCategories migration was applied.
Category name does not appear in Index or Details
Check that the controller queries contain:
.Include(p => p.Category)
Dropdown disappears after validation error
Recreate ViewData["CategoryId"] before returning View(product)
from the Create or Edit POST action.
SelectList is not recognised
Check the top of ProductsController.cs:
using Microsoft.AspNetCore.Mvc.Rendering;
Migration fails with existing Products
Confirm Product.cs uses:
public int? CategoryId { get; set; }
25. Hands-On Exercise
- Create Gaming Laptop under Computers.
- Create Wireless Keyboard under Accessories.
- Create Portable SSD under Storage.
- Confirm Category names appear in Index.
- Open each Details page.
- Edit one Product and change its Category.
- Verify the foreign-key values directly in SQLite.
26. Knowledge Check
- What is a one-to-many relationship?
- What is a primary key?
- What is a foreign key?
- What is the purpose of
CategoryId? - What is the purpose of
Categoryon Product? - Why is
CategoryIdnullable? - What does
ICollection<Product>represent? - What does
Include()do? - Why is
SelectListused? - Why must dropdown data be recreated after validation fails?
Show suggested answers
- One entity can be associated with many entities of another type.
- A key that uniquely identifies a record.
- A value that references the primary key of another table.
- It stores the related Category key.
- It gives access to the related Category object.
- So existing Products can remain unassigned safely.
- The Products belonging to one Category.
- It eagerly loads related data.
- To provide Category choices for a dropdown.
- Because the SelectList is not automatically recreated after a failed POST.
27. Part 8 Summary
- created
Models/Category.cs; - updated
Models/Product.cs; - updated
Data/ApplicationDbContext.cs; - created and applied the
AddCategoriesmigration; - updated
Controllers/ProductsController.cs; - updated Create and Edit dropdowns;
- updated Index and Details to display Category names;
- loaded related data with
Include(); and - verified the relationship directly in SQLite.
Appendix — Full Code for Final Verification
Use this section only after completing all Part 8 steps. Compare each file in your project with the complete examples below. The files should contain the same important code even if minor formatting differs.
Appendix A — Models/Category.cs
Open:
code Models/Category.cs
The complete file should be:
using System.ComponentModel.DataAnnotations;
namespace ProductManagement.Models;
public class Category
{
public int Id { get; set; }
[Required]
[StringLength(100)]
public string Name { get; set; } = string.Empty;
public ICollection<Product> Products { get; set; }
= new List<Product>();
}
Appendix B — Models/Product.cs
Open:
code Models/Product.cs
The complete file should be:
using System.ComponentModel.DataAnnotations;
namespace ProductManagement.Models;
public class Product
{
public int Id { get; set; }
[Required(ErrorMessage = "Product name is required.")]
[StringLength(
100,
ErrorMessage = "Product name cannot exceed 100 characters.")]
public string Name { get; set; } = string.Empty;
[StringLength(
500,
ErrorMessage = "Description cannot exceed 500 characters.")]
public string? Description { get; set; }
[Range(
0.01,
1000000,
ErrorMessage = "Price must be greater than zero.")]
public decimal Price { get; set; }
[Range(
0,
1000000,
ErrorMessage = "Quantity cannot be negative.")]
public int Quantity { get; set; }
public int? CategoryId { get; set; }
public Category? Category { get; set; }
}
Appendix C — Data/ApplicationDbContext.cs
Open:
code Data/ApplicationDbContext.cs
The complete file should be:
using Microsoft.EntityFrameworkCore;
using ProductManagement.Models;
namespace ProductManagement.Data;
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(
DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<Product> Products { get; set; } = null!;
public DbSet<Category> Categories { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Category>().HasData(
new Category { Id = 1, Name = "Computers" },
new Category { Id = 2, Name = "Accessories" },
new Category { Id = 3, Name = "Storage" }
);
}
}
Appendix D — Controllers/ProductsController.cs
Open:
code Controllers/ProductsController.cs
The complete file should be:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using ProductManagement.Data;
using ProductManagement.Models;
namespace ProductManagement.Controllers;
public class ProductsController : Controller
{
private readonly ApplicationDbContext _context;
public ProductsController(ApplicationDbContext context)
{
_context = context;
}
public async Task<IActionResult> Index()
{
var products = await _context.Products
.Include(p => p.Category)
.ToListAsync();
return View(products);
}
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
var product = await _context.Products
.Include(p => p.Category)
.FirstOrDefaultAsync(p => p.Id == id);
if (product == null)
{
return NotFound();
}
return View(product);
}
[HttpGet]
public IActionResult Create()
{
ViewData["CategoryId"] = new SelectList(
_context.Categories,
"Id",
"Name");
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(Product product)
{
if (ModelState.IsValid)
{
_context.Add(product);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
ViewData["CategoryId"] = new SelectList(
_context.Categories,
"Id",
"Name",
product.CategoryId);
return View(product);
}
[HttpGet]
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var product = await _context.Products.FindAsync(id);
if (product == null)
{
return NotFound();
}
ViewData["CategoryId"] = new SelectList(
_context.Categories,
"Id",
"Name",
product.CategoryId);
return View(product);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, Product product)
{
if (id != product.Id)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(product);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!ProductExists(product.Id))
{
return NotFound();
}
throw;
}
return RedirectToAction(nameof(Index));
}
ViewData["CategoryId"] = new SelectList(
_context.Categories,
"Id",
"Name",
product.CategoryId);
return View(product);
}
[HttpGet]
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
var product = await _context.Products
.Include(p => p.Category)
.FirstOrDefaultAsync(p => p.Id == id);
if (product == null)
{
return NotFound();
}
return View(product);
}
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var product = await _context.Products.FindAsync(id);
if (product != null)
{
_context.Products.Remove(product);
await _context.SaveChangesAsync();
}
return RedirectToAction(nameof(Index));
}
private bool ProductExists(int id)
{
return _context.Products.Any(p => p.Id == id);
}
}
Appendix E — Views/Products/Create.cshtml
Open:
code Views/Products/Create.cshtml
The complete file should be:
@model Product
@{
ViewData["Title"] = "Create Product";
}
<h1>Create Product</h1>
<form asp-action="Create" method="post">
<div asp-validation-summary="ModelOnly"
class="text-danger"></div>
<div class="mb-3">
<label asp-for="Name" class="form-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name"
class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Description" class="form-label"></label>
<textarea asp-for="Description"
class="form-control"></textarea>
<span asp-validation-for="Description"
class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Price" class="form-label"></label>
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price"
class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Quantity" class="form-label"></label>
<input asp-for="Quantity" class="form-control" />
<span asp-validation-for="Quantity"
class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="CategoryId"
class="form-label">Category</label>
<select asp-for="CategoryId"
class="form-select"
asp-items="ViewBag.CategoryId">
<option value="">-- Select Category --</option>
</select>
<span asp-validation-for="CategoryId"
class="text-danger"></span>
</div>
<button type="submit"
class="btn btn-primary">Create</button>
<a asp-action="Index"
class="btn btn-secondary">Cancel</a>
</form>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
}
Appendix F — Views/Products/Edit.cshtml
Open:
code Views/Products/Edit.cshtml
The complete file should be:
@model Product
@{
ViewData["Title"] = "Edit Product";
}
<h1>Edit Product</h1>
<form asp-action="Edit" method="post">
<input type="hidden" asp-for="Id" />
<div asp-validation-summary="ModelOnly"
class="text-danger"></div>
<div class="mb-3">
<label asp-for="Name" class="form-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name"
class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Description" class="form-label"></label>
<textarea asp-for="Description"
class="form-control"></textarea>
<span asp-validation-for="Description"
class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Price" class="form-label"></label>
<input asp-for="Price" class="form-control" />
<span asp-validation-for="Price"
class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Quantity" class="form-label"></label>
<input asp-for="Quantity" class="form-control" />
<span asp-validation-for="Quantity"
class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="CategoryId"
class="form-label">Category</label>
<select asp-for="CategoryId"
class="form-select"
asp-items="ViewBag.CategoryId">
<option value="">-- Select Category --</option>
</select>
<span asp-validation-for="CategoryId"
class="text-danger"></span>
</div>
<button type="submit"
class="btn btn-primary">Save Changes</button>
<a asp-action="Index"
class="btn btn-secondary">Cancel</a>
</form>
@section Scripts {
@{
await Html.RenderPartialAsync("_ValidationScriptsPartial");
}
}
Appendix G — Views/Products/Index.cshtml
Open:
code Views/Products/Index.cshtml
The complete file should be:
@model IEnumerable<Product>
@{
ViewData["Title"] = "Products";
}
<h1>Products</h1>
<p>
<a asp-action="Create"
class="btn btn-primary">Create New Product</a>
</p>
@if (!Model.Any())
{
<p>No products have been created yet.</p>
}
else
{
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Price</th>
<th>Quantity</th>
<th>Category</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>@(product.Category?.Name ?? "Unassigned")</td>
<td>
<a asp-action="Details"
asp-route-id="@product.Id">Details</a>
|
<a asp-action="Edit"
asp-route-id="@product.Id">Edit</a>
|
<a asp-action="Delete"
asp-route-id="@product.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>
}
Appendix H — Views/Products/Details.cshtml
Open:
code Views/Products/Details.cshtml
The complete file should be:
@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>@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>
<tr>
<th>Category</th>
<td>@(Model.Category?.Name ?? "Unassigned")</td>
</tr>
</tbody>
</table>
<a asp-action="Index">Back to Products</a>
Appendix I — Views/Products/Delete.cshtml
Part 8 does not require a major change to the Delete view, but this is the complete Part 6 version for comparison:
@model Product
@{
ViewData["Title"] = "Delete Product";
}
<h1>Delete Product</h1>
<p>Are you sure you want to delete this product?</p>
<dl class="row">
<dt class="col-sm-2">ID</dt>
<dd class="col-sm-10">@Model.Id</dd>
<dt class="col-sm-2">Name</dt>
<dd class="col-sm-10">@Model.Name</dd>
<dt class="col-sm-2">Price</dt>
<dd class="col-sm-10">RM @Model.Price.ToString("N2")</dd>
<dt class="col-sm-2">Quantity</dt>
<dd class="col-sm-10">@Model.Quantity</dd>
</dl>
<form asp-action="Delete" method="post">
<input type="hidden" asp-for="Id" />
<button type="submit"
class="btn btn-danger">Delete</button>
<a asp-action="Index"
class="btn btn-secondary">Cancel</a>
</form>
Appendix J — Final Verification Commands
After comparing all files, run:
cd ~/aspnet-mvc-tutorial/ProductManagement
dotnet build
dotnet ef migrations list
dotnet ef database update
dotnet run
Then test:
/Products
/Products/Create
/Products/Details/1
/Products/Edit/1
/Products/Delete/1
Finally, verify the relationship directly in SQLite:
sqlite3 ProductManagement.db
SELECT * FROM Categories;
SELECT
Products.Id,
Products.Name,
Products.CategoryId,
Categories.Name
FROM Products
LEFT JOIN Categories
ON Products.CategoryId = Categories.Id;
.quit
If the project builds, Categories appear in the Create/Edit dropdowns, Category names appear in Index and Details, and the SQLite join returns the expected relationships, Part 8 is complete.
In Part 9, we will use LINQ, query strings, Where(),
OrderBy() and OrderByDescending() to search Products,
filter by Category and price, and sort database records.