From 473913f2836ce368d78d5962c2a12a82b62e6f30 Mon Sep 17 00:00:00 2001 From: Espen Solhaug Date: Mon, 27 Jan 2025 14:53:18 +0100 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=85=20Core?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DTOs/request/CustomerPut.cs | 7 + .../DTOs/request/OrderPost.cs | 8 + .../DTOs/request/PizzaPost.cs | 8 + .../DTOs/response/CustomerDTO.cs | 8 + .../DTOs/response/OrderCustomerDTO.cs | 8 + .../DTOs/response/OrderDTO.cs | 11 + .../DTOs/response/OrderPizzaDTO.cs | 8 + .../DTOs/response/PizzaDTO.cs | 9 + exercise.pizzashopapi/Data/DataContext.cs | 20 +- exercise.pizzashopapi/Data/Seeder.cs | 15 +- .../EndPoints/PizzaShopApi.cs | 216 +++++++++++++++++- ...20250127100718_First_Migration.Designer.cs | 118 ++++++++++ .../20250127100718_First_Migration.cs | 91 ++++++++ .../Migrations/DataContextModelSnapshot.cs | 115 ++++++++++ exercise.pizzashopapi/Models/Customer.cs | 9 + exercise.pizzashopapi/Models/Order.cs | 17 +- exercise.pizzashopapi/Models/Pizza.cs | 8 + exercise.pizzashopapi/Program.cs | 14 +- .../Repository/IRepository.cs | 17 +- .../Repository/Repository.cs | 80 ++++++- exercise.pizzashopapi/Tools/MappingProfile.cs | 22 ++ .../exercise.pizzashopapi.csproj | 17 +- 22 files changed, 799 insertions(+), 27 deletions(-) create mode 100644 exercise.pizzashopapi/DTOs/request/CustomerPut.cs create mode 100644 exercise.pizzashopapi/DTOs/request/OrderPost.cs create mode 100644 exercise.pizzashopapi/DTOs/request/PizzaPost.cs create mode 100644 exercise.pizzashopapi/DTOs/response/CustomerDTO.cs create mode 100644 exercise.pizzashopapi/DTOs/response/OrderCustomerDTO.cs create mode 100644 exercise.pizzashopapi/DTOs/response/OrderDTO.cs create mode 100644 exercise.pizzashopapi/DTOs/response/OrderPizzaDTO.cs create mode 100644 exercise.pizzashopapi/DTOs/response/PizzaDTO.cs create mode 100644 exercise.pizzashopapi/Migrations/20250127100718_First_Migration.Designer.cs create mode 100644 exercise.pizzashopapi/Migrations/20250127100718_First_Migration.cs create mode 100644 exercise.pizzashopapi/Migrations/DataContextModelSnapshot.cs create mode 100644 exercise.pizzashopapi/Tools/MappingProfile.cs diff --git a/exercise.pizzashopapi/DTOs/request/CustomerPut.cs b/exercise.pizzashopapi/DTOs/request/CustomerPut.cs new file mode 100644 index 0000000..9d938b9 --- /dev/null +++ b/exercise.pizzashopapi/DTOs/request/CustomerPut.cs @@ -0,0 +1,7 @@ +namespace exercise.pizzashopapi.DTOs.request +{ + public class CustomerPut + { + public string Name { get; set; } + } +} diff --git a/exercise.pizzashopapi/DTOs/request/OrderPost.cs b/exercise.pizzashopapi/DTOs/request/OrderPost.cs new file mode 100644 index 0000000..c1010ae --- /dev/null +++ b/exercise.pizzashopapi/DTOs/request/OrderPost.cs @@ -0,0 +1,8 @@ +namespace exercise.pizzashopapi.DTOs.request +{ + public class OrderPost + { + public int PizzaId { get; set; } + public int CustomerId { get; set; } + } +} diff --git a/exercise.pizzashopapi/DTOs/request/PizzaPost.cs b/exercise.pizzashopapi/DTOs/request/PizzaPost.cs new file mode 100644 index 0000000..300343f --- /dev/null +++ b/exercise.pizzashopapi/DTOs/request/PizzaPost.cs @@ -0,0 +1,8 @@ +namespace exercise.pizzashopapi.DTOs.request +{ + public class PizzaPost + { + public string Name { get; set; } + public decimal Price { get; set; } + } +} diff --git a/exercise.pizzashopapi/DTOs/response/CustomerDTO.cs b/exercise.pizzashopapi/DTOs/response/CustomerDTO.cs new file mode 100644 index 0000000..4692b24 --- /dev/null +++ b/exercise.pizzashopapi/DTOs/response/CustomerDTO.cs @@ -0,0 +1,8 @@ +namespace exercise.pizzashopapi.DTOs.response +{ + public class CustomerDTO + { + public string Name { get; set; } + public List Orders { get; set; } = new List(); + } +} diff --git a/exercise.pizzashopapi/DTOs/response/OrderCustomerDTO.cs b/exercise.pizzashopapi/DTOs/response/OrderCustomerDTO.cs new file mode 100644 index 0000000..15ae966 --- /dev/null +++ b/exercise.pizzashopapi/DTOs/response/OrderCustomerDTO.cs @@ -0,0 +1,8 @@ +namespace exercise.pizzashopapi.DTOs.response +{ + public class OrderCustomerDTO + { + public int PizzaId { get; set; } + public string PizzaName { get; set; } + } +} diff --git a/exercise.pizzashopapi/DTOs/response/OrderDTO.cs b/exercise.pizzashopapi/DTOs/response/OrderDTO.cs new file mode 100644 index 0000000..3461690 --- /dev/null +++ b/exercise.pizzashopapi/DTOs/response/OrderDTO.cs @@ -0,0 +1,11 @@ +namespace exercise.pizzashopapi.DTOs.response +{ + public class OrderDTO + { + public int PizzaId { get; set; } + public int CustomerId { get; set; } + public string PizzaName { get; set; } + public decimal PizzaPrice { get; set; } + public string CustomerName { get; set; } + } +} diff --git a/exercise.pizzashopapi/DTOs/response/OrderPizzaDTO.cs b/exercise.pizzashopapi/DTOs/response/OrderPizzaDTO.cs new file mode 100644 index 0000000..34097fb --- /dev/null +++ b/exercise.pizzashopapi/DTOs/response/OrderPizzaDTO.cs @@ -0,0 +1,8 @@ +namespace exercise.pizzashopapi.DTOs.response +{ + public class OrderPizzaDTO + { + public int CustomerId { get; set; } + public string CustomerName { get; set; } + } +} diff --git a/exercise.pizzashopapi/DTOs/response/PizzaDTO.cs b/exercise.pizzashopapi/DTOs/response/PizzaDTO.cs new file mode 100644 index 0000000..9abfcde --- /dev/null +++ b/exercise.pizzashopapi/DTOs/response/PizzaDTO.cs @@ -0,0 +1,9 @@ +namespace exercise.pizzashopapi.DTOs.response +{ + public class PizzaDTO + { + public string Name { get; set; } + public decimal Price { get; set; } + public List Orders { get; set; } = new List(); + } +} diff --git a/exercise.pizzashopapi/Data/DataContext.cs b/exercise.pizzashopapi/Data/DataContext.cs index 129199e..8c12d7d 100644 --- a/exercise.pizzashopapi/Data/DataContext.cs +++ b/exercise.pizzashopapi/Data/DataContext.cs @@ -10,8 +10,25 @@ public DataContext() { var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); connectionString = configuration.GetValue("ConnectionStrings:DefaultConnectionString"); - } + + //protected override void OnModelCreating(ModelBuilder modelBuilder) + //{ + // // Set up composite key for Order (pizzaId+customerId) + // modelBuilder.Entity() + // .HasKey(o => new { o.PizzaId, o.CustomerId }); + + // modelBuilder.Entity() + // .HasOne(o => o.Pizza) + // .WithMany(p => p.Orders) + // .HasForeignKey(o => o.PizzaId); + + // modelBuilder.Entity() + // .HasOne(o => o.Customer) + // .WithMany(p => p.Orders) + // .HasForeignKey(o => o.CustomerId); + + //} protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseNpgsql(connectionString); @@ -21,6 +38,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) //seed data? } + public DbSet Pizzas { get; set; } public DbSet Customers { get; set; } public DbSet Orders { get; set; } diff --git a/exercise.pizzashopapi/Data/Seeder.cs b/exercise.pizzashopapi/Data/Seeder.cs index f87fbef..56076f2 100644 --- a/exercise.pizzashopapi/Data/Seeder.cs +++ b/exercise.pizzashopapi/Data/Seeder.cs @@ -12,20 +12,27 @@ public async static void SeedPizzaShopApi(this WebApplication app) { db.Add(new Customer() { Name="Nigel" }); db.Add(new Customer() { Name = "Dave" }); + db.Add(new Customer() { Name = "Espen" }); 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 = 123.45M }); + db.Add(new Pizza() { Name = "Vegan Cheese Tastic", Price = 124.89M }); + db.Add(new Pizza() { Name = "Prosciutto", Price = 131.90M }); await db.SaveChangesAsync(); } //order data - if(1==1) + if(!db.Orders.Any()) { - + db.Add(new Order() { CustomerId = 1, PizzaId = 1 }); + db.Add(new Order() { CustomerId = 2, PizzaId = 2 }); + db.Add(new Order() { CustomerId = 3, PizzaId = 3 }); + db.Add(new Order() { CustomerId = 2, PizzaId = 1 }); + db.Add(new Order() { CustomerId = 1, PizzaId = 3 }); + 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 index f8be2b0..9e3b59a 100644 --- a/exercise.pizzashopapi/EndPoints/PizzaShopApi.cs +++ b/exercise.pizzashopapi/EndPoints/PizzaShopApi.cs @@ -1,5 +1,10 @@ -using exercise.pizzashopapi.Repository; +using AutoMapper; +using exercise.pizzashopapi.DTOs.request; +using exercise.pizzashopapi.DTOs.response; +using exercise.pizzashopapi.Models; +using exercise.pizzashopapi.Repository; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; namespace exercise.pizzashopapi.EndPoints { @@ -7,9 +12,214 @@ public static class PizzaShopApi { public static void ConfigurePizzaShopApi(this WebApplication app) { - + var pizzaGroup = app.MapGroup("/pizza"); + + pizzaGroup.MapGet("/", GetAllPizza); + pizzaGroup.MapGet("/{id}", GetPizza); + pizzaGroup.MapPost("/", CreatePizza); + pizzaGroup.MapPut("/{id}", EditPizza); + pizzaGroup.MapDelete("/{id}", DeletePizza); + + var customerGroup = app.MapGroup("/customer"); + customerGroup.MapGet("/", GetAllCustomers); + customerGroup.MapGet("/{id}", GetCustomer); + customerGroup.MapPost("/", CreateCustomer); + customerGroup.MapPost("/{customerId}/{pizzaId}", OrderPizza); + customerGroup.MapPut("/{id}", EditCustomer); + customerGroup.MapDelete("/{id}", DeleteCustomer); + + var orderGroup = app.MapGroup("/orders"); + orderGroup.MapGet("/", GetAllOrders); + orderGroup.MapGet("/{id}", GetOrder); + orderGroup.MapDelete("/{id}", DeleteOrder); + + } + + private static async Task GetAllPizza(IRepository repository, IMapper mapper) + { + var pizzas = await repository.GetWithNestedIncludes(query => + query.Include(pizzas => pizzas.Orders) + .ThenInclude(o => o.Customer) + ); + var response = mapper.Map>(pizzas); + return TypedResults.Ok(response); + } + + private static async Task GetAllCustomers(IRepository repository, IMapper mapper) + { + var customer = await repository.GetWithNestedIncludes(query => + query.Include(cust => cust.Orders) + .ThenInclude(o => o.Pizza) + ); + var response = mapper.Map>(customer); + return TypedResults.Ok(response); } - + private static async Task GetPizza(IRepository repository, int id, IMapper mapper) + { + Pizza pizza = await repository.GetQueryable() + .Include(p => p.Orders) + .ThenInclude(o => o.Customer) + .FirstOrDefaultAsync(p => p.Id == id); + + if(pizza == null) + { + return TypedResults.NotFound($"Pizza with ID {id} not found"); + } + return TypedResults.Ok(mapper.Map(pizza)); + } + + + private static async Task EditPizza(IRepository repository, int id, PizzaPost pizza, IMapper mapper) + { + if(pizza == null) + { + return TypedResults.BadRequest("Pizza is null"); + } + Pizza pizzaToUpdate = await repository.GetQueryable() + .Include(c => c.Orders) + .ThenInclude(o => o.Customer) + .FirstOrDefaultAsync(p => p.Id == id); + if(pizzaToUpdate == null) + { + return TypedResults.NotFound($"Customer with ID {id} not found"); + } + pizzaToUpdate.Price = pizza.Price; + pizzaToUpdate.Name = pizza.Name; + await repository.Update(pizzaToUpdate); + return TypedResults.Ok(mapper.Map(pizzaToUpdate)); + } + + private static async Task DeletePizza(IRepository pizzaRepository, IRepository orderRepository, int id, IMapper mapper) + { + Pizza pizzaToDelete = await pizzaRepository.GetById(id); + if(pizzaToDelete == null) + { + return TypedResults.NotFound($"Customer with ID {id} not found"); + } + var pizzaOrders = orderRepository.GetQueryable() + .Where(o => o.PizzaId == id).ToList(); + foreach (Order order in pizzaOrders) + { + await orderRepository.Delete(order.Id); + } + await pizzaRepository.Delete(id); + return TypedResults.Ok(mapper.Map(pizzaToDelete)); + } + private static async Task CreatePizza(IRepository repository, PizzaPost payload, IMapper mapper) + { + Pizza pizza = new Pizza(payload.Name, payload.Price); + var inserted = await repository.Insert(pizza); + return TypedResults.Created( + $"https://localhost:7010/pizza/{inserted.Id}", + new PizzaDTO { Name = inserted.Name, Price = inserted.Price }); + } + + private static async Task GetCustomer(IRepository repository, int id, IMapper mapper) + { + Customer customer = await repository.GetQueryable() + .Include(c => c.Orders) + .ThenInclude(o => o.Pizza) + .FirstOrDefaultAsync(p => p.Id == id); + + if(customer == null) + { + return TypedResults.NotFound($"Customer with ID {id} not found"); + } + return TypedResults.Ok(mapper.Map(customer)); + } + + private static async Task CreateCustomer(IRepository repository, CustomerPut payload, IMapper mapper) + { + Customer customer = new Customer(payload.Name); + var inserted = await repository.Insert(customer); + return TypedResults.Created($"https://localhost:7010/customer/{inserted.Id}", + new CustomerDTO { Name = inserted.Name }); + } + private static async Task OrderPizza(IRepository orderRepository, IRepository pizzaRepository, IRepository customerRepository, OrderPost payload, IMapper mapper) + { + Customer customer = await customerRepository.GetById(payload.CustomerId); + Pizza pizza = await pizzaRepository.GetById(payload.PizzaId); + + if(customer == null || pizza == null) + { + return TypedResults.BadRequest("Customer or Pizza not found"); + } + Order order = new Order(payload.PizzaId, payload.CustomerId, pizza, customer); + await orderRepository.Insert(order); + return TypedResults.Ok(mapper.Map(order)); + } + private static async Task EditCustomer(IRepository repository, int id, CustomerPut customer, IMapper mapper) + { + if(customer == null) + { + return TypedResults.BadRequest("Customer is null"); + } + Customer customerToUpdate = await repository.GetQueryable() + .Include(c => c.Orders) + .ThenInclude(o => o.Pizza) + .FirstOrDefaultAsync(p => p.Id == id); + if (customerToUpdate == null) + { + return TypedResults.NotFound($"Customer with ID {id} not found"); + } + customerToUpdate.Name = customer.Name; + await repository.Update(customerToUpdate); + return TypedResults.Ok(mapper.Map(customerToUpdate)); + } + + private static async Task DeleteCustomer(IRepository customerRepository, IRepository orderRepository, int id, IMapper mapper) + { + Customer customerToDelete = await customerRepository.GetById(id); + if(customerToDelete == null) + { + return TypedResults.NotFound($"Customer with ID {id} not found"); + } + var customerOrders = orderRepository.GetQueryable() + .Where(o => o.CustomerId == id).ToList(); + foreach(Order order in customerOrders) + { + await orderRepository.Delete(order.Id); + } + await customerRepository.Delete(id); + return TypedResults.Ok(mapper.Map(customerToDelete)); + } + private static async Task GetOrder(IRepository repository, int id, IMapper mapper) + { + Order order = await repository.GetQueryable() + .Include(o => o.Pizza) + .Include(o => o.Customer) + .FirstOrDefaultAsync(o => o.Id == id); + if(order == null) + { + return TypedResults.NotFound($"Order with ID {id} not found"); + } + return TypedResults.Ok(mapper.Map(order)); + } + + private static async Task GetAllOrders(IRepository repository, IMapper mapper) + { + List orders = await repository.GetQueryable() + .Include(o => o.Pizza) + .Include(o => o.Customer) + .ToListAsync(); + return TypedResults.Ok(mapper.Map>(orders)); + } + private static async Task DeleteOrder(IRepository repository, int id, IMapper mapper) + { + Order order = await repository.GetQueryable() + .Include(o => o.Pizza) + .Include(o => o.Customer) + .FirstOrDefaultAsync(o => o.Id == id); + if (order == null) + { + return TypedResults.NotFound($"Order with ID {id} not found"); + } + await repository.Delete(id); + + return TypedResults.Ok(mapper.Map(order)); + } + + } } diff --git a/exercise.pizzashopapi/Migrations/20250127100718_First_Migration.Designer.cs b/exercise.pizzashopapi/Migrations/20250127100718_First_Migration.Designer.cs new file mode 100644 index 0000000..2fc8bb2 --- /dev/null +++ b/exercise.pizzashopapi/Migrations/20250127100718_First_Migration.Designer.cs @@ -0,0 +1,118 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using exercise.pizzashopapi.Data; + +#nullable disable + +namespace exercise.pizzashopapi.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20250127100718_First_Migration")] + partial class First_Migration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("PizzaId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("PizzaId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Pizza", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("Pizzas"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Order", b => + { + b.HasOne("exercise.pizzashopapi.Models.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("exercise.pizzashopapi.Models.Pizza", "Pizza") + .WithMany("Orders") + .HasForeignKey("PizzaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Pizza"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Pizza", b => + { + b.Navigation("Orders"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/exercise.pizzashopapi/Migrations/20250127100718_First_Migration.cs b/exercise.pizzashopapi/Migrations/20250127100718_First_Migration.cs new file mode 100644 index 0000000..1ddf4a1 --- /dev/null +++ b/exercise.pizzashopapi/Migrations/20250127100718_First_Migration.cs @@ -0,0 +1,91 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace exercise.pizzashopapi.Migrations +{ + /// + public partial class First_Migration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Customers", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Customers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Pizzas", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "text", nullable: false), + Price = table.Column(type: "numeric", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Pizzas", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Orders", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + PizzaId = table.Column(type: "integer", nullable: false), + CustomerId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Orders", x => x.Id); + table.ForeignKey( + name: "FK_Orders_Customers_CustomerId", + column: x => x.CustomerId, + principalTable: "Customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Orders_Pizzas_PizzaId", + column: x => x.PizzaId, + principalTable: "Pizzas", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Orders_CustomerId", + table: "Orders", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_Orders_PizzaId", + table: "Orders", + column: "PizzaId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Orders"); + + migrationBuilder.DropTable( + name: "Customers"); + + migrationBuilder.DropTable( + name: "Pizzas"); + } + } +} diff --git a/exercise.pizzashopapi/Migrations/DataContextModelSnapshot.cs b/exercise.pizzashopapi/Migrations/DataContextModelSnapshot.cs new file mode 100644 index 0000000..34a6ee8 --- /dev/null +++ b/exercise.pizzashopapi/Migrations/DataContextModelSnapshot.cs @@ -0,0 +1,115 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using exercise.pizzashopapi.Data; + +#nullable disable + +namespace exercise.pizzashopapi.Migrations +{ + [DbContext(typeof(DataContext))] + partial class DataContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Customers"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CustomerId") + .HasColumnType("integer"); + + b.Property("PizzaId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("PizzaId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Pizza", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("Pizzas"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Order", b => + { + b.HasOne("exercise.pizzashopapi.Models.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("exercise.pizzashopapi.Models.Pizza", "Pizza") + .WithMany("Orders") + .HasForeignKey("PizzaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Pizza"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Pizza", b => + { + b.Navigation("Orders"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/exercise.pizzashopapi/Models/Customer.cs b/exercise.pizzashopapi/Models/Customer.cs index 2ca83bd..63aab6c 100644 --- a/exercise.pizzashopapi/Models/Customer.cs +++ b/exercise.pizzashopapi/Models/Customer.cs @@ -6,5 +6,14 @@ public class Customer { public int Id { get; set; } public string Name { get; set; } + public List Orders { get; set; } = new List(); + public Customer() { } + + public Customer(string name) + { + Name = name; + } } + + } diff --git a/exercise.pizzashopapi/Models/Order.cs b/exercise.pizzashopapi/Models/Order.cs index fbe6113..84c9f49 100644 --- a/exercise.pizzashopapi/Models/Order.cs +++ b/exercise.pizzashopapi/Models/Order.cs @@ -4,7 +4,20 @@ namespace exercise.pizzashopapi.Models { public class Order { - - + public int Id { get; set; } + public int PizzaId { get; set; } + public int CustomerId { get; set; } + + public Pizza Pizza { get; set; } + public Customer Customer { get; set; } + + public Order() { } + public Order(int pizzaId, int customerId, Pizza pizza, Customer customer) + { + PizzaId = pizzaId; + CustomerId = customerId; + Pizza = pizza; + Customer = customer; + } } } diff --git a/exercise.pizzashopapi/Models/Pizza.cs b/exercise.pizzashopapi/Models/Pizza.cs index 5c085ec..3ba5fa9 100644 --- a/exercise.pizzashopapi/Models/Pizza.cs +++ b/exercise.pizzashopapi/Models/Pizza.cs @@ -8,5 +8,13 @@ public class Pizza public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } + public List Orders { get; set; } = new List(); + + public Pizza() { } + public Pizza(string name, decimal price) + { + Name = name; + Price = price; + } } } \ No newline at end of file diff --git a/exercise.pizzashopapi/Program.cs b/exercise.pizzashopapi/Program.cs index c04a440..deaaffd 100644 --- a/exercise.pizzashopapi/Program.cs +++ b/exercise.pizzashopapi/Program.cs @@ -1,17 +1,29 @@ +using System.Diagnostics; using exercise.pizzashopapi.Data; using exercise.pizzashopapi.EndPoints; +using exercise.pizzashopapi.Models; using exercise.pizzashopapi.Repository; +using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); -builder.Services.AddScoped(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); builder.Services.AddDbContext(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +builder.Services.AddAutoMapper(typeof(Program)); + +//builder.Services.AddDbContext(options => +//{ +// options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnectionString")); +// options.LogTo(message => Debug.WriteLine(message)); +//}); var app = builder.Build(); diff --git a/exercise.pizzashopapi/Repository/IRepository.cs b/exercise.pizzashopapi/Repository/IRepository.cs index b114ea8..9aa8abf 100644 --- a/exercise.pizzashopapi/Repository/IRepository.cs +++ b/exercise.pizzashopapi/Repository/IRepository.cs @@ -1,11 +1,18 @@ -using exercise.pizzashopapi.Models; +using System.Linq.Expressions; +using exercise.pizzashopapi.Models; namespace exercise.pizzashopapi.Repository { - public interface IRepository + public interface IRepository { - Task> GetOrdersByCustomer(int id); - - + Task GetById(int id); + Task> GetAll(); + Task Insert(T entity); + Task Delete(int id); + Task Update(T entity); + Task> GetWithIncludes(params Expression>[] includes); + Task> GetWithNestedIncludes(params Func, IQueryable>[] includeActions); + IQueryable GetQueryable(); + Task Save(); } } diff --git a/exercise.pizzashopapi/Repository/Repository.cs b/exercise.pizzashopapi/Repository/Repository.cs index e109fce..e129053 100644 --- a/exercise.pizzashopapi/Repository/Repository.cs +++ b/exercise.pizzashopapi/Repository/Repository.cs @@ -1,14 +1,84 @@ -using exercise.pizzashopapi.Data; -using exercise.pizzashopapi.Models; +using System.Linq.Expressions; +using exercise.pizzashopapi.Data; +using Microsoft.EntityFrameworkCore; namespace exercise.pizzashopapi.Repository { - public class Repository : IRepository + public class Repository : IRepository where T : class { private DataContext _db; - public Task> GetOrdersByCustomer(int id) + private DbSet _table = null; + + public Repository(DataContext db) + { + _db = db; + _table = _db.Set(); + } + public async Task GetById(int id) + { + return _table.Find(id); + } + + public async Task> GetAll() + { + return _table.ToList(); + } + + public async Task Insert(T entity) + { + _table.Add(entity); + await _db.SaveChangesAsync(); + return entity; + } + public async Task Update(T entity) + { + _table.Attach(entity); + _db.Entry(entity).State = EntityState.Modified; + await _db.SaveChangesAsync(); + return entity; + } + + public async Task Delete(int id) { - throw new NotImplementedException(); + T entity = _table.Find(id); + _table.Remove(entity); + await _db.SaveChangesAsync(); + return entity; } + + public async Task> GetWithIncludes(params Expression>[] includes) + { + IQueryable query = _table; + + foreach (var include in includes) + { + query = query.Include(include); + } + + return await query.ToListAsync(); + } + + public async Task> GetWithNestedIncludes(params Func, IQueryable>[] includeActions) + { + IQueryable query = _table; + + foreach (var includeAction in includeActions) + { + query = includeAction(query); + } + + return await query.ToListAsync(); + } + + public IQueryable GetQueryable() + { + return _table; + } + + public async Task Save() + { + await _db.SaveChangesAsync(); + } + } } diff --git a/exercise.pizzashopapi/Tools/MappingProfile.cs b/exercise.pizzashopapi/Tools/MappingProfile.cs new file mode 100644 index 0000000..c53828f --- /dev/null +++ b/exercise.pizzashopapi/Tools/MappingProfile.cs @@ -0,0 +1,22 @@ +using AutoMapper; +using exercise.pizzashopapi.DTOs.response; +using exercise.pizzashopapi.Models; + +namespace exercise.pizzashopapi.Tools +{ + public class MappingProfile : Profile + { + public MappingProfile() { + CreateMap() + .ForMember(dest => dest.Orders, opt => opt.MapFrom(src => src.Orders)); + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap() + .ForMember(dest => dest.PizzaName, opt => opt.MapFrom(src => src.Pizza.Name)) + .ForMember(dest => dest.PizzaPrice, opt => opt.MapFrom(src => src.Pizza.Price)) + .ForMember(dest => dest.CustomerName, opt => opt.MapFrom(src => src.Customer.Name)); + + } + } +} diff --git a/exercise.pizzashopapi/exercise.pizzashopapi.csproj b/exercise.pizzashopapi/exercise.pizzashopapi.csproj index 624203b..8bbb9fe 100644 --- a/exercise.pizzashopapi/exercise.pizzashopapi.csproj +++ b/exercise.pizzashopapi/exercise.pizzashopapi.csproj @@ -11,18 +11,23 @@ - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + + + + + From 6483fa456d54ffc217df02cc5c64f79331ef3d4d Mon Sep 17 00:00:00 2001 From: Espen Solhaug Date: Fri, 31 Jan 2025 11:19:18 +0100 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=85=20Extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DTOs/response/OrderCustomerDTO.cs | 1 + .../DTOs/response/OrderToppingDTO.cs | 9 ++ .../DTOs/response/ToppingDTO.cs | 7 + exercise.pizzashopapi/Data/DataContext.cs | 1 + exercise.pizzashopapi/Data/Seeder.cs | 21 ++- .../EndPoints/PizzaShopApi.cs | 63 +++++++- .../20250127100718_First_Migration.cs | 91 ----------- ...ner.cs => 20250130145249_init.Designer.cs} | 83 +++++++++- .../Migrations/20250130145249_init.cs | 148 ++++++++++++++++++ .../Migrations/DataContextModelSnapshot.cs | 79 +++++++++- exercise.pizzashopapi/Models/Customer.cs | 1 + exercise.pizzashopapi/Models/Order.cs | 5 +- exercise.pizzashopapi/Models/OrderToppings.cs | 15 ++ exercise.pizzashopapi/Models/Pizza.cs | 4 +- exercise.pizzashopapi/Models/Topping.cs | 13 ++ exercise.pizzashopapi/Program.cs | 1 + exercise.pizzashopapi/Tools/MappingProfile.cs | 2 + 17 files changed, 433 insertions(+), 111 deletions(-) create mode 100644 exercise.pizzashopapi/DTOs/response/OrderToppingDTO.cs create mode 100644 exercise.pizzashopapi/DTOs/response/ToppingDTO.cs delete mode 100644 exercise.pizzashopapi/Migrations/20250127100718_First_Migration.cs rename exercise.pizzashopapi/Migrations/{20250127100718_First_Migration.Designer.cs => 20250130145249_init.Designer.cs} (58%) create mode 100644 exercise.pizzashopapi/Migrations/20250130145249_init.cs create mode 100644 exercise.pizzashopapi/Models/OrderToppings.cs create mode 100644 exercise.pizzashopapi/Models/Topping.cs diff --git a/exercise.pizzashopapi/DTOs/response/OrderCustomerDTO.cs b/exercise.pizzashopapi/DTOs/response/OrderCustomerDTO.cs index 15ae966..d1b4228 100644 --- a/exercise.pizzashopapi/DTOs/response/OrderCustomerDTO.cs +++ b/exercise.pizzashopapi/DTOs/response/OrderCustomerDTO.cs @@ -4,5 +4,6 @@ public class OrderCustomerDTO { public int PizzaId { get; set; } public string PizzaName { get; set; } + public string OrderStatus { get; set; } } } diff --git a/exercise.pizzashopapi/DTOs/response/OrderToppingDTO.cs b/exercise.pizzashopapi/DTOs/response/OrderToppingDTO.cs new file mode 100644 index 0000000..fa318ed --- /dev/null +++ b/exercise.pizzashopapi/DTOs/response/OrderToppingDTO.cs @@ -0,0 +1,9 @@ +namespace exercise.pizzashopapi.DTOs.response +{ + public class OrderToppingDTO + { + public int OrderId { get; set; } + public int ToppingId { get; set; } + public ToppingDTO Topping { get; set; } + } +} diff --git a/exercise.pizzashopapi/DTOs/response/ToppingDTO.cs b/exercise.pizzashopapi/DTOs/response/ToppingDTO.cs new file mode 100644 index 0000000..d20c963 --- /dev/null +++ b/exercise.pizzashopapi/DTOs/response/ToppingDTO.cs @@ -0,0 +1,7 @@ +namespace exercise.pizzashopapi.DTOs.response +{ + public class ToppingDTO + { + public string Name { get; set; } + } +} diff --git a/exercise.pizzashopapi/Data/DataContext.cs b/exercise.pizzashopapi/Data/DataContext.cs index 8c12d7d..6e16849 100644 --- a/exercise.pizzashopapi/Data/DataContext.cs +++ b/exercise.pizzashopapi/Data/DataContext.cs @@ -42,5 +42,6 @@ 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; } } } diff --git a/exercise.pizzashopapi/Data/Seeder.cs b/exercise.pizzashopapi/Data/Seeder.cs index 56076f2..f2afdd9 100644 --- a/exercise.pizzashopapi/Data/Seeder.cs +++ b/exercise.pizzashopapi/Data/Seeder.cs @@ -24,15 +24,22 @@ public async static void SeedPizzaShopApi(this WebApplication app) } - //order data if(!db.Orders.Any()) { - db.Add(new Order() { CustomerId = 1, PizzaId = 1 }); - db.Add(new Order() { CustomerId = 2, PizzaId = 2 }); - db.Add(new Order() { CustomerId = 3, PizzaId = 3 }); - db.Add(new Order() { CustomerId = 2, PizzaId = 1 }); - db.Add(new Order() { CustomerId = 1, PizzaId = 3 }); - db.Add(new Order() { CustomerId = 3, PizzaId = 3 }); + db.Add(new Order() { CustomerId = 1, PizzaId = 1, CreatedAt = DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Utc) }); + db.Add(new Order() { CustomerId = 2, PizzaId = 2, CreatedAt = DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Utc) }); + db.Add(new Order() { CustomerId = 3, PizzaId = 3, CreatedAt = DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Utc) }); + db.Add(new Order() { CustomerId = 2, PizzaId = 1, CreatedAt = DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Utc) }); + db.Add(new Order() { CustomerId = 1, PizzaId = 3, CreatedAt = DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Utc) }); + db.Add(new Order() { CustomerId = 3, PizzaId = 3, CreatedAt = DateTime.SpecifyKind(DateTime.Now, DateTimeKind.Utc) }); + await db.SaveChangesAsync(); + } + + if(!db.Toppings.Any()) + { + db.Add(new Topping() { Name = "Pepperoni" }); + db.Add(new Topping() { Name = "Ham" }); + db.Add(new Topping() { Name = "Chicken" }); await db.SaveChangesAsync(); } } diff --git a/exercise.pizzashopapi/EndPoints/PizzaShopApi.cs b/exercise.pizzashopapi/EndPoints/PizzaShopApi.cs index 9e3b59a..b087f26 100644 --- a/exercise.pizzashopapi/EndPoints/PizzaShopApi.cs +++ b/exercise.pizzashopapi/EndPoints/PizzaShopApi.cs @@ -31,7 +31,13 @@ public static void ConfigurePizzaShopApi(this WebApplication app) var orderGroup = app.MapGroup("/orders"); orderGroup.MapGet("/", GetAllOrders); orderGroup.MapGet("/{id}", GetOrder); - orderGroup.MapDelete("/{id}", DeleteOrder); + orderGroup.MapDelete("/{id}", DeleteOrder); + + var toppingGroup = app.MapGroup("/topping"); + toppingGroup.MapGet("/", GetAllToppings); + toppingGroup.MapGet("/{id}", GetTopping); + toppingGroup.MapPost("/", CreateTopping); + toppingGroup.MapDelete("/{id}", DeleteTopping); } @@ -194,7 +200,18 @@ private static async Task GetOrder(IRepository repository, int i { return TypedResults.NotFound($"Order with ID {id} not found"); } - return TypedResults.Ok(mapper.Map(order)); + string status = ""; + if (DateTime.Compare(order.CreatedAt, DateTime.Now) < 3) { status = "Preparing"; } + else if (DateTime.Compare(order.CreatedAt, DateTime.Now) < 12) { status = "Cooking"; } + else status = "Delivering"; + + OrderCustomerDTO ocDTO = new OrderCustomerDTO() + { + PizzaId = order.PizzaId, + PizzaName = order.Pizza.Name, + OrderStatus = status + }; + return TypedResults.Ok(ocDTO); } private static async Task GetAllOrders(IRepository repository, IMapper mapper) @@ -220,6 +237,48 @@ private static async Task DeleteOrder(IRepository repository, in return TypedResults.Ok(mapper.Map(order)); } + private static async Task GetTopping(IRepository repository, int id, IMapper mapper) + { + Topping topping = await repository.GetById(id); + if (topping == null) + { + return TypedResults.NotFound($"Topping with ID {id} not found"); + } + return TypedResults.Ok(mapper.Map(topping)); + } + + private static async Task GetAllToppings(IRepository repository, IMapper mapper) + { + var orders = await repository.GetAll(); + return TypedResults.Ok(mapper.Map>(orders)); + } + + private static async Task DeleteTopping(IRepository toppingRepository, IRepository orderRepository, int id, IMapper mapper) + { + Topping toppingToDelete = await toppingRepository.GetById(id); + if (toppingToDelete == null) + { + return TypedResults.NotFound($"Topping with ID {id} not found"); + } + var toppingOrders = orderRepository.GetQueryable() + .Any(o => o.ToppingOrders.Any(o => o.ToppingId == id)); + if(toppingOrders) + { + return TypedResults.BadRequest(TypedResults.BadRequest("Order(s) contains topping, could not delete")); + } + await toppingRepository.Delete(id); + return TypedResults.Ok(mapper.Map(toppingToDelete)); + } + + private static async Task CreateTopping(IRepository repository, string payload, IMapper mapper) + { + Topping topping = new Topping(){ Name = payload }; + var inserted = await repository.Insert(topping); + return TypedResults.Created($"https://localhost:7010/customer/{inserted.Id}", + new ToppingDTO { Name = inserted.Name }); + } + + } } diff --git a/exercise.pizzashopapi/Migrations/20250127100718_First_Migration.cs b/exercise.pizzashopapi/Migrations/20250127100718_First_Migration.cs deleted file mode 100644 index 1ddf4a1..0000000 --- a/exercise.pizzashopapi/Migrations/20250127100718_First_Migration.cs +++ /dev/null @@ -1,91 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace exercise.pizzashopapi.Migrations -{ - /// - public partial class First_Migration : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Customers", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(type: "text", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Customers", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Pizzas", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - Name = table.Column(type: "text", nullable: false), - Price = table.Column(type: "numeric", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Pizzas", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Orders", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - PizzaId = table.Column(type: "integer", nullable: false), - CustomerId = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Orders", x => x.Id); - table.ForeignKey( - name: "FK_Orders_Customers_CustomerId", - column: x => x.CustomerId, - principalTable: "Customers", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Orders_Pizzas_PizzaId", - column: x => x.PizzaId, - principalTable: "Pizzas", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Orders_CustomerId", - table: "Orders", - column: "CustomerId"); - - migrationBuilder.CreateIndex( - name: "IX_Orders_PizzaId", - table: "Orders", - column: "PizzaId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Orders"); - - migrationBuilder.DropTable( - name: "Customers"); - - migrationBuilder.DropTable( - name: "Pizzas"); - } - } -} diff --git a/exercise.pizzashopapi/Migrations/20250127100718_First_Migration.Designer.cs b/exercise.pizzashopapi/Migrations/20250130145249_init.Designer.cs similarity index 58% rename from exercise.pizzashopapi/Migrations/20250127100718_First_Migration.Designer.cs rename to exercise.pizzashopapi/Migrations/20250130145249_init.Designer.cs index 2fc8bb2..b71f46e 100644 --- a/exercise.pizzashopapi/Migrations/20250127100718_First_Migration.Designer.cs +++ b/exercise.pizzashopapi/Migrations/20250130145249_init.Designer.cs @@ -1,4 +1,5 @@ // +using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -11,8 +12,8 @@ namespace exercise.pizzashopapi.Migrations { [DbContext(typeof(DataContext))] - [Migration("20250127100718_First_Migration")] - partial class First_Migration + [Migration("20250130145249_init")] + partial class init { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -38,7 +39,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("Customers"); + b.ToTable("customers"); }); modelBuilder.Entity("exercise.pizzashopapi.Models.Order", b => @@ -49,6 +50,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + b.Property("CustomerId") .HasColumnType("integer"); @@ -61,7 +65,30 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasIndex("PizzaId"); - b.ToTable("Orders"); + b.ToTable("orders"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.OrderToppings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("OrderId") + .HasColumnType("integer"); + + b.Property("ToppingId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("ToppingId"); + + b.ToTable("order_toppings"); }); modelBuilder.Entity("exercise.pizzashopapi.Models.Pizza", b => @@ -81,7 +108,24 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("Pizzas"); + b.ToTable("pizzas"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Topping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("toppings"); }); modelBuilder.Entity("exercise.pizzashopapi.Models.Order", b => @@ -103,15 +147,44 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Pizza"); }); + modelBuilder.Entity("exercise.pizzashopapi.Models.OrderToppings", b => + { + b.HasOne("exercise.pizzashopapi.Models.Order", "Order") + .WithMany("ToppingOrders") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("exercise.pizzashopapi.Models.Topping", "Topping") + .WithMany("Orders") + .HasForeignKey("ToppingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + + b.Navigation("Topping"); + }); + modelBuilder.Entity("exercise.pizzashopapi.Models.Customer", b => { b.Navigation("Orders"); }); + modelBuilder.Entity("exercise.pizzashopapi.Models.Order", b => + { + b.Navigation("ToppingOrders"); + }); + modelBuilder.Entity("exercise.pizzashopapi.Models.Pizza", b => { b.Navigation("Orders"); }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Topping", b => + { + b.Navigation("Orders"); + }); #pragma warning restore 612, 618 } } diff --git a/exercise.pizzashopapi/Migrations/20250130145249_init.cs b/exercise.pizzashopapi/Migrations/20250130145249_init.cs new file mode 100644 index 0000000..85beb67 --- /dev/null +++ b/exercise.pizzashopapi/Migrations/20250130145249_init.cs @@ -0,0 +1,148 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace exercise.pizzashopapi.Migrations +{ + /// + public partial class init : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "customers", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_customers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "pizzas", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "text", nullable: false), + Price = table.Column(type: "numeric", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_pizzas", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "toppings", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_toppings", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "orders", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + PizzaId = table.Column(type: "integer", nullable: false), + CustomerId = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_orders", x => x.Id); + table.ForeignKey( + name: "FK_orders_customers_CustomerId", + column: x => x.CustomerId, + principalTable: "customers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_orders_pizzas_PizzaId", + column: x => x.PizzaId, + principalTable: "pizzas", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "order_toppings", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + OrderId = table.Column(type: "integer", nullable: false), + ToppingId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_order_toppings", x => x.Id); + table.ForeignKey( + name: "FK_order_toppings_orders_OrderId", + column: x => x.OrderId, + principalTable: "orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_order_toppings_toppings_ToppingId", + column: x => x.ToppingId, + principalTable: "toppings", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_order_toppings_OrderId", + table: "order_toppings", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_order_toppings_ToppingId", + table: "order_toppings", + column: "ToppingId"); + + migrationBuilder.CreateIndex( + name: "IX_orders_CustomerId", + table: "orders", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_orders_PizzaId", + table: "orders", + column: "PizzaId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "order_toppings"); + + migrationBuilder.DropTable( + name: "orders"); + + migrationBuilder.DropTable( + name: "toppings"); + + migrationBuilder.DropTable( + name: "customers"); + + migrationBuilder.DropTable( + name: "pizzas"); + } + } +} diff --git a/exercise.pizzashopapi/Migrations/DataContextModelSnapshot.cs b/exercise.pizzashopapi/Migrations/DataContextModelSnapshot.cs index 34a6ee8..61ac9eb 100644 --- a/exercise.pizzashopapi/Migrations/DataContextModelSnapshot.cs +++ b/exercise.pizzashopapi/Migrations/DataContextModelSnapshot.cs @@ -1,4 +1,5 @@ // +using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -35,7 +36,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("Customers"); + b.ToTable("customers"); }); modelBuilder.Entity("exercise.pizzashopapi.Models.Order", b => @@ -46,6 +47,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + b.Property("CustomerId") .HasColumnType("integer"); @@ -58,7 +62,30 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("PizzaId"); - b.ToTable("Orders"); + b.ToTable("orders"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.OrderToppings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("OrderId") + .HasColumnType("integer"); + + b.Property("ToppingId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.HasIndex("ToppingId"); + + b.ToTable("order_toppings"); }); modelBuilder.Entity("exercise.pizzashopapi.Models.Pizza", b => @@ -78,7 +105,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("Pizzas"); + b.ToTable("pizzas"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Topping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("toppings"); }); modelBuilder.Entity("exercise.pizzashopapi.Models.Order", b => @@ -100,15 +144,44 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Pizza"); }); + modelBuilder.Entity("exercise.pizzashopapi.Models.OrderToppings", b => + { + b.HasOne("exercise.pizzashopapi.Models.Order", "Order") + .WithMany("ToppingOrders") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("exercise.pizzashopapi.Models.Topping", "Topping") + .WithMany("Orders") + .HasForeignKey("ToppingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + + b.Navigation("Topping"); + }); + modelBuilder.Entity("exercise.pizzashopapi.Models.Customer", b => { b.Navigation("Orders"); }); + modelBuilder.Entity("exercise.pizzashopapi.Models.Order", b => + { + b.Navigation("ToppingOrders"); + }); + modelBuilder.Entity("exercise.pizzashopapi.Models.Pizza", b => { b.Navigation("Orders"); }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Topping", b => + { + b.Navigation("Orders"); + }); #pragma warning restore 612, 618 } } diff --git a/exercise.pizzashopapi/Models/Customer.cs b/exercise.pizzashopapi/Models/Customer.cs index 63aab6c..53508f5 100644 --- a/exercise.pizzashopapi/Models/Customer.cs +++ b/exercise.pizzashopapi/Models/Customer.cs @@ -2,6 +2,7 @@ namespace exercise.pizzashopapi.Models { + [Table("customers")] public class Customer { public int Id { get; set; } diff --git a/exercise.pizzashopapi/Models/Order.cs b/exercise.pizzashopapi/Models/Order.cs index 84c9f49..bc03424 100644 --- a/exercise.pizzashopapi/Models/Order.cs +++ b/exercise.pizzashopapi/Models/Order.cs @@ -2,14 +2,16 @@ namespace exercise.pizzashopapi.Models { + [Table("orders")] public class Order { public int Id { get; set; } public int PizzaId { get; set; } public int CustomerId { get; set; } - + public DateTime CreatedAt { get; set; } = DateTime.Now; public Pizza Pizza { get; set; } public Customer Customer { get; set; } + public List ToppingOrders { get; set; } public Order() { } public Order(int pizzaId, int customerId, Pizza pizza, Customer customer) @@ -18,6 +20,7 @@ public Order(int pizzaId, int customerId, Pizza pizza, Customer customer) CustomerId = customerId; Pizza = pizza; Customer = customer; + CreatedAt = DateTime.Now; } } } diff --git a/exercise.pizzashopapi/Models/OrderToppings.cs b/exercise.pizzashopapi/Models/OrderToppings.cs new file mode 100644 index 0000000..0f73836 --- /dev/null +++ b/exercise.pizzashopapi/Models/OrderToppings.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace exercise.pizzashopapi.Models +{ + [Table("order_toppings")] + public class OrderToppings + { + public int Id { get; set; } + public int OrderId { get; set; } + public int ToppingId { get; set; } + + public Order Order { get; set; } + public Topping Topping { get; set; } + } +} diff --git a/exercise.pizzashopapi/Models/Pizza.cs b/exercise.pizzashopapi/Models/Pizza.cs index 3ba5fa9..31da5f1 100644 --- a/exercise.pizzashopapi/Models/Pizza.cs +++ b/exercise.pizzashopapi/Models/Pizza.cs @@ -2,14 +2,14 @@ namespace exercise.pizzashopapi.Models { - + [Table("pizzas")] + public class Pizza { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } public List Orders { get; set; } = new List(); - public Pizza() { } public Pizza(string name, decimal price) { diff --git a/exercise.pizzashopapi/Models/Topping.cs b/exercise.pizzashopapi/Models/Topping.cs new file mode 100644 index 0000000..55a8dc2 --- /dev/null +++ b/exercise.pizzashopapi/Models/Topping.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace exercise.pizzashopapi.Models +{ + [Table("toppings")] + public class Topping + { + public int Id { get; set; } + public string Name { get; set; } + + public List Orders { get; set; } + } +} diff --git a/exercise.pizzashopapi/Program.cs b/exercise.pizzashopapi/Program.cs index deaaffd..00ee6af 100644 --- a/exercise.pizzashopapi/Program.cs +++ b/exercise.pizzashopapi/Program.cs @@ -13,6 +13,7 @@ builder.Services.AddScoped, Repository>(); builder.Services.AddScoped, Repository>(); builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); builder.Services.AddDbContext(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); diff --git a/exercise.pizzashopapi/Tools/MappingProfile.cs b/exercise.pizzashopapi/Tools/MappingProfile.cs index c53828f..791b9f6 100644 --- a/exercise.pizzashopapi/Tools/MappingProfile.cs +++ b/exercise.pizzashopapi/Tools/MappingProfile.cs @@ -16,6 +16,8 @@ public MappingProfile() { .ForMember(dest => dest.PizzaName, opt => opt.MapFrom(src => src.Pizza.Name)) .ForMember(dest => dest.PizzaPrice, opt => opt.MapFrom(src => src.Pizza.Price)) .ForMember(dest => dest.CustomerName, opt => opt.MapFrom(src => src.Customer.Name)); + CreateMap(); + CreateMap(); } }