Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -482,3 +482,5 @@ $RECYCLE.BIN/
*/**/obj/Release
/exercise.pizzashopapi/appsettings.json
/exercise.pizzashopapi/appsettings.Development.json

/exercise.pizzashopapi/Migrations/*
7 changes: 7 additions & 0 deletions exercise.pizzashopapi/DTO/CustomerResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace exercise.pizzashopapi.DTO;

public class CustomerResponse
{
public int Id { get; set; }
public string Name { get; set; }
}
6 changes: 6 additions & 0 deletions exercise.pizzashopapi/DTO/OrderPut.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace exercise.pizzashopapi.DTO;

public class OrderPut
{
public List<int> ToppingIds { get; set; }
}
15 changes: 15 additions & 0 deletions exercise.pizzashopapi/DTO/OrderResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
using exercise.pizzashopapi.Enums;

namespace exercise.pizzashopapi.DTO;

public class OrderResponse
{
public int Id { get; set; }
public string Status { get; set; }
public DateTime OrderDate { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public CustomerResponse? Customer { get; set; }
public PizzaResponse Pizza { get; set; }
public IEnumerable<string> Toppings { get; set; } = new List<string>();
}
8 changes: 8 additions & 0 deletions exercise.pizzashopapi/DTO/PizzaResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace exercise.pizzashopapi.DTO;

public class PizzaResponse
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
65 changes: 45 additions & 20 deletions exercise.pizzashopapi/Data/DataContext.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,53 @@
using exercise.pizzashopapi.Models;
using System.Diagnostics;
using exercise.pizzashopapi.Models;
using Microsoft.EntityFrameworkCore;

namespace exercise.pizzashopapi.Data
namespace exercise.pizzashopapi.Data;

public class DataContext : DbContext
{
public class DataContext : DbContext
{
private string connectionString;
public DataContext()
{
var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
connectionString = configuration.GetValue<string>("ConnectionStrings:DefaultConnectionString");
private readonly string _connectionString;

}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql(connectionString);
public DataContext()
{
var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
_connectionString = configuration.GetValue<string>("ConnectionStrings:DefaultConnectionString")!;
}

//set primary of order?
public DbSet<Pizza> Pizzas { get; set; }
public DbSet<Customer> Customers { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<Topping> Toppings { get; set; }

//seed data?
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>()
.HasKey(o => o.Id);
modelBuilder.Entity<Order>()
.HasOne(o => o.Customer)
.WithMany(c => c.Orders)
.HasForeignKey(o => o.CustomerId);
modelBuilder.Entity<Order>()
.HasOne(o => o.Pizza)
.WithMany()
.HasForeignKey(o => o.PizzaId);
modelBuilder.Entity<Order>()
.HasMany(o => o.Toppings)
.WithMany()
.UsingEntity<Dictionary<string, object>>(
"OrderToppings",
r => r.HasOne<Topping>().WithMany().HasForeignKey("ToppingId"),
l => l.HasOne<Order>().WithMany().HasForeignKey("OrderId")
);
modelBuilder.Entity<Customer>()
.HasKey(c => c.Id);
modelBuilder.Entity<Pizza>()
.HasKey(p => p.Id);
}

}
public DbSet<Pizza> Pizzas { get; set; }
public DbSet<Customer> Customers { get; set; }
public DbSet<Order> Orders { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql(_connectionString);
optionsBuilder.LogTo(message => Debug.WriteLine(message));
}
}
}
37 changes: 29 additions & 8 deletions exercise.pizzashopapi/Data/Seeder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,43 @@ public async static void SeedPizzaShopApi(this WebApplication app)
{
if(!db.Customers.Any())
{
db.Add(new Customer() { Name="Nigel" });
db.Add(new Customer() { Name = "Dave" });
db.Add(new Customer() { Id = 1, Name="Nigel" });
db.Add(new Customer() { Id = 2, Name = "Dave" });
db.Add(new Customer() { Id = 3, Name = "Magnus" });
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() { Id = 1, Name = "Cheese & Pineapple", Price = 9.99m });
db.Add(new Pizza() { Id = 2, Name = "Vegan Cheese Tastic", Price = 12.99m });
db.Add(new Pizza() { Id = 3, Name = "Mighty Meat", Price = 14.99m });
await db.SaveChangesAsync();

}

//order data
if(1==1)

if (!db.Toppings.Any())
{
db.Add(new Topping() { Id = 1, Name = "Bacon" });
db.Add(new Topping() { Id = 2, Name = "Mushrooms" });
db.Add(new Topping() { Id = 3, Name = "Olives" });
await db.SaveChangesAsync();
}

if(!db.Orders.Any())
{
db.Add(new Order() { CustomerId = 1, PizzaId = 1, OrderDate = DateTime.Parse("2021-01-01").ToUniversalTime() });
db.Add(new Order() { CustomerId = 2, PizzaId = 2, OrderDate = DateTime.Parse("2021-01-01").ToUniversalTime(), Delivered = true });
var order = db.Add(new Order() { CustomerId = 3, PizzaId = 3, OrderDate = DateTime.Parse("2025-01-27").ToUniversalTime() });

await db.SaveChangesAsync();

// Toppings for testing
//var order = db.Orders.Find(2);
order.Entity.Toppings = new List<Topping>
{
await db.Toppings.FindAsync(1),
await db.Toppings.FindAsync(2)
};

await db.SaveChangesAsync();
}
}
Expand Down
81 changes: 78 additions & 3 deletions exercise.pizzashopapi/EndPoints/PizzaShopApi.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using exercise.pizzashopapi.Repository;
using AutoMapper;
using exercise.pizzashopapi.DTO;
using exercise.pizzashopapi.Models;
using exercise.pizzashopapi.Repository;
using Microsoft.AspNetCore.Mvc;

namespace exercise.pizzashopapi.EndPoints
Expand All @@ -7,9 +10,81 @@ public static class PizzaShopApi
{
public static void ConfigurePizzaShopApi(this WebApplication app)
{

var ordergroup = app.MapGroup("orders");
ordergroup.MapGet("/", GetOrders);
ordergroup.MapGet("/{id}", GetOrder);
ordergroup.MapGet("/customer/{id}", GetOrdersByCustomerId);
ordergroup.MapPut("/{id}", UpdateOrder);
ordergroup.MapPost("/markDelivered/{id}", MarkOrderDelivered);
}


private static async Task<IResult> GetOrders(IRepository<Order> repository, IMapper mapper)
{
var orders = await repository.GetAll(o => o.Customer, o => o.Pizza, o => o.Toppings);
var response = mapper.Map<IEnumerable<OrderResponse>>(orders);

return TypedResults.Ok(response);
}

private static async Task<IResult> GetOrder(IRepository<Order> repository, IMapper mapper, int id)
{
var order = await repository.Get(o => o.Id == id, o => o.Customer, o => o.Pizza, o => o.Toppings);
var response = mapper.Map<OrderResponse>(order);

return TypedResults.Ok(response);
}

private static async Task<IResult> GetOrdersByCustomerId(IRepository<Order> repository, IMapper mapper, int id)
{
var orders = await repository.GetAll(o => o.CustomerId == id, o => o.Pizza, o => o.Toppings);
var response = mapper.Map<IEnumerable<OrderResponse>>(orders);

return TypedResults.Ok(response);
}

[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(string), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
private static async Task<IResult> UpdateOrder(IRepository<Order> orderRepository, IRepository<Topping> toppingRepository, IMapper mapper, int id, [FromBody] OrderPut body)
{
var order = await orderRepository.Get(o => o.Id == id);
if (order == null) return TypedResults.NotFound();

var validToppings = new HashSet<Topping>();
foreach (var toppingId in body.ToppingIds)
{
var topping = await toppingRepository.Get(t => t.Id == toppingId);
if (topping != null)
{
validToppings.Add(topping);
}
}

foreach (var topping in validToppings)
{
if (order.Toppings is not null && !order.Toppings.Contains(topping))
{
return TypedResults.BadRequest("Can't remove already added toppings.");
}
}

order.Toppings = validToppings.ToList();
await orderRepository.Update(order);

return TypedResults.NoContent();
}

[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
private static async Task<IResult> MarkOrderDelivered(IRepository<Order> repository, int id)
{
var order = await repository.Get(o => o.Id == id);
if (order == null) return TypedResults.NotFound();

order.Delivered = true;
await repository.Update(order);

return TypedResults.NoContent();
}
}
}
9 changes: 9 additions & 0 deletions exercise.pizzashopapi/Enums/OrderStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace exercise.pizzashopapi.Enums;

public enum OrderStatus
{
Preparing,
Cooking,
Delivering,
Delivered
}
15 changes: 15 additions & 0 deletions exercise.pizzashopapi/Mapper/AutoMapperProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using AutoMapper;

namespace exercise.pizzashopapi.Mapper;

public class AutoMapperProfile : Profile
{
public AutoMapperProfile()
{
CreateMap<Models.Order, DTO.OrderResponse>()
.ForMember(dest => dest.Customer, opt => opt.MapFrom(src => src.Customer))
.ForMember(dest => dest.Toppings, opt => opt.MapFrom(src => src.Toppings.Select(t => t.Name)));
CreateMap<Models.Customer, DTO.CustomerResponse>();
CreateMap<Models.Pizza, DTO.PizzaResponse>();
}
}
1 change: 1 addition & 0 deletions exercise.pizzashopapi/Models/Customer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public virtual List<Order>? Orders { get; set; }
}
}
34 changes: 32 additions & 2 deletions exercise.pizzashopapi/Models/Order.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,40 @@
using System.ComponentModel.DataAnnotations.Schema;
using exercise.pizzashopapi.Enums;

namespace exercise.pizzashopapi.Models
{
public class Order
{


public int Id { get; set; }
public int? CustomerId { get; set; }
public int PizzaId { get; set; }
public Customer? Customer { get; set; }
public Pizza Pizza { get; set; }
//public List<int>? ToppingIds { get; set; }
public List<Topping>? Toppings { get; set; }
public DateTime OrderDate { get; set; }
public bool Delivered { get; set; }
[NotMapped]
public OrderStatus Status
{
get
{
if (Delivered)
{
return OrderStatus.Delivered;
}
return (DateTime.UtcNow - OrderDate) switch
{
var d when d.TotalMinutes < 3 => OrderStatus.Preparing,
var d when d.TotalMinutes < 12 => OrderStatus.Cooking,
_ => OrderStatus.Delivering
};
}
}

public Order()
{
OrderDate = DateTime.UtcNow;
}
}
}
7 changes: 7 additions & 0 deletions exercise.pizzashopapi/Models/Topping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace exercise.pizzashopapi.Models;

public class Topping
{
public int Id { get; set; }
public string Name { get; set; }
}
8 changes: 7 additions & 1 deletion exercise.pizzashopapi/Program.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
using exercise.pizzashopapi.Data;
using exercise.pizzashopapi.EndPoints;
using exercise.pizzashopapi.Mapper;
using exercise.pizzashopapi.Models;
using exercise.pizzashopapi.Repository;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
builder.Services.AddScoped<IRepository, Repository>();
builder.Services.AddScoped<IRepository<Order>, Repository<Order>>();
builder.Services.AddScoped<IRepository<Pizza>, Repository<Pizza>>();
builder.Services.AddScoped<IRepository<Customer>, Repository<Customer>>();
builder.Services.AddScoped<IRepository<Topping>, Repository<Topping>>();
builder.Services.AddDbContext<DataContext>();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddAutoMapper(typeof(AutoMapperProfile));

var app = builder.Build();

Expand Down
11 changes: 6 additions & 5 deletions exercise.pizzashopapi/Repository/IRepository.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using exercise.pizzashopapi.Models;
using System.Linq.Expressions;

namespace exercise.pizzashopapi.Repository
{
public interface IRepository
public interface IRepository<T> where T : class
{
Task<IEnumerable<Order>> GetOrdersByCustomer(int id);


Task<IEnumerable<T>> GetAll(params Expression<Func<T, object>>[] includes);
Task<IEnumerable<T>> GetAll(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includes);
Task<T?> Get(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includes);
Task<T?> Update(T entity);
}
}
Loading