Part 16: Build a Complete "My Orders" History Tracking Page in ASP.NET Core MVC

 


Part 16: Build a Complete "My Orders" History Tracking Page

Step 1: Implementing the User Order Retrieval Query

This code uses Entity Framework Core to fetch a customer's specific order rows while loading nested relational child details in a single database transaction wire call.

C# / Services/OrderService.cs (History Extension)
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MobileShop.Models;

namespace MobileShop.Services
{
    public interface IOrderService
    {
        Task<List<Order>> GetUserOrdersAsync(string userId);
    }

    public class OrderService : IOrderService
    {
        private readonly ApplicationDbContext _context;

        public OrderService(ApplicationDbContext context)
        {
            _context = context;
        }

        public async Task<List<Order>> GetUserOrdersAsync(string userId)
        {
            return await _context.Orders
                .Include(o => o.OrderItems)
                .ThenInclude(oi => oi.Product)
                .Where(o => o.UserId == userId)
                .OrderByDescending(o => o.OrderDate)
                .ToListAsync();
        }
    }
}

Core Mechanics & LINQ Query Breakdown

  • Eager Loading with Entity Framework Core (.Include and .ThenInclude): This is the most critical technical concept in this step. By default, EF Core uses lazy loading, meaning it won't fetch related tables unless explicitly told to.

    • .Include(o => o.OrderItems) explicitly instructs EF Core to join the Orders table with the OrderItems table.

    • .ThenInclude(oi => oi.Product) steps deep inside each child item to join the Products table. This ensures that when your Razor view displays the history page, you can instantly print out the actual phone names (like @item.Product.Name) without causing a crash or triggering hidden, slow secondary queries.

  • Identity Filter Isolation (.Where(o => o.UserId == userId)): This handles fundamental data isolation and application security. It ensures that the logged-in customer can only scan their own invoices, completely blocking unauthorized users from seeing other buyers' checkout details.

  • Chronological Sorting Logic (.OrderByDescending(o => o.OrderDate)): An essential e-commerce user experience pattern. This positions the user's newest transactions at the very peak of their history dashboard dashboard page, moving older orders further down the list.

  • Non-Blocking Asynchronous Execution (.ToListAsync()): By running this operation asynchronously with the await keyword, you ensure that the database query execution thread is handed back to the server pool while waiting for SQL Server to return the records. This keeps your application fast and highly scalable under heavy traffic.

Step 2: Implementing the Orders History Controller Action

By placing this method inside the AccountController, you group it logically alongside other user-specific profile dashboards like address management or account settings.

C# / Controllers/OrdersController.cs (Orders History)
[HttpGet]
[Authorize]
public async Task<IActionResult> Orders()
{
    var user = await _userManager.GetUserAsync(User);
    if (user == null)
        return NotFound();

    var orders = await _orderService.GetUserOrdersAsync(user.Id);
    ViewBag.CartItemCount = await _cartService.GetCartItemCountAsync();
    
    return View(orders);
}

Core Controller Mechanics Breakdown

  • The Security Guard Attribute ([Authorize]): This is your primary defense line. The [Authorize] filter forces ASP.NET Core to intercept incoming requests before the method ever runs. If an anonymous user tries to manually visit /Account/Orders, the system automatically blocks them and redirects them straight to your Login page.

  • Contextual Identity Resolution (_userManager.GetUserAsync(User)): The global User property inside the controller represents the active browser's security principal (claims principal). By passing this context into the Identity Framework's UserManager, the application decrypts the secure authentication cookie and extracts the corresponding user profile record from the database.

  • Defensive Identity Fail-Safe:

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

    Even with the [Authorize] tag in place, this null-check acts as a critical backend guard rail. If a user deletes their account mid-session or their security token becomes corrupted, the system handles the anomaly gracefully by throwing an HTTP 404 NotFound response instead of crashing with a null reference exception.

  • UI Header State Preservation:

    ViewBag.CartItemCount = await _cartService.GetCartItemCountAsync();

    Since your viewers are navigating to a brand new view page, you must explicitly fetch and re-assign the cart count value to a dynamic property. This guarantees that your master layout header template keeps showing the correct, live shopping basket item bubble indicator while the customer views their order history.

Step 3: Explaining the "My Orders" Dashboard UI Layout

This template maps a strongly typed collection payload (List<Order>) into a responsive web structure using Bootstrap 5 UI standards and Bootstrap Icons (bi).

Razor / Views/Orders/Orders.cshtml
@model List<Order>
@{
    ViewData["Title"] = "My Orders";
}

<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">
                    <h5 class="mb-0"><i class="bi bi-bag"></i> My Orders</h5>
                </div>
                <div class="card-body">
                    @if (Model.Count == 0)
                    {
                        <div class="text-center py-5">
                            <i class="bi bi-bag-x display-1 text-muted"></i>
                            <h4 class="mt-3">No orders yet</h4>
                            <p class="text-muted">You haven't placed any orders yet.</p>
                            <a asp-controller="Products" asp-action="Index" class="btn btn-primary">
                                <i class="bi bi-shop"></i> Start Shopping
                            </a>
                        </div>
                    }
                    else
                    {
                        <div class="table-responsive">
                            <table class="table table-hover">
                                <thead>
                                    <tr>
                                        <th>Order #</th>
                                        <th>Date</th>
                                        <th>Total</th>
                                        <th>Status</th>
                                        <th>Payment</th>
                                        <th>Actions</th bind-id>
                                    </tr>
                                </thead>
                                <tbody>
                                    @foreach (var order in Model)
                                    {
                                        <tr>
                                            <td><a asp-action="OrderDetails" asp-route-id="@order.Id" class="text-decoration-none">@order.OrderNumber</a></td>
                                            <td>@order.OrderDate.ToString("MMM dd, yyyy")</td>
                                            <td>RS @order.TotalAmount.ToString("N0")</td>
                                            <td>
                                                <span class="badge @(order.Status switch {
                                                    OrderStatus.Pending => "bg-warning",
                                                    OrderStatus.Processing => "bg-info",
                                                    OrderStatus.Shipped => "bg-primary",
                                                    OrderStatus.Delivered => "bg-success",
                                                    OrderStatus.Cancelled => "bg-danger",
                                                    _ => "bg-secondary"
                                                })">@order.Status</span>
                                            </td>
                                            <td>
                                                <span class="badge @(order.PaymentStatus == PaymentStatus.Paid ? "bg-success" : "bg-warning")">
                                                    @order.PaymentStatus
                                                </span>
                                            </td>
                                            <td>
                                                <a asp-action="OrderDetails" asp-route-id="@order.Id" class="btn btn-sm btn-outline-primary">
                                                    <i class="bi bi-eye"></i> View
                                                </a>
                                                <a class="btn btn-sm btn-outline-info">
                                                    <i class="bi bi-truck"></i> Track
                                                </a>
                                            </td>
                                        </tr>
                                    }
                                </tbody>
                            </table>
                        </div>
                    }
                </div>
            </div>
        </div>
    </div>
</div>

Core UI & Razor Syntax Design Breakdown

  • Two-Column Dashboard Grid Infrastructure (col-lg-3 & col-lg-9): The view establishes a classic modern e-commerce dashboard pattern.

    • The Left Panel (col-lg-3) houses a flush user-profile sidebar navigation menu, adding an .active CSS class indicator onto the My Orders link.

    • The Right Panel (col-lg-9) operates as the primary workspace container, dynamically loading invoice datasets based on user state.

  • Defensive Empty-State Boundary Rendering:

    @if (Model.Count == 0) { ... }

    If a new user accesses their dashboard without any past transactions, showing an empty data table looks unpolished. The view intercepts this state with an informative feedback container—complete with a bi-bag-x callout graphic and a descriptive navigation action button routing them back to your product catalog catalog.

  • Advanced Razor C# Switch Expressions:

    @(order.Status switch {
        OrderStatus.Pending => "bg-warning",
        OrderStatus.Processing => "bg-info",
        OrderStatus.Shipped => "bg-primary",
        OrderStatus.Delivered => "bg-success",
        OrderStatus.Cancelled => "bg-danger",
        _ => "bg-secondary"
    })
    

    Instead of utilizing messy nested conditional blocks, this implementation utilizes a modern C# relational pattern directly within the markup class attribute. This evaluates the system's OrderStatus enum property value on the server, injecting matching contextual Bootstrap contextual class tokens (bg-success, bg-danger) to draw the customer's attention to their package shipping states instantly.

  • Mobile-Friendly Layout Tables (table-responsive): Standard HTML grid tables warp and overflow out of bounds on smartphone screens. Enclosing the table entity inside a .table-responsive class div layer instructs Bootstrap to automatically append a fluid horizontal swipe container, maintaining text clarity on smaller form factor screens.


Comments