MobileShop Website Part 29 | Build a Professional Product Details Page in ASP.NET Core MVC

 

Welcome to Part 29 of our development series! Today we are kicking off the public-facing side of our application by building a high-performance, professional Product Details Page.

In Step 1, we implement the asynchronous GET Details action method. This method serves as the data foundation for the entire page, fetching a single product along with all of its relational data—such as images, specs, and user reviews—in a single database round-trip.

Here is the step-by-step breakdown of how this controller logic executes behind the scenes.

Part 29 — Step 1: Eager Loading with Entity Framework Core

The Data Hydration Architecture

C# / Areas/Admin/Controllers/ProductsController.cs (Details Method)
public async Task<IActionResult> Details(int id)
{
    var product = await _context.Products
        .Include(p => p.Category)
        .Include(p => p.Brand)
        .Include(p => p.ProductImages)
        .Include(p => p.Specifications)
        .Include(p => p.Reviews)
            .ThenInclude(r => r.User)
        .FirstOrDefaultAsync(p => p.Id == id);

    if (product == null)
        return NotFound();

    return View(product);
}

Step-by-Step Code Explanation

Method Signature & Route Parameter

public async Task<IActionResult> Details(int id)
  • async Task<IActionResult>: Defines this method as an asynchronous action. By using non-blocking I/O operations, the underlying web server thread is released to handle other incoming user requests while waiting for SQL Server to return data.

  • int id: This parameter accepts the primary key of the product from the route URL (e.g., /Product/Details/5). ASP.NET Core automatically maps this via model binding.

Eager Loading via Include Expressions

var product = await _context.Products
    .Include(p => p.Category)
    .Include(p => p.Brand)
    .Include(p => p.ProductImages)
    .Include(p => p.Specifications)

By default, Entity Framework Core uses lazy loading or leaves navigation properties null. To prevent the notorious $N+1$ query performance bug when rendering a complex page, we use Eager Loading via the .Include() method.

  • This tells EF Core to generate SQL LEFT JOIN statements immediately, pulling the lookup data for Category and Brand, the full image gallery array (ProductImages), and the technical spec sheets (Specifications) altogether.

Multi-Level Relationships (ThenInclude)

.Include(p => p.Reviews)
    .ThenInclude(r => r.User)
  • .Include(p => p.Reviews): Grabs the collection of rating and review records associated with this specific product.

  • .ThenInclude(r => r.User): Moves down an additional layer into the object graph. It looks inside each individual Review record and loads the corresponding identity profile of the User who wrote it. This allows us to display the reviewer's name alongside their rating on the front-end layout.

Asynchronous Execution Safeguard

.FirstOrDefaultAsync(p => p.Id == id);
  • This terminates our LINQ query stream. It scans the Products table looking for a record matching our passed route parameter id.

  • Because it executes asynchronously via FirstOrDefaultAsync(), it queries the database efficiently and returns either the fully hydrated Product object or a standard null reference if no ID matches.

Resource Validation & View Routing

if (product == null)
    return NotFound();

return View(product);
  • if (product == null): A quick validation check. If an end-user modifies the URL to point to a non-existent tracking ID, the server halts processing and gracefully outputs an HTTP 404 response.

  • return View(product): If found, the heavily packed product model graph is forwarded right into our razor view template engine, where its fields are rendered onto our polished HTML storefront workspace.

n Step 2 of Part 29, we are moving to the front-end rendering layer to build a high-performance administrative Product Details View. This Razor template consumes our deep-loaded Product model graph and organizes it into a sleek, clean, multi-column layout using Bootstrap 5 and Font Awesome icons.

Here is the step-by-step breakdown of how this UI code renders your product data dynamically.

Step 2: Product Details Razor UI Implementation

The Interface Layout Architecture

CSHTML / Areas/Admin/Views/Products/Details.cshtml
@model Product
@{
    ViewData["Title"] = "Product Details";
}

<div class="container-fluid px-4">
    <h1 class="mt-4">Product Details</h1>
    <ol class="breadcrumb mb-4">
        <li class="breadcrumb-item"><a asp-area="Admin" asp-controller="Dashboard" asp-action="Index">Dashboard</a></li>
        <li class="breadcrumb-item"><a asp-action="Index">Products</a></li>
        <li class="breadcrumb-item active">@Model.Name</li>
    </ol>

    <div class="row">
        <!-- Product Images -->
        <div class="col-lg-4 mb-4">
            <div class="card">
                <div class="card-header">
                    <i class="fas fa-images me-1"></i> Product Images
                </div>
                <div class="card-body">
                    @if (!string.IsNullOrEmpty(Model.MainImageUrl))
                    {
                        <img src="@Model.MainImageUrl" class="img-fluid rounded mb-3 w-100" alt="@Model.Name"
                             style="max-height: 300px; object-fit: contain;" />
                    }
                    else
                    {
                        <div class="bg-light text-center p-5 rounded mb-3">
                            <i class="fas fa-image fa-3x text-muted"></i>
                            <p class="mt-2 text-muted">No main image</p>
                        </div>
                    }

                    @if (Model.ProductImages != null && Model.ProductImages.Any())
                    {
                        <div class="row g-2">
                            @foreach (var img in Model.ProductImages.OrderBy(i => i.DisplayOrder))
                            {
                                <div class="col-4">
                                    <img src="@img.ImageUrl" class="img-thumbnail w-100" alt="@img.AltText"
                                         style="height: 80px; object-fit: cover;" />
                                </div>
                            }
                        </div>
                    }
                </div>
            </div>

            <!-- Quick Stats -->
            <div class="card mt-4">
                <div class="card-header">
                    <i class="fas fa-chart-pie me-1"></i> Statistics
                </div>
                <div class="card-body">
                    <div class="d-flex justify-content-between mb-2">
                        <span>Stock Status:</span>
                        @if (Model.StockQuantity > 10)
                        {
                            <span class="badge bg-success">In Stock (@Model.StockQuantity)</span>
                        }
                        else if (Model.StockQuantity > 0)
                        {
                            <span class="badge bg-warning text-dark">Low Stock (@Model.StockQuantity)</span>
                        }
                        else
                        {
                            <span class="badge bg-danger">Out of Stock</span>
                        }
                    </div>
                    <div class="d-flex justify-content-between mb-2">
                        <span>Rating:</span>
                        <span>
                            @for (int i = 1; i <= 5; i++)
                            {
                                if (i <= Model.AverageRating)
                                {
                                    <i class="fas fa-star text-warning"></i>
                                }
                                else
                                {
                                    <i class="far fa-star text-muted"></i>
                                }
                            }
                            <small class="text-muted">(@Model.ReviewCount reviews)</small>
                        </span>
                    </div>
                    <div class="d-flex justify-content-between mb-2">
                        <span>Created:</span>
                        <span class="text-muted">@Model.CreatedAt.ToString("MMM dd, yyyy")</span>
                    </div>
                    @if (Model.UpdatedAt.HasValue)
                    {
                        <div class="d-flex justify-content-between">
                            <span>Last Updated:</span>
                            <span class="text-muted">@Model.UpdatedAt.Value.ToString("MMM dd, yyyy")</span>
                        </div>
                    }
                </div>
            </div>
        </div>

        <!-- Product Info -->
        <div class="col-lg-8">
            <div class="card mb-4">
                <div class="card-header d-flex justify-content-between align-items-center">
                    <div>
                        <i class="fas fa-box me-1"></i> Product Information
                    </div>
                    <div>
                        @if (Model.IsFeatured)
                        {
                            <span class="badge bg-warning me-1">Featured</span>
                        }
                        @if (Model.IsNewArrival)
                        {
                            <span class="badge bg-info me-1">New Arrival</span>
                        }
                        @if (Model.IsBestseller)
                        {
                            <span class="badge bg-success me-1">Bestseller</span>
                        }
                        @if (Model.IsActive)
                        {
                            <span class="badge bg-primary">Active</span>
                        }
                        else
                        {
                            <span class="badge bg-secondary">Inactive</span>
                        }
                    </div>
                </div>
                <div class="card-body">
                    <div class="row mb-3">
                        <div class="col-md-6">
                            <h3 class="mb-1">@Model.Name</h3>
                            <p class="text-muted mb-0">@Model.Model</p>
                        </div>
                        <div class="col-md-6 text-md-end">
                            <h4 class="text-primary mb-1">RS @Model.SalePrice.ToString("N2")</h4>
                            @if (Model.OriginalPrice > Model.SalePrice)
                            {
                                <small class="text-muted text-decoration-line-through">RS @Model.OriginalPrice.ToString("N2")</small>
                                <span class="badge bg-danger ms-1">-@Model.DiscountPercentage%</span>
                            }
                        </div>
                    </div>

                    <hr />

                    <div class="row mb-3">
                        <div class="col-md-6">
                            <p><strong>Category:</strong> @(Model.Category?.Name ?? "N/A")</p>
                            <p><strong>Brand:</strong> @(Model.Brand?.Name ?? "N/A")</p>
                        </div>
                        <div class="col-md-6">
                            <p><strong>Stock Quantity:</strong> @Model.StockQuantity units</p>
                            <p><strong>Product ID:</strong> #@Model.Id</p>
                        </div>
                    </div>

                    <h6>Description</h6>
                    <p class="text-muted">@(Model.Description ?? "No description available.")</p>

                    @if (!string.IsNullOrEmpty(Model.ShortDescription))
                    {
                        <h6>Short Description</h6>
                        <p class="text-muted">@Model.ShortDescription</p>
                    }
                </div>
                <div class="card-footer">
                    <a asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-warning">
                        <i class="fas fa-edit"></i> Edit
                    </a>
                    <a asp-action="Index" class="btn btn-secondary">
                        <i class="fas fa-arrow-left"></i> Back to List
                    </a>
                    <form asp-action="Delete" method="post" class="d-inline float-end"
                          onsubmit="return confirm('Are you sure you want to delete this product?');">
                        @Html.AntiForgeryToken()
                        <input type="hidden" name="id" value="@Model.Id" />
                        <button type="submit" class="btn btn-danger">
                            <i class="fas fa-trash"></i> Delete
                        </button>
                    </form>
                </div>
            </div>

            <!-- Specifications -->
            @if (Model.Specifications != null && Model.Specifications.Any())
            {
                <div class="card mb-4">
                    <div class="card-header">
                        <i class="fas fa-list me-1"></i> Specifications
                    </div>
                    <div class="card-body">
                        @{
                            var groupedSpecs = Model.Specifications.GroupBy(s => s.GroupName ?? "General").OrderBy(g => g.Key);
                        }
                        @foreach (var group in groupedSpecs)
                        {
                            <h6 class="text-muted text-uppercase small fw-bold mt-3 mb-2">@group.Key</h6>
                            <table class="table table-sm table-borderless">
                                <tbody>
                                    @foreach (var spec in group.OrderBy(s => s.DisplayOrder))
                                    {
                                        <tr>
                                            <td style="width: 30%;" class="text-muted">@spec.Name</td>
                                            <td class="fw-bold">@spec.Value</td>
                                        </tr>
                                    }
                                </tbody>
                            </table>
                        }
                    </div>
                </div>
            }

            <!-- Reviews -->
            @if (Model.Reviews != null && Model.Reviews.Any())
            {
                <div class="card">
                    <div class="card-header">
                        <i class="fas fa-star me-1"></i> Reviews (@Model.Reviews.Count)
                    </div>
                    <div class="card-body">
                        @foreach (var review in Model.Reviews.OrderByDescending(r => r.CreatedAt).Take(5))
                        {
                            <div class="border-bottom pb-3 mb-3">
                                <div class="d-flex justify-content-between align-items-start">
                                    <div>
                                        <div class="mb-1">
                                            @for (int i = 1; i <= 5; i++)
                                            {
                                                <i class="@(i <= review.Rating ? "fas" : "far") fa-star text-warning"></i>
                                            }
                                            @if (review.IsVerifiedPurchase)
                                            {
                                                <span class="badge bg-success ms-2 small">Verified Purchase</span>
                                            }
                                        </div>
                                        <h6 class="mb-1">@(review.Title ?? "No Title")</h6>
                                        <p class="mb-1 text-muted">@review.Comment</p>
                                        <small class="text-muted">
                                            By @(review.User?.FullName ?? "Unknown") on @review.CreatedAt.ToString("MMM dd, yyyy")
                                        </small>
                                    </div>
                                    @if (!review.IsApproved)
                                    {
                                        <span class="badge bg-warning text-dark">Pending Approval</span>
                                    }
                                </div>
                            </div>
                        }
                    </div>
                </div>
            }
        </div>
    </div>
</div>

Step-by-Step Layout Breakdown

Model Directive & Breadcrumb Navigation

@model Product
...
<ol class="breadcrumb mb-4">
    <li class="breadcrumb-item"><a asp-area="Admin" asp-controller="Dashboard" asp-action="Index">Dashboard</a></li>
    <li class="breadcrumb-item"><a asp-action="Index">Products</a></li>
    <li class="breadcrumb-item active">@Model.Name</li>
</ol>
  • @model Product: Strong-types our view instance to receive a single Product object.

  • Breadcrumb Navigation: Implements an enterprise UI trail using anchor tag helpers (asp-action, asp-controller). It outputs deep nested links back to your core management interfaces, mapping the final trailing active node to @Model.Name.

Left Column: Image Management Panel

@if (!string.IsNullOrEmpty(Model.MainImageUrl))
{
    <img src="@Model.MainImageUrl" class="img-fluid rounded mb-3 w-100" style="max-height: 300px; object-fit: contain;" />
}
else { ... }
  • Null Check Safeguard: Verifies whether the profile banner image contains a physical reference string. If empty, it drops into an alternate else block containing a fallback gray placeholder box (bg-light).

  • The Thumbnail Gallery:

    @foreach (var img in Model.ProductImages.OrderBy(i => i.DisplayOrder))

    Iterates over the subsidiary image database entries we pulled using .Include(). It automatically sorts them using our custom tracking weight DisplayOrder to build out an organized, evenly spaced structural grid.

Left Column: Badges & Real-Time Statistics

@if (Model.StockQuantity > 10) { <span class="badge bg-success">In Stock (@Model.StockQuantity)</span> }
else if (Model.StockQuantity > 0) { <span class="badge bg-warning text-dark">Low Stock</span> }
else { <span class="badge bg-danger">Out of Stock</span> }
  • This snippet uses inline conditional Razor statement controls to check the product stock levels. It automatically transitions colors (bg-success, bg-warning, or bg-danger) to give store managers instant visual context regarding inventory health.

  • Star Rating System: A basic mathematical index loop iterates from 1 to 5, comparing the loop variable i against @Model.AverageRating to draw a solid gold star icon (fas fa-star) or an empty hollow star icon (far fa-star).

Right Column: Pricing & E-Commerce Flag Badges

<h4 class="text-primary mb-1">RS @Model.SalePrice.ToString("N2")</h4>
@if (Model.OriginalPrice > Model.SalePrice)
{
    <small class="text-muted text-decoration-line-through">RS @Model.OriginalPrice.ToString("N2")</small>
    <span class="badge bg-danger ms-1">-@Model.DiscountPercentage%</span>
}
  • Prints the current running market value formatted as standard currency decimals using .ToString("N2").

  • If a markdown discount exists, it targets the old manufacturer retail values with a strikeout line layout rule (text-decoration-line-through) and appends a bright promotional badge calculating the exact discount percentage remaining.

Technical Specifications: LINQ Grouping Records

@{
    var groupedSpecs = Model.Specifications.GroupBy(s => s.GroupName ?? "General").OrderBy(g => g.Key);
}

This leverages standard LINQ grouping right inside the markup canvas. It buckets custom metadata fields into neat corporate partitions based on their database assigned string identity values (like Screen, Battery, Processor). It loops through these categories, wrapping the key-value sets into sleek, responsive HTML info-tables.

Contextual Verification Controls & Reviews

@foreach (var review in Model.Reviews.OrderByDescending(r => r.CreatedAt).Take(5))

Sorts incoming client reviews so the most recent submissions appear first, using .Take(5) to truncate the list and prevent long page scrolling. It also renders optional Verified Purchase tags and Pending Approval tracking status indicators for review moderation tasks.


Comments