Part 14: Create Checkout Controller & Order Summary View | ASP.NET Core MVC Full Project

 


In Step 1, you are creating a beautifully structured, unified compound view model. It acts as the entire data contract between your final checkout view and the order processing system.

Part 14: Create Checkout Controller & Order Summary View

Step 1: Implementing the CheckoutViewModel Structure

This view model uses a composition pattern to bundle four major data pillars together into one strongly typed object: the active cart snapshot, shipping data annotations, payment routing flags, and user profile persistence.

C# / ViewModels/CheckoutViewModel.cs
using MobileShop.Models;
using System.ComponentModel.DataAnnotations;

namespace MobileShop.ViewModels
{
    public class CheckoutViewModel
    {
        public ShoppingCartViewModel Cart { get; set; } = new ShoppingCartViewModel();

        // Shipping Information
        [Required(ErrorMessage = "First name is required")]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstName { get; set; } = string.Empty;

        [Required(ErrorMessage = "Last name is required")]
        [Display(Name = "Last Name")]
        [StringLength(50)]
        public string LastName { get; set; } = string.Empty;

        [Required(ErrorMessage = "Email is required")]
        [EmailAddress]
        [Display(Name = "Email Address")]
        public string Email { get; set; } = string.Empty;

        [Required(ErrorMessage = "Phone number is required")]
        [Phone]
        [Display(Name = "Phone Number")]
        [StringLength(20)]
        public string Phone { get; set; } = string.Empty;

        [Required(ErrorMessage = "Address is required")]
        [Display(Name = "Street Address")]
        [StringLength(200)]
        public string Address { get; set; } = string.Empty;

        [StringLength(100)]
        [Display(Name = "Apartment, Suite, etc.")]
        public string? Address2 { get; set; }

        [Required(ErrorMessage = "City is required")]
        [Display(Name = "City")]
        [StringLength(100)]
        public string City { get; set; } = string.Empty;

        [Display(Name = "State / Province")]
        [StringLength(100)]
        public string? State { get; set; }

        [Required(ErrorMessage = "Postal code is required")]
        [Display(Name = "Postal Code")]
        [StringLength(20)]
        public string PostalCode { get; set; } = string.Empty;

        [Required(ErrorMessage = "Country is required")]
        [Display(Name = "Country")]
        [StringLength(50)]
        public string Country { get; set; } = "Pakistan";

        [StringLength(500)]
        [Display(Name = "Order Notes")]
        [DataType(DataType.MultilineText)]
        public string? OrderNotes { get; set; }

        // Payment Information
        [Required(ErrorMessage = "Please select a payment method")]
        [Display(Name = "Payment Method")]
        public PaymentMethod PaymentMethod { get; set; }

        // For Stripe
        public string? StripeToken { get; set; }

        // Save address for future
        [Display(Name = "Save this address for future orders")]
        public bool SaveAddress { get; set; }

        public bool IsAuthenticated { get; set; }
        public List<SavedAddressViewModel>? SavedAddresses { get; set; }
    }

    public class SavedAddressViewModel
    {
        public int Id { get; set; }
        public string Label { get; set; } = string.Empty;
        public string FullAddress { get; set; } = string.Empty;
    }
}

Core Properties & Architectural Breakdown

  • Compound Composition Model (ShoppingCartViewModel Cart): Instead of rewriting your shopping cart items, line-item pricing calculations, or totals inside this model, you inject your existing ShoppingCartViewModel directly. This allows your Razor view to display a side-by-side transactional summary panel while your customer fills out their billing fields.

  • Strict Input Validation Filters (DataAnnotations): Attributes like [Required], [EmailAddress], and [Phone] give your application out-of-the-box server-side and client-side validation. By forcing maximum length boundaries via [StringLength(50)], you protect your application's input buffer points and keep malicious code out of your SQL schema.

  • Localized Context Injection (Country = "Pakistan";): Setting a default fallback literal (such as "Pakistan") inside your auto-initialized property layout is an excellent technique for lowering friction on your forms. It saves your local user base from repetitive typing while keeping the field editable for international customers.

  • Advanced Scalability Triggers (PaymentMethod, StripeToken): The inclusion of an enum-typed PaymentMethod property lets you handle split-path payment processing logic (e.g., Cash on Delivery vs. Online Credit Cards). The optional StripeToken string acts as an elegant integration anchor point, preparing your codebase to capture secure payment signatures down the line.

  • Profile Retention Framework (SavedAddresses): Including a lightweight, secondary child model nested directly below your main logic class (List<SavedAddressViewModel>) shows your senior architecture skills. If a customer is logged in, you can pull their historical address book records straight from the database into this collection, letting them skip the form entirely.

Step 2: Implementing the Checkout Controller and Constructor Dependency Injection

This code block establishes the foundational routing pipeline and core runtime engines required to process a customer's order checkout safely.

C# / Controllers/CheckoutController.cs (Constructor)
public class CheckoutController : Controller
{
    private readonly IShoppingCartService _cartService;
    private readonly UserManager<ApplicationUser> _userManager;

    public CheckoutController(
        IShoppingCartService cartService,
        UserManager<ApplicationUser> userManager,
        IConfiguration configuration,
        ILogger<CheckoutController> logger)
    {
        _cartService = cartService;
        _userManager = userManager;
    }
}

Core Components & Infrastructure Breakdown

  • The Core Controller Base Class (Controller): Inheriting from the base Controller class natively hooks this component straight into the ASP.NET Core MVC routing stack. This immediately gives you access to vital runtime web utilities such as User context mappings, authentication state parsing, and return action formatters like View() or RedirectToAction().

  • Decoupled Cart Operations Isolation (IShoppingCartService): Instead of writing direct database query calls inside the controller to inspect items, you inject your custom IShoppingCartService. This architectural separation ensures that if you ever swap your cart data layer (e.g., changing from a SQL database table back to an encrypted browser session cookie), your controller codebase will require zero modification.

  • Secure Authentication Management (UserManager<ApplicationUser>): The identity engine constructor parameter enables the application to inspect, update, and manage profile properties for logged-in accounts. This dependency is crucial for pulling down historical address sheets or assigning unique membership keys to an order record right during the final verification step.

  • Runtime Application Environment Configurations (IConfiguration): Although it is parsed through the constructor signature parameters, storing this instance gives your application instant runtime access to your appsettings.json file. This pattern is ideal for securely pulling down external live API secrets, such as your Stripe Payment public and secret keys, when initializing payment processing code blocks.

  • Diagnostics Engine (ILogger<CheckoutController>): Including the strongly typed logger utility allows the controller to drop persistent trace tags into your console or external monitoring logs. This is essential for auditing e-commerce transaction pathways, letting you debug dropped webhooks or tracing checkout validation errors instantly in production.

Step 3: Implementing the Checkout Index Method (HTTP GET)

This method orchestrates pulling the live shopping cart contents and merging them with the logged-in user's profile details to prepare the checkout page data context.

C# / Controllers/CheckoutController.cs (Index Action)
public async Task<IActionResult> Index()
{
    var cart = await _cartService.GetCartAsync();
    if (cart.CartItems.Count == 0)
    {
        TempData["Error"] = "Your cart is empty.";
        return RedirectToAction("Index", "ShoppingCart");
    }

    var user = await _userManager.GetUserAsync(User);
    var model = new CheckoutViewModel
    {
        Cart = cart,
        IsAuthenticated = user != null,
        FirstName = user?.FirstName ?? "",
        LastName = user?.LastName ?? "",
        Email = user?.Email ?? "",
        Phone = user?.PhoneNumber ?? "",
        Address = user?.Address ?? "",
        City = user?.City ?? "",
        PostalCode = user?.PostalCode ?? "",
        Country = user?.Country ?? "Pakistan"
    };

    ViewBag.CartItemCount = cart.ItemCount;
    return View(model);
}

Core Logic & Architectural Breakdown

  • Empty Cart Guard Protection:

    if (cart.CartItems.Count == 0)

    This is a fundamental e-commerce guard rail. It prevents users from accessing or manually forcing their way onto the checkout page with an empty basket. If an empty state is detected, the execution breaks immediately, sets a temporary error alert, and routes the shopper straight back to the shopping cart interface.

  • Smart User Profile Hydration: The action checks if a user is logged in via _userManager.GetUserAsync(User). If they are authenticated, the code utilizes the null-conditional operator (user?.Property) coupled with the null-coalescing fallback operator (?? "") to dynamically pre-fill fields like FirstName, Email, and Address. This saves your users from re-typing their information, drastically lowering cart abandonment rates.

  • Unified State Tracking (IsAuthenticated): IsAuthenticated = user != null Passing this clean boolean flag into your CheckoutViewModel allows your frontend Razor view layout to conditionally show or hide custom features—such as displaying a list of saved addresses or prompting guest users to create an account.

  • Navigation Synchronization Consistency: ViewBag.CartItemCount = cart.ItemCount; By continually assigning your header badge indicators across all entry-level route actions, you protect your site layout from losing its dynamic global values when transitioning between different application features.

Step 4: Designing the Dual-Column Checkout & Order Summary Interface

This markup transforms your CheckoutViewModel data structure into an interactive form layout inside Views/Checkout/Index.cshtml.

Razor / Views/Checkout/Index.cshtml
@model MobileShop.ViewModels.CheckoutViewModel
@{
    ViewData["Title"] = "Checkout";
}

<div class="container py-4">
    <h2><i class="bi bi-credit-card"></i> Checkout</h2>

    <form asp-action="Process" method="post" id="checkoutForm">
        <div class="row">
            <div class="col-lg-8">
                <!-- Shipping Information -->
                <div class="card shadow-sm mb-4">
                    <div class="card-header bg-primary text-white">
                        <h5 class="mb-0"><i class="bi bi-truck"></i> Shipping Information</h5>
                    </div>
                    <div class="card-body">
                        <div class="row">
                            <div class="col-md-6 mb-3">
                                <label asp-for="FirstName" class="form-label"></label>
                                <input asp-for="FirstName" class="form-control" />
                                <span asp-validation-for="FirstName" class="text-danger"></span>
                            </div>
                            <div class="col-md-6 mb-3">
                                <label asp-for="LastName" class="form-label"></label>
                                <input asp-for="LastName" class="form-control" />
                                <span asp-validation-for="LastName" class="text-danger"></span>
                            </div>
                        </div>

                        <div class="row">
                            <div class="col-md-6 mb-3">
                                <label asp-for="Email" class="form-label"></label>
                                <input asp-for="Email" class="form-control" type="email" />
                                <span asp-validation-for="Email" class="text-danger"></span>
                            </div>
                            <div class="col-md-6 mb-3">
                                <label asp-for="Phone" class="form-label"></label>
                                <input asp-for="Phone" class="form-control" type="tel" />
                                <span asp-validation-for="Phone" class="text-danger"></span>
                            </div>
                        </div>

                        <div class="mb-3">
                            <label asp-for="Address" class="form-label"></label>
                            <input asp-for="Address" class="form-control" placeholder="Street address" />
                            <span asp-validation-for="Address" class="text-danger"></span>
                        </div>

                        <div class="mb-3">
                            <label asp-for="Address2" class="form-label"></label>
                            <input asp-for="Address2" class="form-control" placeholder="Apartment, suite, etc. (optional)" />
                        </div>

                        <div class="row">
                            <div class="col-md-4 mb-3">
                                <label asp-for="City" class="form-label"></label>
                                <input asp-for="City" class="form-control" />
                                <span asp-validation-for="City" class="text-danger"></span>
                            </div>
                            <div class="col-md-4 mb-3">
                                <label asp-for="State" class="form-label"></label>
                                <input asp-for="State" class="form-control" />
                            </div>
                            <div class="col-md-4 mb-3">
                                <label asp-for="PostalCode" class="form-label"></label>
                                <input asp-for="PostalCode" class="form-control" />
                                <span asp-validation-for="PostalCode" class="text-danger"></span>
                            </div>
                        </div>

                        <div class="mb-3">
                            <label asp-for="Country" class="form-label"></label>
                            <input asp-for="Country" class="form-control" />
                            <span asp-validation-for="Country" class="text-danger"></span>
                        </div>

                        <div class="mb-3">
                            <label asp-for="OrderNotes" class="form-label"></label>
                            <textarea asp-for="OrderNotes" class="form-control" rows="3" placeholder="Special instructions for delivery..."></textarea>
                        </div>

                        @if (Model.IsAuthenticated)
                        {
                            <div class="form-check">
                                <input asp-for="SaveAddress" class="form-check-input" />
                                <label asp-for="SaveAddress" class="form-check-label"></label>
                            </div>
                        }
                    </div>
                </div>

                <!-- Payment Method -->
                <div class="card shadow-sm mb-4">
                    <div class="card-header bg-primary text-white">
                        <h5 class="mb-0"><i class="bi bi-credit-card"></i> Payment Method</h5>
                    </div>
                    <div class="card-body">
                        <div class="row g-3">
                            <div class="col-md-6">
                                <div class="form-check card p-3">
                                    <input class="form-check-input" type="radio" asp-for="PaymentMethod" value="CreditCard" id="paymentCard" checked />
                                    <label class="form-check-label" for="paymentCard">
                                        <i class="bi bi-credit-card"></i> Credit/Debit Card
                                    </label>
                                </div>
                            </div>
                            <div class="col-md-6">
                                <div class="form-check card p-3">
                                    <input class="form-check-input" type="radio" asp-for="PaymentMethod" value="Stripe" id="paymentStripe" />
                                    <label class="form-check-label" for="paymentStripe">
                                        <i class="bi bi-stripe"></i> Stripe
                                    </label>
                                </div>
                            </div>
                            <div class="col-md-6">
                                <div class="form-check card p-3">
                                    <input class="form-check-input" type="radio" asp-for="PaymentMethod" value="PayPal" id="paymentPayPal" />
                                    <label class="form-check-label" for="paymentPayPal">
                                        <i class="bi bi-paypal"></i> PayPal
                                    </label>
                                </div>
                            </div>
                            <div class="col-md-6">
                                <div class="form-check card p-3">
                                    <input class="form-check-input" type="radio" asp-for="PaymentMethod" value="UPI" id="paymentUPI" />
                                    <label class="form-check-label" for="paymentUPI">
                                        <i class="bi bi-phone"></i> UPI / Net Banking
                                    </label>
                                </div>
                            </div>
                            <div class="col-md-6">
                                <div class="form-check card p-3">
                                    <input class="form-check-input" type="radio" asp-for="PaymentMethod" value="CashOnDelivery" id="paymentCOD" />
                                    <label class="form-check-label" for="paymentCOD">
                                        <i class="bi bi-cash"></i> Cash on Delivery
                                    </label>
                                </div>
                            </div>
                        </div>

                        <!-- Stripe Card Element -->
                        <div id="stripePayment" class="mt-3" style="display: none;">
                            <div class="form-group">
                                <label class="form-label">Card Details</label>
                                <div id="cardElement" class="form-control p-3"></div>
                                <div id="cardErrors" class="text-danger mt-2"></div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>

            <!-- Order Summary -->
            <div class="col-lg-4">
                <div class="card shadow-sm">
                    <div class="card-header bg-primary text-white">
                        <h5 class="mb-0">Order Summary</h5>
                    </div>
                    <div class="card-body">
                        @foreach (var item in Model.Cart.CartItems)
                        {
                            <div class="d-flex justify-content-between mb-2">
                                <span>@item.ProductName x @item.Quantity</span>
                                <span>RS @item.TotalPrice.ToString("N0")</span>
                            </div>
                        }
                        <hr />
                        <div class="d-flex justify-content-between mb-2">
                            <span>Subtotal</span>
                            <span>RS @Model.Cart.CartTotal.ToString("N0")</span>
                        </div>
                        <div class="d-flex justify-content-between mb-2">
                            <span>Tax (18% GST)</span>
                            <span>RS @Model.Cart.Tax.ToString("N0")</span>
                        </div>
                        <div class="d-flex justify-content-between mb-2">
                            <span>Shipping</span>
                            <span&gt biographical>@(Model.Cart.Shipping == 0 ? "FREE" : $"RS {Model.Cart.Shipping:N0}")</span>
                        </div>
                        <hr />
                        <div class="d-flex justify-content-between mb-3">
                            <h5 class="mb-0">Total</h5>
                            <h5 class="mb-0 text-primary">RS @Model.Cart.GrandTotal.ToString("N0")</h5>
                        </div>

                        <button type="submit" class="btn btn-primary w-100 btn-lg" id="submitOrder">
                            <i class="bi bi-lock"></i> Place Order
                        </button>

                        <div class="text-center mt-3">
                            <small class="text-muted"><i class="bi bi-shield-check"></i> Secure checkout</small>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </form>
</div>

Core Razor & UI Engine Breakdown

  • Robust Form Tag Helper Architecture (asp-action="Process"): The <form> container binds itself straight to a future Process endpoint via POST. By using built-in ASP.NET Core Tag Helpers (asp-for and asp-validation-for), the view automatically maps each DOM input element to your view model's validation attributes. It handles rendering accurate label text and injecting real-time validation alerts seamlessly.

  • Conditional Element Rendering Flags (Model.IsAuthenticated):

    @if (Model.IsAuthenticated) { ... }

    This block controls profile persistence interface options. It checks the identity flag to conditionally show the "Save this address" checkbox option only to signed-in platform users, avoiding form clutter for guest checkout shoppers.

  • Multi-Channel Payment Router Matrix: Your design implements a list of styled radio selection options for processing payment. Each individual choice targets the same PaymentMethod view model field but submits unique text descriptors (CreditCard, Stripe, CashOnDelivery). The view sets up an empty container block (#cardElement) to mount Stripe elements dynamically as soon as the online option is checked.

  • Formatted Price Serialization (ToString("N0")):

    RS @Model.Cart.GrandTotal.ToString("N0")

    This formatting approach ensures a polished local user experience. By choosing the "N0" numeric formatter string, raw integers turn into standard local currencies featuring precise thousand-separator breaks (e.g., turning 185000 into RS 185,000), keeping your layout looking professional.

Comments