diff --git a/.gitignore b/.gitignore index 8a30d25..7f3509b 100644 --- a/.gitignore +++ b/.gitignore @@ -396,3 +396,11 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml + +*/**/bin/Debug +*/**/bin/Release +*/**/obj/Debug +*/**/obj/Release +/exercise.pizzashopapi/appsettings.json +/exercise.pizzashopapi/appsettings.Development.json +/exercise.pizzashopapi/out diff --git a/GUIDE.md b/GUIDE.md index e44888e..1a0472a 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -1,13 +1,49 @@ -**Note: Change any headings in this document** - # Project Guide ## Setup +Backend running on Elastic Beanstalk, with an RDS database, with a SQS/SNS queueing system. + +Follow the references for hosting this application on Elastic Beanstalk, for creating an RDS database, +and for setting up SQS/SNS. + +Start off with setting up the SNS Topic and SQS Queue, and replace the _queueUrl and _topicArn variables +in Endpoints/PizzaShopApi.cs. + +Then, set up an RDS database, add the credentials for the db in an appsettings.json file, +and migrate to the db with **update-database** in Packet Manager Console in VS. + +Then follow the steps to publish this application on Elastic Beanstalk. ## Introduction +Backend PizzaShop API written in C#/.NET. Frontend solution is currently just using Swagger for +accessing the data. You could write your own frontend for this, and host it on s3 bucket +(reference in same repo as Elastic Beanstalk/RDS). Just use the elastic beanstalk link as an API! ## Technical Designs +Users of the PizzaShop API can create and view customers, pizzas and orders. (Links won't work +after AWS instance gets removed...) Use these endpoints for accessing data from your own hosted Elastic Beanstalk instance. + +Access Swagger from: + ++ ***[/swagger/index.html](http://aws-day-5-tvaltn-env.eba-kxmh9vpj.eu-north-1.elasticbeanstalk.com/swagger/index.html)*** Swagger + +Endpoints: + ++ ***[/](http://aws-day-5-tvaltn-api-env.eba-js3ghmsk.eu-north-1.elasticbeanstalk.com/)*** Root directory ++ ***[/customers](http://aws-day-5-tvaltn-api-env.eba-js3ghmsk.eu-north-1.elasticbeanstalk.com/customers)*** Post/Get for customers ++ ***[/pizzas](http://aws-day-5-tvaltn-api-env.eba-js3ghmsk.eu-north-1.elasticbeanstalk.com/pizzas)*** Post/Get for pizzas ++ ***[/processorders](http://aws-day-5-tvaltn-api-env.eba-js3ghmsk.eu-north-1.elasticbeanstalk.com/processorders)*** Post order to queue, Get orders to process (opens slow if no orders in queue) ++ ***[/vieworders](http://aws-day-5-tvaltn-api-env.eba-js3ghmsk.eu-north-1.elasticbeanstalk.com/vieworders)*** View the orders that have been processed (they are in the db) ## Technical Descriptions +The orders work through a SQS/SNS queueing system, where an order gets placed in the queue from post, and the orders +then get processed by going to the /processorders endpoint. You can view processed orders from the /vieworders endpoint. + +When an order gets processed, it also gets pushed to the RDS database. Non-processed orders will not show up here. ## References +Look through the old AWS github repos for an idea on how to work with Elastic Beanstalk, RDS, SQS/SNS... + +[Elastic Beanstalk, RDS](https://github.com/boolean-uk/csharp-cloud-aws-day-1) + +[SQS/SNS](https://github.com/boolean-uk/csharp-cloud-aws-day-4) diff --git a/Images/aws_elastic_beanstalk.png b/Images/aws_elastic_beanstalk.png new file mode 100644 index 0000000..66dbfa0 Binary files /dev/null and b/Images/aws_elastic_beanstalk.png differ diff --git a/Images/aws_rds_db.png b/Images/aws_rds_db.png new file mode 100644 index 0000000..ab724e0 Binary files /dev/null and b/Images/aws_rds_db.png differ diff --git a/Images/aws_sns_topic.png b/Images/aws_sns_topic.png new file mode 100644 index 0000000..c9afba5 Binary files /dev/null and b/Images/aws_sns_topic.png differ diff --git a/Images/aws_sqs_queue.png b/Images/aws_sqs_queue.png new file mode 100644 index 0000000..e53373c Binary files /dev/null and b/Images/aws_sqs_queue.png differ diff --git a/Images/sns_sqs.png b/Images/sns_sqs.png new file mode 100644 index 0000000..68d9557 Binary files /dev/null and b/Images/sns_sqs.png differ diff --git a/Images/swagger.png b/Images/swagger.png new file mode 100644 index 0000000..879aa63 Binary files /dev/null and b/Images/swagger.png differ diff --git a/Images/swagger_get.png b/Images/swagger_get.png new file mode 100644 index 0000000..509f956 Binary files /dev/null and b/Images/swagger_get.png differ diff --git a/exercise.pizzashopapi/DTO/CustomerDTO.cs b/exercise.pizzashopapi/DTO/CustomerDTO.cs new file mode 100644 index 0000000..8badaba --- /dev/null +++ b/exercise.pizzashopapi/DTO/CustomerDTO.cs @@ -0,0 +1,8 @@ +namespace exercise.pizzashopapi.DTO +{ + public class CustomerDTO + { + public int Id { get; set; } + public string Name { get; set; } + } +} diff --git a/exercise.pizzashopapi/DTO/CustomerView.cs b/exercise.pizzashopapi/DTO/CustomerView.cs new file mode 100644 index 0000000..c667031 --- /dev/null +++ b/exercise.pizzashopapi/DTO/CustomerView.cs @@ -0,0 +1,7 @@ +namespace exercise.pizzashopapi.DTO +{ + public class CustomerView + { + public string Name { get; set; } + } +} diff --git a/exercise.pizzashopapi/DTO/OrderDTO.cs b/exercise.pizzashopapi/DTO/OrderDTO.cs new file mode 100644 index 0000000..7c96bd1 --- /dev/null +++ b/exercise.pizzashopapi/DTO/OrderDTO.cs @@ -0,0 +1,11 @@ +using exercise.pizzashopapi.Models; + +namespace exercise.pizzashopapi.DTO +{ + public class OrderDTO + { + public Pizza Pizza { get; set; } + public Customer Customer { get; set; } + public string Status { get; set; } + } +} diff --git a/exercise.pizzashopapi/DTO/OrderUpdateView.cs b/exercise.pizzashopapi/DTO/OrderUpdateView.cs new file mode 100644 index 0000000..7c821a5 --- /dev/null +++ b/exercise.pizzashopapi/DTO/OrderUpdateView.cs @@ -0,0 +1,8 @@ +namespace exercise.pizzashopapi.DTO +{ + public class OrderUpdateView + { + public int PizzaId { get; set; } + public int Status { get; set; } + } +} diff --git a/exercise.pizzashopapi/DTO/OrderView.cs b/exercise.pizzashopapi/DTO/OrderView.cs new file mode 100644 index 0000000..4004269 --- /dev/null +++ b/exercise.pizzashopapi/DTO/OrderView.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace exercise.pizzashopapi.DTO +{ + public class OrderView + { + public int PizzaId { get; set; } + public int CustomerId { get; set; } + } +} diff --git a/exercise.pizzashopapi/DTO/PizzaDTO.cs b/exercise.pizzashopapi/DTO/PizzaDTO.cs new file mode 100644 index 0000000..5ff66fa --- /dev/null +++ b/exercise.pizzashopapi/DTO/PizzaDTO.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace exercise.pizzashopapi.DTO +{ + public class PizzaDTO + { + public int Id { get; set; } + public string Name { get; set; } + public decimal Price { get; set; } + } +} diff --git a/exercise.pizzashopapi/DTO/PizzaView.cs b/exercise.pizzashopapi/DTO/PizzaView.cs new file mode 100644 index 0000000..a803c57 --- /dev/null +++ b/exercise.pizzashopapi/DTO/PizzaView.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace exercise.pizzashopapi.DTO +{ + public class PizzaView + { + public string Name { get; set; } + public decimal Price { get; set; } + } +} diff --git a/exercise.pizzashopapi/Data/DataContext.cs b/exercise.pizzashopapi/Data/DataContext.cs new file mode 100644 index 0000000..dc15717 --- /dev/null +++ b/exercise.pizzashopapi/Data/DataContext.cs @@ -0,0 +1,29 @@ +using exercise.pizzashopapi.Models; +using Microsoft.EntityFrameworkCore; +using System.Reflection.Emit; + +namespace exercise.pizzashopapi.Data +{ + public class DataContext : DbContext + { + private string connectionString; + public DataContext() + { + var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + connectionString = configuration.GetValue("ConnectionStrings:DefaultConnectionString")!; + + } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + //set primary of order? + modelBuilder.Entity().HasKey(k => new { k.PizzaId, k.CustomerId }); + } + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseNpgsql(connectionString); + } + 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 new file mode 100644 index 0000000..113d228 --- /dev/null +++ b/exercise.pizzashopapi/Data/Seeder.cs @@ -0,0 +1,36 @@ +using exercise.pizzashopapi.Models; + +namespace exercise.pizzashopapi.Data +{ + public static 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" }); + db.Add(new Customer() { Name = "Toni" }); + db.SaveChanges(); + } + if(!db.Pizzas.Any()) + { + db.Add(new Pizza() { Name = "Cheese & Pineapple", Price = 60 }); + db.Add(new Pizza() { Name = "Vegan Cheese Tastic", Price = 60 }); + db.Add(new Pizza() { Name = "Pepperoni", Price = 50 }); + await db.SaveChangesAsync(); + + } + if (!db.Orders.Any()) + { + db.Add(new Order() { CustomerId = 1, PizzaId = 2, Status = (PizzaStatus) 5 }); + db.Add(new Order() { CustomerId = 2, PizzaId = 1, Status = (PizzaStatus) 5 }); + db.Add(new Order() { CustomerId = 3, PizzaId = 3, Status = (PizzaStatus) 5 }); + await db.SaveChangesAsync(); + } + } + } + } +} diff --git a/exercise.pizzashopapi/EndPoints/PizzaShopApi.cs b/exercise.pizzashopapi/EndPoints/PizzaShopApi.cs new file mode 100644 index 0000000..be2b93d --- /dev/null +++ b/exercise.pizzashopapi/EndPoints/PizzaShopApi.cs @@ -0,0 +1,191 @@ +using Amazon.EventBridge; +using Amazon.EventBridge.Model; +using Amazon.SimpleNotificationService; +using Amazon.SimpleNotificationService.Model; +using Amazon.SQS; +using Amazon.SQS.Model; +using exercise.pizzashopapi.DTO; +using exercise.pizzashopapi.Models; +using exercise.pizzashopapi.Repository; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; +using System.Text.Json; + +namespace exercise.pizzashopapi.EndPoints +{ + public static class PizzaShopApi + { + private static string _queueUrl = "https://sqs.eu-north-1.amazonaws.com/637423341661/tvaltnOrderQueue"; // Format of https://.* + private static string _topicArn = "arn:aws:sns:eu-north-1:637423341661:tvaltnOrderCreatedTopic"; // Format of arn:aws.* + public static void ConfigurePizzaShopApi(this WebApplication app) + { + var shop = app.MapGroup(""); + shop.MapGet("/", ApiGet); + shop.MapPost("/processorders", CreateOrder); + shop.MapGet("/processorders", ProcessOrders); + shop.MapGet("/vieworders", GetOrders); + + shop.MapPost("/pizzas", CreatePizza); + shop.MapGet("/pizzas", GetPizzas); + + shop.MapPost("/customers", CreateCustomer); + shop.MapGet("/customers", GetCustomers); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static IResult ApiGet() + { + return TypedResults.Ok("API Works"); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task ProcessOrders(IRepository repository) + { + IAmazonSQS sqs = new AmazonSQSClient(); + + var request = new ReceiveMessageRequest + { + QueueUrl = _queueUrl, + MaxNumberOfMessages = 10, + WaitTimeSeconds = 20 + }; + + var response = await sqs.ReceiveMessageAsync(request); + + var resultOrders = new List(); + + foreach (var message in response.Messages) + { + Order? order = null; + // Get the message that is nested in the queue request + using (JsonDocument document = JsonDocument.Parse(message.Body)) + { + string innerMessage = document.RootElement.GetProperty("Message").GetString()!; + + // Deserialize the inner message + order = JsonSerializer.Deserialize(innerMessage); + } + + // Just delete duplicate orders, lazy... + var existingOrder = await repository.Get([], o => o.PizzaId == order.PizzaId && o.CustomerId == order.CustomerId); + if (existingOrder != null) + { + await sqs.DeleteMessageAsync(_queueUrl, message.ReceiptHandle); + continue; + } + + // Process order (e.g., update inventory) + order!.Status = (PizzaStatus) 5; + var result = await repository.Create(["Customer", "Pizza"], order!); + resultOrders.Add(result); // add this to our resultorders list that we render + + // Delete message after processing + await sqs.DeleteMessageAsync(_queueUrl, message.ReceiptHandle); + } + if (resultOrders.Count == 0) + { + return TypedResults.Ok("0 Orders have been added"); + } + + var resultDTO = new List(); + foreach (var res in resultOrders) + { + resultDTO.Add(new OrderDTO() { Customer = res.Customer, Pizza = res.Pizza, Status = res.Status.ToString() }); + } + + + return TypedResults.Ok(resultDTO); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetOrders(IRepository repository) + { + var result = await repository.GetAll(["Customer", "Pizza"]); + var resultDTO = new List(); + foreach (var res in result) + { + resultDTO.Add(new OrderDTO() { Customer = res.Customer, Pizza = res.Pizza, Status = res.Status.ToString() }); + } + return TypedResults.Ok(resultDTO); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task CreateOrder(IRepository repository, OrderView view) + { + IAmazonSimpleNotificationService sns = new AmazonSimpleNotificationServiceClient(); + IAmazonEventBridge eventBridge = new AmazonEventBridgeClient(); + + var order = new Order() { CustomerId = view.CustomerId, PizzaId = view.PizzaId, Status = (PizzaStatus) 1 }; + + // Publish to SNS + var message = JsonSerializer.Serialize(order); + var publishRequest = new PublishRequest + { + TopicArn = _topicArn, + Message = message + }; + + await sns.PublishAsync(publishRequest); + + // Publish to EventBridge + var eventEntry = new PutEventsRequestEntry + { + Source = "order.service", + DetailType = "OrderCreated", + Detail = JsonSerializer.Serialize(order), + EventBusName = "CustomEventBus" + }; + + var putEventsRequest = new PutEventsRequest + { + Entries = new List { eventEntry } + }; + + await eventBridge.PutEventsAsync(putEventsRequest); + + return TypedResults.Ok("order put in queue"); + } + + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetPizzas(IRepository repository) + { + var result = await repository.GetAll([]); + var resultDTO = new List(); + foreach (var res in result) + { + resultDTO.Add(new PizzaDTO() { Id = res.Id, Name = res.Name, Price = res.Price }); + } + return TypedResults.Ok(resultDTO); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task CreatePizza(IRepository repository, PizzaView view) + { + var result = await repository.Create([], new Pizza() { Name = view.Name, Price = view.Price }); + var resultDTO = new PizzaDTO() { Id = result.Id, Name = result.Name, Price = result.Price }; + return TypedResults.Ok(resultDTO); + } + + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task GetCustomers(IRepository repository) + { + var result = await repository.GetAll([]); + var resultDTO = new List(); + foreach (var res in result) + { + resultDTO.Add(new CustomerDTO() { Id = res.Id, Name = res.Name }); + } + return TypedResults.Ok(resultDTO); + } + + [ProducesResponseType(StatusCodes.Status200OK)] + public static async Task CreateCustomer(IRepository repository, CustomerView view) + { + var result = await repository.Create([], new Customer() { Name = view.Name }); + var resultDTO = new CustomerDTO() { Id = result.Id, Name = result.Name }; + return TypedResults.Ok(resultDTO); + } + } +} diff --git a/exercise.pizzashopapi/Migrations/20240911080956_FirstMigration.Designer.cs b/exercise.pizzashopapi/Migrations/20240911080956_FirstMigration.Designer.cs new file mode 100644 index 0000000..a24e363 --- /dev/null +++ b/exercise.pizzashopapi/Migrations/20240911080956_FirstMigration.Designer.cs @@ -0,0 +1,111 @@ +// +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("20240911080956_FirstMigration")] + partial class FirstMigration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("customers"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Order", b => + { + b.Property("PizzaId") + .HasColumnType("integer") + .HasColumnName("pizzaid"); + + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customerid"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("PizzaId", "CustomerId"); + + b.HasIndex("CustomerId"); + + b.ToTable("orders"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Pizza", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Price") + .HasColumnType("numeric") + .HasColumnName("price"); + + b.HasKey("Id"); + + b.ToTable("pizzas"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Order", b => + { + b.HasOne("exercise.pizzashopapi.Models.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("exercise.pizzashopapi.Models.Pizza", "Pizza") + .WithMany() + .HasForeignKey("PizzaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Pizza"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/exercise.pizzashopapi/Migrations/20240911080956_FirstMigration.cs b/exercise.pizzashopapi/Migrations/20240911080956_FirstMigration.cs new file mode 100644 index 0000000..440a4e7 --- /dev/null +++ b/exercise.pizzashopapi/Migrations/20240911080956_FirstMigration.cs @@ -0,0 +1,85 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace exercise.pizzashopapi.Migrations +{ + /// + public partial class FirstMigration : 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 + { + pizzaid = table.Column(type: "integer", nullable: false), + customerid = table.Column(type: "integer", nullable: false), + status = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_orders", x => new { x.pizzaid, x.customerid }); + 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"); + } + + /// + 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..6c09933 --- /dev/null +++ b/exercise.pizzashopapi/Migrations/DataContextModelSnapshot.cs @@ -0,0 +1,108 @@ +// +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", "7.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id"); + + b.ToTable("customers"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Order", b => + { + b.Property("PizzaId") + .HasColumnType("integer") + .HasColumnName("pizzaid"); + + b.Property("CustomerId") + .HasColumnType("integer") + .HasColumnName("customerid"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("status"); + + b.HasKey("PizzaId", "CustomerId"); + + b.HasIndex("CustomerId"); + + b.ToTable("orders"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Pizza", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Price") + .HasColumnType("numeric") + .HasColumnName("price"); + + b.HasKey("Id"); + + b.ToTable("pizzas"); + }); + + modelBuilder.Entity("exercise.pizzashopapi.Models.Order", b => + { + b.HasOne("exercise.pizzashopapi.Models.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("exercise.pizzashopapi.Models.Pizza", "Pizza") + .WithMany() + .HasForeignKey("PizzaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Customer"); + + b.Navigation("Pizza"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/exercise.pizzashopapi/Models/Customer.cs b/exercise.pizzashopapi/Models/Customer.cs new file mode 100644 index 0000000..12e06e2 --- /dev/null +++ b/exercise.pizzashopapi/Models/Customer.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace exercise.pizzashopapi.Models +{ + [Table("customers")] + public class Customer + { + [Column("id")] + public int Id { get; set; } + [Column("name")] + public string Name { get; set; } + } +} diff --git a/exercise.pizzashopapi/Models/Order.cs b/exercise.pizzashopapi/Models/Order.cs new file mode 100644 index 0000000..e0c7069 --- /dev/null +++ b/exercise.pizzashopapi/Models/Order.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace exercise.pizzashopapi.Models +{ + public enum PizzaStatus + { + Received = 1, + Prepared = 2, + Cooked = 3, + Transit = 4, + Processed = 5 + } + + [Table("orders")] + public class Order + { + [Column("pizzaid")] + public int PizzaId { get; set; } + [Column("customerid")] + public int CustomerId { get; set; } + [Column("status")] + public PizzaStatus Status { get; set; } + public Pizza Pizza { get; set; } + public Customer Customer { get; set; } + } +} diff --git a/exercise.pizzashopapi/Models/Pizza.cs b/exercise.pizzashopapi/Models/Pizza.cs new file mode 100644 index 0000000..99108b1 --- /dev/null +++ b/exercise.pizzashopapi/Models/Pizza.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace exercise.pizzashopapi.Models +{ + [Table("pizzas")] + public class Pizza + { + [Column("id")] + public int Id { get; set; } + [Column("name")] + public string Name { get; set; } + [Column("price")] + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/exercise.pizzashopapi/Program.cs b/exercise.pizzashopapi/Program.cs new file mode 100644 index 0000000..a8ed59f --- /dev/null +++ b/exercise.pizzashopapi/Program.cs @@ -0,0 +1,32 @@ +using exercise.pizzashopapi.Data; +using exercise.pizzashopapi.EndPoints; +using exercise.pizzashopapi.Models; +using exercise.pizzashopapi.Repository; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddScoped, Repository>(); +builder.Services.AddDbContext(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment() || app.Environment.IsProduction()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.ConfigurePizzaShopApi(); + +app.SeedPizzaShopApi(); +app.Run(); diff --git a/exercise.pizzashopapi/Properties/launchSettings.json b/exercise.pizzashopapi/Properties/launchSettings.json new file mode 100644 index 0000000..1d7269e --- /dev/null +++ b/exercise.pizzashopapi/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$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", + "applicationUrl": "http://localhost:5070", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "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 new file mode 100644 index 0000000..c98bb03 --- /dev/null +++ b/exercise.pizzashopapi/Repository/IRepository.cs @@ -0,0 +1,14 @@ +using exercise.pizzashopapi.Models; +using System.Linq.Expressions; + +namespace exercise.pizzashopapi.Repository +{ + public interface IRepository where Model : class + { + public Task> GetAll(string[] inclusions); + public Task> GetAll(string[] inclusions, Expression> predicate); + public Task Get(string[] inclusions, Expression> predicate); + public Task Update(string[] inclusions, Model model); + public Task Create(string[] inclusions, Model model); + } +} diff --git a/exercise.pizzashopapi/Repository/Repository.cs b/exercise.pizzashopapi/Repository/Repository.cs new file mode 100644 index 0000000..9b2e5dd --- /dev/null +++ b/exercise.pizzashopapi/Repository/Repository.cs @@ -0,0 +1,74 @@ +using exercise.pizzashopapi.Data; +using exercise.pizzashopapi.Models; +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; +using static Microsoft.EntityFrameworkCore.DbLoggerCategory; + +namespace exercise.pizzashopapi.Repository +{ + public class Repository : IRepository + where Model : class + { + private DataContext _db; + private DbSet _dbSet; + + public Repository(DataContext db) + { + _db = db; + _dbSet = _db.Set(); + } + + public async Task Create(string[] inclusions, Model model) + { + _dbSet.Add(model); + await _db.SaveChangesAsync(); + foreach (string inclusion in inclusions) + { + await _db.Entry(model).Reference(inclusion).LoadAsync(); + } + return model; + } + + public async Task Get(string[] inclusions, Expression> predicate) + { + var query = _dbSet.AsQueryable(); + foreach (string inclusion in inclusions) + { + query = query.Include(inclusion); + } + return await query.FirstOrDefaultAsync(predicate); + } + + public async Task> GetAll(string[] inclusions, Expression> predicate) + { + var query = _dbSet.AsQueryable(); + foreach (string inclusion in inclusions) + { + query = query.Include(inclusion); + } + return await query.Where(predicate).ToListAsync(); + } + + public async Task> GetAll(string[] inclusions) + { + var query = _dbSet.AsQueryable(); + foreach (string inclusion in inclusions) + { + query = query.Include(inclusion); + } + return await query.ToListAsync(); + } + + public async Task Update(string[] inclusions, Model model) + { + _dbSet.Attach(model); + _db.Entry(model).State = EntityState.Modified; + await _db.SaveChangesAsync(); + foreach (string inclusion in inclusions) + { + await _db.Entry(model).Reference(inclusion).LoadAsync(); + } + return model; + } + } +} diff --git a/exercise.pizzashopapi/appsettings.Example.json b/exercise.pizzashopapi/appsettings.Example.json new file mode 100644 index 0000000..a64dee9 --- /dev/null +++ b/exercise.pizzashopapi/appsettings.Example.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnectionString": "Host=HOST; Port=PORT; Database=DATABASE; User id=USERNAME; Password=PASSWORD; " + } +} \ No newline at end of file diff --git a/exercise.pizzashopapi/exercise.pizzashopapi.csproj b/exercise.pizzashopapi/exercise.pizzashopapi.csproj new file mode 100644 index 0000000..e2bc5a1 --- /dev/null +++ b/exercise.pizzashopapi/exercise.pizzashopapi.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/exercise.sln b/exercise.sln new file mode 100644 index 0000000..f24f083 --- /dev/null +++ b/exercise.sln @@ -0,0 +1,32 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "exercise.pizzashopapi", "exercise.pizzashopapi\exercise.pizzashopapi.csproj", "{9ADDE5F2-B3CC-4BD5-98F3-3999C5059890}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E3DD87B3-716C-42D7-A894-2DCA702A46B2}" + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + GUIDE.md = GUIDE.md + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9ADDE5F2-B3CC-4BD5-98F3-3999C5059890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9ADDE5F2-B3CC-4BD5-98F3-3999C5059890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9ADDE5F2-B3CC-4BD5-98F3-3999C5059890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9ADDE5F2-B3CC-4BD5-98F3-3999C5059890}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BA03841E-1E94-4AA1-951B-8B23E0F42108} + EndGlobalSection +EndGlobal