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
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
40 changes: 38 additions & 2 deletions GUIDE.md
Original file line number Diff line number Diff line change
@@ -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)
Binary file added Images/aws_elastic_beanstalk.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Images/aws_rds_db.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Images/aws_sns_topic.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Images/aws_sqs_queue.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Images/sns_sqs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Images/swagger.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Images/swagger_get.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions exercise.pizzashopapi/DTO/CustomerDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace exercise.pizzashopapi.DTO
{
public class CustomerDTO
{
public int Id { get; set; }
public string Name { get; set; }
}
}
7 changes: 7 additions & 0 deletions exercise.pizzashopapi/DTO/CustomerView.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace exercise.pizzashopapi.DTO
{
public class CustomerView
{
public string Name { get; set; }
}
}
11 changes: 11 additions & 0 deletions exercise.pizzashopapi/DTO/OrderDTO.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
8 changes: 8 additions & 0 deletions exercise.pizzashopapi/DTO/OrderUpdateView.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace exercise.pizzashopapi.DTO
{
public class OrderUpdateView
{
public int PizzaId { get; set; }
public int Status { get; set; }
}
}
10 changes: 10 additions & 0 deletions exercise.pizzashopapi/DTO/OrderView.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
11 changes: 11 additions & 0 deletions exercise.pizzashopapi/DTO/PizzaDTO.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
10 changes: 10 additions & 0 deletions exercise.pizzashopapi/DTO/PizzaView.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
29 changes: 29 additions & 0 deletions exercise.pizzashopapi/Data/DataContext.cs
Original file line number Diff line number Diff line change
@@ -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<string>("ConnectionStrings:DefaultConnectionString")!;

}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//set primary of order?
modelBuilder.Entity<Order>().HasKey(k => new { k.PizzaId, k.CustomerId });
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseNpgsql(connectionString);
}
public DbSet<Pizza> Pizzas { get; set; }
public DbSet<Customer> Customers { get; set; }
public DbSet<Order> Orders { get; set; }
}
}
36 changes: 36 additions & 0 deletions exercise.pizzashopapi/Data/Seeder.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
}
}
191 changes: 191 additions & 0 deletions exercise.pizzashopapi/EndPoints/PizzaShopApi.cs
Original file line number Diff line number Diff line change
@@ -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<IResult> ProcessOrders(IRepository<Order> 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<Order>();

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<Order>(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<OrderDTO>();
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<IResult> GetOrders(IRepository<Order> repository)
{
var result = await repository.GetAll(["Customer", "Pizza"]);
var resultDTO = new List<OrderDTO>();
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<IResult> CreateOrder(IRepository<Order> 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<PutEventsRequestEntry> { eventEntry }
};

await eventBridge.PutEventsAsync(putEventsRequest);

return TypedResults.Ok("order put in queue");
}


[ProducesResponseType(StatusCodes.Status200OK)]
public static async Task<IResult> GetPizzas(IRepository<Pizza> repository)
{
var result = await repository.GetAll([]);
var resultDTO = new List<PizzaDTO>();
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<IResult> CreatePizza(IRepository<Pizza> 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<IResult> GetCustomers(IRepository<Customer> repository)
{
var result = await repository.GetAll([]);
var resultDTO = new List<CustomerDTO>();
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<IResult> CreateCustomer(IRepository<Customer> 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);
}
}
}
Loading