From 6237a4ffec39d5d3810e872626172e2cee7b33e0 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Sun, 28 Sep 2025 10:51:57 +0100 Subject: [PATCH 01/64] Initial project commit --- .../ECommerceApi.JJHH17.csproj | 14 +++++++ .../ECommerceApi.JJHH17.http | 6 +++ .../ECommerceApi.JJHH17/Program.cs | 17 ++++++++ .../Properties/launchSettings.json | 41 +++++++++++++++++++ .../appsettings.Development.json | 8 ++++ .../ECommerceApi.JJHH17/appsettings.json | 9 ++++ 6 files changed, 95 insertions(+) create mode 100644 ECommerceApi.JJHH17/ECommerceApi.JJHH17/ECommerceApi.JJHH17.csproj create mode 100644 ECommerceApi.JJHH17/ECommerceApi.JJHH17/ECommerceApi.JJHH17.http create mode 100644 ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs create mode 100644 ECommerceApi.JJHH17/ECommerceApi.JJHH17/Properties/launchSettings.json create mode 100644 ECommerceApi.JJHH17/ECommerceApi.JJHH17/appsettings.Development.json create mode 100644 ECommerceApi.JJHH17/ECommerceApi.JJHH17/appsettings.json diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/ECommerceApi.JJHH17.csproj b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/ECommerceApi.JJHH17.csproj new file mode 100644 index 00000000..29705f5c --- /dev/null +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/ECommerceApi.JJHH17.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/ECommerceApi.JJHH17.http b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/ECommerceApi.JJHH17.http new file mode 100644 index 00000000..8d3dea3f --- /dev/null +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/ECommerceApi.JJHH17.http @@ -0,0 +1,6 @@ +@ECommerceApi.JJHH17_HostAddress = http://localhost:5005 + +GET {{ECommerceApi.JJHH17_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs new file mode 100644 index 00000000..ca9dc2c5 --- /dev/null +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs @@ -0,0 +1,17 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.Run(); \ No newline at end of file diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Properties/launchSettings.json b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Properties/launchSettings.json new file mode 100644 index 00000000..c05dfa80 --- /dev/null +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:8562", + "sslPort": 44386 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5005", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7054;http://localhost:5005", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/appsettings.Development.json b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/appsettings.json b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} From 187d5c97d35a2edd9d9af06c40cf76aeea807ff7 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Sun, 28 Sep 2025 10:54:25 +0100 Subject: [PATCH 02/64] Installed entity framework packages --- .../ECommerceApi.JJHH17/ECommerceApi.JJHH17.csproj | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/ECommerceApi.JJHH17.csproj b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/ECommerceApi.JJHH17.csproj index 29705f5c..9bc594ee 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/ECommerceApi.JJHH17.csproj +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/ECommerceApi.JJHH17.csproj @@ -8,6 +8,15 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + From c4f807b3eff820ef35e2d23745a31783b96a3920 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Sun, 28 Sep 2025 10:56:13 +0100 Subject: [PATCH 03/64] Added dbContext --- .../ECommerceApi.JJHH17/Data/ProductsDbContext.cs | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs new file mode 100644 index 00000000..9ada7d7e --- /dev/null +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs @@ -0,0 +1,9 @@ +using Microsoft.EntityFrameworkCore; + +namespace ECommerceApi.JJHH17.Data +{ + public class ProductsDbContext : DbContext + { + public ProductsDbContext(DbContextOptions options) : base(options) { } + } +} From e2ab51e8de50f7c057d4d4d83e3bf3f7156fcd1b Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Sun, 28 Sep 2025 11:03:42 +0100 Subject: [PATCH 04/64] Added connection string to localDb SqlServer instance --- ECommerceApi.JJHH17/ECommerceApi.JJHH17/appsettings.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/appsettings.json b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/appsettings.json index 10f68b8c..619a6176 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/appsettings.json +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/appsettings.json @@ -1,4 +1,7 @@ { + "ConnectionStrings": { + "DefaultConnection": "Server=(localdb)\\eCommerceDb:Integrated Security=true;" + }, "Logging": { "LogLevel": { "Default": "Information", From 83ee73c6f72ee5eb91f45aebc9c4b533a44bc9e1 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Sun, 28 Sep 2025 11:05:08 +0100 Subject: [PATCH 05/64] Added reference to connection string --- ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs index ca9dc2c5..b54dacf6 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs @@ -1,9 +1,15 @@ +using ECommerceApi.JJHH17.Data; +using Microsoft.EntityFrameworkCore; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +builder.Services.AddControllers(); + +builder.Services.AddDbContext(opt => opt.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); var app = builder.Build(); @@ -14,4 +20,6 @@ app.UseSwaggerUI(); } +app.MapControllers(); + app.Run(); \ No newline at end of file From 67f56b089d6de7cf49bff06da5cd1e25f90bf6cc Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Sun, 28 Sep 2025 11:09:03 +0100 Subject: [PATCH 06/64] Added reference to product model --- .../ECommerceApi.JJHH17/Data/ProductsDbContext.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs index 9ada7d7e..765d2616 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs @@ -1,9 +1,12 @@ -using Microsoft.EntityFrameworkCore; +using ECommerceApi.JJHH17.Models; +using Microsoft.EntityFrameworkCore; namespace ECommerceApi.JJHH17.Data { public class ProductsDbContext : DbContext { public ProductsDbContext(DbContextOptions options) : base(options) { } + + public DbSet Products { get; set; } } } From 86e669d32768e10711f050b9fbfc6d28a568685f Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Sun, 28 Sep 2025 11:09:16 +0100 Subject: [PATCH 07/64] Created initial product model --- .../ECommerceApi.JJHH17/Models/Product.cs | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs new file mode 100644 index 00000000..43187452 --- /dev/null +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs @@ -0,0 +1,9 @@ +namespace ECommerceApi.JJHH17.Models +{ + public class Product + { + public int ProductId { get; set; } + public string ProductName { get; set; } + public decimal? Price { get; set; } + } +} From e5cd1c0d9471c907d4ebea410cfe6356fe6ec38d Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Sun, 28 Sep 2025 11:18:56 +0100 Subject: [PATCH 08/64] Added required tags to product variables --- ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs index 43187452..6f5df8f9 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs @@ -3,7 +3,7 @@ public class Product { public int ProductId { get; set; } - public string ProductName { get; set; } - public decimal? Price { get; set; } + public required string ProductName { get; set; } + public required decimal Price { get; set; } } } From d6dfc0a1296871b8c6ace14acf817bed62d62aa1 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Sun, 28 Sep 2025 11:19:11 +0100 Subject: [PATCH 09/64] Resolved typo in connection string --- ECommerceApi.JJHH17/ECommerceApi.JJHH17/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/appsettings.json b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/appsettings.json index 619a6176..0966defa 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/appsettings.json +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/appsettings.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "DefaultConnection": "Server=(localdb)\\eCommerceDb:Integrated Security=true;" + "DefaultConnection": "Server=(localdb)\\eCommerceDb;Integrated Security=true;" }, "Logging": { "LogLevel": { From 9d1a61723ba05905c6417326be82c3e9f5c76e1c Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Sun, 28 Sep 2025 11:32:09 +0100 Subject: [PATCH 10/64] Added reference to productService --- ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs index b54dacf6..d57d87b9 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs @@ -1,19 +1,17 @@ using ECommerceApi.JJHH17.Data; +using ECommerceApi.JJHH17.Services; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); -// Add services to the container. -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddControllers(); - builder.Services.AddDbContext(opt => opt.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); +builder.Services.AddScoped(); var app = builder.Build(); -// Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); From 21f70568fa3ef22eb8a212667563b1f01d2c38e1 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Sun, 28 Sep 2025 11:39:21 +0100 Subject: [PATCH 11/64] Created product service methods --- .../Services/ProductService.cs | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs new file mode 100644 index 00000000..e92de3bc --- /dev/null +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs @@ -0,0 +1,66 @@ +using ECommerceApi.JJHH17.Data; +using ECommerceApi.JJHH17.Models; + +namespace ECommerceApi.JJHH17.Services +{ + public interface IProductService + { + public List GetAllProducts(); + public Product? GetProductById(int id); + public Product CreateProduct(Product product); + public Product UpdateProduct(int id, Product product); + public string? DeleteProduct(int id); + } + + public class ProductService : IProductService + { + private readonly ProductsDbContext _dbContext; + + public ProductService(ProductsDbContext dbContext) + { + _dbContext = dbContext; + } + + public Product CreateProduct(Product product) + { + var savedProduct = _dbContext.Products.Add(product); + _dbContext.SaveChanges(); + return savedProduct.Entity; + } + + public string? DeleteProduct(int id) + { + Product savedProduct = _dbContext.Products.Find(id); + + if (savedProduct == null) { return null; } + + _dbContext.Products.Remove(savedProduct); + _dbContext.SaveChanges(); + + return $"Successfully deleted product with ID: {id}"; + } + + public List GetAllProducts() + { + return _dbContext.Products.ToList(); + } + + public Product GetProductById(int id) + { + Product savedProduct = _dbContext.Products.Find(id); + return savedProduct; + } + + public Product UpdateProduct(int id, Product product) + { + Product savedProduct = _dbContext.Products.Find(id); + + if (savedProduct == null) { return null; } + + _dbContext.Entry(savedProduct).CurrentValues.SetValues(product); + _dbContext.SaveChanges(); + + return savedProduct; + } + } +} From 36e2f385cbac21fd13b9afd9eeeabbcb072184a2 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Mon, 29 Sep 2025 10:51:52 +0100 Subject: [PATCH 12/64] Created products controller file --- .../Controllers/ProductsController.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs new file mode 100644 index 00000000..88b1fd97 --- /dev/null +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs @@ -0,0 +1,49 @@ +using ECommerceApi.JJHH17.Models; +using ECommerceApi.JJHH17.Services; +using Microsoft.AspNetCore.Mvc; + +namespace ECommerceApi.JJHH17.Controllers +{ + [ApiController] + [Route("api/[controller]")] + // Example route = http://localhost:5609/api/product/ + public class ProductsController : ControllerBase + { + private readonly IProductService _productService; + public ProductsController(IProductService productService) + { + _productService = productService; + } + + // Endpoints part of course + [HttpGet] + public ActionResult> GetAllProducts() + { + return Ok(_productService.GetAllProducts()); + } + + [HttpGet("{id}")] + public ActionResult GetProduct(int id) + { + return Ok(_productService.GetProductById(id)); + } + + [HttpPost] + public ActionResult CreateProduct(Product product) + { + return Ok(_productService.CreateProduct(product)); + } + + [HttpPut("{id}")] + public ActionResult UpdateProduct(int id, Product updatedProduct) + { + return Ok(_productService.UpdateProduct(id, updatedProduct)); + } + + [HttpDelete("{id}")] + public ActionResult DeleteProduct(int id) + { + return Ok(_productService.DeleteProduct(id)); + } + } +} From 960eae85bc71f6c6ba14a2829be3a7b868f9051d Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Mon, 29 Sep 2025 10:57:19 +0100 Subject: [PATCH 13/64] Added basic 'not found' errors to controller --- .../Controllers/ProductsController.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs index 88b1fd97..6eccbe4b 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs @@ -25,7 +25,11 @@ public ActionResult> GetAllProducts() [HttpGet("{id}")] public ActionResult GetProduct(int id) { - return Ok(_productService.GetProductById(id)); + var result = _productService.GetProductById(id); + + if (result == null) { return NotFound(); } + + return Ok(result); } [HttpPost] @@ -37,13 +41,21 @@ public ActionResult CreateProduct(Product product) [HttpPut("{id}")] public ActionResult UpdateProduct(int id, Product updatedProduct) { - return Ok(_productService.UpdateProduct(id, updatedProduct)); + var result = _productService.UpdateProduct(id, updatedProduct); + + if (result == null) { return NotFound(); } + + return Ok(result); } [HttpDelete("{id}")] public ActionResult DeleteProduct(int id) { - return Ok(_productService.DeleteProduct(id)); + var result = _productService.DeleteProduct(id); + + if (result == null) { return NotFound(); } + + return Ok(result); } } } From d10cdbb4c9c52cd568833b308802747e6a8fabf7 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Mon, 29 Sep 2025 11:07:57 +0100 Subject: [PATCH 14/64] Added basic 'not found' errors to controller --- .../ECommerceApi.JJHH17/Controllers/ProductsController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs index 6eccbe4b..9f4b3b74 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs @@ -15,7 +15,6 @@ public ProductsController(IProductService productService) _productService = productService; } - // Endpoints part of course [HttpGet] public ActionResult> GetAllProducts() { From 83633dae51e596fe2ca45f681e4a61714516dd98 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Tue, 30 Sep 2025 21:42:30 +0100 Subject: [PATCH 15/64] Created endpoints for Products --- .../Controllers/ProductsController.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs index 9f4b3b74..554aaf5b 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs @@ -1,12 +1,14 @@ -using ECommerceApi.JJHH17.Models; +using ECommerceApi.JJHH17.Data; +using ECommerceApi.JJHH17.Models; using ECommerceApi.JJHH17.Services; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; namespace ECommerceApi.JJHH17.Controllers { [ApiController] [Route("api/[controller]")] - // Example route = http://localhost:5609/api/product/ + // Example call: http://localhost:5609/api/product/ public class ProductsController : ControllerBase { private readonly IProductService _productService; @@ -18,11 +20,11 @@ public ProductsController(IProductService productService) [HttpGet] public ActionResult> GetAllProducts() { - return Ok(_productService.GetAllProducts()); + return Ok(new List()); } - [HttpGet("{id}")] - public ActionResult GetProduct(int id) + [HttpGet("{id)")] + public ActionResult GetProductById(int id) { var result = _productService.GetProductById(id); From a9010d262dc8cbbff5bc5d978e058873d2b0aa92 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Tue, 30 Sep 2025 21:42:51 +0100 Subject: [PATCH 16/64] Created db context for products --- .../ECommerceApi.JJHH17/Data/ProductsDbContext.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs index 765d2616..a751b53e 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs @@ -1,12 +1,16 @@ using ECommerceApi.JJHH17.Models; using Microsoft.EntityFrameworkCore; + namespace ECommerceApi.JJHH17.Data { public class ProductsDbContext : DbContext { - public ProductsDbContext(DbContextOptions options) : base(options) { } + public ProductsDbContext(DbContextOptions options) : base(options) + { + + } - public DbSet Products { get; set; } + public DbSet Products { get; set; } } } From 2f17a786336534c7a89687870b9956e962098f32 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Tue, 30 Sep 2025 21:43:09 +0100 Subject: [PATCH 17/64] Refactored products class, pre category introductions --- ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs index 6f5df8f9..27c06f3a 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs @@ -3,7 +3,7 @@ public class Product { public int ProductId { get; set; } - public required string ProductName { get; set; } - public required decimal Price { get; set; } + public string ProductName { get; set; } + public decimal Price { get; set; } } } From 4ca62e7e6927cce966bd2b82da29d7ee52875b16 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Tue, 30 Sep 2025 21:43:20 +0100 Subject: [PATCH 18/64] Created base products service --- .../ECommerceApi.JJHH17/Services/ProductService.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs index e92de3bc..fe896936 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs @@ -1,5 +1,6 @@ using ECommerceApi.JJHH17.Data; using ECommerceApi.JJHH17.Models; +using Microsoft.EntityFrameworkCore; namespace ECommerceApi.JJHH17.Services { @@ -8,7 +9,7 @@ public interface IProductService public List GetAllProducts(); public Product? GetProductById(int id); public Product CreateProduct(Product product); - public Product UpdateProduct(int id, Product product); + public Product UpdateProduct(int id, Product updatedProduct); public string? DeleteProduct(int id); } @@ -45,7 +46,7 @@ public List GetAllProducts() return _dbContext.Products.ToList(); } - public Product GetProductById(int id) + public Product? GetProductById(int id) { Product savedProduct = _dbContext.Products.Find(id); return savedProduct; From 33f27d0463458d8bc9bdecf712300d1c7856f372 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Tue, 30 Sep 2025 21:55:27 +0100 Subject: [PATCH 19/64] Resolved typo in Get header, bug due to mistyped bracket --- .../ECommerceApi.JJHH17/Controllers/ProductsController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs index 554aaf5b..c20f5edb 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs @@ -20,10 +20,10 @@ public ProductsController(IProductService productService) [HttpGet] public ActionResult> GetAllProducts() { - return Ok(new List()); + return Ok(_productService.GetAllProducts()); } - [HttpGet("{id)")] + [HttpGet("{id}")] public ActionResult GetProductById(int id) { var result = _productService.GetProductById(id); From 1906d214e60afbb5da1b1d9fc35451ab28a32a8a Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Tue, 30 Sep 2025 22:19:26 +0100 Subject: [PATCH 20/64] Created products relationship with categories --- .../ECommerceApi.JJHH17/Data/ProductsDbContext.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs index a751b53e..b1da892a 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs @@ -6,11 +6,18 @@ namespace ECommerceApi.JJHH17.Data { public class ProductsDbContext : DbContext { - public ProductsDbContext(DbContextOptions options) : base(options) - { + public ProductsDbContext(DbContextOptions options) : base(options) { } - } + public DbSet Products => Set(); + public DbSet Categories => Set(); - public DbSet Products { get; set; } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasOne(e => e.Category) + .WithMany(e => e.Products) + .HasForeignKey(e => e.CategoryId) + .IsRequired(); + } } } From 9c6ea8bf22c34a05671984e1c18eb9539cfd6275 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Tue, 30 Sep 2025 22:19:44 +0100 Subject: [PATCH 21/64] Created model for product which feeds to categories --- ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs index 27c06f3a..397ecc15 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs @@ -5,5 +5,8 @@ public class Product public int ProductId { get; set; } public string ProductName { get; set; } public decimal Price { get; set; } + + public int CategoryId { get; set; } + public Category Category { get; set; } = null!; } } From 9d25885a1a3455e64f81a681a630e49cff365308 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Tue, 30 Sep 2025 22:20:03 +0100 Subject: [PATCH 22/64] Created category model --- .../ECommerceApi.JJHH17/Models/Category.cs | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Category.cs diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Category.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Category.cs new file mode 100644 index 00000000..b6fe510b --- /dev/null +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Category.cs @@ -0,0 +1,9 @@ +namespace ECommerceApi.JJHH17.Models +{ + public class Category + { + public int CategoryId { get; set; } + public string CategoryName { get; set; } + public ICollection Products { get; set; } = new List(); + } +} \ No newline at end of file From b10df847866a82583ff7fd3ada0c244e722e9ee4 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Wed, 1 Oct 2025 20:21:21 +0100 Subject: [PATCH 23/64] Created categories controller class --- .../Controllers/CategoriesController.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/CategoriesController.cs diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/CategoriesController.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/CategoriesController.cs new file mode 100644 index 00000000..5ff8dd09 --- /dev/null +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/CategoriesController.cs @@ -0,0 +1,41 @@ +using ECommerceApi.JJHH17.Models; +using ECommerceApi.JJHH17.Services; +using Microsoft.AspNetCore.Mvc; + +namespace ECommerceApi.JJHH17.Controllers +{ + [ApiController] + [Route("api/[controller]")] + // Example call: http://localhost:5609/api/category/ + public class CategoriesController : ControllerBase + { + private readonly ICategoryService _categoryService; + + public CategoriesController(ICategoryService categoryService) + { + _categoryService = categoryService; + } + + [HttpGet] + public ActionResult> GetAllCategories() + { + return Ok(_categoryService.GetAllCategories()); + } + + [HttpGet("{id}")] + public ActionResult GetCategoryById(int id) + { + var result = _categoryService.GetCategoryById(id); + + if (result == null) { return NotFound(); } + + return Ok(result); + } + + [HttpPost] + public ActionResult CreateCategory(Category category) + { + return Ok(_categoryService.CreateCategory(category)); + } + } +} From cc565fd1c5a1b7832f0664aad3bcd9cc4907f20a Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Wed, 1 Oct 2025 20:21:37 +0100 Subject: [PATCH 24/64] Created categories service --- .../Services/CategoryService.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/CategoryService.cs diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/CategoryService.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/CategoryService.cs new file mode 100644 index 00000000..07a11118 --- /dev/null +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/CategoryService.cs @@ -0,0 +1,40 @@ +using ECommerceApi.JJHH17.Data; +using ECommerceApi.JJHH17.Models; + +namespace ECommerceApi.JJHH17.Services +{ + public interface ICategoryService + { + public List GetAllCategories(); + public Category GetCategoryById(int id); + public Category CreateCategory(Category category); + } + + public class CategoryService : ICategoryService + { + private readonly ProductsDbContext _dbContext; + + public CategoryService(ProductsDbContext dbContext) + { + _dbContext = dbContext; + } + + public List GetAllCategories() + { + return _dbContext.Categories.ToList(); + } + + public Category? GetCategoryById(int id) + { + Category savedCategory = _dbContext.Categories.Find(id); + return savedCategory; + } + + public Category CreateCategory(Category category) + { + var savedCategory = _dbContext.Categories.Add(category); + _dbContext.SaveChanges(); + return savedCategory.Entity; + } + } +} From 32f5b8f0fb2817d0cb49f9223d8678146d8035c0 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Wed, 1 Oct 2025 21:02:55 +0100 Subject: [PATCH 25/64] Created a dto which allows users to create category --- ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Category.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Category.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Category.cs index b6fe510b..a834013e 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Category.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Category.cs @@ -6,4 +6,6 @@ public class Category public string CategoryName { get; set; } public ICollection Products { get; set; } = new List(); } + + public record CreateCategoryDto(string Name); } \ No newline at end of file From 65226b698f2e9c72a0f4e098fd1162bfc7331204 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Wed, 1 Oct 2025 21:03:10 +0100 Subject: [PATCH 26/64] Added support for category creation dto --- .../ECommerceApi.JJHH17/Controllers/CategoriesController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/CategoriesController.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/CategoriesController.cs index 5ff8dd09..f31fa0d7 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/CategoriesController.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/CategoriesController.cs @@ -33,7 +33,7 @@ public ActionResult GetCategoryById(int id) } [HttpPost] - public ActionResult CreateCategory(Category category) + public ActionResult CreateCategory(CreateCategoryDto category) { return Ok(_categoryService.CreateCategory(category)); } From 59fe6a74849e5a561ce7bf7144a7ccfdde389d7f Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Wed, 1 Oct 2025 21:03:24 +0100 Subject: [PATCH 27/64] Added create category dto support --- .../Services/CategoryService.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/CategoryService.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/CategoryService.cs index 07a11118..21202475 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/CategoryService.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/CategoryService.cs @@ -7,7 +7,7 @@ public interface ICategoryService { public List GetAllCategories(); public Category GetCategoryById(int id); - public Category CreateCategory(Category category); + public Category CreateCategory(CreateCategoryDto category); } public class CategoryService : ICategoryService @@ -30,11 +30,20 @@ public List GetAllCategories() return savedCategory; } - public Category CreateCategory(Category category) + public Category CreateCategory(CreateCategoryDto category) { - var savedCategory = _dbContext.Categories.Add(category); + if (string.IsNullOrWhiteSpace(category.Name)) + throw new ArgumentException("Category name is required.", nameof(category)); + + bool exists = _dbContext.Categories.Any(c => c.CategoryName == category.Name); + if (exists) + throw new InvalidOperationException($"Category {category.Name} already exists"); + + var newCategory = new Category { CategoryName = category.Name.Trim() }; + + _dbContext.Categories.Add(newCategory); _dbContext.SaveChanges(); - return savedCategory.Entity; + return newCategory; } } } From a0f5e6468d9251f81dc4c1646fd2e912dc959d1f Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Wed, 1 Oct 2025 21:03:40 +0100 Subject: [PATCH 28/64] Added category creation support via API, now supports those endpoints --- ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs index d57d87b9..fef3a3e4 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs @@ -9,6 +9,7 @@ builder.Services.AddControllers(); builder.Services.AddDbContext(opt => opt.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddScoped(); +builder.Services.AddScoped(); var app = builder.Build(); From 98f1e19f3d38509f143fba024e161e8d3fd25d46 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Thu, 2 Oct 2025 11:41:43 +0100 Subject: [PATCH 29/64] Amended products controller which now references the Category ID --- .../ECommerceApi.JJHH17/Controllers/ProductsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs index c20f5edb..6c3cf3b2 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs @@ -34,7 +34,7 @@ public ActionResult GetProductById(int id) } [HttpPost] - public ActionResult CreateProduct(Product product) + public ActionResult CreateProduct(CreateProductDto product) { return Ok(_productService.CreateProduct(product)); } From 634aa742dc4fc727d8ccfcd62296fd5aef44c171 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Thu, 2 Oct 2025 11:42:00 +0100 Subject: [PATCH 30/64] Amended product DTO for creation to reference category by ID --- ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs index 397ecc15..48124348 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs @@ -9,4 +9,6 @@ public class Product public int CategoryId { get; set; } public Category Category { get; set; } = null!; } + + public record CreateProductDto(string name, decimal price, int CategoryId); } From 8db809b4d8392d20cd63852b469e3f083fa3bcd0 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Thu, 2 Oct 2025 11:42:25 +0100 Subject: [PATCH 31/64] Amended createProduct call to take in the category ID --- .../ECommerceApi.JJHH17/Services/ProductService.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs index fe896936..640806a6 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs @@ -8,7 +8,7 @@ public interface IProductService { public List GetAllProducts(); public Product? GetProductById(int id); - public Product CreateProduct(Product product); + public Product CreateProduct(CreateProductDto product); public Product UpdateProduct(int id, Product updatedProduct); public string? DeleteProduct(int id); } @@ -22,11 +22,16 @@ public ProductService(ProductsDbContext dbContext) _dbContext = dbContext; } - public Product CreateProduct(Product product) + public Product CreateProduct(CreateProductDto product) { - var savedProduct = _dbContext.Products.Add(product); + if (product == null) + throw new ArgumentNullException("Product name is required.",nameof(product)); + + var newProduct = new Product { ProductName = product.name.Trim(), Price = product.price, CategoryId = product.CategoryId}; + + _dbContext.Products.Add(newProduct); _dbContext.SaveChanges(); - return savedProduct.Entity; + return newProduct; } public string? DeleteProduct(int id) From 736ccc6be17b192a924a13489bc16d1acf388cfc Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Thu, 2 Oct 2025 12:14:16 +0100 Subject: [PATCH 32/64] GetAllProducts method now returns a DTO list --- .../ECommerceApi.JJHH17/Controllers/ProductsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs index 6c3cf3b2..7bc33f5f 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs @@ -18,7 +18,7 @@ public ProductsController(IProductService productService) } [HttpGet] - public ActionResult> GetAllProducts() + public ActionResult> GetAllProducts() { return Ok(_productService.GetAllProducts()); } From 9c1113ceedd6ac36781e4ae6775138c589cc6cfe Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Thu, 2 Oct 2025 12:14:32 +0100 Subject: [PATCH 33/64] Created a DTO for fetching a list of products --- ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs index 48124348..49be4754 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs @@ -11,4 +11,6 @@ public class Product } public record CreateProductDto(string name, decimal price, int CategoryId); + + public record GetProductsDto(int productId, string productName, decimal price, int CategoryId, string CategoryName); } From df39166847dda43eccf1eea69dd80f8a7bfc4a80 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Thu, 2 Oct 2025 12:15:00 +0100 Subject: [PATCH 34/64] We now return a list of products based on the DTO model --- .../Services/ProductService.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs index 640806a6..dfbc7a7d 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs @@ -6,7 +6,7 @@ namespace ECommerceApi.JJHH17.Services { public interface IProductService { - public List GetAllProducts(); + public List GetAllProducts(); public Product? GetProductById(int id); public Product CreateProduct(CreateProductDto product); public Product UpdateProduct(int id, Product updatedProduct); @@ -46,9 +46,18 @@ public Product CreateProduct(CreateProductDto product) return $"Successfully deleted product with ID: {id}"; } - public List GetAllProducts() + public List GetAllProducts() { - return _dbContext.Products.ToList(); + return _dbContext.Products + .AsNoTracking() + .Include(p => p.Category) + .Select(p => new GetProductsDto( + p.ProductId, + p.ProductName, + p.Price, + p.CategoryId, + p.Category.CategoryName)) + .ToList(); } public Product? GetProductById(int id) From 97483f3b1d4cb38d163cb9808c1e20f89fb022b8 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Thu, 2 Oct 2025 16:47:24 +0100 Subject: [PATCH 35/64] Created dto helper for displaying categories for GET request --- ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Category.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Category.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Category.cs index a834013e..39ca78f9 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Category.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Category.cs @@ -8,4 +8,8 @@ public class Category } public record CreateCategoryDto(string Name); + + public record ProductDto(int ProductId, string ProductName, decimal Price); + + public record CategoryWithProductsDto(int CategoryId, string CategoryName, IReadOnlyList Products); } \ No newline at end of file From 855fbf304f648c69eda839c0a85da8ae238ff0ef Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Thu, 2 Oct 2025 16:47:41 +0100 Subject: [PATCH 36/64] Added support for getAllCategories method --- .../Services/CategoryService.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/CategoryService.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/CategoryService.cs index 21202475..95a7072f 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/CategoryService.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/CategoryService.cs @@ -1,11 +1,12 @@ using ECommerceApi.JJHH17.Data; using ECommerceApi.JJHH17.Models; +using Microsoft.EntityFrameworkCore; namespace ECommerceApi.JJHH17.Services { public interface ICategoryService { - public List GetAllCategories(); + public List GetAllCategories(); public Category GetCategoryById(int id); public Category CreateCategory(CreateCategoryDto category); } @@ -19,9 +20,20 @@ public CategoryService(ProductsDbContext dbContext) _dbContext = dbContext; } - public List GetAllCategories() + public List GetAllCategories() { - return _dbContext.Categories.ToList(); + return _dbContext.Categories + .AsNoTracking() + .OrderBy(c => c.CategoryName) + .Select(c => new CategoryWithProductsDto( + c.CategoryId, + c.CategoryName, + c.Products + .OrderBy(p => p.ProductName) + .Select(p => new ProductDto(p.ProductId, p.ProductName, p.Price)) + .ToList() + )) + .ToList(); } public Category? GetCategoryById(int id) From f577e56baf0003f1ec853d2db0f686ef6a28e969 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Thu, 2 Oct 2025 21:01:05 +0100 Subject: [PATCH 37/64] Added product creator dto --- ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs index 49be4754..a5fef623 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs @@ -10,7 +10,7 @@ public class Product public Category Category { get; set; } = null!; } - public record CreateProductDto(string name, decimal price, int CategoryId); + public record CreateProductDto(string name, decimal price, int categoryId); public record GetProductsDto(int productId, string productName, decimal price, int CategoryId, string CategoryName); } From 287cab756a175145afc739f6cc715ce7f6bea1e5 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Thu, 2 Oct 2025 21:01:31 +0100 Subject: [PATCH 38/64] Altered createProduct method which now returns the name of the category that the product belongs to --- .../Services/ProductService.cs | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs index dfbc7a7d..b37f3b6c 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs @@ -1,6 +1,7 @@ using ECommerceApi.JJHH17.Data; using ECommerceApi.JJHH17.Models; using Microsoft.EntityFrameworkCore; +using System.Diagnostics; namespace ECommerceApi.JJHH17.Services { @@ -8,7 +9,7 @@ public interface IProductService { public List GetAllProducts(); public Product? GetProductById(int id); - public Product CreateProduct(CreateProductDto product); + public GetProductsDto CreateProduct(CreateProductDto product); public Product UpdateProduct(int id, Product updatedProduct); public string? DeleteProduct(int id); } @@ -22,16 +23,31 @@ public ProductService(ProductsDbContext dbContext) _dbContext = dbContext; } - public Product CreateProduct(CreateProductDto product) + public GetProductsDto CreateProduct(CreateProductDto product) { - if (product == null) - throw new ArgumentNullException("Product name is required.",nameof(product)); - - var newProduct = new Product { ProductName = product.name.Trim(), Price = product.price, CategoryId = product.CategoryId}; + if (product == null) + throw new ArgumentNullException("Product name is required.", nameof(product)); + + var categoryExists = _dbContext.Categories + .Any(c => c.CategoryId == product.categoryId); + if (!categoryExists) + throw new ArgumentException("Category does not exist", nameof(product.categoryId)); + + var newProduct = new Product { ProductName = product.name.Trim(), Price = product.price, CategoryId = product.categoryId }; _dbContext.Products.Add(newProduct); _dbContext.SaveChanges(); - return newProduct; + + return _dbContext.Products + .AsNoTracking() + .Where(p => p.ProductId == newProduct.ProductId) + .Select(p => new GetProductsDto( + p.ProductId, + p.ProductName, + p.Price, + p.CategoryId, + p.Category.CategoryName)) + .Single(); } public string? DeleteProduct(int id) From e999b0dbc5bce273b4ca219685aa544ffd37fb24 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Thu, 2 Oct 2025 21:17:08 +0100 Subject: [PATCH 39/64] Removed delete and update requests for products --- .../Controllers/ProductsController.cs | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs index 7bc33f5f..4e7a0310 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs @@ -38,25 +38,5 @@ public ActionResult CreateProduct(CreateProductDto product) { return Ok(_productService.CreateProduct(product)); } - - [HttpPut("{id}")] - public ActionResult UpdateProduct(int id, Product updatedProduct) - { - var result = _productService.UpdateProduct(id, updatedProduct); - - if (result == null) { return NotFound(); } - - return Ok(result); - } - - [HttpDelete("{id}")] - public ActionResult DeleteProduct(int id) - { - var result = _productService.DeleteProduct(id); - - if (result == null) { return NotFound(); } - - return Ok(result); - } } } From 666c1f01c01121186f94eaa4a229dcc1dbed8a2d Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Thu, 2 Oct 2025 21:17:27 +0100 Subject: [PATCH 40/64] Removed delete and update calls --- .../Services/ProductService.cs | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs index b37f3b6c..af494db6 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs @@ -10,8 +10,6 @@ public interface IProductService public List GetAllProducts(); public Product? GetProductById(int id); public GetProductsDto CreateProduct(CreateProductDto product); - public Product UpdateProduct(int id, Product updatedProduct); - public string? DeleteProduct(int id); } public class ProductService : IProductService @@ -50,18 +48,6 @@ public GetProductsDto CreateProduct(CreateProductDto product) .Single(); } - public string? DeleteProduct(int id) - { - Product savedProduct = _dbContext.Products.Find(id); - - if (savedProduct == null) { return null; } - - _dbContext.Products.Remove(savedProduct); - _dbContext.SaveChanges(); - - return $"Successfully deleted product with ID: {id}"; - } - public List GetAllProducts() { return _dbContext.Products @@ -81,17 +67,5 @@ public List GetAllProducts() Product savedProduct = _dbContext.Products.Find(id); return savedProduct; } - - public Product UpdateProduct(int id, Product product) - { - Product savedProduct = _dbContext.Products.Find(id); - - if (savedProduct == null) { return null; } - - _dbContext.Entry(savedProduct).CurrentValues.SetValues(product); - _dbContext.SaveChanges(); - - return savedProduct; - } } } From 090df33d3bca2812310c0dd126f2c330cc998ae4 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Thu, 2 Oct 2025 22:18:14 +0100 Subject: [PATCH 41/64] Enhanced method to fetch categories based on ID --- .../Services/CategoryService.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/CategoryService.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/CategoryService.cs index 95a7072f..df556f16 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/CategoryService.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/CategoryService.cs @@ -7,7 +7,7 @@ namespace ECommerceApi.JJHH17.Services public interface ICategoryService { public List GetAllCategories(); - public Category GetCategoryById(int id); + public CategoryWithProductsDto GetCategoryById(int id); public Category CreateCategory(CreateCategoryDto category); } @@ -36,10 +36,20 @@ public List GetAllCategories() .ToList(); } - public Category? GetCategoryById(int id) + public CategoryWithProductsDto GetCategoryById(int id) { - Category savedCategory = _dbContext.Categories.Find(id); - return savedCategory; + return _dbContext.Categories + .AsNoTracking() + .Where(p => p.CategoryId == id) + .Select(p => new CategoryWithProductsDto( + p.CategoryId, + p.CategoryName, + p.Products + .OrderBy(pr => pr.ProductName) + .Select(pr => new ProductDto(pr.ProductId, pr.ProductName, pr.Price)) + .ToList() + )) + .Single(); } public Category CreateCategory(CreateCategoryDto category) From 5ac32210111bf10793ba4eeeb8d6ecc738b13d18 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Thu, 2 Oct 2025 22:18:42 +0100 Subject: [PATCH 42/64] Enhanced get product by ID response --- .../Services/ProductService.cs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs index af494db6..1e7b942f 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs @@ -1,5 +1,6 @@ using ECommerceApi.JJHH17.Data; using ECommerceApi.JJHH17.Models; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.EntityFrameworkCore; using System.Diagnostics; @@ -8,7 +9,7 @@ namespace ECommerceApi.JJHH17.Services public interface IProductService { public List GetAllProducts(); - public Product? GetProductById(int id); + public GetProductsDto? GetProductById(int id); public GetProductsDto CreateProduct(CreateProductDto product); } @@ -62,10 +63,21 @@ public List GetAllProducts() .ToList(); } - public Product? GetProductById(int id) + public GetProductsDto GetProductById(int id) { - Product savedProduct = _dbContext.Products.Find(id); - return savedProduct; + var productItem = _dbContext.Products.Find(id); + + if (productItem == null) + { + return null; + } + + return _dbContext.Products + .AsNoTracking() + .Where(p => p.ProductId == id) + .Select(p => new GetProductsDto( + p.ProductId, p.ProductName, p.Price, p.CategoryId, p.Category.CategoryName)) + .Single(); } } } From 27e7581bee417c379a610ac7b832f203c389a03d Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Fri, 3 Oct 2025 13:41:30 +0100 Subject: [PATCH 43/64] Created Sale model --- ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Sale.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Sale.cs diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Sale.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Sale.cs new file mode 100644 index 00000000..b73a20cf --- /dev/null +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Sale.cs @@ -0,0 +1,10 @@ +namespace ECommerceApi.JJHH17.Models +{ + public class Sale + { + public int SaleId { get; set; } + public List Products { get; } = []; + public decimal SalePrice => Products.Sum(p => p.Price); + public int ItemCount => Products.Count(); + } +} From e7dedffd43868c5794a4074db46174545bc0ecf7 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Fri, 3 Oct 2025 13:41:43 +0100 Subject: [PATCH 44/64] Added reference to sales model --- ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs index a5fef623..aa2256a4 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Product.cs @@ -8,6 +8,7 @@ public class Product public int CategoryId { get; set; } public Category Category { get; set; } = null!; + public List Sales { get; } = []; } public record CreateProductDto(string name, decimal price, int categoryId); From ce681f1ac78be8e79d4930417981f3d1b014627b Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Fri, 3 Oct 2025 13:55:38 +0100 Subject: [PATCH 45/64] Added sales to product many to many modelBuilder --- .../ECommerceApi.JJHH17/Data/ProductsDbContext.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs index b1da892a..6d5027f8 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs @@ -18,6 +18,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .WithMany(e => e.Products) .HasForeignKey(e => e.CategoryId) .IsRequired(); + + modelBuilder.Entity() + .HasMany(e => e.Products) + .WithMany(e => e.Sales); } } } From 3225fd642bd8dffd9c5cf5a575be185eeb16bb96 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Fri, 3 Oct 2025 21:23:51 +0100 Subject: [PATCH 46/64] Added reference to sales table --- .../ECommerceApi.JJHH17/Data/ProductsDbContext.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs index 6d5027f8..ead5c189 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs @@ -10,6 +10,7 @@ public ProductsDbContext(DbContextOptions options) : base(opt public DbSet Products => Set(); public DbSet Categories => Set(); + public DbSet Sales => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -24,4 +25,4 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .WithMany(e => e.Sales); } } -} +} \ No newline at end of file From 11a2e6912a72086ab94e698ec4c47bd7aeb3c2c6 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Fri, 3 Oct 2025 21:24:04 +0100 Subject: [PATCH 47/64] Created many to many sales model --- ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Sale.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Sale.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Sale.cs index b73a20cf..c2251a03 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Sale.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Sale.cs @@ -7,4 +7,8 @@ public class Sale public decimal SalePrice => Products.Sum(p => p.Price); public int ItemCount => Products.Count(); } + + public record CreateSaleDto(List ProductIds); + + public record SaleWithProductsDto(int SaleId, int ItemCount, decimal SalePrice, List Products); } From f0088770d9081e0302b07e41b60c0a59dc8dc0a6 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Fri, 3 Oct 2025 21:24:22 +0100 Subject: [PATCH 48/64] Added inclusion of sales table --- ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs index fef3a3e4..488cba37 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Program.cs @@ -10,6 +10,7 @@ builder.Services.AddDbContext(opt => opt.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); var app = builder.Build(); From 11ee02c9840b79c17eadbd4af4f1c89186226bef Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Fri, 3 Oct 2025 21:24:58 +0100 Subject: [PATCH 49/64] Created sales controller for API calls --- .../Controllers/SalesController.cs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/SalesController.cs diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/SalesController.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/SalesController.cs new file mode 100644 index 00000000..ebdc1080 --- /dev/null +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/SalesController.cs @@ -0,0 +1,54 @@ +using ECommerceApi.JJHH17.Models; +using ECommerceApi.JJHH17.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + + +namespace ECommerceApi.JJHH17.Controllers +{ + [ApiController] + [Route("api/[controller]")] + // Example call: http://localhost:5609/api/sale/ + public class SalesController : ControllerBase + { + private readonly ISaleService _saleService; + public SalesController(ISaleService saleService) + { + _saleService = saleService; + } + + [HttpGet] + public ActionResult> GetAllSales() + { + return Ok(_saleService.GetAllSales()); + } + + [HttpGet("{id}")] + public ActionResult GetSaleById(int id) + { + var result = _saleService.GetSaleById(id); + + if (result == null) { return NotFound(); } + + return Ok(result); + } + + [HttpPost] + public ActionResult CreateSale([FromBody] CreateSaleDto sale) + { + var created = _saleService.CreateSale(sale); + + var dto = new SaleWithProductsDto( + created.SaleId, + created.ItemCount, + created.SalePrice, + created.Products + .OrderBy(p => p.ProductName) + .Select(p => new ProductDto(p.ProductId, p.ProductName, p.Price)) + .ToList() + ); + + return CreatedAtAction(nameof(GetSaleById), new { id = created.SaleId }, dto); + } + } +} From 5e2093439cfcdd5d686313938d3dd5e1fbcaf6ed Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Fri, 3 Oct 2025 21:25:12 +0100 Subject: [PATCH 50/64] Created sales service class --- .../Services/SaleService.cs | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/SaleService.cs diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/SaleService.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/SaleService.cs new file mode 100644 index 00000000..2405b464 --- /dev/null +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/SaleService.cs @@ -0,0 +1,89 @@ +using ECommerceApi.JJHH17.Data; +using ECommerceApi.JJHH17.Models; +using Microsoft.EntityFrameworkCore; + +namespace ECommerceApi.JJHH17.Services +{ + public interface ISaleService + { + public List GetAllSales(); + public CategoryWithProductsDto GetSaleById(int id); + public Sale CreateSale(CreateSaleDto sale); + } + + public class SaleService : ISaleService + { + private readonly ProductsDbContext _dbContext; + + public SaleService(ProductsDbContext dbContext) + { + _dbContext = dbContext; + } + + public List GetAllSales() + { + return _dbContext.Sales + .AsNoTracking() + .Include(s => s.Products) + .Select(s => new SaleWithProductsDto( + s.SaleId, + s.ItemCount, + s.SalePrice, + s.Products + .OrderBy(p => p.ProductName) + .Select(p => new ProductDto(p.ProductId, p.ProductName, p.Price)) + .ToList() + )) + .ToList(); + } + + public CategoryWithProductsDto GetSaleById(int id) + { + return _dbContext.Categories + .AsNoTracking() + .Where(p => p.CategoryId == id) + .Select(p => new CategoryWithProductsDto( + p.CategoryId, + p.CategoryName, + p.Products + .OrderBy(pr => pr.ProductName) + .Select(pr => new ProductDto(pr.ProductId, pr.ProductName, pr.Price)) + .ToList() + )) + .Single(); + } + + public Sale CreateSale(CreateSaleDto sale) + { + if (sale is null) + { + throw new ArgumentNullException(nameof(sale)); + } + + if (sale.ProductIds is null || sale.ProductIds.Count == 0) + { + throw new ArgumentException("Provide atleast one product id", nameof(sale)); + } + + var uniqueId = sale.ProductIds.Distinct().ToList(); + var products = _dbContext.Products + .Where(p => uniqueId.Contains(p.ProductId)) + .ToList(); + + var foundId = products.Select(p => p.ProductId).ToHashSet(); + var missing = uniqueId.Where(id => !foundId.Contains(id)).ToList(); + if (missing.Count > 0) + { + throw new InvalidOperationException($"Unknown product ID's {string.Join(", ", missing)}"); + } + + var newSale = new Sale(); + newSale.Products.AddRange(products); + + _dbContext.Sales.Add(newSale); + _dbContext.SaveChanges(); + + return newSale; + } + } +} From ad868eb0b0dc7159a3b389ec78de505a40715b5a Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Mon, 6 Oct 2025 11:54:05 +0100 Subject: [PATCH 51/64] Added singleorDefault method, preventing an errored response if ID is not found --- .../ECommerceApi.JJHH17/Services/CategoryService.cs | 4 ++-- .../ECommerceApi.JJHH17/Services/ProductService.cs | 2 +- .../ECommerceApi.JJHH17/Services/SaleService.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/CategoryService.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/CategoryService.cs index df556f16..e1122887 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/CategoryService.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/CategoryService.cs @@ -49,7 +49,7 @@ public CategoryWithProductsDto GetCategoryById(int id) .Select(pr => new ProductDto(pr.ProductId, pr.ProductName, pr.Price)) .ToList() )) - .Single(); + .SingleOrDefault(); } public Category CreateCategory(CreateCategoryDto category) @@ -57,7 +57,7 @@ public Category CreateCategory(CreateCategoryDto category) if (string.IsNullOrWhiteSpace(category.Name)) throw new ArgumentException("Category name is required.", nameof(category)); - bool exists = _dbContext.Categories.Any(c => c.CategoryName == category.Name); + bool exists = _dbContext.Categories.Any(c => c.CategoryName.ToLower() == category.Name.ToLower()); if (exists) throw new InvalidOperationException($"Category {category.Name} already exists"); diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs index 1e7b942f..4bf10c1e 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/ProductService.cs @@ -77,7 +77,7 @@ public GetProductsDto GetProductById(int id) .Where(p => p.ProductId == id) .Select(p => new GetProductsDto( p.ProductId, p.ProductName, p.Price, p.CategoryId, p.Category.CategoryName)) - .Single(); + .SingleOrDefault(); } } } diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/SaleService.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/SaleService.cs index 2405b464..c3b3248a 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/SaleService.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Services/SaleService.cs @@ -50,7 +50,7 @@ public CategoryWithProductsDto GetSaleById(int id) .Select(pr => new ProductDto(pr.ProductId, pr.ProductName, pr.Price)) .ToList() )) - .Single(); + .SingleOrDefault(); } public Sale CreateSale(CreateSaleDto sale) @@ -71,7 +71,7 @@ public Sale CreateSale(CreateSaleDto sale) .ToList(); var foundId = products.Select(p => p.ProductId).ToHashSet(); - var missing = uniqueId.Where(id => !foundId.Contains(id)).ToList(); + var missing = uniqueId.Where(id => !foundId.Contains(id)).ToList(); if (missing.Count > 0) { throw new InvalidOperationException($"Unknown product ID's {string.Join(", ", missing)}"); From 60712e1544f05361b84c94aa6985f67c8c695370 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Mon, 6 Oct 2025 16:31:16 +0100 Subject: [PATCH 52/64] Added try catch area for adding sales --- .../Controllers/SalesController.cs | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/SalesController.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/SalesController.cs index ebdc1080..21775436 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/SalesController.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/SalesController.cs @@ -36,19 +36,34 @@ public ActionResult GetSaleById(int id) [HttpPost] public ActionResult CreateSale([FromBody] CreateSaleDto sale) { - var created = _saleService.CreateSale(sale); - - var dto = new SaleWithProductsDto( - created.SaleId, - created.ItemCount, - created.SalePrice, - created.Products - .OrderBy(p => p.ProductName) - .Select(p => new ProductDto(p.ProductId, p.ProductName, p.Price)) - .ToList() - ); - - return CreatedAtAction(nameof(GetSaleById), new { id = created.SaleId }, dto); + try + { + var created = _saleService.CreateSale(sale); + + var dto = new SaleWithProductsDto( + created.SaleId, + created.ItemCount, + created.SalePrice, + created.Products + .OrderBy(p => p.ProductName) + .Select(p => new ProductDto(p.ProductId, p.ProductName, p.Price)) + .ToList() + ); + + return CreatedAtAction(nameof(GetSaleById), new { id = created.SaleId }, dto); + } + catch (ArgumentNullException e) + { + return BadRequest(new { error = e.Message }); + } + catch (ArgumentException ex) + { + return BadRequest(new { error = ex.Message }); + } + catch (InvalidOperationException ex) + { + return NotFound(new { error = ex.Message }); + } } } } From 10bfd34a13d3bced64f6b8f811613594650cb910 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Mon, 6 Oct 2025 16:36:39 +0100 Subject: [PATCH 53/64] Added postman collection --- ...rce-API Collection.postman_collection.json | 296 ++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 ECommerceApi.JJHH17/ECommerceApi.JJHH17/PostmanCollection/ECommerce-API Collection.postman_collection.json diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/PostmanCollection/ECommerce-API Collection.postman_collection.json b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/PostmanCollection/ECommerce-API Collection.postman_collection.json new file mode 100644 index 00000000..08c9a633 --- /dev/null +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/PostmanCollection/ECommerce-API Collection.postman_collection.json @@ -0,0 +1,296 @@ +{ + "info": { + "_postman_id": "774cbdb7-e2d2-4f7e-9a7b-696f4ae3b86c", + "name": "ECommerce-API Collection", + "description": "A collection used for testing endpoints created in the ECommerce API.\n\nThis project was built following the C# Academy course: [https://www.thecsharpacademy.com/project/18/ecommerce-api](https://www.thecsharpacademy.com/project/18/ecommerce-api)\n\nThe collection contains 3 main endpoints:\n\n- Products\n \n- Categories\n \n- Sales\n \n\nProduct -> Categories - many to one relationship.\n\nProduct -> Sales - many to many relationship.\n\n**Variables**\n\n- You'll see a full list of variables under the \"Variables\" title in Postman.\n \n- This includes the port used during the API's lifecycle, as well as any relevant ID's for endpoints which can be altered at any point.\n \n\nPlease see below \"View Complete Documentation\" on Postman for further endpoint information.", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "31055143" + }, + "item": [ + { + "name": "Category", + "item": [ + { + "name": "Get Categories", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:{{port}}/api/Categories/", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "api", + "Categories", + "" + ] + }, + "description": "This returns a list of all categories that exist." + }, + "response": [] + }, + { + "name": "Get Category by ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:{{port}}/api/Categories/{{categoryId}}", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "api", + "Categories", + "{{categoryId}}" + ] + }, + "description": "This returns a category based on its ID, as well as the products that fall under the category." + }, + "response": [] + }, + { + "name": "Add New Category", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"Postman Test new\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:{{port}}/api/Categories/", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "api", + "Categories", + "" + ] + }, + "description": "This allows you to add a new category - Please use the \"Body - RAW Json\" section of Postman to do this, before sending the request.\n\nThis is a simple format, \"name\": \"string\" as noted in the placeholder value below." + }, + "response": [] + } + ], + "description": "This is used as a parent for Products, each product has one category and a category can have mny products.\n\nThis section contains:\n\n- Fetching all Categories\n \n- Fetching categories from their ID\n \n- Creating a new category\n \n\nPlease note that you can't have duplicate categories in the database, so you'll receive a relevant response if you attempt to create a category that exists." + }, + { + "name": "Product", + "item": [ + { + "name": "Get Products", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:{{port}}/api/Products/", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "api", + "Products", + "" + ] + }, + "description": "Returns a list of all products that exist." + }, + "response": [] + }, + { + "name": "Get Product by ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:{{port}}/api/Products/{{productId}}", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "api", + "Products", + "{{productId}}" + ] + }, + "description": "Returns a product based on its ID." + }, + "response": [] + }, + { + "name": "Add New Product", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"name\": \"New Product Postman\",\r\n \"price\": 20.50,\r\n \"categoryId\": 2\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:{{port}}/api/Products/", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "api", + "Products", + "" + ] + }, + "description": "This allows you to add a new product- Please use the \"Body - RAW Json\" section of Postman to do this, before sending the request.\n\nBelow is an example of the JSON that can be used, which includes a \"name\", \"price\" and \"categoryID\" field.\n\nThe Category ID should be that of the category that you wish to link the product to." + }, + "response": [] + } + ], + "description": "A product is an individual item that belongs to a category as a child item - a product can only belong to a single category, and contains a name, price, and category information.\n\nThis section contains:\n\n- Fetching all Products\n \n- Fetching a product from their ID\n \n- Creating a new product" + }, + { + "name": "Sale", + "item": [ + { + "name": "Get Sales", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:{{port}}/api/Sales/", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "api", + "Sales", + "" + ] + }, + "description": "This fetches a list of all sales." + }, + "response": [] + }, + { + "name": "Get Sale by ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "https://localhost:{{port}}/api/Sales/{{SaleId}}", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "api", + "Sales", + "{{SaleId}}" + ] + }, + "description": "This fetches an individual sale, based on its ID." + }, + "response": [] + }, + { + "name": "Add New Sale", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"productIds\": [\r\n 1\r\n ]\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://localhost:{{port}}/api/Sales/", + "protocol": "https", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "api", + "Sales", + "" + ] + }, + "description": "This allows you to add a new sale- Please use the \"Body - RAW Json\" section of Postman to do this, before sending the request.\n\nThe JSON used is essentially a list of product IDs, and can be used by appending a comma between each product ID entered.\n\nBelow is an example use case." + }, + "response": [] + } + ], + "description": "A sale is an event whereby individual products are purchased.\n\nDuring a sale, the client enters the product id's that they wish to purchase, and those products are then added to the sale event.\n\nThe quantity of items and overrall cost of the sale is calculated once the data is sent by the client." + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "requests": {}, + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "requests": {}, + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "port", + "value": "" + }, + { + "key": "categoryId", + "value": "" + }, + { + "key": "productId", + "value": "" + }, + { + "key": "SaleId", + "value": "" + } + ] +} \ No newline at end of file From 2d506ee79e607ac8822355546a939b3d2d95823e Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Mon, 6 Oct 2025 21:40:09 +0100 Subject: [PATCH 54/64] Created paged response model --- .../Models/PagedResponse.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/PagedResponse.cs diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/PagedResponse.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/PagedResponse.cs new file mode 100644 index 00000000..95054f70 --- /dev/null +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/PagedResponse.cs @@ -0,0 +1,18 @@ +namespace ECommerceApi.JJHH17.Models +{ + public class PagedResponse + { + public List Data { get; set; } = new(); + public int PageNumber { get; set; } + public int PageSize { get; set; } + public int TotalRecords { get; set; } + + public PagedResponse(List data, int pageNumber, int pageSize, int totalRecords) + { + this.Data = data; + this.PageNumber = pageNumber; + this.PageSize = pageSize; + this.TotalRecords = totalRecords; + } + } +} \ No newline at end of file From 01360dbfc09a3ad579c6c35281a0722ecc03b29d Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Mon, 6 Oct 2025 21:40:26 +0100 Subject: [PATCH 55/64] Added initial response pagination model --- .../Controllers/CategoriesController.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/CategoriesController.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/CategoriesController.cs index f31fa0d7..e53ed0ec 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/CategoriesController.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/CategoriesController.cs @@ -17,9 +17,21 @@ public CategoriesController(ICategoryService categoryService) } [HttpGet] - public ActionResult> GetAllCategories() + public ActionResult> GetAllCategories([FromQuery] Pagination pagination) { - return Ok(_categoryService.GetAllCategories()); + if (pagination.PageNumber < 1) pagination.PageNumber = 1; + if (pagination.PageSize < 1) pagination.PageSize = 10; + + var allCategories = _categoryService.GetAllCategories(); + + var totalRecords = allCategories.Count; + var pagedData = allCategories + .OrderBy(c => c.CategoryId) + .Skip((pagination.PageNumber - 1) * pagination.PageSize) + .Take(pagination.PageSize) + .ToList(); + + return Ok(pagedData); } [HttpGet("{id}")] From ba8f483cf2e8029c1d5b1fabc4499401a90061eb Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Mon, 6 Oct 2025 21:40:41 +0100 Subject: [PATCH 56/64] Added pagination model --- .../ECommerceApi.JJHH17/Models/Pagination.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Pagination.cs diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Pagination.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Pagination.cs new file mode 100644 index 00000000..6fd281e2 --- /dev/null +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Models/Pagination.cs @@ -0,0 +1,15 @@ +namespace ECommerceApi.JJHH17.Models +{ + public class Pagination + { + private const int MaxPageSize = 50; + public int PageNumber { get; set; } = 1; + + private int _pageSize = 10; + public int PageSize + { + get => _pageSize; + set => _pageSize = (value > MaxPageSize) ? MaxPageSize : value; + } + } +} \ No newline at end of file From 315fb77b8565c6107bc02b798a5de5770f7e293e Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Tue, 7 Oct 2025 14:27:39 +0100 Subject: [PATCH 57/64] Added pagination for the GetAllProducts endpoint --- .../Controllers/ProductsController.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs index 4e7a0310..ab7cb68b 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs @@ -18,9 +18,21 @@ public ProductsController(IProductService productService) } [HttpGet] - public ActionResult> GetAllProducts() + public ActionResult> GetAllProducts([FromQuery] Pagination pagination) { - return Ok(_productService.GetAllProducts()); + if (pagination.PageNumber < 1) pagination.PageNumber = 1; + if (pagination.PageSize < 1) pagination.PageSize = 10; + + var allProducts = _productService.GetAllProducts(); + + var totalRecords = allProducts.Count; + var pagedData = allProducts + .OrderBy(c => c.productId) + .Skip((pagination.PageNumber - 1) * pagination.PageSize) + .Take(pagination.PageSize) + .ToList(); + + return Ok(pagedData); } [HttpGet("{id}")] From d9282c95e45ee98867f5319b08ee8dcabdd62950 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Tue, 7 Oct 2025 20:13:55 +0100 Subject: [PATCH 58/64] Added pagination model --- .../Controllers/SalesController.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/SalesController.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/SalesController.cs index 21775436..d769ef3e 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/SalesController.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/SalesController.cs @@ -18,9 +18,21 @@ public SalesController(ISaleService saleService) } [HttpGet] - public ActionResult> GetAllSales() + public ActionResult> GetAllSales([FromQuery] Pagination pagination) { - return Ok(_saleService.GetAllSales()); + if (pagination.PageNumber < 1) pagination.PageNumber = 1; + if (pagination.PageSize < 1) pagination.PageSize = 10; + + var allSales = _saleService.GetAllSales(); + + var totalRecords = allSales.Count; + var pagedData = allSales + .OrderBy(c => c.SaleId) + .Skip((pagination.PageNumber - 1) * pagination.PageSize) + .Take(pagination.PageSize) + .ToList(); + + return Ok(pagedData); } [HttpGet("{id}")] From 5080b8b3445bccca613a2169ec690b1ff9600868 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Tue, 7 Oct 2025 21:14:53 +0100 Subject: [PATCH 59/64] Added data seeding for products and categories --- .../Data/ProductsDbContext.cs | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs index ead5c189..8e33494f 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/ProductsDbContext.cs @@ -14,6 +14,12 @@ public ProductsDbContext(DbContextOptions options) : base(opt protected override void OnModelCreating(ModelBuilder modelBuilder) { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .Property(e => e.Price) + .HasColumnType("decimal(18,2)"); + modelBuilder.Entity() .HasOne(e => e.Category) .WithMany(e => e.Products) @@ -22,7 +28,26 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity() .HasMany(e => e.Products) - .WithMany(e => e.Sales); + .WithMany(e => e.Sales) + .UsingEntity>( + "ProductSales", + j => j.HasOne().WithMany().HasForeignKey("ProductId"), + j => j.HasOne().WithMany().HasForeignKey("SaleId"), + j => + { + j.HasKey("ProductId", "SaleId"); + j.ToTable("ProductSales"); + }); + + // Optional data seeding + modelBuilder.Entity().HasData( + new Category { CategoryId = 1, CategoryName = "Nike" }, + new Category { CategoryId = 2, CategoryName = "Adidas" }); + + modelBuilder.Entity().HasData( + new Product { ProductId = 1, ProductName = "Jordan", Price = 100m, CategoryId = 1 }, + new Product { ProductId = 2, ProductName = "Air Force 1", Price = 75m, CategoryId = 1 }, + new Product { ProductId = 3, ProductName = "Ultra Boost", Price = 50m, CategoryId = 1 }); } } } \ No newline at end of file From 4daf0b88676ae5fcdb023de41a4be1243a799d5a Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Tue, 7 Oct 2025 21:15:58 +0100 Subject: [PATCH 60/64] Added initial migrations --- .../20251007201154_InitialCreate.Designer.cs | 165 ++++++++++++++++++ .../20251007201154_InitialCreate.cs | 131 ++++++++++++++ .../ProductsDbContextModelSnapshot.cs | 162 +++++++++++++++++ 3 files changed, 458 insertions(+) create mode 100644 ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/Migrations/20251007201154_InitialCreate.Designer.cs create mode 100644 ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/Migrations/20251007201154_InitialCreate.cs create mode 100644 ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/Migrations/ProductsDbContextModelSnapshot.cs diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/Migrations/20251007201154_InitialCreate.Designer.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/Migrations/20251007201154_InitialCreate.Designer.cs new file mode 100644 index 00000000..6c906d31 --- /dev/null +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/Migrations/20251007201154_InitialCreate.Designer.cs @@ -0,0 +1,165 @@ +// +using ECommerceApi.JJHH17.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace ECommerceApi.JJHH17.Data.Migrations +{ + [DbContext(typeof(ProductsDbContext))] + [Migration("20251007201154_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("ECommerceApi.JJHH17.Models.Category", b => + { + b.Property("CategoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("CategoryId")); + + b.Property("CategoryName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("CategoryId"); + + b.ToTable("Categories"); + + b.HasData( + new + { + CategoryId = 1, + CategoryName = "Nike" + }, + new + { + CategoryId = 2, + CategoryName = "Adidas" + }); + }); + + modelBuilder.Entity("ECommerceApi.JJHH17.Models.Product", b => + { + b.Property("ProductId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("ProductId")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("ProductName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("ProductId"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + + b.HasData( + new + { + ProductId = 1, + CategoryId = 1, + Price = 100m, + ProductName = "Jordan" + }, + new + { + ProductId = 2, + CategoryId = 1, + Price = 75m, + ProductName = "Air Force 1" + }, + new + { + ProductId = 3, + CategoryId = 1, + Price = 50m, + ProductName = "Ultra Boost" + }); + }); + + modelBuilder.Entity("ECommerceApi.JJHH17.Models.Sale", b => + { + b.Property("SaleId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SaleId")); + + b.HasKey("SaleId"); + + b.ToTable("Sales"); + }); + + modelBuilder.Entity("ProductSales", b => + { + b.Property("ProductId") + .HasColumnType("int"); + + b.Property("SaleId") + .HasColumnType("int"); + + b.HasKey("ProductId", "SaleId"); + + b.HasIndex("SaleId"); + + b.ToTable("ProductSales", (string)null); + }); + + modelBuilder.Entity("ECommerceApi.JJHH17.Models.Product", b => + { + b.HasOne("ECommerceApi.JJHH17.Models.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("ProductSales", b => + { + b.HasOne("ECommerceApi.JJHH17.Models.Product", null) + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ECommerceApi.JJHH17.Models.Sale", null) + .WithMany() + .HasForeignKey("SaleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ECommerceApi.JJHH17.Models.Category", b => + { + b.Navigation("Products"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/Migrations/20251007201154_InitialCreate.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/Migrations/20251007201154_InitialCreate.cs new file mode 100644 index 00000000..9094b84b --- /dev/null +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/Migrations/20251007201154_InitialCreate.cs @@ -0,0 +1,131 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace ECommerceApi.JJHH17.Data.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Categories", + columns: table => new + { + CategoryId = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + CategoryName = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Categories", x => x.CategoryId); + }); + + migrationBuilder.CreateTable( + name: "Sales", + columns: table => new + { + SaleId = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1") + }, + constraints: table => + { + table.PrimaryKey("PK_Sales", x => x.SaleId); + }); + + migrationBuilder.CreateTable( + name: "Products", + columns: table => new + { + ProductId = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + ProductName = table.Column(type: "nvarchar(max)", nullable: false), + Price = table.Column(type: "decimal(18,2)", nullable: false), + CategoryId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Products", x => x.ProductId); + table.ForeignKey( + name: "FK_Products_Categories_CategoryId", + column: x => x.CategoryId, + principalTable: "Categories", + principalColumn: "CategoryId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ProductSales", + columns: table => new + { + ProductId = table.Column(type: "int", nullable: false), + SaleId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProductSales", x => new { x.ProductId, x.SaleId }); + table.ForeignKey( + name: "FK_ProductSales_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "ProductId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ProductSales_Sales_SaleId", + column: x => x.SaleId, + principalTable: "Sales", + principalColumn: "SaleId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + table: "Categories", + columns: new[] { "CategoryId", "CategoryName" }, + values: new object[,] + { + { 1, "Nike" }, + { 2, "Adidas" } + }); + + migrationBuilder.InsertData( + table: "Products", + columns: new[] { "ProductId", "CategoryId", "Price", "ProductName" }, + values: new object[,] + { + { 1, 1, 100m, "Jordan" }, + { 2, 1, 75m, "Air Force 1" }, + { 3, 1, 50m, "Ultra Boost" } + }); + + migrationBuilder.CreateIndex( + name: "IX_Products_CategoryId", + table: "Products", + column: "CategoryId"); + + migrationBuilder.CreateIndex( + name: "IX_ProductSales_SaleId", + table: "ProductSales", + column: "SaleId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ProductSales"); + + migrationBuilder.DropTable( + name: "Products"); + + migrationBuilder.DropTable( + name: "Sales"); + + migrationBuilder.DropTable( + name: "Categories"); + } + } +} diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/Migrations/ProductsDbContextModelSnapshot.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/Migrations/ProductsDbContextModelSnapshot.cs new file mode 100644 index 00000000..5e36b464 --- /dev/null +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Data/Migrations/ProductsDbContextModelSnapshot.cs @@ -0,0 +1,162 @@ +// +using ECommerceApi.JJHH17.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace ECommerceApi.JJHH17.Data.Migrations +{ + [DbContext(typeof(ProductsDbContext))] + partial class ProductsDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("ECommerceApi.JJHH17.Models.Category", b => + { + b.Property("CategoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("CategoryId")); + + b.Property("CategoryName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("CategoryId"); + + b.ToTable("Categories"); + + b.HasData( + new + { + CategoryId = 1, + CategoryName = "Nike" + }, + new + { + CategoryId = 2, + CategoryName = "Adidas" + }); + }); + + modelBuilder.Entity("ECommerceApi.JJHH17.Models.Product", b => + { + b.Property("ProductId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("ProductId")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("ProductName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("ProductId"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + + b.HasData( + new + { + ProductId = 1, + CategoryId = 1, + Price = 100m, + ProductName = "Jordan" + }, + new + { + ProductId = 2, + CategoryId = 1, + Price = 75m, + ProductName = "Air Force 1" + }, + new + { + ProductId = 3, + CategoryId = 1, + Price = 50m, + ProductName = "Ultra Boost" + }); + }); + + modelBuilder.Entity("ECommerceApi.JJHH17.Models.Sale", b => + { + b.Property("SaleId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("SaleId")); + + b.HasKey("SaleId"); + + b.ToTable("Sales"); + }); + + modelBuilder.Entity("ProductSales", b => + { + b.Property("ProductId") + .HasColumnType("int"); + + b.Property("SaleId") + .HasColumnType("int"); + + b.HasKey("ProductId", "SaleId"); + + b.HasIndex("SaleId"); + + b.ToTable("ProductSales", (string)null); + }); + + modelBuilder.Entity("ECommerceApi.JJHH17.Models.Product", b => + { + b.HasOne("ECommerceApi.JJHH17.Models.Category", "Category") + .WithMany("Products") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("ProductSales", b => + { + b.HasOne("ECommerceApi.JJHH17.Models.Product", null) + .WithMany() + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ECommerceApi.JJHH17.Models.Sale", null) + .WithMany() + .HasForeignKey("SaleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ECommerceApi.JJHH17.Models.Category", b => + { + b.Navigation("Products"); + }); +#pragma warning restore 612, 618 + } + } +} From 0a3abb4e866e580a7d48f7d305693343ee4d6c42 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Tue, 7 Oct 2025 21:16:32 +0100 Subject: [PATCH 61/64] Removed unread using string --- .../ECommerceApi.JJHH17/Controllers/SalesController.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/SalesController.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/SalesController.cs index d769ef3e..af1f1584 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/SalesController.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/SalesController.cs @@ -1,8 +1,6 @@ using ECommerceApi.JJHH17.Models; using ECommerceApi.JJHH17.Services; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; - namespace ECommerceApi.JJHH17.Controllers { From 6d246d4df24f49d8db9d036efd333773593b6259 Mon Sep 17 00:00:00 2001 From: James Hatfield Date: Tue, 7 Oct 2025 21:17:30 +0100 Subject: [PATCH 62/64] removed unused using strings --- .../ECommerceApi.JJHH17/Controllers/ProductsController.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs index ab7cb68b..06a324c7 100644 --- a/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs +++ b/ECommerceApi.JJHH17/ECommerceApi.JJHH17/Controllers/ProductsController.cs @@ -1,8 +1,6 @@ -using ECommerceApi.JJHH17.Data; -using ECommerceApi.JJHH17.Models; +using ECommerceApi.JJHH17.Models; using ECommerceApi.JJHH17.Services; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; namespace ECommerceApi.JJHH17.Controllers { From 2006e78589d5f27af0fa0270b3811a43524edf14 Mon Sep 17 00:00:00 2001 From: JJHH17 Date: Wed, 8 Oct 2025 20:40:35 +0100 Subject: [PATCH 63/64] Create README.md Post project completion README commit --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..d81f17d6 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# ECommerce API +An ASP.NET and Entity Framework API, built following the C# Academy specifications found on: https://www.thecsharpacademy.com/project/18/ecommerce-api + +This project has a "frontend" in the form of a console application; this repository can be found on: https://github.com/JJHH17/ECommerce-Console + +## Project Overview +This project: +- Use SQL Server for database storage. +- Allows users to Add and Fetch products, categories and sales. +- A single item is a "Product" - a product can have a single category, and a category may have multiple products. +- A sale is a request to order a list of products that exist on the database. +- This project also has a Postman collection which can be found within the "postmanCollection" folder of the project - please install the json file and import it into Postman in order to use it. +- The collection also contains full documentation on the endpoints and supported parameters. + +## Endpoint Structure: +- Product = This is an individual item - it contains an ID, name, price - it also contains a one to many relationship with "Category" and a many to many relationship with "Sale". +- Category = This is the type of item that a product is, such as "Nike" or "Adidas" as a brand of shoe - This has a many to one relationship with "Product". +- Sale = This allows the client to create a list of products to execute a sale on. This has a many to many relationship with Product, and will also calculate the overall quantity of items and overall cost of the transaction once a product list is provided. + +## Technology Used +- ASP.NET +- SQL Server +- Entity Framework +- Postman +- Swagger / OpenAPI + +## Installation steps and requirements +- A localdb instance of SQL Server is needed for this project - I'd recommend following this tutorial for steps on how to create an instance: https://www.youtube.com/watch?v=M5DhHYQlnq8&t=201s&pp=ygUPTE9DQUwgc3Fsc2VydmVy +- Once done, the connection string can be configured within the "DefaultConnection" variable within the projects appsettings.json file. +Once done, run the .sln file and run the project - you'll be directed to Swagger via your default browser where you'll also be able to test the API's endpoints. + +## Additional Details +- The project also supports pagination, as well as data seeding once the database instance is created. +- A console based UI can be found and installed via: https://github.com/JJHH17/ECommerce-Console + +## Endpoints and usage +- Product endpoints (GET, GET ID, POST) - Allows the client to get a list of, a single, or create individual products. +- Sale endpoints (GET, GET ID, POST) - Allows the client to get a list of, a single, or create individual sales. +- Category endpoints (GET, GET ID, POST) - Allows the client to get a list of, a single, or create individual categories. + +## Key takeaways and learnings from project +This was a great and challenging project to complete - initially, I had a few blockers with understanding how to configure many to many (as well as one to many) relationships within Entity Framework and then mapping them for my API endpoints. + +In the end, I was overcomplicating my thoughts and solutions with these, and I decided to create 3 seperate controllers and services to process them, and then mapping each one within the DB Context file - It took around half a week or playing around with different options and solutions to stick to this specific route, although I'm pleased that I did get stuck here, as it was a great learning experienced and allowed me to also lean on Microsoft's documentation more. + +I then set about creating the Postman collection which I have some experience with already, although it was very cool to create a collection pointing towards an API that I had created myself, versus consuming a public or third party API which was the only real prior experience I had before. + +Finally, creating the console application (found via: https://github.com/JJHH17/ECommerce-Console) was also a great experience - again, I've not had massive amounts of experience consuming my own API that I've created, so it was extremely rewarding on that front - I ensured to create the projects separately; previously, I'd mixed my console app with the backend API, which was causing both apps to mix with eachothers logs, so separating these two project should prevent that in the future. + +I'd like to thank, as always, the C# Academy team for the opportunity to work on this project, the Discord community for their inspiration, as well as the team of individuals who review these projects - I've had amazing feedback so far from previous projects which has always been a great and rewarding experience and I look forward to giving back to the same community now that I'm starting to review projects myself. From 205a710e6c18cef5b9f179087fbd98c19c3bb43b Mon Sep 17 00:00:00 2001 From: JJHH17 Date: Wed, 8 Oct 2025 20:49:42 +0100 Subject: [PATCH 64/64] Update README.md Added examples of API endpoints --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index d81f17d6..6ee0fc0c 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,14 @@ Once done, run the .sln file and run the project - you'll be directed to Swagger ## Endpoints and usage - Product endpoints (GET, GET ID, POST) - Allows the client to get a list of, a single, or create individual products. +image + - Sale endpoints (GET, GET ID, POST) - Allows the client to get a list of, a single, or create individual sales. +image + - Category endpoints (GET, GET ID, POST) - Allows the client to get a list of, a single, or create individual categories. +image + ## Key takeaways and learnings from project This was a great and challenging project to complete - initially, I had a few blockers with understanding how to configure many to many (as well as one to many) relationships within Entity Framework and then mapping them for my API endpoints.