Part 17: Build Order Details Page | ASP.NET Core MVC Full Project

 


Part 17: Build Order Details Page

Step 1: Implementing the Relational Order Details Fetcher

This backend contract method targets a single transaction record using its primary tracking key while explicitly pulling down three layers of related database nodes across a single connection pipeline.

C# / Services/OrderService.cs (Details Extension)
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;
using MobileShop.Models;

namespace MobileShop.Services
{
    public interface IOrderService
    {
        Task<Order?> GetOrderByIdAsync(int orderId);
    }

    public class OrderService : IOrderService
    {
        private readonly ApplicationDbContext _context;

        public OrderService(ApplicationDbContext context, ILogger<OrderService> logger)
        {
            _context = context;
        }

        public async Task<Order?> GetOrderByIdAsync(int orderId)
        {
            return await _context.Orders
                .Include(o => o.OrderItems)
                .ThenInclude(oi => oi.Product)
                .Include(o => o.User)
                .FirstOrDefaultAsync(o => o.Id == orderId);
        }
    }
}

Core Data Architecture & Query Breakdown

  • Nullable Return Contract Handling (Task<Order?>): By utilizing the C# nullable token (Order?), your service interface gracefully acknowledges that a user might request an ID that does not exist in the database (e.g., /Checkout/OrderDetails/99999). This forces downstream controllers to handle the missing record safely instead of crashing.

  • Advanced Relational Eager Loading Chain: Because Entity Framework Core defaults to not loading linked data tables, we construct an explicit data-fetching chain using .Include() and .ThenInclude():

    1. .Include(o => o.OrderItems) joins the primary Orders record to its associated list of purchased items.

    2. .ThenInclude(oi => oi.Product) steps directly inside that item collection to resolve the link to the Products table. This allows your view to read product information like names and image URLs.

    3. .Include(o => o.User) runs a parallel join back up to the Identity system's ApplicationUser table, enabling you to display buyer metadata (like account username and profile flags) on the invoice sheet.

  • Deterministic Match Execution (.FirstOrDefaultAsync): Instead of using general sorting collections, this query uses .FirstOrDefaultAsync(o => o.Id == orderId). It scans your database index table for an exact match on your primary key (Id). As soon as it finds the record, it stops scanning and returns the data, ensuring optimal performance.

Step 2: Implementing the Order Details Controller Action

By placing this method inside your authenticated AccountController, you extend your user dashboard capabilities, allowing shoppers to transition seamlessly from a high-level history list to a specific, deep-dive invoice tracking sheet.

C# / Controllers/OrdersController.cs (Order Details)
[HttpGet]
[Authorize]
public async Task<IActionResult> OrderDetails(int id)
{
    var order = await _orderService.GetOrderByIdAsync(id);
    if (order == null)
        return NotFound();

    ViewBag.CartItemCount = await _cartService.GetCartItemCountAsync();
    return View(order);
}

Core Method Mechanics & Architectural Breakdown

  • Route Parameter Dependency Mapping (int id): The method accepts an integer parameter named id directly from the URL route context (e.g., matching the routing pattern /Account/OrderDetails/15). ASP.NET Core MVC uses dynamic Model Binding to parse this integer value directly out of the incoming browser request path and supply it to your data service.

  • Defensive Resource Validation Boundary:

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

    This is an essential security and user-experience guardrail. If a user manually alters the route variable inside their browser address bar to a record that doesn't exist (like looking for an out-of-range ID), your controller intercepts the null result returned by the service and drops a clean, standard HTTP 404 NotFound response instead of throwing a null-reference application crash.

  • UI Cart Counter Persistence Syncing:

    ViewBag.CartItemCount = await _cartService.GetCartItemCountAsync();

    Just like your previous master layout dashboards, moving to a completely separate view file means the server needs to re-evaluate the active shopper's session layout metadata. Fetching the asynchronous cart count and injecting it into the dynamic ViewBag payload ensures that your top navbar shopping basket bubble continues to show accurate items in real-time while the customer reviews their purchase summary.

Step 3: Explaining the Itemized Order Details View Layout

This Razor layout binds directly to a single domain context model (@model Order), mapping deeply nested properties onto high-fidelity invoice visual elements.

Razor / Views/Orders/OrderDetails.cshtml
@model Order
@{
    ViewData["Title"] = $"Order {Model.OrderNumber}";
}
<div class="container py-4">
    <div class="row">
        <!-- Sidebar -->
        <div class="col-lg-3">
            <div class="card shadow-sm mb-4">
                <div class="list-group list-group-flush">
                    <a asp-action="Profile" class="list-group-item list-group-item-action">
                        <i class="bi bi-person"></i> Profile
                    </a>
                    <a asp-action="Orders" class="list-group-item list-group-item-action active">
                        <i class="bi bi-bag"></i> My Orders
                    </a>
                    <a asp-action="Wishlist" class="list-group-item list-group-item-action">
                        <i class="bi bi-heart"></i> Wishlist
                    </a>
                    <a asp-action="ChangePassword" class="list-group-item list-group-item-action">
                        <i class="bi bi-key"></i> Change Password
                    </a>
                </div>
            </div>
        </div>

        <!-- Main Content -->
        <div class="col-lg-9">
            <div class="card shadow-sm">
                <div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
                    <h5 class="mb-0"><i class="bi bi-receipt"></i> Order @Model.OrderNumber</h5>
                    <a asp-action="Orders" class="btn btn-light btn-sm"><i class="bi bi-arrow-left"></i> Back to Orders</a>
                </div>
                <div class="card-body">
                    <div class="row mb-4">
                        <div class="col-md-6">
                            <h6>Order Information</h6>
                            <p class="mb-1"><strong>Order Number:</strong> @Model.OrderNumber</p>
                            <p class="mb-1"><strong>Order Date:</strong> @Model.OrderDate.ToString("MMM dd, yyyy HH:mm")</p>
                            <p class="mb-1"><strong>Status:</strong> 
                                <span class="badge @(Model.Status switch {
                                    OrderStatus.Pending => "bg-warning",
                                    OrderStatus.Processing => "bg-info",
                                    OrderStatus.Shipped => "bg-primary",
                                    OrderStatus.Delivered => "bg-success",
                                    OrderStatus.Cancelled => "bg-danger",
                                    _ => "bg-secondary"
                                })">@Model.Status</span>
                            </p>
                            <p class="mb-0"><strong>Payment Method:</strong> @Model.PaymentMethod</p>
                        </div>
                        <div class="col-md-6">
                            <h6>Shipping Address</h6>
                            <p class="mb-1">@Model.ShippingAddress</p>
                            <p class="mb-1">@Model.ShippingCity, @Model.ShippingPostalCode</p>
                            <p class="mb-0">@Model.ShippingCountry</p>
                            @if (!string.IsNullOrEmpty(Model.ShippingPhone))
                            {
                                <p class="mb-0"><strong>Phone:</strong> @Model.ShippingPhone</p>
                            }
                        </div>
                    </div>

                    <h6>Order Items</h6>
                    <div class="table-responsive">
                        <table class="table">
                            <thead>
                                <tr>
                                    <th>Product</th>
                                    <th class="text-center">Quantity</th>
                                    <th class="text-end">Price</th>
                                    <th class="text-end">Total</th>
                                </tr>
                            </thead>
                            <tbody>
                                @foreach (var item in Model.OrderItems)
                                {
                                    <tr>
                                        <td>
                                            <div class="d-flex align-items-center">
                                                <img src="@(item.Product?.MainImageUrl ?? "https://via.placeholder.com/50x50?text=No+Image")" 
                                                     class="rounded me-3" style="width: 50px; height: 50px; object-fit: cover;" />
                                                <div>
                                                    <h6 class="mb-0">@item.Product?.Name</h6>
                                                    <small class="text-muted">@item.Product?.Model</small>
                                                </div>
                                            </div>
                                        </td>
                                        <td class="text-center">@item.Quantity</td>
                                        <td class="text-end">RS @item.UnitPrice.ToString("N0")</td>
                                        <td class="text-end">RS @item.TotalPrice.ToString("N0")</td>
                                    </tr>
                                }
                            </tbody>
                            <tfoot>
                                <tr>
                                    <td colspan="3" class="text-end"><strong>Subtotal:</strong></td>
                                    <td class="text-end">RS @Model.Subtotal.ToString("N0")</td>
                                </tr>
                                <tr>
                                    <td colspan="3" class="text-end"><strong>Tax:</strong></td>
                                    <td class="text-end">RS @Model.TaxAmount.ToString("N0")</td>
                                </tr>
                                <tr>
                                    <td colspan="3" class="text-end"><strong>Shipping:</strong></td>
                                    <td class="text-end">RS @Model.ShippingCost.ToString("N0")</td>
                                </tr>
                                <tr class="table-primary">
                                    <td colspan="3" class="text-end"><h5 class="mb-0">Total:</h5></td>
                                    <td class="text-end"><h5 class="mb-0">RS @Model.TotalAmount.ToString("N0")</h5></td>
                                </tr>
                            </tfoot>
                        </table>
                    </div>

                    @if (!string.IsNullOrEmpty(Model.Notes))
                    {
                        <div class="alert alert-info mt-3">
                            <strong>Order Notes:</strong> @Model.Notes
                        </div>
                    }
                </div>
            </div>
        </div>
    </div>
</div>

Core UI Elements & Architecture Breakdown

  • Consistent Dashboard Master Frame Structure (col-lg-3): The template includes the identical structural account menu block on the left panel. Retaining this layout across multiple subpages (Profile, Orders, Wishlist) is an essential UX paradigm that creates a unified dashboard application experience.

  • Flexbox Header Controls (d-flex justify-content-between): Inside the main .card-header container, you utilize Bootstrap 5 Flexbox alignment utilities. This repositions the tracking title perfectly to the left while keeping a responsive, high-contrast "Back to Orders" button pinned to the absolute right, optimizing back-and-forth page navigation.

  • Null-Safe Media Asset Injection with Default Fallbacks:

    <img src="@(item.Product?.MainImageUrl ?? "https://via.placeholder.com/50x50?text=No+Image")" ... />

    This implementation incorporates a brilliant defensive design element using the C# Null-Coalescing Operator (??). If an administrative manager accidentally deletes a mobile phone's main preview asset, or if the URL string is missing, the system catches the empty parameter and seamlessly loads a clean SVG placeholder image rather than generating a broken browser graphic box.

  • Detailed Multi-Line Structural Descriptions: Inside the product loop row cell, instead of merely displaying a flat description string, the view stacks relational item components cleanly:

    <h6 class="mb-0">@item.Product?.Name</h6>
    <small class="text-muted">@item.Product?.Model</small>
    

    This grouping allows your customers to simultaneously distinguish the commercial product title alongside its exact hardware model serial key.

Comments