Conversation
chrisjamiecarter
left a comment
There was a problem hiding this comment.
Hey @RyanW84 👋,
Excellent work on your ECommerce API project submission 🎉!
I have performed a peer review. Review/ignore any comments as you wish.
Overview
This is an impressive, production-ready API implementation that demonstrates sophisticated understanding of ASP.NET Core patterns. The project showcases:
- Clean Architecture: Well-separated concerns with Controllers → Services → Repositories → Data layer
- Soft Delete Pattern: Elegant implementation using global query filters and BaseEntity
- Performance Optimizations: Response caching, output caching, response compression, compiled queries
- Documentation Excellence: Comprehensive README, XML docs, Swagger/Scalar integration
- Testing Strategy: Unit tests with proper Arrange-Act-Assert patterns (61 passing tests!)
Suggestions for Improvement
- Project Structure 🟠 - Code in Root Instead of Username Folder
The project files are in the repository root instead of a proper project folder containing the username. This violates the requirement that all student code must be inside a single top-level folder named [ProjectName].[Username]. I think at this point in your journey you should be following this basic step during project submission.
📁 CodeReviews.Console.EcommerceApi ⬅️ Forked repo
┣ 📄 .codacy.yml
┣ 📄 .gitignore
┣ 📁 EcommerceApi.RyanW84 ⬅️ Top-level folder *Missing!
┃ ┗ 📄 EcommerceApi.sln
┃ ┗ 📄 README.md
┃ ┗ 📁 EcommerceApi.Api
┃ ┗ 📁 EcommerceApi.Ui
Also, as you have included tests, a common standard is to include a src and a tests folder. So that would slightly change your structure:
📁 CodeReviews.Console.EcommerceApi ⬅️ Forked repo
┣ 📄 .codacy.yml
┣ 📄 .gitignore
┣ 📁 EcommerceApi.RyanW84 ⬅️ Top-level folder *Missing!
┃ ┣ 📄 EcommerceApi.sln
┃ ┣ 📄 README.md
┃ ┣ 📁 src ⬅️ src-level folder *New!
┃ ┃ ┗ 📁 EcommerceApi.Api
┃ ┃ ┗ 📁 EcommerceApi.Ui
┃ ┣ 📁 tests
┃ ┃ ┗ 📁 EcommerceApi.Api.Tests
┃ ┃ ┗ 📁 EcommerceApi.Ui.Tests
-
Integration Tests Broken 🟠 - The
WebApplicationFactory<Program>cannot access the internal Program class. Addpublic partial class Program { }to make the entry point public for testing. -
Namespace Inconsistency 🟠 - Controllers use file-scoped namespaces (
namespace X;) while Services and Repositories use traditional block namespaces (namespace X { }). Choose one style and apply consistently. -
XML Doc Placement 🟠 - In CategoriesController and SalesController, the class documentation appears after attributes instead of before them.
-
Required Properties Pattern 🟡 - Using
required string Name { get; set; } = null!;suppresses null warnings without providing safety. Remove the= null!initializers. -
Generic Exception Handling 🟡 - Services catch
Exceptionbroadly. Consider whether the global exception middleware could handle some of these cases, reducing duplicated try/catch blocks.
I will go ahead and mark as approved, keep up the excellent work on the next projects! 😊
Thanks,
@chrisjamiecarter 👍
| [ApiController] | ||
| [Route("api/[controller]")] | ||
| [Route("api/v1/categories")] | ||
| /// <summary> | ||
| /// Category CRUD API endpoints. | ||
| /// Provides create, read (by id/name), update, delete, and soft-delete restore operations for <see cref="Category"/>. | ||
| /// Responses are wrapped in the project's standard API response DTOs and follow the same error mapping conventions. | ||
| /// </summary> | ||
| public class CategoriesController : ControllerBase |
There was a problem hiding this comment.
🟠 XML Documentation Placement
💡 The class-level XML documentation comment is placed after the [ApiController] and [Route] attributes. Per standard conventions, documentation comments should precede all attributes:
| [ApiController] | |
| [Route("api/[controller]")] | |
| [Route("api/v1/categories")] | |
| /// <summary> | |
| /// Category CRUD API endpoints. | |
| /// Provides create, read (by id/name), update, delete, and soft-delete restore operations for <see cref="Category"/>. | |
| /// Responses are wrapped in the project's standard API response DTOs and follow the same error mapping conventions. | |
| /// </summary> | |
| public class CategoriesController : ControllerBase | |
| /// <summary> | |
| /// Category CRUD API endpoints. | |
| /// Provides create, read (by id/name), update, delete, and soft-delete restore operations for <see cref="Category"/>. | |
| /// Responses are wrapped in the project's standard API response DTOs and follow the same error mapping conventions. | |
| /// </summary> | |
| [ApiController] | |
| [Route("api/[controller]")] | |
| [Route("api/v1/categories")] | |
| public class CategoriesController : ControllerBase |
| [ApiController] | ||
| [Route("api/[controller]")] | ||
| [Route("api/v1/sales")] | ||
| /// <summary> | ||
| /// Sales API endpoints. | ||
| /// Supports creating and managing sales, including read operations that can optionally include historical (soft-deleted) products. | ||
| /// </summary> | ||
| public class SalesController : ControllerBase |
There was a problem hiding this comment.
🟠 XML Documentation Placement
💡 The class-level XML documentation comment is placed after the [ApiController] and [Route] attributes. Per standard conventions, documentation comments should precede all attributes:
| [ApiController] | |
| [Route("api/[controller]")] | |
| [Route("api/v1/sales")] | |
| /// <summary> | |
| /// Sales API endpoints. | |
| /// Supports creating and managing sales, including read operations that can optionally include historical (soft-deleted) products. | |
| /// </summary> | |
| public class SalesController : ControllerBase | |
| /// <summary> | |
| /// Sales API endpoints. | |
| /// Supports creating and managing sales, including read operations that can optionally include historical (soft-deleted) products. | |
| /// </summary> | |
| [ApiController] | |
| [Route("api/[controller]")] | |
| [Route("api/v1/sales")] | |
| public class SalesController : ControllerBase |
| // GET /api/v1/sales/history | ||
| // Noun-based alternative to "with-deleted-products". | ||
| [HttpGet("history")] | ||
| [ResponseCache(Duration = 30, Location = ResponseCacheLocation.Any)] | ||
| public Task<IActionResult> GetHistory(CancellationToken cancellationToken) => | ||
| GetAllWithDeletedProducts(cancellationToken); |
There was a problem hiding this comment.
🔵 Noun-Based Route Alternative Comment
💡 The comment // Noun-based alternative to "with-deleted-products". is helpful, but consider if maintaining both endpoints is necessary. Having both /with-deleted-products and /history could confuse API consumers. Consider deprecating one or documenting which is preferred.
| /// <summary> | ||
| /// Display name shown in lists and detail views. | ||
| /// </summary> | ||
| public required string Name { get; set; } = null!; |
There was a problem hiding this comment.
🟡 Required Properties with null! Initialization
💡 Using required string Name { get; set; } = null!; defeats the purpose of the required modifier. The null! suppresses null warnings but doesn't provide compile-time safety. Consider removing the = null! initializer to ensure consumers must set the value.
| namespace ECommerceApp.RyanW84.Services | ||
| { |
There was a problem hiding this comment.
🟠 Block Namespace Instead of File-Scoped
💡 This file uses traditional block namespaces while Controllers use file-scoped namespaces. For consistency across the codebase, prefer file-scoped namespaces.
| namespace ECommerceApp.RyanW84.Services | |
| { | |
| namespace ECommerceApp.RyanW84.Services; |
| [ApiController] | ||
| [Route("api/[controller]")] | ||
| [Route("api/products")] | ||
| [Route("api/v1/products")] | ||
| public class ProductController(IProductService productService) : ControllerBase |
There was a problem hiding this comment.
🟡 Redundant Route Attributes
❓ Why do you have duplication in your routes? Why not just keep only the versioned route: [Route("api/v1/[controller]")]
| "price" => descending | ||
| ? query.OrderByDescending(p => p.Price) | ||
| : query.OrderBy(p => p.Price), | ||
| "createdat" => descending |
There was a problem hiding this comment.
❓ Why createdat and not createdate? Also in CategoryRepository.
| public class SmokeTests | ||
| { | ||
| [Fact] | ||
| public void True_is_true() |
There was a problem hiding this comment.
❓ What benefit does this bring?
| namespace ECommerceApp.UnitTests.ConsoleClient; | ||
|
|
||
| [Collection(SpectreConsoleCollection.Name)] | ||
| public class TableRendererDynamicPagingTests |
There was a problem hiding this comment.
❓ What is the purpose of these tests? It seems you are testing Spectre.Console. If so, I would leave that to the owners of that library and not worry about it.
| private readonly CategoryValidator _validator = new(); | ||
|
|
||
| [Fact] | ||
| public void Validate_WithValidCategory_ShouldSucceed() |
There was a problem hiding this comment.
🟢 Good Test Structure
💡 Tests follow Arrange-Act-Assert pattern with clear naming. Excellent test structure!
Codacy errors ignored, they break the app if changed