From f0d8a88dfdb9ce86d4ed99f127d9acdbc7728eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolai=20Lillemark=20B=C3=B8rseth?= Date: Mon, 27 Jan 2025 10:49:58 +0100 Subject: [PATCH 1/4] Finished core and a few of the extensions. Might get to the logic ones now --- exercise.pizzashopapi/DTO/Customer.cs | 6 + exercise.pizzashopapi/DTO/Order.cs | 9 + exercise.pizzashopapi/DTO/Product.cs | 4 + exercise.pizzashopapi/DTO/Topping.cs | 5 + exercise.pizzashopapi/Data/DataContext.cs | 39 ++- exercise.pizzashopapi/Data/Seeder.cs | 62 ++-- .../EndPoints/PizzaShopApi.cs | 15 - .../Endpoints/CustomerEndpoints.cs | 94 ++++++ .../Endpoints/OrderEndpoints.cs | 139 ++++++++ .../Endpoints/ProductEnpoints.cs | 72 +++++ .../Endpoints/ToppingEndpoints.cs | 60 ++++ exercise.pizzashopapi/Enums/Enum.cs | 22 ++ .../Exceptions/IdNotFoundException.cs | 11 + .../20250127094519_Init.Designer.cs | 297 ++++++++++++++++++ .../Migrations/20250127094519_Init.cs | 197 ++++++++++++ .../Migrations/DataContextModelSnapshot.cs | 294 +++++++++++++++++ exercise.pizzashopapi/Models/Customer.cs | 2 + exercise.pizzashopapi/Models/Order.cs | 13 +- exercise.pizzashopapi/Models/OrderTopping.cs | 8 + .../Models/{Pizza.cs => Product.cs} | 7 +- exercise.pizzashopapi/Models/Topping.cs | 11 + exercise.pizzashopapi/Program.cs | 46 ++- .../Properties/launchSettings.json | 24 +- .../Repository/IRepository.cs | 18 +- .../Repository/Repository.cs | 76 ++++- exercise.pizzashopapi/Tools/MappingProfile.cs | 26 ++ .../exercise.pizzashopapi.csproj | 13 +- 27 files changed, 1464 insertions(+), 106 deletions(-) create mode 100644 exercise.pizzashopapi/DTO/Customer.cs create mode 100644 exercise.pizzashopapi/DTO/Order.cs create mode 100644 exercise.pizzashopapi/DTO/Product.cs create mode 100644 exercise.pizzashopapi/DTO/Topping.cs delete mode 100644 exercise.pizzashopapi/EndPoints/PizzaShopApi.cs create mode 100644 exercise.pizzashopapi/Endpoints/CustomerEndpoints.cs create mode 100644 exercise.pizzashopapi/Endpoints/OrderEndpoints.cs create mode 100644 exercise.pizzashopapi/Endpoints/ProductEnpoints.cs create mode 100644 exercise.pizzashopapi/Endpoints/ToppingEndpoints.cs create mode 100644 exercise.pizzashopapi/Enums/Enum.cs create mode 100644 exercise.pizzashopapi/Exceptions/IdNotFoundException.cs create mode 100644 exercise.pizzashopapi/Migrations/20250127094519_Init.Designer.cs create mode 100644 exercise.pizzashopapi/Migrations/20250127094519_Init.cs create mode 100644 exercise.pizzashopapi/Migrations/DataContextModelSnapshot.cs create mode 100644 exercise.pizzashopapi/Models/OrderTopping.cs rename exercise.pizzashopapi/Models/{Pizza.cs => Product.cs} (58%) create mode 100644 exercise.pizzashopapi/Models/Topping.cs create mode 100644 exercise.pizzashopapi/Tools/MappingProfile.cs diff --git a/exercise.pizzashopapi/DTO/Customer.cs b/exercise.pizzashopapi/DTO/Customer.cs new file mode 100644 index 0000000..5a125aa --- /dev/null +++ b/exercise.pizzashopapi/DTO/Customer.cs @@ -0,0 +1,6 @@ +namespace exercise.pizzashopapi.DTO +{ + public record CustomerPost(string Name); + public record CustomerView(int Id, string Name, IEnumerable Orders); + public record CustomerInternal(int Id, string Name); +} diff --git a/exercise.pizzashopapi/DTO/Order.cs b/exercise.pizzashopapi/DTO/Order.cs new file mode 100644 index 0000000..0bd7d39 --- /dev/null +++ b/exercise.pizzashopapi/DTO/Order.cs @@ -0,0 +1,9 @@ +namespace exercise.pizzashopapi.DTO +{ + public record OrderPost(int ProductId, int CustomerId); + public record OrderPostAddTopping(int ToppingId); + public record OrderView(int Id, decimal TotalPrice, CustomerInternal Customer, ProductView Product, IEnumerable Toppings); + public record OrderProduct(int Id, decimal TotalPrice, ProductView Product); + public record OrderProductToppings(int Id, decimal TotalPrice, ProductView Product, IEnumerable Toppings); + public record OrderCustomerProduct(int Id, decimal TotalPrice, CustomerInternal Customer, ProductView Product); +} diff --git a/exercise.pizzashopapi/DTO/Product.cs b/exercise.pizzashopapi/DTO/Product.cs new file mode 100644 index 0000000..1831359 --- /dev/null +++ b/exercise.pizzashopapi/DTO/Product.cs @@ -0,0 +1,4 @@ +namespace exercise.pizzashopapi.DTO +{ + public record ProductView(int Id, string ProductType, string Name, decimal Price); +} diff --git a/exercise.pizzashopapi/DTO/Topping.cs b/exercise.pizzashopapi/DTO/Topping.cs new file mode 100644 index 0000000..97d8e6b --- /dev/null +++ b/exercise.pizzashopapi/DTO/Topping.cs @@ -0,0 +1,5 @@ +namespace exercise.pizzashopapi.DTO +{ + public record ToppingView(int Id, string Name, decimal Price, IEnumerable Orders); + public record ToppingInternal(int Id, string Name, decimal Price); +} diff --git a/exercise.pizzashopapi/Data/DataContext.cs b/exercise.pizzashopapi/Data/DataContext.cs index 129199e..d24c656 100644 --- a/exercise.pizzashopapi/Data/DataContext.cs +++ b/exercise.pizzashopapi/Data/DataContext.cs @@ -3,26 +3,37 @@ namespace exercise.pizzashopapi.Data { - public class DataContext : DbContext + public class DataContext(DbContextOptions options) : DbContext(options) { - private string connectionString; - public DataContext() + protected override void OnModelCreating(ModelBuilder modelBuilder) { - var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); - connectionString = configuration.GetValue("ConnectionStrings:DefaultConnectionString"); + modelBuilder.Entity().HasKey(ot => new { ot.OrderId, ot.ToppingId }); + modelBuilder.Entity() + .HasMany(o => o.Toppings) + .WithMany(t => t.Orders) + .UsingEntity(); + modelBuilder.Entity() + .HasOne(o => o.Product) + .WithMany(p => p.Orders) + .HasForeignKey(o => o.ProductId) + .OnDelete(DeleteBehavior.SetNull); + modelBuilder.Entity() + .HasOne(o => o.Customer) + .WithMany(c => c.Orders) + .HasForeignKey(o => o.CustomerId) + .OnDelete(DeleteBehavior.SetNull); + Seeder seeder = new Seeder(); + modelBuilder.Entity().HasData(seeder.Customers); + modelBuilder.Entity().HasData(seeder.Products); + modelBuilder.Entity().HasData(seeder.Orders); + modelBuilder.Entity().HasData(seeder.Toppings); + modelBuilder.Entity().HasData(seeder.OrderToppings); } - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseNpgsql(connectionString); - //set primary of order? - - //seed data? - - } - public DbSet Pizzas { get; set; } + public DbSet Products { 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 f87fbef..110729a 100644 --- a/exercise.pizzashopapi/Data/Seeder.cs +++ b/exercise.pizzashopapi/Data/Seeder.cs @@ -1,34 +1,48 @@ using exercise.pizzashopapi.Models; +using exercise.pizzashopapi.Enums; namespace exercise.pizzashopapi.Data { - public static class Seeder + public class Seeder { - public async static void SeedPizzaShopApi(this WebApplication app) - { - using(var db = new DataContext()) - { - if(!db.Customers.Any()) - { - db.Add(new Customer() { Name="Nigel" }); - db.Add(new Customer() { Name = "Dave" }); - await db.SaveChangesAsync(); - } - if(!db.Pizzas.Any()) - { - db.Add(new Pizza() { Name = "Cheese & Pineapple" }); - db.Add(new Pizza() { Name = "Vegan Cheese Tastic" }); - await db.SaveChangesAsync(); + private List _customers = [ + new Customer { Id = 1, Name = "Nigel"}, + new Customer { Id = 2, Name = "Dave"}, + new Customer { Id = 3, Name = "Nikolai"} + ]; - } + private List _products = [ + new Product { Id = 1, ProductType = ProductType.Pizza, Name = "Hawaiian Pizza", Price = 200 }, + new Product { Id = 2, ProductType = ProductType.Pizza, Name = "Vegan Cheese Tastic", Price = 165 }, + new Product { Id = 3, ProductType = ProductType.Pizza, Name = "Pizza Vendetta", Price = 210 }, + new Product { Id = 4, ProductType = ProductType.Burger, Name = "Bacon Cheese Burger", Price = 195 }, + new Product { Id = 5, ProductType = ProductType.Fries, Name = "Regular Fries", Price = 110 }, + new Product { Id = 6, ProductType = ProductType.Drinks, Name = "Coca Cola 0.4L", Price = 65 }, + new Product { Id = 7, ProductType = ProductType.Drinks, Name = "Fanta Orange 0.4L", Price = 65 }, + ]; - //order data - if(1==1) - { + private List _orders = [ + new Order { Id = 1, CustomerId = 1, ProductId = 2 }, + new Order { Id = 2, CustomerId = 2, ProductId = 1 }, + new Order { Id = 3, CustomerId = 3, ProductId = 3 }, + new Order { Id = 4, CustomerId = 3, ProductId = 7 }, + ]; - await db.SaveChangesAsync(); - } - } - } + private List _toppings = [ + new Topping {Id = 1, Name = "Bacon", Price = 20 }, + new Topping {Id = 2, Name = "Onion Rings", Price = 15 }, + new Topping {Id = 3, Name = "Hot Sauce", Price = 12 }, + ]; + + private List _orderToppings = [ + new OrderTopping { OrderId = 3, ToppingId = 3 }, + new OrderTopping { OrderId = 1, ToppingId = 1 } + ]; + + public List Customers { get { return _customers; } } + public List Products { get { return _products; } } + public List Orders { get { return _orders; } } + public List Toppings { get { return _toppings; } } + public List OrderToppings { get { return _orderToppings; } } } } 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/CustomerEndpoints.cs b/exercise.pizzashopapi/Endpoints/CustomerEndpoints.cs new file mode 100644 index 0000000..22bcedb --- /dev/null +++ b/exercise.pizzashopapi/Endpoints/CustomerEndpoints.cs @@ -0,0 +1,94 @@ +using AutoMapper; +using exercise.pizzashopapi.DTO; +using exercise.pizzashopapi.Exceptions; +using exercise.pizzashopapi.Models; +using exercise.pizzashopapi.Repository; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace exercise.pizzashopapi.Endpoints +{ + public static class CustomerEndpoints + { + public static string Path { get; private set; } = "customers"; + public static void ConfigureCustomersEndpoints(this WebApplication app) + { + var group = app.MapGroup(Path); + + group.MapGet("/", GetCustomers); + group.MapPost("/", CreateCustomer); + group.MapGet("/{id}", GetCustomer); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static async Task GetCustomers(IRepository repository, IMapper mapper) + { + try + { + IEnumerable customers = await repository.GetAll( + q => q.Include(x => x.Orders).ThenInclude(x => x.Toppings), + q => q.Include(x => x.Orders).ThenInclude(x => x.Product) + ); + return TypedResults.Ok(mapper.Map>(customers)); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static async Task GetCustomer(IRepository repository, IMapper mapper, int id) + { + try + { + Customer customer = await repository.Get(id, + q => q.Include(x => x.Orders).ThenInclude(x => x.Toppings), + q => q.Include(x => x.Orders).ThenInclude(x => x.Product) + ); + return TypedResults.Ok(mapper.Map(customer)); + } + catch (IdNotFoundException ex) + { + return TypedResults.NotFound(new { ex.Message }); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static async Task CreateCustomer( + IRepository repository, + IRepository customerRepository, + IRepository productRepository, + IMapper mapper, + CustomerPost entity) + { + try + { + Customer customer = await repository.Add(new Customer + { + Name = entity.Name + }); + customer = await customerRepository.Add(customer); + return TypedResults.Created($"{Path}/{customer.Id}", mapper.Map(customer)); + } + catch (IdNotFoundException ex) + { + return TypedResults.NotFound(new { ex.Message }); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + } +} diff --git a/exercise.pizzashopapi/Endpoints/OrderEndpoints.cs b/exercise.pizzashopapi/Endpoints/OrderEndpoints.cs new file mode 100644 index 0000000..5575ff0 --- /dev/null +++ b/exercise.pizzashopapi/Endpoints/OrderEndpoints.cs @@ -0,0 +1,139 @@ +using AutoMapper; +using exercise.pizzashopapi.DTO; +using exercise.pizzashopapi.Exceptions; +using exercise.pizzashopapi.Models; +using exercise.pizzashopapi.Repository; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace exercise.pizzashopapi.Endpoints +{ + public static class OrderEndpoints + { + public static string Path { get; private set; } = "orders"; + public static void ConfigureOrdersEndpoints(this WebApplication app) + { + var group = app.MapGroup(Path); + + group.MapGet("/", GetOrders); + group.MapPost("/", CreateOrder); + group.MapGet("/{id}", GetOrder); + group.MapPost("/{id}", AddTopping); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static async Task GetOrders( + IRepository repository, + IMapper mapper, + string? customerId) + { + try + { + IEnumerable orders = await repository.GetAll( + q => q.Include(x => x.Customer), + q => q.Include(x => x.Product), + q => q.Include(x => x.Toppings) + ); + + if (!string.IsNullOrEmpty(customerId)) + { + int id; + if (!int.TryParse(customerId, out id)) return TypedResults.BadRequest("The customerId must be of type int!"); + orders = orders.Where(a => a.CustomerId == id); + } + + return TypedResults.Ok(mapper.Map>(orders)); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static async Task GetOrder(IRepository repository, IMapper mapper, int id) + { + try + { + Order order = await repository.Get(id, + q => q.Include(x => x.Customer), + q => q.Include(x => x.Product), + q => q.Include(x => x.Toppings) + ); + return TypedResults.Ok(mapper.Map(order)); + } + catch (IdNotFoundException ex) + { + return TypedResults.NotFound(new { ex.Message }); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static async Task CreateOrder( + IRepository repository, + IRepository customerRepository, + IRepository productRepository, + IMapper mapper, + OrderPost entity) + { + try + { + Customer customer = await customerRepository.Get(entity.CustomerId); + Product product = await productRepository.Get(entity.ProductId); + + Order order = await repository.Add(new Order + { + CustomerId = customer.Id, + Customer = customer, + ProductId = product.Id, + Product = product, + }); + return TypedResults.Created($"{Path}/{order.Id}", mapper.Map(order)); + } + catch (IdNotFoundException ex) + { + return TypedResults.NotFound(new { ex.Message }); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + public static async Task AddTopping( + IRepository repository, + IRepository toppingRepository, + IMapper mapper, + int id, + OrderPostAddTopping entity) + { + try + { + Order order = await repository.Get(id, q => q.Include(x => x.Product).Include(x => x.Toppings)); + Topping topping = await toppingRepository.Get(entity.ToppingId); + + order.Toppings.Add(topping); + await repository.Update(order); + + return TypedResults.Created($"{Path}/{order.Id}", mapper.Map(order)); + } + catch (IdNotFoundException ex) + { + return TypedResults.NotFound(new { ex.Message }); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + } +} diff --git a/exercise.pizzashopapi/Endpoints/ProductEnpoints.cs b/exercise.pizzashopapi/Endpoints/ProductEnpoints.cs new file mode 100644 index 0000000..4b01630 --- /dev/null +++ b/exercise.pizzashopapi/Endpoints/ProductEnpoints.cs @@ -0,0 +1,72 @@ +using AutoMapper; +using exercise.pizzashopapi.DTO; +using exercise.pizzashopapi.Enums; +using exercise.pizzashopapi.Exceptions; +using exercise.pizzashopapi.Models; +using exercise.pizzashopapi.Repository; +using Microsoft.AspNetCore.Mvc; + +namespace exercise.pizzashopapi.EndPoints +{ + public static class ProductEnpoints + { + public static string Path { get; private set; } = "products"; + public static void ConfigureProductsEndpoints(this WebApplication app) + { + var group = app.MapGroup(Path); + + group.MapGet("/", GetProducts); + group.MapGet("/{id}", GetProduct); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static async Task GetProducts( + IRepository repository, + IMapper mapper, + string? productType) + { + try + { + IEnumerable products = await repository.GetAll(); + + if (!string.IsNullOrEmpty(productType)) + { + ProductType t; + if (!Enum.TryParse(productType, true, out t)) + { + return TypedResults.BadRequest($"That is not a valid appointment type! Choose one of {string.Join(", ", Enum.GetValues())}"); + } + products = products.Where(p => p.ProductType == t); + } + + return TypedResults.Ok(mapper.Map>(products)); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static async Task GetProduct(IRepository repository, IMapper mapper, int id) + { + try + { + Product product = await repository.Get(id); + return TypedResults.Ok(mapper.Map(product)); + } + catch (IdNotFoundException ex) + { + return TypedResults.NotFound(new { ex.Message }); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + } +} diff --git a/exercise.pizzashopapi/Endpoints/ToppingEndpoints.cs b/exercise.pizzashopapi/Endpoints/ToppingEndpoints.cs new file mode 100644 index 0000000..ae97ce7 --- /dev/null +++ b/exercise.pizzashopapi/Endpoints/ToppingEndpoints.cs @@ -0,0 +1,60 @@ +using AutoMapper; +using exercise.pizzashopapi.DTO; +using exercise.pizzashopapi.Enums; +using exercise.pizzashopapi.Exceptions; +using exercise.pizzashopapi.Models; +using exercise.pizzashopapi.Repository; +using Microsoft.AspNetCore.Mvc; + +namespace exercise.pizzashopapi.EndPoints +{ + public static class ToppingEnpoints + { + public static string Path { get; private set; } = "toppings"; + public static void ConfigureToppingsEndpoints(this WebApplication app) + { + var group = app.MapGroup(Path); + + group.MapGet("/", GetToppings); + group.MapGet("/{id}", GetTopping); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static async Task GetToppings( + IRepository repository, + IMapper mapper) + { + try + { + IEnumerable toppings = await repository.GetAll(); + return TypedResults.Ok(mapper.Map>(toppings)); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static async Task GetTopping(IRepository repository, IMapper mapper, int id) + { + try + { + Topping topping = await repository.Get(id); + return TypedResults.Ok(mapper.Map(topping)); + } + catch (IdNotFoundException ex) + { + return TypedResults.NotFound(new { ex.Message }); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + } +} diff --git a/exercise.pizzashopapi/Enums/Enum.cs b/exercise.pizzashopapi/Enums/Enum.cs new file mode 100644 index 0000000..4ae3059 --- /dev/null +++ b/exercise.pizzashopapi/Enums/Enum.cs @@ -0,0 +1,22 @@ +namespace exercise.pizzashopapi.Enums +{ + public enum ProductType + { + Pizza, + Burger, + Fries, + Drinks + } + + public static class ProductInfo + { + public static Dictionary> ProductPrepTimes = new Dictionary> + { + { ProductType.Pizza, new Tuple(3, 12) }, + { ProductType.Burger, new Tuple(2, 10) }, + { ProductType.Fries, new Tuple(0, 10) }, + { ProductType.Drinks, new Tuple(1, 0) }, + }; + + } +} diff --git a/exercise.pizzashopapi/Exceptions/IdNotFoundException.cs b/exercise.pizzashopapi/Exceptions/IdNotFoundException.cs new file mode 100644 index 0000000..aa7fd9d --- /dev/null +++ b/exercise.pizzashopapi/Exceptions/IdNotFoundException.cs @@ -0,0 +1,11 @@ +namespace exercise.pizzashopapi.Exceptions +{ + public class IdNotFoundException : ArgumentException + { + public IdNotFoundException() { } + + public IdNotFoundException(string? message) : base(message) { } + + public IdNotFoundException(string? message, Exception? innerException) : base(message, innerException) { } + } +} diff --git a/exercise.pizzashopapi/Migrations/20250127094519_Init.Designer.cs b/exercise.pizzashopapi/Migrations/20250127094519_Init.Designer.cs new file mode 100644 index 0000000..989ebd4 --- /dev/null +++ b/exercise.pizzashopapi/Migrations/20250127094519_Init.Designer.cs @@ -0,0 +1,297 @@ +// +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("20250127094519_Init")] + partial class Init + { + /// + 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"); + + b.HasData( + new + { + Id = 1, + Name = "Nigel" + }, + new + { + Id = 2, + Name = "Dave" + }, + new + { + Id = 3, + Name = "Nikolai" + }); + }); + + 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("ProductId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("ProductId"); + + b.ToTable("Orders"); + + b.HasData( + new + { + Id = 1, + CustomerId = 1, + ProductId = 2 + }, + new + { + Id = 2, + CustomerId = 2, + ProductId = 1 + }, + new + { + Id = 3, + CustomerId = 3, + ProductId = 3 + }, + new + { + Id = 4, + CustomerId = 3, + ProductId = 7 + }); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.OrderTopping", b => + { + b.Property("OrderId") + .HasColumnType("integer"); + + b.Property("ToppingId") + .HasColumnType("integer"); + + b.HasKey("OrderId", "ToppingId"); + + b.HasIndex("ToppingId"); + + b.ToTable("OrderTopping"); + + b.HasData( + new + { + OrderId = 3, + ToppingId = 3 + }, + new + { + OrderId = 1, + ToppingId = 1 + }); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("ProductType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Products"); + + b.HasData( + new + { + Id = 1, + Name = "Hawaiian Pizza", + Price = 200m, + ProductType = 0 + }, + new + { + Id = 2, + Name = "Vegan Cheese Tastic", + Price = 165m, + ProductType = 0 + }, + new + { + Id = 3, + Name = "Pizza Vendetta", + Price = 210m, + ProductType = 0 + }, + new + { + Id = 4, + Name = "Bacon Cheese Burger", + Price = 195m, + ProductType = 1 + }, + new + { + Id = 5, + Name = "Regular Fries", + Price = 110m, + ProductType = 2 + }, + new + { + Id = 6, + Name = "Coca Cola 0.4L", + Price = 65m, + ProductType = 3 + }, + new + { + Id = 7, + Name = "Fanta Orange 0.4L", + Price = 65m, + ProductType = 3 + }); + }); + + 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.Property("Price") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("Toppings"); + + b.HasData( + new + { + Id = 1, + Name = "Bacon", + Price = 20m + }, + new + { + Id = 2, + Name = "Onion Rings", + Price = 15m + }, + new + { + Id = 3, + Name = "Hot Sauce", + Price = 12m + }); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Order", b => + { + b.HasOne("exercise.pizzashopapi.Models.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.HasOne("exercise.pizzashopapi.Models.Product", "Product") + .WithMany("Orders") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.OrderTopping", b => + { + b.HasOne("exercise.pizzashopapi.Models.Order", null) + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("exercise.pizzashopapi.Models.Topping", null) + .WithMany() + .HasForeignKey("ToppingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Product", b => + { + b.Navigation("Orders"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/exercise.pizzashopapi/Migrations/20250127094519_Init.cs b/exercise.pizzashopapi/Migrations/20250127094519_Init.cs new file mode 100644 index 0000000..b1632cf --- /dev/null +++ b/exercise.pizzashopapi/Migrations/20250127094519_Init.cs @@ -0,0 +1,197 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +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: "Products", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + ProductType = table.Column(type: "integer", nullable: false), + Name = table.Column(type: "text", nullable: false), + Price = table.Column(type: "numeric", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Products", 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), + Price = table.Column(type: "numeric", 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), + ProductId = 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.SetNull); + table.ForeignKey( + name: "FK_Orders_Products_ProductId", + column: x => x.ProductId, + principalTable: "Products", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateTable( + name: "OrderTopping", + columns: table => new + { + OrderId = table.Column(type: "integer", nullable: false), + ToppingId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderTopping", x => new { x.OrderId, x.ToppingId }); + table.ForeignKey( + name: "FK_OrderTopping_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_OrderTopping_Toppings_ToppingId", + column: x => x.ToppingId, + principalTable: "Toppings", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + table: "Customers", + columns: new[] { "Id", "Name" }, + values: new object[,] + { + { 1, "Nigel" }, + { 2, "Dave" }, + { 3, "Nikolai" } + }); + + migrationBuilder.InsertData( + table: "Products", + columns: new[] { "Id", "Name", "Price", "ProductType" }, + values: new object[,] + { + { 1, "Hawaiian Pizza", 200m, 0 }, + { 2, "Vegan Cheese Tastic", 165m, 0 }, + { 3, "Pizza Vendetta", 210m, 0 }, + { 4, "Bacon Cheese Burger", 195m, 1 }, + { 5, "Regular Fries", 110m, 2 }, + { 6, "Coca Cola 0.4L", 65m, 3 }, + { 7, "Fanta Orange 0.4L", 65m, 3 } + }); + + migrationBuilder.InsertData( + table: "Toppings", + columns: new[] { "Id", "Name", "Price" }, + values: new object[,] + { + { 1, "Bacon", 20m }, + { 2, "Onion Rings", 15m }, + { 3, "Hot Sauce", 12m } + }); + + migrationBuilder.InsertData( + table: "Orders", + columns: new[] { "Id", "CustomerId", "ProductId" }, + values: new object[,] + { + { 1, 1, 2 }, + { 2, 2, 1 }, + { 3, 3, 3 }, + { 4, 3, 7 } + }); + + migrationBuilder.InsertData( + table: "OrderTopping", + columns: new[] { "OrderId", "ToppingId" }, + values: new object[,] + { + { 1, 1 }, + { 3, 3 } + }); + + migrationBuilder.CreateIndex( + name: "IX_Orders_CustomerId", + table: "Orders", + column: "CustomerId"); + + migrationBuilder.CreateIndex( + name: "IX_Orders_ProductId", + table: "Orders", + column: "ProductId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderTopping_ToppingId", + table: "OrderTopping", + column: "ToppingId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OrderTopping"); + + migrationBuilder.DropTable( + name: "Orders"); + + migrationBuilder.DropTable( + name: "Toppings"); + + migrationBuilder.DropTable( + name: "Customers"); + + migrationBuilder.DropTable( + name: "Products"); + } + } +} diff --git a/exercise.pizzashopapi/Migrations/DataContextModelSnapshot.cs b/exercise.pizzashopapi/Migrations/DataContextModelSnapshot.cs new file mode 100644 index 0000000..817aadd --- /dev/null +++ b/exercise.pizzashopapi/Migrations/DataContextModelSnapshot.cs @@ -0,0 +1,294 @@ +// +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"); + + b.HasData( + new + { + Id = 1, + Name = "Nigel" + }, + new + { + Id = 2, + Name = "Dave" + }, + new + { + Id = 3, + Name = "Nikolai" + }); + }); + + 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("ProductId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("ProductId"); + + b.ToTable("Orders"); + + b.HasData( + new + { + Id = 1, + CustomerId = 1, + ProductId = 2 + }, + new + { + Id = 2, + CustomerId = 2, + ProductId = 1 + }, + new + { + Id = 3, + CustomerId = 3, + ProductId = 3 + }, + new + { + Id = 4, + CustomerId = 3, + ProductId = 7 + }); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.OrderTopping", b => + { + b.Property("OrderId") + .HasColumnType("integer"); + + b.Property("ToppingId") + .HasColumnType("integer"); + + b.HasKey("OrderId", "ToppingId"); + + b.HasIndex("ToppingId"); + + b.ToTable("OrderTopping"); + + b.HasData( + new + { + OrderId = 3, + ToppingId = 3 + }, + new + { + OrderId = 1, + ToppingId = 1 + }); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Product", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("ProductType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Products"); + + b.HasData( + new + { + Id = 1, + Name = "Hawaiian Pizza", + Price = 200m, + ProductType = 0 + }, + new + { + Id = 2, + Name = "Vegan Cheese Tastic", + Price = 165m, + ProductType = 0 + }, + new + { + Id = 3, + Name = "Pizza Vendetta", + Price = 210m, + ProductType = 0 + }, + new + { + Id = 4, + Name = "Bacon Cheese Burger", + Price = 195m, + ProductType = 1 + }, + new + { + Id = 5, + Name = "Regular Fries", + Price = 110m, + ProductType = 2 + }, + new + { + Id = 6, + Name = "Coca Cola 0.4L", + Price = 65m, + ProductType = 3 + }, + new + { + Id = 7, + Name = "Fanta Orange 0.4L", + Price = 65m, + ProductType = 3 + }); + }); + + 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.Property("Price") + .HasColumnType("numeric"); + + b.HasKey("Id"); + + b.ToTable("Toppings"); + + b.HasData( + new + { + Id = 1, + Name = "Bacon", + Price = 20m + }, + new + { + Id = 2, + Name = "Onion Rings", + Price = 15m + }, + new + { + Id = 3, + Name = "Hot Sauce", + Price = 12m + }); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Order", b => + { + b.HasOne("exercise.pizzashopapi.Models.Customer", "Customer") + .WithMany("Orders") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.HasOne("exercise.pizzashopapi.Models.Product", "Product") + .WithMany("Orders") + .HasForeignKey("ProductId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Product"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.OrderTopping", b => + { + b.HasOne("exercise.pizzashopapi.Models.Order", null) + .WithMany() + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("exercise.pizzashopapi.Models.Topping", null) + .WithMany() + .HasForeignKey("ToppingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Customer", b => + { + b.Navigation("Orders"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Product", 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..521d59a 100644 --- a/exercise.pizzashopapi/Models/Customer.cs +++ b/exercise.pizzashopapi/Models/Customer.cs @@ -6,5 +6,7 @@ public class Customer { public int Id { get; set; } public string Name { get; set; } + + public List Orders { get; set; } = []; } } diff --git a/exercise.pizzashopapi/Models/Order.cs b/exercise.pizzashopapi/Models/Order.cs index fbe6113..62b195e 100644 --- a/exercise.pizzashopapi/Models/Order.cs +++ b/exercise.pizzashopapi/Models/Order.cs @@ -4,7 +4,16 @@ namespace exercise.pizzashopapi.Models { public class Order { - - + public int Id { get; set; } + public int ProductId { get; set; } + public Product Product { get; set; } + + public int CustomerId { get; set; } + public Customer Customer { get; set; } + + public List Toppings { get; set; } = []; + + [NotMapped] + public decimal TotalPrice => Product.Price + Toppings.Sum(t => t.Price); } } diff --git a/exercise.pizzashopapi/Models/OrderTopping.cs b/exercise.pizzashopapi/Models/OrderTopping.cs new file mode 100644 index 0000000..0820bf7 --- /dev/null +++ b/exercise.pizzashopapi/Models/OrderTopping.cs @@ -0,0 +1,8 @@ +namespace exercise.pizzashopapi.Models +{ + public class OrderTopping + { + public int OrderId { get; set; } + public int ToppingId { get; set; } + } +} diff --git a/exercise.pizzashopapi/Models/Pizza.cs b/exercise.pizzashopapi/Models/Product.cs similarity index 58% rename from exercise.pizzashopapi/Models/Pizza.cs rename to exercise.pizzashopapi/Models/Product.cs index 5c085ec..cbcc9ca 100644 --- a/exercise.pizzashopapi/Models/Pizza.cs +++ b/exercise.pizzashopapi/Models/Product.cs @@ -1,12 +1,15 @@ using System.ComponentModel.DataAnnotations.Schema; +using exercise.pizzashopapi.Enums; namespace exercise.pizzashopapi.Models { - - public class Pizza + public class Product { public int Id { get; set; } + public ProductType ProductType { get; set; } public string Name { get; set; } public decimal Price { get; set; } + + public List Orders { 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..5940fda --- /dev/null +++ b/exercise.pizzashopapi/Models/Topping.cs @@ -0,0 +1,11 @@ +namespace exercise.pizzashopapi.Models +{ + public class Topping + { + public int Id { get; set; } + public string Name { get; set; } + public decimal Price { get; set; } + + public List Orders { get; set; } = []; + } +} diff --git a/exercise.pizzashopapi/Program.cs b/exercise.pizzashopapi/Program.cs index c04a440..69545e9 100644 --- a/exercise.pizzashopapi/Program.cs +++ b/exercise.pizzashopapi/Program.cs @@ -1,35 +1,51 @@ +using System.Numerics; using exercise.pizzashopapi.Data; using exercise.pizzashopapi.EndPoints; +using exercise.pizzashopapi.Models; using exercise.pizzashopapi.Repository; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore; +using Scalar.AspNetCore; +using exercise.pizzashopapi.Endpoints; 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.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +var configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); + +builder.Services.AddOpenApi(); +builder.Services.AddDbContext(options => +{ + var connectionString = configuration.GetConnectionString("DefaultConnectionString"); + options.UseNpgsql(connectionString); + + options.ConfigureWarnings(warnings => + warnings.Ignore(RelationalEventId.PendingModelChangesWarning)); +}); + +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddAutoMapper(typeof(Program)); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { - app.UseSwagger(); - app.UseSwaggerUI(); + app.MapOpenApi(); + app.MapScalarApiReference(); } app.UseHttpsRedirection(); -app.UseAuthorization(); - -app.MapControllers(); - -app.ConfigurePizzaShopApi(); - -app.SeedPizzaShopApi(); +app.ConfigureProductsEndpoints(); +app.ConfigureCustomersEndpoints(); +app.ConfigureOrdersEndpoints(); +app.ConfigureToppingsEndpoints(); app.Run(); diff --git a/exercise.pizzashopapi/Properties/launchSettings.json b/exercise.pizzashopapi/Properties/launchSettings.json index 1d7269e..a0cadab 100644 --- a/exercise.pizzashopapi/Properties/launchSettings.json +++ b/exercise.pizzashopapi/Properties/launchSettings.json @@ -1,19 +1,11 @@ { "$schema": "https://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:39663", - "sslPort": 44368 - } - }, "profiles": { "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", + "launchBrowser": false, + "launchUrl": "scalar/v1", "applicationUrl": "http://localhost:5070", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -22,20 +14,12 @@ "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", + "launchBrowser": false, + "launchUrl": "scalar/v1", "applicationUrl": "https://localhost:7138;http://localhost:5070", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "swagger", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } } } } diff --git a/exercise.pizzashopapi/Repository/IRepository.cs b/exercise.pizzashopapi/Repository/IRepository.cs index b114ea8..bc00846 100644 --- a/exercise.pizzashopapi/Repository/IRepository.cs +++ b/exercise.pizzashopapi/Repository/IRepository.cs @@ -1,11 +1,21 @@ -using exercise.pizzashopapi.Models; +using System.Linq.Expressions; namespace exercise.pizzashopapi.Repository { - public interface IRepository + public interface IRepository + where T : class + where U : struct { - Task> GetOrdersByCustomer(int id); - + Task> GetAll(params Func, IQueryable>[] includeChains); + Task Get(U id, string idField, params Func, IQueryable>[] includeChains); + Task Get(U id, params Func, IQueryable>[] includeChains); + + + Task Add(T entity); + Task Update(T entity); + Task Delete(U id); + Task Save(); + Task Find(Expression> condition, params Func, IQueryable>[] includeChains); } } diff --git a/exercise.pizzashopapi/Repository/Repository.cs b/exercise.pizzashopapi/Repository/Repository.cs index e109fce..df1a638 100644 --- a/exercise.pizzashopapi/Repository/Repository.cs +++ b/exercise.pizzashopapi/Repository/Repository.cs @@ -1,14 +1,82 @@ using exercise.pizzashopapi.Data; -using exercise.pizzashopapi.Models; +using exercise.pizzashopapi.Exceptions; +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; namespace exercise.pizzashopapi.Repository { - public class Repository : IRepository + public class Repository : IRepository + where T : class + where U : struct { private DataContext _db; - public Task> GetOrdersByCustomer(int id) + private DbSet _table = null!; + public Repository(DataContext db) { - throw new NotImplementedException(); + _db = db; + _table = _db.Set(); + } + + public async Task Add(T entity) + { + await _table.AddAsync(entity); + await _db.SaveChangesAsync(); + return entity; + } + + public async Task Delete(U id) + { + T entity = await Get(id); + _table.Remove(entity); + await _db.SaveChangesAsync(); + return entity; + } + + public async Task Get(U id, string idField, params Func, IQueryable>[] includeChains) + { + IQueryable query = GetIncludeTable(includeChains); + T? entity = await query.FirstOrDefaultAsync(e => EF.Property(e, idField).Equals(id)); + return entity ?? throw new IdNotFoundException($"That ID does not exist for {typeof(T).Name}"); + } + public async Task Get(U id, params Func, IQueryable>[] includeChains) + { + return await Get(id, "Id", includeChains); + } + + public async Task> GetAll(params Func, IQueryable>[] includeChains) + { + IQueryable query = GetIncludeTable(includeChains); + return await query.ToListAsync(); + } + + public async Task Save() + { + await _db.SaveChangesAsync(); + } + + public async Task Find(Expression> condition, params Func, IQueryable>[] includeChains) + { + IQueryable query = GetIncludeTable(includeChains); + T? entity = await query.FirstOrDefaultAsync(condition); + return entity ?? throw new IdNotFoundException($"That ID does not exist for {typeof(T).Name}"); + } + + public async Task Update(T entity) + { + _table.Attach(entity); + _db.Entry(entity).State = EntityState.Modified; + await _db.SaveChangesAsync(); + return entity; + } + + private IQueryable GetIncludeTable(params Func, IQueryable>[] includeChains) + { + IQueryable query = _table; + foreach (var includeChain in includeChains) + { + query = includeChain(query); + } + return query; } } } diff --git a/exercise.pizzashopapi/Tools/MappingProfile.cs b/exercise.pizzashopapi/Tools/MappingProfile.cs new file mode 100644 index 0000000..f96b39e --- /dev/null +++ b/exercise.pizzashopapi/Tools/MappingProfile.cs @@ -0,0 +1,26 @@ +using AutoMapper; +using exercise.pizzashopapi.DTO; +using exercise.pizzashopapi.Models; + +namespace exercise.pizzashopapi.Tools +{ + public class MappingProfile : Profile + { + public MappingProfile() + { + CreateMap() + .ForMember(p => p.ProductType, opt => opt.MapFrom(src => src.ProductType.ToString())); + + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + } + } +} diff --git a/exercise.pizzashopapi/exercise.pizzashopapi.csproj b/exercise.pizzashopapi/exercise.pizzashopapi.csproj index 624203b..b38f832 100644 --- a/exercise.pizzashopapi/exercise.pizzashopapi.csproj +++ b/exercise.pizzashopapi/exercise.pizzashopapi.csproj @@ -11,18 +11,19 @@ - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + From 14ac54f333cf92bae8ae34f07b5628ac54aa4919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolai=20Lillemark=20B=C3=B8rseth?= Date: Mon, 27 Jan 2025 13:28:28 +0100 Subject: [PATCH 2/4] Added tracking and logic for cooking time and preparation status for orders --- exercise.pizzashopapi/DTO/Order.cs | 24 ++++- exercise.pizzashopapi/Data/Seeder.cs | 18 +++- .../Endpoints/OrderEndpoints.cs | 88 ++++++++++++++++++- .../Enums/PreparationStage.cs | 10 +++ .../Enums/{Enum.cs => ProductType.cs} | 0 exercise.pizzashopapi/Models/Order.cs | 9 +- .../Repository/IRepository.cs | 5 ++ .../Repository/Repository.cs | 18 ++++ exercise.pizzashopapi/Tools/MappingProfile.cs | 12 ++- 9 files changed, 169 insertions(+), 15 deletions(-) create mode 100644 exercise.pizzashopapi/Enums/PreparationStage.cs rename exercise.pizzashopapi/Enums/{Enum.cs => ProductType.cs} (100%) diff --git a/exercise.pizzashopapi/DTO/Order.cs b/exercise.pizzashopapi/DTO/Order.cs index 0bd7d39..3d2d5b9 100644 --- a/exercise.pizzashopapi/DTO/Order.cs +++ b/exercise.pizzashopapi/DTO/Order.cs @@ -2,8 +2,24 @@ { public record OrderPost(int ProductId, int CustomerId); public record OrderPostAddTopping(int ToppingId); - public record OrderView(int Id, decimal TotalPrice, CustomerInternal Customer, ProductView Product, IEnumerable Toppings); - public record OrderProduct(int Id, decimal TotalPrice, ProductView Product); - public record OrderProductToppings(int Id, decimal TotalPrice, ProductView Product, IEnumerable Toppings); - public record OrderCustomerProduct(int Id, decimal TotalPrice, CustomerInternal Customer, ProductView Product); + public record OrderView( + int Id, decimal TotalPrice, bool IsDelivered, string PreparationStage, + DateTime CreatedAt, DateTime StartedAt, DateTime CompletedAt, + CustomerInternal Customer, ProductView Product, IEnumerable Toppings + ); + public record OrderProduct( + int Id, decimal TotalPrice, bool IsDelivered, string PreparationStage, + DateTime CreatedAt, DateTime StartedAt, DateTime CompletedAt, + ProductView Product + ); + public record OrderProductToppings( + int Id, decimal TotalPrice, bool IsDelivered, string PreparationStage, + DateTime CreatedAt, DateTime StartedAt, DateTime CompletedAt, + ProductView Product, IEnumerable Toppings + ); + public record OrderCustomerProduct( + int Id, decimal TotalPrice, bool IsDelivered, string PreparationStage, + DateTime CreatedAt, DateTime StartedAt, DateTime CompletedAt, + CustomerInternal Customer, ProductView Product + ); } diff --git a/exercise.pizzashopapi/Data/Seeder.cs b/exercise.pizzashopapi/Data/Seeder.cs index 110729a..eae0cbe 100644 --- a/exercise.pizzashopapi/Data/Seeder.cs +++ b/exercise.pizzashopapi/Data/Seeder.cs @@ -22,10 +22,20 @@ public class Seeder ]; private List _orders = [ - new Order { Id = 1, CustomerId = 1, ProductId = 2 }, - new Order { Id = 2, CustomerId = 2, ProductId = 1 }, - new Order { Id = 3, CustomerId = 3, ProductId = 3 }, - new Order { Id = 4, CustomerId = 3, ProductId = 7 }, + new Order { + Id = 1, CustomerId = 1, ProductId = 2, IsDelivered = true, + CreatedAt = DateTime.UtcNow.AddDays(-1), StartedAt = DateTime.UtcNow.AddDays(-1).AddMinutes(4), + CompletedAt = DateTime.UtcNow.AddDays(-1).AddMinutes(12), + PreparationStage = PreparationStage.Finished }, + new Order { Id = 2, CustomerId = 2, ProductId = 1, IsDelivered = false }, + new Order { Id = 3, CustomerId = 3, ProductId = 3, IsDelivered = true, + CreatedAt = DateTime.UtcNow.AddDays(-1), StartedAt = DateTime.UtcNow.AddDays(-1).AddMinutes(4), + CompletedAt = DateTime.UtcNow.AddDays(-1).AddMinutes(12), + PreparationStage = PreparationStage.Finished}, + new Order { Id = 4, CustomerId = 3, ProductId = 7, IsDelivered = true, + CreatedAt = DateTime.UtcNow.AddDays(-1), StartedAt = DateTime.UtcNow.AddDays(-1).AddMinutes(4), + CompletedAt = DateTime.UtcNow.AddDays(-1).AddMinutes(12), + PreparationStage = PreparationStage.Finished}, ]; private List _toppings = [ diff --git a/exercise.pizzashopapi/Endpoints/OrderEndpoints.cs b/exercise.pizzashopapi/Endpoints/OrderEndpoints.cs index 5575ff0..4fcfd63 100644 --- a/exercise.pizzashopapi/Endpoints/OrderEndpoints.cs +++ b/exercise.pizzashopapi/Endpoints/OrderEndpoints.cs @@ -1,5 +1,6 @@ using AutoMapper; using exercise.pizzashopapi.DTO; +using exercise.pizzashopapi.Enums; using exercise.pizzashopapi.Exceptions; using exercise.pizzashopapi.Models; using exercise.pizzashopapi.Repository; @@ -18,7 +19,9 @@ public static void ConfigureOrdersEndpoints(this WebApplication app) group.MapGet("/", GetOrders); group.MapPost("/", CreateOrder); group.MapGet("/{id}", GetOrder); - group.MapPost("/{id}", AddTopping); + group.MapPost("/{id}/add-topping", AddTopping); + group.MapPost("/{id}/set-delivered", SetDelivered); + group.MapPost("/update-orders", UpdateOrders); } [ProducesResponseType(StatusCodes.Status200OK)] @@ -109,6 +112,10 @@ public static async Task CreateOrder( return TypedResults.Problem(ex.Message); } } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] public static async Task AddTopping( IRepository repository, IRepository toppingRepository, @@ -135,5 +142,84 @@ public static async Task AddTopping( return TypedResults.Problem(ex.Message); } } + + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public static async Task SetDelivered( + IRepository repository, + IMapper mapper, + int id, + bool isDelivered) + { + try + { + Order order = await repository.Get(id, q => q.Include(x => x.Product).Include(x => x.Toppings).Include(x => x.Customer)); + + order.IsDelivered = isDelivered; + await repository.Update(order); + + return TypedResults.Ok(mapper.Map(order)); + } + catch (IdNotFoundException ex) + { + return TypedResults.NotFound(new { ex.Message }); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } + + public static async Task UpdateOrders( + IRepository repository, + IMapper mapper) + { + try + { + IEnumerable orders = await repository.FindAll( + condition: o => o.PreparationStage != PreparationStage.Finished, + orderBy: o => o.CreatedAt, + ascending: true, + q => q.Include(x => x.Customer).Include(x => x.Product).Include(x => x.Toppings) + ); + int cookingLimit = 4; // TODO: This and the following logic should be moved elsewhere. + int count = 0; + foreach (var order in orders.Where(o => o.PreparationStage != PreparationStage.Waiting)) + { + Tuple prepTime = ProductInfo.ProductPrepTimes[order.Product.ProductType]; + int totalPrepTime = prepTime.Item1 + prepTime.Item2; + decimal workTime = (decimal) (DateTime.UtcNow - order.StartedAt)!.Value.TotalMinutes; + if (workTime > totalPrepTime) + { + order.PreparationStage = PreparationStage.Finished; + order.CompletedAt = DateTime.UtcNow; + continue; + } else if (workTime > prepTime.Item1) + { + order.PreparationStage = PreparationStage.Cooking; + } + count++; + } + if (count < cookingLimit) + { + foreach (var order in orders.Where(o => o.PreparationStage == PreparationStage.Waiting).Take(cookingLimit - count)) + { + order.PreparationStage = PreparationStage.Preparing; + order.StartedAt = DateTime.UtcNow; + } + } + await repository.Save(); + return TypedResults.Ok(mapper.Map>(await repository.GetAll(q => q.Include(x => x.Customer).Include(x => x.Product).Include(x => x.Toppings)))); + } + catch (IdNotFoundException ex) + { + return TypedResults.NotFound(new { ex.Message }); + } + catch (Exception ex) + { + return TypedResults.Problem(ex.Message); + } + } } } diff --git a/exercise.pizzashopapi/Enums/PreparationStage.cs b/exercise.pizzashopapi/Enums/PreparationStage.cs new file mode 100644 index 0000000..95699cb --- /dev/null +++ b/exercise.pizzashopapi/Enums/PreparationStage.cs @@ -0,0 +1,10 @@ +namespace exercise.pizzashopapi.Enums +{ + public enum PreparationStage + { + Waiting, + Preparing, + Cooking, + Finished + } +} diff --git a/exercise.pizzashopapi/Enums/Enum.cs b/exercise.pizzashopapi/Enums/ProductType.cs similarity index 100% rename from exercise.pizzashopapi/Enums/Enum.cs rename to exercise.pizzashopapi/Enums/ProductType.cs diff --git a/exercise.pizzashopapi/Models/Order.cs b/exercise.pizzashopapi/Models/Order.cs index 62b195e..ffdc9c0 100644 --- a/exercise.pizzashopapi/Models/Order.cs +++ b/exercise.pizzashopapi/Models/Order.cs @@ -1,16 +1,21 @@ using System.ComponentModel.DataAnnotations.Schema; +using exercise.pizzashopapi.Enums; namespace exercise.pizzashopapi.Models { public class Order { public int Id { get; set; } + public bool IsDelivered { get; set; } = false; + public PreparationStage PreparationStage { get; set; } = PreparationStage.Waiting; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime? StartedAt { get; set; } = null; + public DateTime? CompletedAt { get; set; } = null; + public int ProductId { get; set; } public Product Product { get; set; } - public int CustomerId { get; set; } public Customer Customer { get; set; } - public List Toppings { get; set; } = []; [NotMapped] diff --git a/exercise.pizzashopapi/Repository/IRepository.cs b/exercise.pizzashopapi/Repository/IRepository.cs index bc00846..515f15d 100644 --- a/exercise.pizzashopapi/Repository/IRepository.cs +++ b/exercise.pizzashopapi/Repository/IRepository.cs @@ -17,5 +17,10 @@ public interface IRepository Task Delete(U id); Task Save(); Task Find(Expression> condition, params Func, IQueryable>[] includeChains); + Task> FindAll( + Expression>? condition = null, + Expression>? orderBy = null, + bool ascending = true, + params Func, IQueryable>[] includeChains); } } diff --git a/exercise.pizzashopapi/Repository/Repository.cs b/exercise.pizzashopapi/Repository/Repository.cs index df1a638..c4cfc24 100644 --- a/exercise.pizzashopapi/Repository/Repository.cs +++ b/exercise.pizzashopapi/Repository/Repository.cs @@ -69,6 +69,24 @@ public async Task Update(T entity) return entity; } + public async Task> FindAll( + Expression>? condition = null, + Expression>? orderBy = null, + bool ascending = true, + params Func, IQueryable>[] includeChains) + { + IQueryable query = GetIncludeTable(includeChains); + + if (condition != null) + { + query = query.Where(condition); + } + if (orderBy != null) + { + query = ascending ? query.OrderBy(orderBy) : query.OrderByDescending(orderBy); + } + return await query.ToListAsync(); + } private IQueryable GetIncludeTable(params Func, IQueryable>[] includeChains) { IQueryable query = _table; diff --git a/exercise.pizzashopapi/Tools/MappingProfile.cs b/exercise.pizzashopapi/Tools/MappingProfile.cs index f96b39e..e72825e 100644 --- a/exercise.pizzashopapi/Tools/MappingProfile.cs +++ b/exercise.pizzashopapi/Tools/MappingProfile.cs @@ -11,10 +11,14 @@ public MappingProfile() CreateMap() .ForMember(p => p.ProductType, opt => opt.MapFrom(src => src.ProductType.ToString())); - CreateMap(); - CreateMap(); - CreateMap(); - CreateMap(); + CreateMap() + .ForMember(p => p.PreparationStage, opt => opt.MapFrom(src => src.PreparationStage.ToString())); + CreateMap() + .ForMember(p => p.PreparationStage, opt => opt.MapFrom(src => src.PreparationStage.ToString())); + CreateMap() + .ForMember(p => p.PreparationStage, opt => opt.MapFrom(src => src.PreparationStage.ToString())); + CreateMap() + .ForMember(p => p.PreparationStage, opt => opt.MapFrom(src => src.PreparationStage.ToString())); CreateMap(); CreateMap(); From 05c253cd923b04ee3281d7e40dbcd5cd50453c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolai=20Lillemark=20B=C3=B8rseth?= Date: Mon, 27 Jan 2025 13:30:45 +0100 Subject: [PATCH 3/4] Trying to remove migrations --- .../20250127094519_Init.Designer.cs | 297 ------------------ .../Migrations/20250127094519_Init.cs | 197 ------------ .../Migrations/DataContextModelSnapshot.cs | 294 ----------------- 3 files changed, 788 deletions(-) delete mode 100644 exercise.pizzashopapi/Migrations/20250127094519_Init.Designer.cs delete mode 100644 exercise.pizzashopapi/Migrations/20250127094519_Init.cs delete mode 100644 exercise.pizzashopapi/Migrations/DataContextModelSnapshot.cs diff --git a/exercise.pizzashopapi/Migrations/20250127094519_Init.Designer.cs b/exercise.pizzashopapi/Migrations/20250127094519_Init.Designer.cs deleted file mode 100644 index 989ebd4..0000000 --- a/exercise.pizzashopapi/Migrations/20250127094519_Init.Designer.cs +++ /dev/null @@ -1,297 +0,0 @@ -// -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("20250127094519_Init")] - partial class Init - { - /// - 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"); - - b.HasData( - new - { - Id = 1, - Name = "Nigel" - }, - new - { - Id = 2, - Name = "Dave" - }, - new - { - Id = 3, - Name = "Nikolai" - }); - }); - - 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("ProductId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.HasIndex("ProductId"); - - b.ToTable("Orders"); - - b.HasData( - new - { - Id = 1, - CustomerId = 1, - ProductId = 2 - }, - new - { - Id = 2, - CustomerId = 2, - ProductId = 1 - }, - new - { - Id = 3, - CustomerId = 3, - ProductId = 3 - }, - new - { - Id = 4, - CustomerId = 3, - ProductId = 7 - }); - }); - - modelBuilder.Entity("exercise.pizzashopapi.Models.OrderTopping", b => - { - b.Property("OrderId") - .HasColumnType("integer"); - - b.Property("ToppingId") - .HasColumnType("integer"); - - b.HasKey("OrderId", "ToppingId"); - - b.HasIndex("ToppingId"); - - b.ToTable("OrderTopping"); - - b.HasData( - new - { - OrderId = 3, - ToppingId = 3 - }, - new - { - OrderId = 1, - ToppingId = 1 - }); - }); - - modelBuilder.Entity("exercise.pizzashopapi.Models.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Price") - .HasColumnType("numeric"); - - b.Property("ProductType") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.ToTable("Products"); - - b.HasData( - new - { - Id = 1, - Name = "Hawaiian Pizza", - Price = 200m, - ProductType = 0 - }, - new - { - Id = 2, - Name = "Vegan Cheese Tastic", - Price = 165m, - ProductType = 0 - }, - new - { - Id = 3, - Name = "Pizza Vendetta", - Price = 210m, - ProductType = 0 - }, - new - { - Id = 4, - Name = "Bacon Cheese Burger", - Price = 195m, - ProductType = 1 - }, - new - { - Id = 5, - Name = "Regular Fries", - Price = 110m, - ProductType = 2 - }, - new - { - Id = 6, - Name = "Coca Cola 0.4L", - Price = 65m, - ProductType = 3 - }, - new - { - Id = 7, - Name = "Fanta Orange 0.4L", - Price = 65m, - ProductType = 3 - }); - }); - - 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.Property("Price") - .HasColumnType("numeric"); - - b.HasKey("Id"); - - b.ToTable("Toppings"); - - b.HasData( - new - { - Id = 1, - Name = "Bacon", - Price = 20m - }, - new - { - Id = 2, - Name = "Onion Rings", - Price = 15m - }, - new - { - Id = 3, - Name = "Hot Sauce", - Price = 12m - }); - }); - - modelBuilder.Entity("exercise.pizzashopapi.Models.Order", b => - { - b.HasOne("exercise.pizzashopapi.Models.Customer", "Customer") - .WithMany("Orders") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.SetNull) - .IsRequired(); - - b.HasOne("exercise.pizzashopapi.Models.Product", "Product") - .WithMany("Orders") - .HasForeignKey("ProductId") - .OnDelete(DeleteBehavior.SetNull) - .IsRequired(); - - b.Navigation("Customer"); - - b.Navigation("Product"); - }); - - modelBuilder.Entity("exercise.pizzashopapi.Models.OrderTopping", b => - { - b.HasOne("exercise.pizzashopapi.Models.Order", null) - .WithMany() - .HasForeignKey("OrderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("exercise.pizzashopapi.Models.Topping", null) - .WithMany() - .HasForeignKey("ToppingId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("exercise.pizzashopapi.Models.Customer", b => - { - b.Navigation("Orders"); - }); - - modelBuilder.Entity("exercise.pizzashopapi.Models.Product", b => - { - b.Navigation("Orders"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/exercise.pizzashopapi/Migrations/20250127094519_Init.cs b/exercise.pizzashopapi/Migrations/20250127094519_Init.cs deleted file mode 100644 index b1632cf..0000000 --- a/exercise.pizzashopapi/Migrations/20250127094519_Init.cs +++ /dev/null @@ -1,197 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional - -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: "Products", - columns: table => new - { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - ProductType = table.Column(type: "integer", nullable: false), - Name = table.Column(type: "text", nullable: false), - Price = table.Column(type: "numeric", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Products", 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), - Price = table.Column(type: "numeric", 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), - ProductId = 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.SetNull); - table.ForeignKey( - name: "FK_Orders_Products_ProductId", - column: x => x.ProductId, - principalTable: "Products", - principalColumn: "Id", - onDelete: ReferentialAction.SetNull); - }); - - migrationBuilder.CreateTable( - name: "OrderTopping", - columns: table => new - { - OrderId = table.Column(type: "integer", nullable: false), - ToppingId = table.Column(type: "integer", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_OrderTopping", x => new { x.OrderId, x.ToppingId }); - table.ForeignKey( - name: "FK_OrderTopping_Orders_OrderId", - column: x => x.OrderId, - principalTable: "Orders", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_OrderTopping_Toppings_ToppingId", - column: x => x.ToppingId, - principalTable: "Toppings", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.InsertData( - table: "Customers", - columns: new[] { "Id", "Name" }, - values: new object[,] - { - { 1, "Nigel" }, - { 2, "Dave" }, - { 3, "Nikolai" } - }); - - migrationBuilder.InsertData( - table: "Products", - columns: new[] { "Id", "Name", "Price", "ProductType" }, - values: new object[,] - { - { 1, "Hawaiian Pizza", 200m, 0 }, - { 2, "Vegan Cheese Tastic", 165m, 0 }, - { 3, "Pizza Vendetta", 210m, 0 }, - { 4, "Bacon Cheese Burger", 195m, 1 }, - { 5, "Regular Fries", 110m, 2 }, - { 6, "Coca Cola 0.4L", 65m, 3 }, - { 7, "Fanta Orange 0.4L", 65m, 3 } - }); - - migrationBuilder.InsertData( - table: "Toppings", - columns: new[] { "Id", "Name", "Price" }, - values: new object[,] - { - { 1, "Bacon", 20m }, - { 2, "Onion Rings", 15m }, - { 3, "Hot Sauce", 12m } - }); - - migrationBuilder.InsertData( - table: "Orders", - columns: new[] { "Id", "CustomerId", "ProductId" }, - values: new object[,] - { - { 1, 1, 2 }, - { 2, 2, 1 }, - { 3, 3, 3 }, - { 4, 3, 7 } - }); - - migrationBuilder.InsertData( - table: "OrderTopping", - columns: new[] { "OrderId", "ToppingId" }, - values: new object[,] - { - { 1, 1 }, - { 3, 3 } - }); - - migrationBuilder.CreateIndex( - name: "IX_Orders_CustomerId", - table: "Orders", - column: "CustomerId"); - - migrationBuilder.CreateIndex( - name: "IX_Orders_ProductId", - table: "Orders", - column: "ProductId"); - - migrationBuilder.CreateIndex( - name: "IX_OrderTopping_ToppingId", - table: "OrderTopping", - column: "ToppingId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "OrderTopping"); - - migrationBuilder.DropTable( - name: "Orders"); - - migrationBuilder.DropTable( - name: "Toppings"); - - migrationBuilder.DropTable( - name: "Customers"); - - migrationBuilder.DropTable( - name: "Products"); - } - } -} diff --git a/exercise.pizzashopapi/Migrations/DataContextModelSnapshot.cs b/exercise.pizzashopapi/Migrations/DataContextModelSnapshot.cs deleted file mode 100644 index 817aadd..0000000 --- a/exercise.pizzashopapi/Migrations/DataContextModelSnapshot.cs +++ /dev/null @@ -1,294 +0,0 @@ -// -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"); - - b.HasData( - new - { - Id = 1, - Name = "Nigel" - }, - new - { - Id = 2, - Name = "Dave" - }, - new - { - Id = 3, - Name = "Nikolai" - }); - }); - - 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("ProductId") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("CustomerId"); - - b.HasIndex("ProductId"); - - b.ToTable("Orders"); - - b.HasData( - new - { - Id = 1, - CustomerId = 1, - ProductId = 2 - }, - new - { - Id = 2, - CustomerId = 2, - ProductId = 1 - }, - new - { - Id = 3, - CustomerId = 3, - ProductId = 3 - }, - new - { - Id = 4, - CustomerId = 3, - ProductId = 7 - }); - }); - - modelBuilder.Entity("exercise.pizzashopapi.Models.OrderTopping", b => - { - b.Property("OrderId") - .HasColumnType("integer"); - - b.Property("ToppingId") - .HasColumnType("integer"); - - b.HasKey("OrderId", "ToppingId"); - - b.HasIndex("ToppingId"); - - b.ToTable("OrderTopping"); - - b.HasData( - new - { - OrderId = 3, - ToppingId = 3 - }, - new - { - OrderId = 1, - ToppingId = 1 - }); - }); - - modelBuilder.Entity("exercise.pizzashopapi.Models.Product", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("Price") - .HasColumnType("numeric"); - - b.Property("ProductType") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.ToTable("Products"); - - b.HasData( - new - { - Id = 1, - Name = "Hawaiian Pizza", - Price = 200m, - ProductType = 0 - }, - new - { - Id = 2, - Name = "Vegan Cheese Tastic", - Price = 165m, - ProductType = 0 - }, - new - { - Id = 3, - Name = "Pizza Vendetta", - Price = 210m, - ProductType = 0 - }, - new - { - Id = 4, - Name = "Bacon Cheese Burger", - Price = 195m, - ProductType = 1 - }, - new - { - Id = 5, - Name = "Regular Fries", - Price = 110m, - ProductType = 2 - }, - new - { - Id = 6, - Name = "Coca Cola 0.4L", - Price = 65m, - ProductType = 3 - }, - new - { - Id = 7, - Name = "Fanta Orange 0.4L", - Price = 65m, - ProductType = 3 - }); - }); - - 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.Property("Price") - .HasColumnType("numeric"); - - b.HasKey("Id"); - - b.ToTable("Toppings"); - - b.HasData( - new - { - Id = 1, - Name = "Bacon", - Price = 20m - }, - new - { - Id = 2, - Name = "Onion Rings", - Price = 15m - }, - new - { - Id = 3, - Name = "Hot Sauce", - Price = 12m - }); - }); - - modelBuilder.Entity("exercise.pizzashopapi.Models.Order", b => - { - b.HasOne("exercise.pizzashopapi.Models.Customer", "Customer") - .WithMany("Orders") - .HasForeignKey("CustomerId") - .OnDelete(DeleteBehavior.SetNull) - .IsRequired(); - - b.HasOne("exercise.pizzashopapi.Models.Product", "Product") - .WithMany("Orders") - .HasForeignKey("ProductId") - .OnDelete(DeleteBehavior.SetNull) - .IsRequired(); - - b.Navigation("Customer"); - - b.Navigation("Product"); - }); - - modelBuilder.Entity("exercise.pizzashopapi.Models.OrderTopping", b => - { - b.HasOne("exercise.pizzashopapi.Models.Order", null) - .WithMany() - .HasForeignKey("OrderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("exercise.pizzashopapi.Models.Topping", null) - .WithMany() - .HasForeignKey("ToppingId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("exercise.pizzashopapi.Models.Customer", b => - { - b.Navigation("Orders"); - }); - - modelBuilder.Entity("exercise.pizzashopapi.Models.Product", b => - { - b.Navigation("Orders"); - }); -#pragma warning restore 612, 618 - } - } -} From 7a68fcd4e1820f8d3009337ba4b9b74c9a6894be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikolai=20Lillemark=20B=C3=B8rseth?= Date: Mon, 27 Jan 2025 13:32:05 +0100 Subject: [PATCH 4/4] updated gitignore with migrations --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a62e968..b2e042c 100644 --- a/.gitignore +++ b/.gitignore @@ -482,3 +482,4 @@ $RECYCLE.BIN/ */**/obj/Release /exercise.pizzashopapi/appsettings.json /exercise.pizzashopapi/appsettings.Development.json +*/**/Migrations \ No newline at end of file