Back to: ASP.NET Core Web API Tutorials
How to Design and Develop User Feedback Module in Ecommerce Application
In this article, I will discuss How to Design and Develop the User Feedback Module for our ECommerce Application using ASP.NET Core Web API and EF Core. Please read our previous article discussing the Refund Module in our ECommerce Application.
The User Feedback Module in our E-Commerce Application enhances customer engagement and gathers valuable insights into product performance. This module ensures that feedback is authentic and relevant by restricting submissions to customers who have actually purchased the product. This maintains the integrity of the feedback, making it a reliable resource for both customers and administrators.
Key Features of User Feedback Module:
The following are the Key Features of the User Feedback Module that we will implement in our Ecommerce Application:
- Feedback Submission: Allows verified customers to submit ratings and feedback on purchased products.
- Feedback Retrieval: Enables retrieval of individual feedback, all feedback for a specific product, and all feedback in the system (primarily for admin use).
- Feedback Management: Provides functionalities to update and delete feedback, ensuring that feedback remains accurate and up-to-date.
- Data Integrity: Ensures that only customers who have completed purchases can provide feedback, maintaining the relevance and authenticity of the reviews.
Creating Data Transfer Objects (DTOs)
Data Transfer Objects (DTOs) are essential in the User Feedback Module for structuring the data exchanged between the client and the server. They serve as a contract, ensuring that only necessary and validated data is transmitted, enhancing security and maintaining a clear separation between the internal data models and the exposed API contracts. So, first, create a folder named FeedbackDTOs within the DTOs folder, where we will create all our DTOs related to user feedback.
CreateFeedbackDTO
Create a new class file named FeedbackCreateDTO.cs within the DTOs/FeedbackDTOs folder and add the following code. The CreateFeedbackDTO Captures the essential information required for a customer to submit feedback on a purchased product. It ensures that all necessary fields (CustomerId, ProductId, Rating) are provided and adhere to specified validation rules (e.g., rating between 1 and 5). It is used when a customer fills out a feedback form to rate and comment on a purchased product. The client sends this DTO to the server to initiate the feedback creation process.
using System.ComponentModel.DataAnnotations; namespace ECommerceApp.DTOs.FeedbackDTOs { // DTO for submitting feedback public class FeedbackCreateDTO { [Required(ErrorMessage = "CustomerId is required.")] public int CustomerId { get; set; } [Required(ErrorMessage = "ProductId is required.")] public int ProductId { get; set; } [Required(ErrorMessage = "Rating is required.")] [Range(1, 5, ErrorMessage = "Rating must be between 1 and 5.")] public int Rating { get; set; } [StringLength(1000, ErrorMessage = "Comment cannot exceed 1000 characters.")] public string Comment { get; set; } } }
FeedbackResponseDTO
Create a new class file named FeedbackResponseDTO.cs within the DTOs/FeedbackDTOs folder and add the following code. The FeedbackResponseDTO Structures the feedback data to be sent back to clients, including relevant customer and product information. It ensures that only necessary feedback details, such as CustomerName and ProductName, are shared while maintaining data security.
namespace ECommerceApp.DTOs.FeedbackDTOs { // DTO for returning feedback details public class FeedbackResponseDTO { public int Id { get; set; } public int CustomerId { get; set; } public string CustomerName { get; set; } // Combines FirstName and LastName public int ProductId { get; set; } public string ProductName { get; set; } public int Rating { get; set; } public string? Comment { get; set; } public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } } }
FeedbackUpdateDTO
Create a new class file named FeedbackUpdateDTO.cs within the DTOs/FeedbackDTOs folder and add the following code. The UpdateFeedbackDTO Facilitates updating an existing feedback entry by allowing customers to modify their Ratings and/or Comments. It ensures that the FeedbackId and CustomerId are provided and that the new Rating falls within the acceptable range. It is used when customers edit their previously submitted feedback to reflect their current opinions or experiences.
using System.ComponentModel.DataAnnotations; namespace ECommerceApp.DTOs.FeedbackDTOs { public class FeedbackUpdateDTO { [Required(ErrorMessage = "FeedbackId is required.")] public int FeedbackId { get; set; } [Required(ErrorMessage = "CustomerId is required.")] public int CustomerId { get; set; } [Required(ErrorMessage = "Rating is required.")] [Range(1, 5, ErrorMessage = "Rating must be between 1 and 5.")] public int Rating { get; set; } [StringLength(1000, ErrorMessage = "Comment cannot exceed 1000 characters.")] public string Comment { get; set; } } }
FeedbackDeleteDTO
Create a new class file named FeedbackDeleteDTO.cs within the DTOs/FeedbackDTOs folder and add the following code. The DeleteFeedbackDTO enables the deletion of an existing feedback entry by ensuring that only the feedback owner can perform the deletion. It is used when a customer wishes to remove their feedback from a product, preventing unauthorized deletions.
using System.ComponentModel.DataAnnotations; namespace ECommerceApp.DTOs.FeedbackDTOs { public class FeedbackDeleteDTO { [Required(ErrorMessage = "FeedbackId is required.")] public int FeedbackId { get; set; } [Required(ErrorMessage = "CustomerId is required.")] public int CustomerId { get; set; } } }
ProductFeedbackResponseDTO
Create a new class file named ProductFeedbackResponseDTO.cs within the DTOs/FeedbackDTOs folder and add the following code. The ProductFeedbackResponseDTO combines a product’s average rating with a list of individual feedback entries. It provides both qualitative (comments) and quantitative (average rating) data to offer a holistic view of the product’s performance. It is used in API responses when retrieving all feedback for a specific product, aiding customers and administrators in assessing product quality and customer satisfaction.
namespace ECommerceApp.DTOs.FeedbackDTOs { // DTO for returning feedback details along with average rating for a product public class ProductFeedbackResponseDTO { public int ProductId { get; set; } public string ProductName { get; set; } public double AverageRating { get; set; } public List<CustomerFeedback> Feedbacks { get; set; } } public class CustomerFeedback { public int Id { get; set; } public int CustomerId { get; set; } public string CustomerName { get; set; } // Combines FirstName and LastName public int Rating { get; set; } public string? Comment { get; set; } public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } } }
Creating Feedback Service Class
The FeedbackService class encapsulates all business and data access logic for managing user feedback. It provides methods to submit, retrieve, update, and delete feedback while ensuring that only authorized customers can perform operations on their feedback. Using this service improves modularity, maintainability, and testability. So, create a class file named FeedbackService.cs within the Services folder and then copy and paste the following code:
using Microsoft.EntityFrameworkCore; using ECommerceApp.Data; using ECommerceApp.DTOs.FeedbackDTOs; using ECommerceApp.Models; using ECommerceApp.DTOs; namespace ECommerceApp.Services { public class FeedbackService { private readonly ApplicationDbContext _context; public FeedbackService(ApplicationDbContext context) { _context = context; } // Submits new feedback for a product. public async Task<ApiResponse<FeedbackResponseDTO>> SubmitFeedbackAsync(FeedbackCreateDTO feedbackCreateDTO) { try { // Verify customer exists var customer = await _context.Customers .AsNoTracking() .FirstOrDefaultAsync(c => c.Id == feedbackCreateDTO.CustomerId); if (customer == null) { return new ApiResponse<FeedbackResponseDTO>(404, "Customer not found."); } // Verify product exists var product = await _context.Products .AsNoTracking() .FirstOrDefaultAsync(p => p.Id == feedbackCreateDTO.ProductId); if (product == null) { return new ApiResponse<FeedbackResponseDTO>(404, "Product not found."); } // Verify order item exists and belongs to customer and product (Order must be delivered) var orderItem = await _context.OrderItems .Include(oi => oi.Order) .AsNoTracking() .FirstOrDefaultAsync(oi => oi.ProductId == feedbackCreateDTO.ProductId && oi.Order.CustomerId == feedbackCreateDTO.CustomerId && oi.Order.OrderStatus == OrderStatus.Delivered); if (orderItem == null) { return new ApiResponse<FeedbackResponseDTO>(400, "Invalid OrderItemId. Customer must have purchased the product."); } // Check if feedback already exists for this order item if (await _context.Feedbacks.AnyAsync(fed => fed.CustomerId == feedbackCreateDTO.CustomerId && fed.ProductId == feedbackCreateDTO.ProductId)) { return new ApiResponse<FeedbackResponseDTO>(400, "Feedback for this product and order item already exists."); } // Create new feedback entity var feedback = new Feedback { CustomerId = feedbackCreateDTO.CustomerId, ProductId = feedbackCreateDTO.ProductId, Rating = feedbackCreateDTO.Rating, Comment = feedbackCreateDTO.Comment, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; _context.Feedbacks.Add(feedback); await _context.SaveChangesAsync(); // Prepare response DTO (manual mapping) var feedbackResponse = new FeedbackResponseDTO { Id = feedback.Id, CustomerId = customer.Id, CustomerName = $"{customer.FirstName} {customer.LastName}", ProductId = product.Id, ProductName = product.Name, Rating = feedback.Rating, Comment = feedback.Comment, CreatedAt = feedback.CreatedAt, UpdatedAt = feedback.UpdatedAt }; return new ApiResponse<FeedbackResponseDTO>(200, feedbackResponse); } catch (Exception ex) { // Log the exception (implementation depends on your logging setup) return new ApiResponse<FeedbackResponseDTO>(500, $"An unexpected error occurred while processing your request, Error: {ex.Message}"); } } // Retrieves all feedback for a specific product along with the average rating. public async Task<ApiResponse<ProductFeedbackResponseDTO>> GetFeedbackForProductAsync(int productId) { try { // Verify product exists var product = await _context.Products .AsNoTracking() .FirstOrDefaultAsync(p => p.Id == productId); if (product == null) { return new ApiResponse<ProductFeedbackResponseDTO>(404, "Product not found."); } // Retrieve feedbacks for the specified product, including customer details, with no tracking for performance var feedbacks = await _context.Feedbacks .Where(f => f.ProductId == productId) .Include(f => f.Customer) .AsNoTracking() .ToListAsync(); double averageRating = 0; List<CustomerFeedback> customerFeedbacks = new List<CustomerFeedback>(); if (feedbacks.Any()) { averageRating = feedbacks.Average(f => f.Rating); customerFeedbacks = feedbacks.Select(f => new CustomerFeedback { Id = f.Id, CustomerId = f.CustomerId, CustomerName = $"{f.Customer.FirstName} {f.Customer.LastName}", Rating = f.Rating, Comment = f.Comment, CreatedAt = f.CreatedAt, UpdatedAt = f.UpdatedAt }).ToList(); } var productFeedbackResponse = new ProductFeedbackResponseDTO { ProductId = product.Id, ProductName = product.Name, AverageRating = Math.Round(averageRating, 2), Feedbacks = customerFeedbacks }; return new ApiResponse<ProductFeedbackResponseDTO>(200, productFeedbackResponse); } catch (Exception ex) { // Log the exception (implementation depends on your logging setup) return new ApiResponse<ProductFeedbackResponseDTO>(500, $"An unexpected error occurred while processing your request, Error: {ex.Message}"); } } // Retrieves all feedback entries in the system. public async Task<ApiResponse<List<FeedbackResponseDTO>>> GetAllFeedbackAsync() { try { var feedbacks = await _context.Feedbacks .Include(f => f.Customer) .Include(f => f.Product) .AsNoTracking() .ToListAsync(); var feedbackResponseList = feedbacks.Select(f => new FeedbackResponseDTO { Id = f.Id, CustomerId = f.CustomerId, CustomerName = $"{f.Customer.FirstName} {f.Customer.LastName}", ProductId = f.ProductId, ProductName = f.Product.Name, Rating = f.Rating, Comment = f.Comment, CreatedAt = f.CreatedAt, UpdatedAt = f.UpdatedAt }).ToList(); return new ApiResponse<List<FeedbackResponseDTO>>(200, feedbackResponseList); } catch (Exception ex) { // Log the exception (implementation depends on your logging setup) return new ApiResponse<List<FeedbackResponseDTO>>(500, $"An unexpected error occurred while processing your request, Error: {ex.Message}"); } } // Updates an existing feedback entry. public async Task<ApiResponse<FeedbackResponseDTO>> UpdateFeedbackAsync(FeedbackUpdateDTO feedbackUpdateDTO) { try { // Retrieve the feedback along with its customer and product information var feedback = await _context.Feedbacks .Include(f => f.Customer) .Include(f => f.Product) .FirstOrDefaultAsync(f => f.Id == feedbackUpdateDTO.FeedbackId && f.CustomerId == feedbackUpdateDTO.CustomerId); if (feedback == null) { return new ApiResponse<FeedbackResponseDTO>(404, "Either Feedback or Customer not found."); } // Update the feedback details feedback.Rating = feedbackUpdateDTO.Rating; feedback.Comment = feedbackUpdateDTO.Comment; feedback.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); var feedbackResponse = new FeedbackResponseDTO { Id = feedback.Id, CustomerId = feedback.CustomerId, CustomerName = $"{feedback.Customer.FirstName} {feedback.Customer.LastName}", ProductId = feedback.ProductId, ProductName = feedback.Product.Name, Rating = feedback.Rating, Comment = feedback.Comment, CreatedAt = feedback.CreatedAt, UpdatedAt = feedback.UpdatedAt }; return new ApiResponse<FeedbackResponseDTO>(200, feedbackResponse); } catch (Exception ex) { // Log the exception (implementation depends on your logging setup) return new ApiResponse<FeedbackResponseDTO>(500, $"An unexpected error occurred while processing your request, Error: {ex.Message}"); } } // Deletes a feedback entry. public async Task<ApiResponse<ConfirmationResponseDTO>> DeleteFeedbackAsync(FeedbackDeleteDTO feedbackDeleteDTO) { try { var feedback = await _context.Feedbacks.FindAsync(feedbackDeleteDTO.FeedbackId); if (feedback == null) { return new ApiResponse<ConfirmationResponseDTO>(404, "Feedback not found."); } // Ensure that only the owner can delete the feedback if (feedback.CustomerId != feedbackDeleteDTO.CustomerId) { return new ApiResponse<ConfirmationResponseDTO>(401, "You are not authorized to delete this feedback."); } _context.Feedbacks.Remove(feedback); await _context.SaveChangesAsync(); var confirmation = new ConfirmationResponseDTO { Message = $"Feedback with Id {feedbackDeleteDTO.FeedbackId} deleted successfully." }; return new ApiResponse<ConfirmationResponseDTO>(200, confirmation); } catch (Exception ex) { // Log the exception (implementation depends on your logging setup) return new ApiResponse<ConfirmationResponseDTO>(500, $"An unexpected error occurred while processing your request, Error: {ex.Message}"); } } } }
Registering Feedback Service in Dependency Injection Container
We need to register the service within the dependency injection container to enable dependency injection of the User Feedback Service. So, please modify the Program.cs class file as follows:
using ECommerceApp.Data; using ECommerceApp.Services; using Microsoft.EntityFrameworkCore; namespace ECommerceApp { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers() .AddJsonOptions(options => { // This will use the property names as defined in the C# model options.JsonSerializerOptions.PropertyNamingPolicy = null; }); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // Configure EF Core with SQL Server builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("EFCoreDBConnection"))); // Registering the CustomerService builder.Services.AddScoped<CustomerService>(); // Registering the AddressService builder.Services.AddScoped<AddressService>(); // Registering the CategoryService builder.Services.AddScoped<CategoryService>(); // Registering the ProductService builder.Services.AddScoped<ProductService>(); // Registering the ShoppingCartService builder.Services.AddScoped<ShoppingCartService>(); // Registering the OrderService builder.Services.AddScoped<OrderService>(); // Registering the PaymentService builder.Services.AddScoped<PaymentService>(); // Registering the EmailService builder.Services.AddScoped<EmailService>(); // Registering the CancellationService builder.Services.AddScoped<CancellationService>(); // Registering the RefundService builder.Services.AddScoped<RefundService>(); // Registering the FeedbackService builder.Services.AddScoped<FeedbackService>(); // Register Refund Processing Background Service builder.Services.AddHostedService<RefundProcessingBackgroundService>(); // Register Payment Background Service builder.Services.AddHostedService<PendingPaymentService>(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run(); } } }
Implementing the Feedback Controller
The FeedbackController is the central component managing all operations related to user feedback within the Ecommerce Application. It handles incoming HTTP requests, interacts with the database, and ensures all feedback-related processes adhere to business rules and data integrity constraints. So, create a new API Empty controller named FeedbackController.cs within the Controllers folder and add the following code:
using Microsoft.AspNetCore.Mvc; using ECommerceApp.DTOs.FeedbackDTOs; using ECommerceApp.Services; using ECommerceApp.DTOs; namespace ECommerceApp.Controllers { [ApiController] [Route("api/[controller]")] public class FeedbackController : ControllerBase { private readonly FeedbackService _feedbackService; public FeedbackController(FeedbackService feedbackService) { _feedbackService = feedbackService; } // Submits feedback for a product. [HttpPost("SubmitFeedback")] public async Task<ActionResult<ApiResponse<FeedbackResponseDTO>>> SubmitFeedback([FromBody] FeedbackCreateDTO feedbackCreateDTO) { var response = await _feedbackService.SubmitFeedbackAsync(feedbackCreateDTO); if (response.StatusCode != 200) { return StatusCode(response.StatusCode, response); } return Ok(response); } // Retrieves all feedback for a specific product. [HttpGet("GetFeedbackForProduct/{productId}")] public async Task<ActionResult<ApiResponse<ProductFeedbackResponseDTO>>> GetFeedbackForProduct(int productId) { var response = await _feedbackService.GetFeedbackForProductAsync(productId); if (response.StatusCode != 200) { return StatusCode(response.StatusCode, response); } return Ok(response); } // Retrieves all feedback (Admin use). [HttpGet("GetAllFeedback")] public async Task<ActionResult<ApiResponse<List<FeedbackResponseDTO>>>> GetAllFeedback() { var response = await _feedbackService.GetAllFeedbackAsync(); if (response.StatusCode != 200) { return StatusCode(response.StatusCode, response); } return Ok(response); } // Updates a specific feedback entry. [HttpPut("UpdateFeedback")] public async Task<ActionResult<ApiResponse<FeedbackResponseDTO>>> UpdateFeedback([FromBody] FeedbackUpdateDTO feedbackUpdateDTO) { var response = await _feedbackService.UpdateFeedbackAsync(feedbackUpdateDTO); if (response.StatusCode != 200) { return StatusCode(response.StatusCode, response); } return Ok(response); } // Deletes a specific feedback entry. [HttpDelete("DeleteFeedback")] public async Task<ActionResult<ApiResponse<ConfirmationResponseDTO>>> DeleteFeedback([FromBody] FeedbackDeleteDTO feedbackDeleteDTO) { var response = await _feedbackService.DeleteFeedbackAsync(feedbackDeleteDTO); if (response.StatusCode != 200) { return StatusCode(response.StatusCode, response); } return Ok(response); } } }
Testing the User Feedback Endpoints:
Let us test each user feedback-related endpoint of our e-commerce application. Please replace {port} with the port number your application is running on.
Submit Feedback:
Handles the creation of new feedback for a product, ensuring the customer has purchased the item and validating the input data. Prevents duplicate feedback, ensures the feedback is tied to a legitimate purchase, and returns the created feedback’s details.
Method: POST
URL: http://localhost:<port>/api/Feedback/SubmitFeedback
Headers: Content-Type: application/json
Body (raw JSON):
{ "CustomerId": 1, "ProductId": 1, "Rating": 5, "Comment": "Great product! Highly recommended." }
Expected Response: A JSON object with status 200 and the submitted feedback details.
Get Feedback for a Specific Product:
It retrieves all feedback entries for a given product and calculates the average rating. By showing customers aggregated feedback data, it helps them make informed purchasing decisions.
Method: GET
URL: http://localhost:<port>/api/Feedback/GetFeedbackForProduct/1
Expected Response: A JSON object with status 200 containing the product’s average rating and a list of individual feedback entries for the product with ID 1.
Get All Feedback (Admin Use):
It captures all feedback in the system, primarily for administrative purposes. It allows administrators to review, analyze, and monitor user feedback across all products.
Method: GET
URL: http://localhost:<port>/api/Feedback/GetAllFeedback
Expected Response: A JSON object with status 200 containing a list of all product feedback entries.
Update Feedback:
This function updates an existing feedback entry’s rating and/or comment, ensuring the request is authorized. It allows customers to revise their feedback and ensures that only the original author can make changes.
Method: PUT
URL: http://localhost:<port>/api/Feedback/UpdateFeedback
Headers: Content-Type: application/json
Body (raw JSON):
{ "FeedbackId": 1, "CustomerId": 1, "Rating": 4, "Comment": "Updated my rating after further use." }
Expected Response: A JSON object with status 200 and the updated feedback details.
Delete Feedback:
Deletes a feedback entry, ensuring that the customer is authorized to do so. Removes feedback that is no longer relevant or needed, maintaining data integrity by providing only the feedback owner can delete their entry.
Method: DELETE
URL: http://localhost:<port>/api/Feedback/DeleteFeedback
Headers: Content-Type: application/json
Body (raw JSON):
{ "FeedbackId": 1, "CustomerId": 1 }
Expected Response: A JSON object with status 200 confirms the deletion of the feedback.
So, we have completed the User Feedback Module implementation for our e-commerce application using ASP.NET Core Web API and Entity Framework Core. All the modules have been implemented, and I hope you enjoyed these ECommerce Application Development articles. Please give your feedback in the comment section. Next, we will start a Real-time Hotel Booking Application using ASP.NET Core Web API and ADO.NET Core.
It was a very good series of session. It will help us to build practical and production level software coding a lot.