From 4f3492767cf24a8dfb9fb8c5c681d679dc2920ff Mon Sep 17 00:00:00 2001 From: Jone <64496635+JoneTheBuilder@users.noreply.github.com> Date: Mon, 27 Jan 2025 10:02:12 +0100 Subject: [PATCH 1/4] Done with Core --- .../{DataContext.cs => DatabaseContext.cs} | 8 +- exercise.pizzashopapi/Data/Seeder.cs | 16 +- .../EndPoints/PizzaShopApi.cs | 15 -- .../EndPoints/PizzaShopEndpoint.cs | 255 ++++++++++++++++++ exercise.pizzashopapi/Models/Customer.cs | 8 +- exercise.pizzashopapi/Models/Order.cs | 20 +- exercise.pizzashopapi/Models/Pizza.cs | 13 +- exercise.pizzashopapi/Program.cs | 13 +- .../Repository/IRepository.cs | 23 +- .../Repository/Repository.cs | 82 +++++- .../exercise.pizzashopapi.csproj | 12 +- 11 files changed, 410 insertions(+), 55 deletions(-) rename exercise.pizzashopapi/Data/{DataContext.cs => DatabaseContext.cs} (81%) delete mode 100644 exercise.pizzashopapi/EndPoints/PizzaShopApi.cs create mode 100644 exercise.pizzashopapi/EndPoints/PizzaShopEndpoint.cs diff --git a/exercise.pizzashopapi/Data/DataContext.cs b/exercise.pizzashopapi/Data/DatabaseContext.cs similarity index 81% rename from exercise.pizzashopapi/Data/DataContext.cs rename to exercise.pizzashopapi/Data/DatabaseContext.cs index 129199e..f1d5908 100644 --- a/exercise.pizzashopapi/Data/DataContext.cs +++ b/exercise.pizzashopapi/Data/DatabaseContext.cs @@ -3,14 +3,14 @@ namespace exercise.pizzashopapi.Data { - public class DataContext : DbContext + public class DatabaseContext : DbContext { private string connectionString; - public DataContext() + public DatabaseContext() { var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); - connectionString = configuration.GetValue("ConnectionStrings:DefaultConnectionString"); - + connectionString = configuration.GetValue("ConnectionStrings:DefaultConnectionString") ?? ""; + this.Database.EnsureCreated(); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { diff --git a/exercise.pizzashopapi/Data/Seeder.cs b/exercise.pizzashopapi/Data/Seeder.cs index f87fbef..b588405 100644 --- a/exercise.pizzashopapi/Data/Seeder.cs +++ b/exercise.pizzashopapi/Data/Seeder.cs @@ -6,26 +6,28 @@ public static class Seeder { public async static void SeedPizzaShopApi(this WebApplication app) { - using(var db = new DataContext()) + using(var db = new DatabaseContext()) { if(!db.Customers.Any()) { db.Add(new Customer() { Name="Nigel" }); db.Add(new Customer() { Name = "Dave" }); + db.Add(new Customer() { Name = "Jone" }); await db.SaveChangesAsync(); } if(!db.Pizzas.Any()) { - db.Add(new Pizza() { Name = "Cheese & Pineapple" }); - db.Add(new Pizza() { Name = "Vegan Cheese Tastic" }); + db.Add(new Pizza() { Name = "Cheese & Pineapple", Price = 12.34m }); + db.Add(new Pizza() { Name = "Vegan Cheese Tastic", Price = 23.45m }); + db.Add(new Pizza() { Name = "Pepperoni", Price = 34.56m}); await db.SaveChangesAsync(); } - - //order data - if(1==1) + if(!db.Orders.Any()) { - + db.Add(new Order() { CustomerId = 1, PizzaId = 2 }); + db.Add(new Order() { CustomerId = 2, PizzaId = 1 }); + db.Add(new Order() { CustomerId = 3, PizzaId = 3 }); await db.SaveChangesAsync(); } } diff --git a/exercise.pizzashopapi/EndPoints/PizzaShopApi.cs b/exercise.pizzashopapi/EndPoints/PizzaShopApi.cs deleted file mode 100644 index f8be2b0..0000000 --- a/exercise.pizzashopapi/EndPoints/PizzaShopApi.cs +++ /dev/null @@ -1,15 +0,0 @@ -using exercise.pizzashopapi.Repository; -using Microsoft.AspNetCore.Mvc; - -namespace exercise.pizzashopapi.EndPoints -{ - public static class PizzaShopApi - { - public static void ConfigurePizzaShopApi(this WebApplication app) - { - - } - - - } -} diff --git a/exercise.pizzashopapi/EndPoints/PizzaShopEndpoint.cs b/exercise.pizzashopapi/EndPoints/PizzaShopEndpoint.cs new file mode 100644 index 0000000..b5148a6 --- /dev/null +++ b/exercise.pizzashopapi/EndPoints/PizzaShopEndpoint.cs @@ -0,0 +1,255 @@ +using exercise.pizzashopapi.Repository; +using Microsoft.AspNetCore.Mvc; +using exercise.pizzashopapi.Models; + +namespace exercise.pizzashopapi.EndPoints +{ + public static class PizzaShopEndpoint + { + public static void ConfigurePizzaShopEndpoint(this WebApplication app) + { + var customerGroup = app.MapGroup("customer"); + var orderGroup = app.MapGroup("order"); + var pizzaGroup = app.MapGroup("pizza"); + + customerGroup.MapPost("/", CreateCustomer); + customerGroup.MapGet("/", GetCustomers); + customerGroup.MapGet("/{id}", GetCustomerById); + customerGroup.MapPut("/{id}", UpdateCustomer); + customerGroup.MapDelete("/{id}", DeleteCustomer); + + orderGroup.MapPost("/", CreateOrder); + orderGroup.MapGet("/", GetOrders); + orderGroup.MapGet("/{id}", GetOrderById); + orderGroup.MapPut("/{id}", UpdateOrder); + orderGroup.MapDelete("/{id}", DeleteOrder); + + pizzaGroup.MapPost("/", CreatePizza); + pizzaGroup.MapGet("/", GetPizzas); + pizzaGroup.MapGet("/{id}", GetPizzaById); + pizzaGroup.MapPut("/{id}", UpdatePizza); + pizzaGroup.MapDelete("/{id}", DeletePizza); + } + + #region customer + + public static async Task CreateCustomer(IRepository customerRepository, string name) + { + var customer = new Customer { Name = name }; + var createdCustomer = await customerRepository.Insert(customer); + return TypedResults.Created($"/customer/{createdCustomer.Id}", createdCustomer); + } + + public static async Task GetCustomers(IRepository customerRepository) + { + var customers = await customerRepository.Get(); + return TypedResults.Ok(customers); + } + + public static async Task GetCustomerById(IRepository customerRepository, int id) + { + var customer = await customerRepository.GetById(id); + if (customer == null) + { + return TypedResults.NotFound(new { Message = "Customer not found" }); + } + return TypedResults.Ok(customer); + } + + public static async Task UpdateCustomer(IRepository customerRepository, int id, string? name) + { + var customer = await customerRepository.GetById(id); + if (customer == null) + { + return TypedResults.NotFound(new { Message = "Customer not found" }); + } + if (name != null && name != customer.Name) + { + customer.Name = name; + } + var updatedCustomer = await customerRepository.Update(customer); + return TypedResults.Ok(updatedCustomer); + } + + public static async Task DeleteCustomer(IRepository customerRepository, int id) + { + var customer = await customerRepository.GetById(id); + if (customer == null) + { + return TypedResults.NotFound(new { Message = "Customer not found" }); + } + var deletedCustomer = await customerRepository.Delete(id); + return TypedResults.Ok(deletedCustomer); + } + + #endregion + + #region order + + public static async Task CreateOrder(IRepository orderRepository, IRepository customerRepository, IRepository pizzaRepository, int customerId, int pizzaId) + { + // Check if the customer exists + var customer = await customerRepository.GetById(customerId); + if (customer == null) + { + return TypedResults.NotFound(new { Message = "Customer not found" }); + } + + // Check if the pizza exists + var pizza = await pizzaRepository.GetById(pizzaId); + if (pizza == null) + { + return TypedResults.NotFound(new { Message = "Pizza not found" }); + } + + // Create the order + var order = new Order { CustomerId = customerId, PizzaId = pizzaId }; + var createdOrder = await orderRepository.Insert(order); + return TypedResults.Created($"/order/{createdOrder.Id}", createdOrder); + } + + + public static async Task GetOrders(IRepository orderRepository) + { + var orders = await orderRepository.GetWithIncludes( + o => o.customer, + o => o.pizza + ); + return TypedResults.Ok(orders); + } + + public static async Task GetOrderById(IRepository orderRepository, int id) + { + var orders = await orderRepository.GetWithIncludes( + o => o.customer, + o => o.pizza + ); + + var order = orders.FirstOrDefault(o => o.Id == id); + if (order == null) + { + return TypedResults.NotFound(new { Message = "Order not found" }); + } + return TypedResults.Ok(order); + } + + + public static async Task UpdateOrder( + IRepository orderRepository, + IRepository customerRepository, + IRepository pizzaRepository, + int id, + int? customerId, + int? pizzaId) + { + var orders = await orderRepository.GetWithIncludes(o => o.customer, o => o.pizza); + var order = orders.FirstOrDefault(o => o.Id == id); + + if (order == null) + { + return TypedResults.NotFound(new { Message = "Order not found" }); + } + + if (customerId != null) + { + var customer = await customerRepository.GetById((int)customerId); + if (customer == null) + { + return TypedResults.NotFound(new { Message = "Customer not found" }); + } + order.CustomerId = (int)customerId; + } + + if (pizzaId != null) + { + var pizza = await pizzaRepository.GetById((int)pizzaId); + if (pizza == null) + { + return TypedResults.NotFound(new { Message = "Pizza not found" }); + } + order.PizzaId = (int)pizzaId; + } + + var updatedOrder = await orderRepository.Update(order); + return TypedResults.Ok(updatedOrder); + } + + + public static async Task DeleteOrder(IRepository orderRepository, int id) + { + var orders = await orderRepository.GetWithIncludes(o => o.customer, o => o.pizza); + var order = orders.FirstOrDefault(o => o.Id == id); + + if (order == null) + { + return TypedResults.NotFound(new { Message = "Order not found" }); + } + + var deletedOrder = await orderRepository.Delete(id); + + return TypedResults.Ok(deletedOrder); + } + + + #endregion + + #region pizza + + public static async Task CreatePizza(IRepository pizzaRepository, string name, decimal price) + { + var pizza = new Pizza { Name = name, Price = price }; + var createdPizza = await pizzaRepository.Insert(pizza); + return TypedResults.Created($"/pizza/{createdPizza.Id}", createdPizza); + } + + public static async Task GetPizzas(IRepository pizzaRepository) + { + var pizzas = await pizzaRepository.Get(); + return TypedResults.Ok(pizzas); + } + + public static async Task GetPizzaById(IRepository pizzaRepository, int id) + { + var pizza = await pizzaRepository.GetById(id); + if (pizza == null) + { + return TypedResults.NotFound(new { Message = "Pizza not found" }); + } + return TypedResults.Ok(pizza); + } + + public static async Task UpdatePizza(IRepository pizzaRepository, int id, string? name, decimal? price) + { + var pizza = await pizzaRepository.GetById(id); + if (pizza == null) + { + return TypedResults.NotFound(new { Message = "Pizza not found" }); + } + + if (!string.IsNullOrWhiteSpace(name)) + { + pizza.Name = name; + } + if (price >= 0 && price != null) + { + pizza.Price = (decimal) price; + } + + var updatedPizza = await pizzaRepository.Update(pizza); + return TypedResults.Ok(updatedPizza); + } + + public static async Task DeletePizza(IRepository pizzaRepository, int id) + { + var pizza = await pizzaRepository.GetById(id); + if (pizza == null) + { + return TypedResults.NotFound(new { Message = "Pizza not found" }); + } + var deletedPizza = await pizzaRepository.Delete(id); + return TypedResults.Ok(deletedPizza); + } + + #endregion + } +} diff --git a/exercise.pizzashopapi/Models/Customer.cs b/exercise.pizzashopapi/Models/Customer.cs index 2ca83bd..dbb40fd 100644 --- a/exercise.pizzashopapi/Models/Customer.cs +++ b/exercise.pizzashopapi/Models/Customer.cs @@ -1,10 +1,16 @@ -using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace exercise.pizzashopapi.Models { + [Table("customer")] public class Customer { + [Key] + [Column("id")] public int Id { get; set; } + + [Column("name")] public string Name { get; set; } } } diff --git a/exercise.pizzashopapi/Models/Order.cs b/exercise.pizzashopapi/Models/Order.cs index fbe6113..b883940 100644 --- a/exercise.pizzashopapi/Models/Order.cs +++ b/exercise.pizzashopapi/Models/Order.cs @@ -1,10 +1,24 @@ -using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace exercise.pizzashopapi.Models { + [Table("order")] public class Order { - - + [Key] + [Column("id")] + public int Id { get; set; } + + [ForeignKey("customer")] + [Column("customer_id")] + public int CustomerId { get; set; } + + [ForeignKey("pizza")] + [Column("pizza_id")] + public int PizzaId { get; set; } + + public Customer customer { get; set; } + public Pizza pizza { get; set; } } } diff --git a/exercise.pizzashopapi/Models/Pizza.cs b/exercise.pizzashopapi/Models/Pizza.cs index 5c085ec..77d3c70 100644 --- a/exercise.pizzashopapi/Models/Pizza.cs +++ b/exercise.pizzashopapi/Models/Pizza.cs @@ -1,12 +1,19 @@ -using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace exercise.pizzashopapi.Models { - + [Table("pizza")] public class Pizza - { + { + [Key] + [Column("id")] public int Id { get; set; } + + [Column("name")] public string Name { get; set; } + + [Column("price")] public decimal Price { get; set; } } } \ No newline at end of file diff --git a/exercise.pizzashopapi/Program.cs b/exercise.pizzashopapi/Program.cs index c04a440..a563483 100644 --- a/exercise.pizzashopapi/Program.cs +++ b/exercise.pizzashopapi/Program.cs @@ -1,21 +1,20 @@ using exercise.pizzashopapi.Data; using exercise.pizzashopapi.EndPoints; +using exercise.pizzashopapi.Models; using exercise.pizzashopapi.Repository; var builder = WebApplication.CreateBuilder(args); -// Add services to the container. - builder.Services.AddControllers(); -builder.Services.AddScoped(); -builder.Services.AddDbContext(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddDbContext(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); -// Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); @@ -28,7 +27,7 @@ app.MapControllers(); -app.ConfigurePizzaShopApi(); +app.ConfigurePizzaShopEndpoint(); app.SeedPizzaShopApi(); diff --git a/exercise.pizzashopapi/Repository/IRepository.cs b/exercise.pizzashopapi/Repository/IRepository.cs index b114ea8..76c6a86 100644 --- a/exercise.pizzashopapi/Repository/IRepository.cs +++ b/exercise.pizzashopapi/Repository/IRepository.cs @@ -1,11 +1,26 @@ -using exercise.pizzashopapi.Models; +using System.Linq.Expressions; +using exercise.pizzashopapi.Models; namespace exercise.pizzashopapi.Repository { - public interface IRepository + // CRUD = Create, Read, Update, Delete + public interface IRepository { - Task> GetOrdersByCustomer(int id); - + // Create + Task Insert(T entity); + // Read + Task> Get(); + Task GetById(int id); + Task> GetWithIncludes(params Expression>[] includes); + + // Update + Task Update(T entity); + + // Delete + Task Delete(object id); + + // Save + Task Save(); } } diff --git a/exercise.pizzashopapi/Repository/Repository.cs b/exercise.pizzashopapi/Repository/Repository.cs index e109fce..1be32b9 100644 --- a/exercise.pizzashopapi/Repository/Repository.cs +++ b/exercise.pizzashopapi/Repository/Repository.cs @@ -1,14 +1,86 @@ -using exercise.pizzashopapi.Data; +using System.Linq.Expressions; +using exercise.pizzashopapi.Data; using exercise.pizzashopapi.Models; +using Microsoft.EntityFrameworkCore; namespace exercise.pizzashopapi.Repository { - public class Repository : IRepository + // CRUD = Create, Read, Update, Delete + public class Repository : IRepository where T : class { - private DataContext _db; - public Task> GetOrdersByCustomer(int id) + private DatabaseContext _db; + private DbSet _table = null!; + + public Repository(DatabaseContext dataContext) + { + _db = dataContext; + _table = _db.Set(); + } + + // Create + public async Task Insert(T entity) + { + try + { + _table.Add(entity); + await _db.SaveChangesAsync(); + return entity; + } + catch (Exception ex) + { + throw new Exception("Error inserting entity", ex); + } + } + + // Read + public async Task> Get() + { + return await _table.ToListAsync(); + } + + + public async Task GetById(int id) + { + return await _table.FindAsync(id); + } + + public async Task> GetWithIncludes(params Expression>[] includes) + { + IQueryable query = _table; + foreach (var include in includes) + { + query = query.Include(include); + } + return await query.ToListAsync(); + } + + // Update + public async Task Update(T entity) + { + _table.Attach(entity); + _db.Entry(entity).State = EntityState.Modified; + await _db.SaveChangesAsync(); + return entity; + } + + // Delete + public async Task Delete(object id) + { + T entity = await _table.FindAsync(id); + if (entity == null) + { + throw new KeyNotFoundException("Entity not found for deletion."); + } + + _table.Remove(entity); + await _db.SaveChangesAsync(); + return entity; + } + + // Save + public async Task Save() { - throw new NotImplementedException(); + await _db.SaveChangesAsync(); } } } diff --git a/exercise.pizzashopapi/exercise.pizzashopapi.csproj b/exercise.pizzashopapi/exercise.pizzashopapi.csproj index 624203b..e2c5efb 100644 --- a/exercise.pizzashopapi/exercise.pizzashopapi.csproj +++ b/exercise.pizzashopapi/exercise.pizzashopapi.csproj @@ -11,18 +11,18 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + From 3cc1f84e0901513425fb792486475c6ee389e8a8 Mon Sep 17 00:00:00 2001 From: Jone <64496635+JoneTheBuilder@users.noreply.github.com> Date: Mon, 27 Jan 2025 10:45:12 +0100 Subject: [PATCH 2/4] Done with first extension --- exercise.pizzashopapi/Data/DatabaseContext.cs | 2 + exercise.pizzashopapi/Data/Seeder.cs | 34 +++- .../EndPoints/PizzaShopEndpoint.cs | 145 ++++++++++++++++-- exercise.pizzashopapi/Models/Order.cs | 6 +- exercise.pizzashopapi/Models/OrderTopping.cs | 28 ++++ exercise.pizzashopapi/Models/Pizza.cs | 1 + exercise.pizzashopapi/Models/Topping.cs | 19 +++ exercise.pizzashopapi/Program.cs | 2 + 8 files changed, 224 insertions(+), 13 deletions(-) create mode 100644 exercise.pizzashopapi/Models/OrderTopping.cs create mode 100644 exercise.pizzashopapi/Models/Topping.cs diff --git a/exercise.pizzashopapi/Data/DatabaseContext.cs b/exercise.pizzashopapi/Data/DatabaseContext.cs index f1d5908..66163e4 100644 --- a/exercise.pizzashopapi/Data/DatabaseContext.cs +++ b/exercise.pizzashopapi/Data/DatabaseContext.cs @@ -24,5 +24,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) public DbSet Pizzas { get; set; } public DbSet Customers { get; set; } public DbSet Orders { get; set; } + public DbSet Toppings { get; set; } + public DbSet OrderToppings { get; set; } } } diff --git a/exercise.pizzashopapi/Data/Seeder.cs b/exercise.pizzashopapi/Data/Seeder.cs index b588405..84a8ce2 100644 --- a/exercise.pizzashopapi/Data/Seeder.cs +++ b/exercise.pizzashopapi/Data/Seeder.cs @@ -23,13 +23,45 @@ public async static void SeedPizzaShopApi(this WebApplication app) await db.SaveChangesAsync(); } - if(!db.Orders.Any()) + if (!db.Toppings.Any()) + { + db.Add(new Topping() { Name = "Extra Cheese", Price = 2.50m }); + db.Add(new Topping() { Name = "Olives", Price = 1.75m }); + db.Add(new Topping() { Name = "Bacon", Price = 3.00m }); + await db.SaveChangesAsync(); + } + if (!db.Orders.Any()) { db.Add(new Order() { CustomerId = 1, PizzaId = 2 }); db.Add(new Order() { CustomerId = 2, PizzaId = 1 }); db.Add(new Order() { CustomerId = 3, PizzaId = 3 }); await db.SaveChangesAsync(); } + + /*var cheesePineapplePizza = db.Pizzas.FirstOrDefault(p => p.Name == "Cheese & Pineapple"); + var veganPizza = db.Pizzas.FirstOrDefault(p => p.Name == "Vegan Cheese Tastic"); + var pepperoniPizza = db.Pizzas.FirstOrDefault(p => p.Name == "Pepperoni"); + + var extraCheese = db.Toppings.FirstOrDefault(t => t.Name == "Extra Cheese"); + var olives = db.Toppings.FirstOrDefault(t => t.Name == "Olives"); + var bacon = db.Toppings.FirstOrDefault(t => t.Name == "Bacon"); + + if (cheesePineapplePizza != null && extraCheese != null) + { + cheesePineapplePizza.Toppings.Add(extraCheese); + } + + if (veganPizza != null && olives != null) + { + veganPizza.Toppings.Add(olives); + } + + if (pepperoniPizza != null && bacon != null) + { + pepperoniPizza.Toppings.Add(bacon); + } + + await db.SaveChangesAsync();*/ } } } diff --git a/exercise.pizzashopapi/EndPoints/PizzaShopEndpoint.cs b/exercise.pizzashopapi/EndPoints/PizzaShopEndpoint.cs index b5148a6..19d2ef6 100644 --- a/exercise.pizzashopapi/EndPoints/PizzaShopEndpoint.cs +++ b/exercise.pizzashopapi/EndPoints/PizzaShopEndpoint.cs @@ -11,6 +11,7 @@ public static void ConfigurePizzaShopEndpoint(this WebApplication app) var customerGroup = app.MapGroup("customer"); var orderGroup = app.MapGroup("order"); var pizzaGroup = app.MapGroup("pizza"); + var toppingGroup = app.MapGroup("topping"); customerGroup.MapPost("/", CreateCustomer); customerGroup.MapGet("/", GetCustomers); @@ -23,12 +24,20 @@ public static void ConfigurePizzaShopEndpoint(this WebApplication app) orderGroup.MapGet("/{id}", GetOrderById); orderGroup.MapPut("/{id}", UpdateOrder); orderGroup.MapDelete("/{id}", DeleteOrder); + orderGroup.MapPost("/{orderId}/topping/{toppingId}", AddToppingToOrder); pizzaGroup.MapPost("/", CreatePizza); pizzaGroup.MapGet("/", GetPizzas); pizzaGroup.MapGet("/{id}", GetPizzaById); pizzaGroup.MapPut("/{id}", UpdatePizza); pizzaGroup.MapDelete("/{id}", DeletePizza); + pizzaGroup.MapPost("/{pizzaId}/topping/{toppingId}", AddToppingToPizza); + + toppingGroup.MapPost("/", CreateTopping); + toppingGroup.MapGet("/", GetToppings); + toppingGroup.MapGet("/{id}", GetToppingById); + toppingGroup.MapPut("/{id}", UpdateTopping); + toppingGroup.MapDelete("/{id}", DeleteTopping); } #region customer @@ -88,32 +97,30 @@ public static async Task DeleteCustomer(IRepository customerR public static async Task CreateOrder(IRepository orderRepository, IRepository customerRepository, IRepository pizzaRepository, int customerId, int pizzaId) { - // Check if the customer exists var customer = await customerRepository.GetById(customerId); if (customer == null) { return TypedResults.NotFound(new { Message = "Customer not found" }); } - // Check if the pizza exists var pizza = await pizzaRepository.GetById(pizzaId); if (pizza == null) { return TypedResults.NotFound(new { Message = "Pizza not found" }); } - // Create the order var order = new Order { CustomerId = customerId, PizzaId = pizzaId }; var createdOrder = await orderRepository.Insert(order); return TypedResults.Created($"/order/{createdOrder.Id}", createdOrder); } - public static async Task GetOrders(IRepository orderRepository) { var orders = await orderRepository.GetWithIncludes( o => o.customer, - o => o.pizza + o => o.pizza, + o => o.pizza.Toppings, + o => o.OrderToppings ); return TypedResults.Ok(orders); } @@ -122,7 +129,10 @@ public static async Task GetOrderById(IRepository orderRepositor { var orders = await orderRepository.GetWithIncludes( o => o.customer, - o => o.pizza + o => o.pizza, + o => o.pizza.Toppings, + o => o.OrderToppings + ); var order = orders.FirstOrDefault(o => o.Id == id); @@ -133,7 +143,6 @@ public static async Task GetOrderById(IRepository orderRepositor return TypedResults.Ok(order); } - public static async Task UpdateOrder( IRepository orderRepository, IRepository customerRepository, @@ -142,7 +151,13 @@ public static async Task UpdateOrder( int? customerId, int? pizzaId) { - var orders = await orderRepository.GetWithIncludes(o => o.customer, o => o.pizza); + var orders = await orderRepository.GetWithIncludes( + o => o.customer, + o => o.pizza, + o => o.pizza.Toppings, + o => o.OrderToppings + ); + var order = orders.FirstOrDefault(o => o.Id == id); if (order == null) @@ -174,10 +189,15 @@ public static async Task UpdateOrder( return TypedResults.Ok(updatedOrder); } - public static async Task DeleteOrder(IRepository orderRepository, int id) { - var orders = await orderRepository.GetWithIncludes(o => o.customer, o => o.pizza); + var orders = await orderRepository.GetWithIncludes( + o => o.customer, + o => o.pizza, + o => o.pizza.Toppings, + o => o.OrderToppings + ); + var order = orders.FirstOrDefault(o => o.Id == id); if (order == null) @@ -190,7 +210,6 @@ public static async Task DeleteOrder(IRepository orderRepository return TypedResults.Ok(deletedOrder); } - #endregion #region pizza @@ -251,5 +270,109 @@ public static async Task DeletePizza(IRepository pizzaRepository } #endregion + + #region topping + + public static async Task CreateTopping(IRepository toppingRepository, string name, decimal price) + { + var topping = new Topping { Name = name, Price = price }; + var createdTopping = await toppingRepository.Insert(topping); + return TypedResults.Created($"/topping/{createdTopping.Id}", createdTopping); + } + + public static async Task GetToppings(IRepository toppingRepository) + { + var toppings = await toppingRepository.Get(); + return TypedResults.Ok(toppings); + } + + public static async Task GetToppingById(IRepository toppingRepository, int id) + { + var topping = await toppingRepository.GetById(id); + if (topping == null) + { + return TypedResults.NotFound(new { Message = "Topping not found" }); + } + return TypedResults.Ok(topping); + } + + public static async Task UpdateTopping(IRepository toppingRepository, int id, string? name, decimal? price) + { + var topping = await toppingRepository.GetById(id); + if (topping == null) + { + return TypedResults.NotFound(new { Message = "Topping not found" }); + } + + if (!string.IsNullOrWhiteSpace(name)) + { + topping.Name = name; + } + if (price >= 0 && price != null) + { + topping.Price = (decimal)price; + } + + var updatedTopping = await toppingRepository.Update(topping); + return TypedResults.Ok(updatedTopping); + } + + public static async Task DeleteTopping(IRepository toppingRepository, int id) + { + var topping = await toppingRepository.GetById(id); + if (topping == null) + { + return TypedResults.NotFound(new { Message = "Topping not found" }); + } + var deletedTopping = await toppingRepository.Delete(id); + return TypedResults.Ok(deletedTopping); + } + + public static async Task AddToppingToPizza( + IRepository pizzaRepository, + IRepository toppingRepository, + int pizzaId, + int toppingId) + { + var pizza = await pizzaRepository.GetWithIncludes(p => p.Toppings); + var selectedPizza = pizza.FirstOrDefault(p => p.Id == pizzaId); + + if (selectedPizza == null) + return TypedResults.NotFound(new { Message = "Pizza not found" }); + + var topping = await toppingRepository.GetById(toppingId); + + if (topping == null) + return TypedResults.NotFound(new { Message = "Topping not found" }); + + selectedPizza.Toppings ??= new List(); + selectedPizza.Toppings.Add(topping); + + await pizzaRepository.Update(selectedPizza); + return TypedResults.Ok(selectedPizza); + } + + public static async Task AddToppingToOrder( + IRepository orderToppingRepository, + IRepository orderRepository, + IRepository toppingRepository, + int orderId, + int toppingId) + { + var order = await orderRepository.GetById(orderId); + if (order == null) + return TypedResults.NotFound(new { Message = "Order not found" }); + + var topping = await toppingRepository.GetById(toppingId); + if (topping == null) + return TypedResults.NotFound(new { Message = "Topping not found" }); + + var orderTopping = new OrderTopping { OrderId = orderId, ToppingId = toppingId }; + var createdOrderTopping = await orderToppingRepository.Insert(orderTopping); + return TypedResults.Ok(createdOrderTopping); + } + + #endregion + } } diff --git a/exercise.pizzashopapi/Models/Order.cs b/exercise.pizzashopapi/Models/Order.cs index b883940..5bbb3b2 100644 --- a/exercise.pizzashopapi/Models/Order.cs +++ b/exercise.pizzashopapi/Models/Order.cs @@ -14,11 +14,15 @@ public class Order [Column("customer_id")] public int CustomerId { get; set; } + public Customer customer { get; set; } + [ForeignKey("pizza")] [Column("pizza_id")] public int PizzaId { get; set; } - public Customer customer { get; set; } public Pizza pizza { get; set; } + + public List? OrderToppings { get; set; } + } } diff --git a/exercise.pizzashopapi/Models/OrderTopping.cs b/exercise.pizzashopapi/Models/OrderTopping.cs new file mode 100644 index 0000000..62bed73 --- /dev/null +++ b/exercise.pizzashopapi/Models/OrderTopping.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; + +namespace exercise.pizzashopapi.Models +{ + [Table("order_topping")] + public class OrderTopping + { + [Key] + [Column("id")] + public int Id { get; set; } + + [ForeignKey("order")] + [Column("order_id")] + public int OrderId { get; set; } + + [JsonIgnore] + public Order Order { get; set; } + + [ForeignKey("topping")] + [Column("topping_id")] + public int ToppingId { get; set; } + + // [JsonIgnore] + public Topping Topping { get; set; } + } +} diff --git a/exercise.pizzashopapi/Models/Pizza.cs b/exercise.pizzashopapi/Models/Pizza.cs index 77d3c70..8291395 100644 --- a/exercise.pizzashopapi/Models/Pizza.cs +++ b/exercise.pizzashopapi/Models/Pizza.cs @@ -15,5 +15,6 @@ public class Pizza [Column("price")] public decimal Price { get; set; } + public List? Toppings { get; set; } } } \ No newline at end of file diff --git a/exercise.pizzashopapi/Models/Topping.cs b/exercise.pizzashopapi/Models/Topping.cs new file mode 100644 index 0000000..aecdbbc --- /dev/null +++ b/exercise.pizzashopapi/Models/Topping.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace exercise.pizzashopapi.Models +{ + [Table("topping")] + public class Topping + { + [Key] + [Column("id")] + public int Id { get; set; } + + [Column("name")] + public string Name { get; set; } + + [Column("price")] + public decimal Price { get; set; } + } +} diff --git a/exercise.pizzashopapi/Program.cs b/exercise.pizzashopapi/Program.cs index a563483..b4f28b2 100644 --- a/exercise.pizzashopapi/Program.cs +++ b/exercise.pizzashopapi/Program.cs @@ -9,6 +9,8 @@ builder.Services.AddScoped, Repository>(); builder.Services.AddScoped, Repository>(); builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); builder.Services.AddDbContext(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); From ce985894e003f0e682db436bee536bbae750fe16 Mon Sep 17 00:00:00 2001 From: Jone <64496635+JoneTheBuilder@users.noreply.github.com> Date: Mon, 27 Jan 2025 12:47:46 +0100 Subject: [PATCH 3/4] Done with two extension tasks: Add extra toppings to Pizzas and allow customers to add toppings to their order. Add a new table for Toppings and a new table for OrderToppings. Add any endpoints you think necessary to add toppings. Assume that Pizzas take 3 minutes to prepare and 12 minutes to cook in the oven. Modify your code so your customers see at what stage their order is and add an endpoint so the delivery drivers app can set the order as Delivered --- exercise.pizzashopapi/Data/Seeder.cs | 25 ------ .../EndPoints/PizzaShopEndpoint.cs | 78 +++++++++++++++++-- exercise.pizzashopapi/Models/Order.cs | 14 ++++ .../OrderStatusUpdaterService.cs | 50 ++++++++++++ exercise.pizzashopapi/Program.cs | 5 ++ 5 files changed, 140 insertions(+), 32 deletions(-) create mode 100644 exercise.pizzashopapi/OrderStatusUpdaterService.cs diff --git a/exercise.pizzashopapi/Data/Seeder.cs b/exercise.pizzashopapi/Data/Seeder.cs index 84a8ce2..4cd6ed0 100644 --- a/exercise.pizzashopapi/Data/Seeder.cs +++ b/exercise.pizzashopapi/Data/Seeder.cs @@ -37,31 +37,6 @@ public async static void SeedPizzaShopApi(this WebApplication app) db.Add(new Order() { CustomerId = 3, PizzaId = 3 }); await db.SaveChangesAsync(); } - - /*var cheesePineapplePizza = db.Pizzas.FirstOrDefault(p => p.Name == "Cheese & Pineapple"); - var veganPizza = db.Pizzas.FirstOrDefault(p => p.Name == "Vegan Cheese Tastic"); - var pepperoniPizza = db.Pizzas.FirstOrDefault(p => p.Name == "Pepperoni"); - - var extraCheese = db.Toppings.FirstOrDefault(t => t.Name == "Extra Cheese"); - var olives = db.Toppings.FirstOrDefault(t => t.Name == "Olives"); - var bacon = db.Toppings.FirstOrDefault(t => t.Name == "Bacon"); - - if (cheesePineapplePizza != null && extraCheese != null) - { - cheesePineapplePizza.Toppings.Add(extraCheese); - } - - if (veganPizza != null && olives != null) - { - veganPizza.Toppings.Add(olives); - } - - if (pepperoniPizza != null && bacon != null) - { - pepperoniPizza.Toppings.Add(bacon); - } - - await db.SaveChangesAsync();*/ } } } diff --git a/exercise.pizzashopapi/EndPoints/PizzaShopEndpoint.cs b/exercise.pizzashopapi/EndPoints/PizzaShopEndpoint.cs index 19d2ef6..c59ddad 100644 --- a/exercise.pizzashopapi/EndPoints/PizzaShopEndpoint.cs +++ b/exercise.pizzashopapi/EndPoints/PizzaShopEndpoint.cs @@ -25,6 +25,8 @@ public static void ConfigurePizzaShopEndpoint(this WebApplication app) orderGroup.MapPut("/{id}", UpdateOrder); orderGroup.MapDelete("/{id}", DeleteOrder); orderGroup.MapPost("/{orderId}/topping/{toppingId}", AddToppingToOrder); + orderGroup.MapPut("/{id}/delivered", SetOrderAsDelivered); + orderGroup.MapGet("/{id}/status", GetOrderStatus); pizzaGroup.MapPost("/", CreatePizza); pizzaGroup.MapGet("/", GetPizzas); @@ -95,7 +97,12 @@ public static async Task DeleteCustomer(IRepository customerR #region order - public static async Task CreateOrder(IRepository orderRepository, IRepository customerRepository, IRepository pizzaRepository, int customerId, int pizzaId) + public static async Task CreateOrder( + IRepository orderRepository, + IRepository customerRepository, + IRepository pizzaRepository, + int customerId, + int pizzaId) { var customer = await customerRepository.GetById(customerId); if (customer == null) @@ -109,22 +116,48 @@ public static async Task CreateOrder(IRepository orderRepository return TypedResults.NotFound(new { Message = "Pizza not found" }); } - var order = new Order { CustomerId = customerId, PizzaId = pizzaId }; + var order = new Order + { + CustomerId = customerId, + PizzaId = pizzaId, + Status = "Preparing", + CreatedAt = DateTime.UtcNow + }; + var createdOrder = await orderRepository.Insert(order); + return TypedResults.Created($"/order/{createdOrder.Id}", createdOrder); } + public static async Task GetOrders(IRepository orderRepository) { var orders = await orderRepository.GetWithIncludes( - o => o.customer, - o => o.pizza, - o => o.pizza.Toppings, - o => o.OrderToppings + o => o.customer, + o => o.pizza, + o => o.pizza.Toppings, + o => o.OrderToppings ); - return TypedResults.Ok(orders); + + var orderDetails = orders.Select(order => new + { + order.Id, + order.CustomerId, + Customer = order.customer, + order.PizzaId, + Pizza = order.pizza, + Toppings = order.pizza?.Toppings, + OrderToppings = order.OrderToppings, + order.Status, + order.CreatedAt, + order.CookingStartedAt, + order.DeliveredAt + }).ToList(); + + return TypedResults.Ok(orderDetails); } + public static async Task GetOrderById(IRepository orderRepository, int id) { var orders = await orderRepository.GetWithIncludes( @@ -210,6 +243,37 @@ public static async Task DeleteOrder(IRepository orderRepository return TypedResults.Ok(deletedOrder); } + public static async Task SetOrderAsDelivered( + IRepository orderRepository, + int orderId) + { + var order = await orderRepository.GetById(orderId); + if (order == null) + { + return TypedResults.NotFound(new { Message = "Order not found" }); + } + + order.Status = "Delivered"; + order.DeliveredAt = DateTime.UtcNow; + + var updatedOrder = await orderRepository.Update(order); + + return TypedResults.Ok(updatedOrder); + } + + public static async Task GetOrderStatus( + IRepository orderRepository, + int orderId) + { + var order = await orderRepository.GetById(orderId); + if (order == null) + { + return TypedResults.NotFound(new { Message = "Order not found" }); + } + + return TypedResults.Ok(new { order.Id, order.Status }); + } + #endregion #region pizza diff --git a/exercise.pizzashopapi/Models/Order.cs b/exercise.pizzashopapi/Models/Order.cs index 5bbb3b2..76394a4 100644 --- a/exercise.pizzashopapi/Models/Order.cs +++ b/exercise.pizzashopapi/Models/Order.cs @@ -24,5 +24,19 @@ public class Order public List? OrderToppings { get; set; } + [Column("status")] + public string Status { get; set; } = "Preparing"; + + [Column("created_at")] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + //[Column("estimated_delivery_time")] + //public DateTime? EstimatedDeliveryTime { get; set; } + + [Column("cooking_started_at")] + public DateTime? CookingStartedAt { get; set; } + + [Column("delivered_at")] + public DateTime? DeliveredAt { get; set; } } } diff --git a/exercise.pizzashopapi/OrderStatusUpdaterService.cs b/exercise.pizzashopapi/OrderStatusUpdaterService.cs new file mode 100644 index 0000000..5fbb7fd --- /dev/null +++ b/exercise.pizzashopapi/OrderStatusUpdaterService.cs @@ -0,0 +1,50 @@ +using exercise.pizzashopapi.Models; +using exercise.pizzashopapi.Repository; + +public class OrderStatusUpdaterService : BackgroundService +{ + private readonly IServiceScopeFactory _scopeFactory; + + public OrderStatusUpdaterService(IServiceScopeFactory scopeFactory) + { + _scopeFactory = scopeFactory; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + using (var scope = _scopeFactory.CreateScope()) + { + var orderRepository = scope.ServiceProvider.GetRequiredService>(); + + await UpdateOrderStatuses(orderRepository); + } + + await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); + } + } + + private async Task UpdateOrderStatuses(IRepository orderRepository) + { + var orders = await orderRepository.Get(); + + foreach (var order in orders) + { + if (order.Status == "Preparing" && DateTime.UtcNow - order.CreatedAt > TimeSpan.FromMinutes(3)) + { + order.Status = "Cooking"; + order.CookingStartedAt = DateTime.UtcNow; + + await orderRepository.Update(order); + } + else if (order.Status == "Cooking" && order.CookingStartedAt != null && DateTime.UtcNow - order.CookingStartedAt.Value > TimeSpan.FromMinutes(15)) + { + order.Status = "Delivered"; + order.DeliveredAt = DateTime.UtcNow; + + await orderRepository.Update(order); + } + } + } +} diff --git a/exercise.pizzashopapi/Program.cs b/exercise.pizzashopapi/Program.cs index b4f28b2..a82059b 100644 --- a/exercise.pizzashopapi/Program.cs +++ b/exercise.pizzashopapi/Program.cs @@ -12,6 +12,11 @@ builder.Services.AddScoped, Repository>(); builder.Services.AddScoped, Repository>(); builder.Services.AddDbContext(); + +builder.Services.AddHostedService(); + + + builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); From 34f56be6bd8731f701e6fb34d99c046ad37b46f8 Mon Sep 17 00:00:00 2001 From: Jone <64496635+JoneTheBuilder@users.noreply.github.com> Date: Mon, 27 Jan 2025 12:54:37 +0100 Subject: [PATCH 4/4] Moved OrderStatusUpdaterService.cs to Services folder. --- exercise.pizzashopapi/Data/Seeder.cs | 2 +- exercise.pizzashopapi/EndPoints/PizzaShopEndpoint.cs | 2 -- exercise.pizzashopapi/Program.cs | 2 -- .../{ => Services}/OrderStatusUpdaterService.cs | 0 4 files changed, 1 insertion(+), 5 deletions(-) rename exercise.pizzashopapi/{ => Services}/OrderStatusUpdaterService.cs (100%) diff --git a/exercise.pizzashopapi/Data/Seeder.cs b/exercise.pizzashopapi/Data/Seeder.cs index 4cd6ed0..c336f8f 100644 --- a/exercise.pizzashopapi/Data/Seeder.cs +++ b/exercise.pizzashopapi/Data/Seeder.cs @@ -10,7 +10,7 @@ public async static void SeedPizzaShopApi(this WebApplication app) { if(!db.Customers.Any()) { - db.Add(new Customer() { Name="Nigel" }); + db.Add(new Customer() { Name = "Nigel" }); db.Add(new Customer() { Name = "Dave" }); db.Add(new Customer() { Name = "Jone" }); await db.SaveChangesAsync(); diff --git a/exercise.pizzashopapi/EndPoints/PizzaShopEndpoint.cs b/exercise.pizzashopapi/EndPoints/PizzaShopEndpoint.cs index c59ddad..736292a 100644 --- a/exercise.pizzashopapi/EndPoints/PizzaShopEndpoint.cs +++ b/exercise.pizzashopapi/EndPoints/PizzaShopEndpoint.cs @@ -157,7 +157,6 @@ public static async Task GetOrders(IRepository orderRepository) return TypedResults.Ok(orderDetails); } - public static async Task GetOrderById(IRepository orderRepository, int id) { var orders = await orderRepository.GetWithIncludes( @@ -437,6 +436,5 @@ public static async Task AddToppingToOrder( } #endregion - } } diff --git a/exercise.pizzashopapi/Program.cs b/exercise.pizzashopapi/Program.cs index a82059b..6f77bed 100644 --- a/exercise.pizzashopapi/Program.cs +++ b/exercise.pizzashopapi/Program.cs @@ -15,8 +15,6 @@ builder.Services.AddHostedService(); - - builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); diff --git a/exercise.pizzashopapi/OrderStatusUpdaterService.cs b/exercise.pizzashopapi/Services/OrderStatusUpdaterService.cs similarity index 100% rename from exercise.pizzashopapi/OrderStatusUpdaterService.cs rename to exercise.pizzashopapi/Services/OrderStatusUpdaterService.cs