Part 12: Product Review & Rating System | ASP.NET Core MVC Full Project Tutorial

 

Welcome to Part 12 of our advanced series: Build a Full Mobile Shop E-Commerce Website in ASP.NET Core MVC! 🚀

In this tutorial, we are implementing an essential e-commerce feature that builds customer trust: a complete Product Review and Star Rating System. You will learn how to build the backend logic and frontend UI to allow authenticated users to submit reviews, leave star ratings, and display them dynamically on the product details page using Entity Framework Core.

Step 1: Creating the ReviewViewModel Grouping

By adding this class inside ProductViewModel.cs, you are organizing your code by context, ensuring that any view utilizing a product can easily access the review structure.

C# / Models/ViewModels/ReviewViewModel.cs
public class ReviewViewModel
{
    [Required]
    [Range(1, 5, ErrorMessage = "Please select a rating between 1 and 5")]
    public int Rating { get; set; }

    [StringLength(100)]
    public string? Title { get; set; }

    [StringLength(1000)]
    public string? Comment { get; set; }
}

Core Properties & Validation Breakdown

  • Rating (The Star Value): public int Rating { get; set; } The combination of [Required] and [Range(1, 5)] enforces strict data integrity. It ensures the user must actively pick a score, and blocks malicious requests trying to insert invalid ratings (like 0 or 100 stars) into your system before the database is even hit.

  • Title (Brief Header): public string? Title { get; set; } The nullable string (string?) means a headline is optional, giving flexibility to users who just want to leave a quick score. The [StringLength(100)] attribute caps the length, keeping your UI titles clean and your database safe from massive string overflows.

  • Comment (The Detailed Body): public string? Comment { get; set; } Like the title, this is optional, allowing users to submit ratings without words. The [StringLength(1000)] attribute provides a generous yet safe limit for thorough customer feedback without risking database text-bloat.

Step 2: Implementing the Product Reviews Interface inside Tabs

This code block integrates directly into your tab panels in the Product Details View, instantly adding interactive capabilities beneath your hardware specifications.

Razor / Views/Product/_ReviewsPartial.cshtml
<!-- Reviews -->
<div class="tab-pane fade" id="reviews">
    @if (User.Identity?.IsAuthenticated == true)
    {
        <div class="card mb-4">
            <div class="card-body">
                <h6>Write a Review</h6>
                <form asp-action="AddReview" method="post">
                    <input type="hidden" name="productId" value="@Model.Product.Id" />
                    <div class="mb-3">
                        <label class="form-label">Rating</label>
                        <div class="star-rating">
                            @for (int i = 5; i >= 1; i--)
                            {
                                <input type="radio" name="Rating" value="@i" id="star@i" required />
                                <label for="star@i"><i class="bi bi-star-fill"></i></label>
                            }
                        </div>
                    </div>
                    <div class="mb-3">
                        <label class="form-label">Title</label>
                        <input type="text" name="Title" class="form-control" maxlength="100" />
                    </div>
                    <div class="mb-3">
                        <label class="form-label">Review</label>
                        <textarea name="Comment" class="form-control" rows="3" maxlength="1000"></textarea>
                    </div>
                    <button type="submit" class="btn btn-primary">Submit Review</button>
                </form>
            </div>
        </div>
    }
    else
    {
        <div class="alert alert-info">
            Please <a asp-controller="Account" asp-action="Login">login</a> to write a review.
        </div>
    }

    @if (Model.Product.Reviews.Any())
    {
        @foreach (var review in Model.Product.Reviews.Where(r => r.IsApproved).OrderByDescending(r => r.CreatedAt))
        {
            <div class="card mb-3">
                <div class="card-body">
                    <div class="d-flex justify-content-between">
                        <div>
                            <h6 class="mb-1">@review.User?.FullName</h6>
                            <div class="text-warning">
                                @for (int i = 1; i <= 5; i++)
                                {
                                    <i class="bi @(i <= review.Rating ? "bi-star-fill" : "bi-star")"></i>
                                }
                            </div>
                        </div>
                        <small class="text-muted">@review.CreatedAt.ToString("MMM dd, yyyy")</small>
                    </div>
                    @if (!string.IsNullOrEmpty(review.Title))
                    {
                        <h6 class="mt-2">@review.Title</h6>
                    }
                    <p class="mb-0">@review.Comment</p>
                    @if (review.IsVerifiedPurchase)
                    {
                        <span class="badge bg-success mt-2"><i class="bi bi-check-circle"></i> Verified Purchase</span>
                    }
                </div>
            </div>
        }
    }
    else
    {
        <p class="text-muted">No reviews yet. Be the first to review this product!</p>
    }
</div>

Core Components & Razor Logic Breakdown

  • Conditional Submission Access (User.Identity?.IsAuthenticated): This check acts as a strict user gate. If the user is logged in, they are shown a clean, structured review form. If they are a guest, the form is replaced by a Bootstrap alert-info box with a direct link to the login page. This guarantees that anonymous users cannot spam your database.

  • Reverse Loop for CSS Star Rating Control:

    @for (int i = 5; i >= 1; i--)

    Running the loop in reverse (from 5 down to 1) is a classic web development strategy. It allows CSS sibling selectors (~) to properly highlight preceding stars when a user hovers over an option, creating a smooth, responsive desktop star selection interface.

  • Approved Content Filtering (Where(r => r.IsApproved)): This clause filters content at the database rendering level. By only displaying reviews where IsApproved is true, you protect your storefront from immediate offensive content or spam, giving you an administrative layer of control.

  • Dynamic Rating Indicator Generation: The internal loop calculates and fills star icons on the fly for existing comments. If a review has a score of 4, the ternary operator outputs bi-star-fill four times and bi-star (the empty outline icon) for the remaining spot, giving customers an immediate visual summary.

  • The Verified Purchase Trust Element: The badge check adds massive credibility to your app. If the backend flag detects the active user has a completed order invoice matching this product ID, it applies a green badge stating "Verified Purchase," a hallmark of elite storefront setups.

Step 3: Implementing the AddReviewAsync Service Method

This asynchronous method safely handles the business rules of map-transforming your user's form submission data into a permanent database record.

C# / Services/IProductService.cs (AddReviewAsync Method)
public async Task<bool> AddReviewAsync(int productId, string userId, ReviewViewModel review)
{
    var product = await _context.Products.FindAsync(productId);
    if (product == null) return false;

    var newReview = new Review
    {
        ProductId = productId,
        UserId = userId,
        Rating = review.Rating,
        Title = review.Title,
        Comment = review.Comment,
        IsApproved = true // Auto-approve for now
    };

    _context.Reviews.Add(newReview);
    await _context.SaveChangesAsync();
    return true;
}

Core Logic & Architectural Breakdown

  • Defensive Existence Verification: var product = await _context.Products.FindAsync(productId); Before executing any database writes, the code runs a verification check to ensure the targeted smartphone actually exists in your catalog. If a bad or manipulated product ID is requested, the service layer instantly cuts off execution and safely returns false.

  • Separation of Concerns Mapping: The code acts as a translator, mapping properties directly from your incoming ReviewViewModel over to your backend Review database tracking entity. This translation layer keeps your presentation data safely insulated from your core data storage engine.

  • Smart Business Control Flag (IsApproved = true): Setting IsApproved = true allows reviews to pass directly onto your digital storefront for now. This is a great placeholder tactic for tutorials; you can easily mention to your viewers that this flag can later be tied to an Admin approval toggle screen to fight spam bots.

  • Optimized Async Performance: By leveraging Add and SaveChangesAsync(), this operation runs entirely non-blocking. This means your online store remains incredibly responsive, scaling effortlessly even if hundreds of users are dropping feedback on hot products at the exact same moment.

Step 4: Implementing the AddReview Controller Action (HTTP POST)

By placing this AddReview method in your ProductController, you keep the endpoint intuitively aligned with product features. This structure facilitates easy routing and seamless view updates.

C# / Controllers/ProductController.cs (AddReview Post Action)
[HttpPost]
[Authorize]
public async Task<IActionResult> AddReview(int productId, ReviewViewModel model)
{
    if (!ModelState.IsValid)
    {
        TempData["Error"] = "Please fill in all required fields.";
        return RedirectToAction(nameof(Details), new { id = productId });
    }

    var user = await _userManager.GetUserAsync(User);
    if (user == null)
    {
        return Unauthorized();
    }

    var result = await _productService.AddReviewAsync(productId, user.Id, model);
    if (result)
    {
        TempData["Success"] = "Thank you for your review!";
    }
    else
    {
        TempData["Error"] = "Failed to add review. Please try again.";
    }

    return RedirectToAction(nameof(Details), new { id = productId });
}

Core Components & Logic Breakdown

  • Strict Validation Gating (ModelState.IsValid): The method instantly checks if incoming data matches the rules defined in your nested ReviewViewModel (e.g., verifying the rating is between 1 and 5). If validation fails, it records a helpful message into TempData and routes the user straight back to the item page without touching your database.

  • Secure Identity Resolution via Identity: var user = await _userManager.GetUserAsync(User); Instead of trusting a user ID passed invisibly inside a hidden form field—which can easily be altered by malicious users—this line securely grabs the user's authentic profile straight from the encrypted cookie context.

  • Cross-Request Messaging via TempData: TempData is used here rather than a standard ViewBag because the action finishes by redirecting the user to a different route (RedirectToAction). TempData utilizes session storage behind the scenes to survive this redirect, letting you show clean, green success alerts on the newly loaded target page.

  • Clean Return PRG (Post-Redirect-Get) Pattern: Ending with RedirectToAction(nameof(Details), ...) is an important web development best practice. It prevents the annoying "Resubmit Form Data" popup that happens if a customer manually refreshes their browser after submitting a comment, completely optimizing the user journey.

Step 5: Hydrating the Wishlist State on Product Details View

This code runs when a user clicks on a product card to view its full details. It acts as an internal check to see if the filled heart or empty heart icon should be rendered.

C# / Controllers/ProductController.cs (Wishlist Check Snippet)
if (User.Identity?.IsAuthenticated == true)
{
    var user = await _userManager.GetUserAsync(User);
    isInWishlist = user?.WishlistItems.Any(w => w.ProductId == id) ?? false;
}

Core Components & Logic Breakdown

  • Safe Identity Boundary Check: if (User.Identity?.IsAuthenticated == true) This wrapper ensures that your code doesn't waste precious server resources running database lookups for guest shoppers. If the visitor is not logged in, it skips the check entirely, keeping the page loading instantly for unauthenticated traffic.

  • Contextual User Resolution: var user = await _userManager.GetUserAsync(User); Just like in your POST methods, this queries the official ASP.NET Core Identity store to grab the full profile data of the logged-in user, including their related tables.

  • LINQ Collection Evaluation (.Any): user?.WishlistItems.Any(w => w.ProductId == id) ?? false; Instead of writing a complex SQL query manually, you use LINQ to scan through the user's WishlistItems collection. If a row exists where the ProductId matches the current product's id, it returns true.

  • The Null-Coalescing Fallback (?? false): The ?? false operator is a professional defensive coding practice. If the user object or the collection returns null for any reason, it safely defaults the isInWishlist variable to false instead of throwing a dangerous NullReferenceException error that could crash the page.

Comments