├── .editorconfig ├── .gitignore ├── DockerFile ├── README.md ├── global.json ├── nuget.config ├── publish.cmd ├── src └── BasketService │ ├── Baskets │ ├── Basket.cs │ ├── BasketActor.Events.cs │ ├── BasketActor.Messages.cs │ ├── BasketActor.cs │ ├── BasketItem.cs │ ├── BasketsActor.cs │ ├── BasketsActorProvider.cs │ ├── Routes │ │ ├── AddItemToBasket.cs │ │ ├── ApiDsl.cs │ │ ├── BasketApiController.cs │ │ ├── GetBasket.cs │ │ └── RemoveItemFromBasket.cs │ └── Services.cs │ ├── Core │ └── Messaging │ │ └── MessageWithCustomerId.cs │ ├── Products │ ├── Product.cs │ ├── ProductsActor.Events.cs │ ├── ProductsActor.Messages.cs │ ├── ProductsActor.cs │ ├── ProductsActorProvider.cs │ ├── Routes │ │ ├── GetAllProducts.cs │ │ └── ProductApiController.cs │ ├── SampleData.cs │ └── Services.cs │ ├── Program.cs │ ├── Startup.cs │ ├── appsettings.json │ └── project.json └── test └── BasketServiceTests ├── Baskets └── BasketActorTests.cs ├── Products └── ProductsActorTests.cs └── project.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*] 4 | charset = utf-8 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | indent_style = space 8 | 9 | [*.cs] 10 | indent_size = 4 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Development Environments 26 | .vscode 27 | 28 | # DNX 29 | project.lock.json 30 | artifacts/ 31 | 32 | # NuGet Packages 33 | *.nupkg 34 | # The packages folder can be ignored because of Package Restore 35 | **/packages/* 36 | # except build/, which is used as an MSBuild target. 37 | !**/packages/build/ 38 | # Uncomment if necessary however generally it will be regenerated when needed 39 | #!**/packages/repositories.config 40 | -------------------------------------------------------------------------------- /DockerFile: -------------------------------------------------------------------------------- 1 | FROM microsoft/dotnet:latest 2 | COPY src/BasketService/bin/Release/netcoreapp1.0/publish/ /root/ 3 | EXPOSE 5000/tcp 4 | ENTRYPOINT dotnet /root/BasketService.dll -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple BasketService API on ASP.NET Core 2 | 3 | A simple basket service API built on ASP.NET Core 1.0 with Akka.NET. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | dotnet restore 9 | ``` 10 | 11 | ## Starting the API 12 | 13 | The API can be started using: 14 | 15 | ```bash 16 | dotnet run 17 | ``` 18 | 19 | ## API Usage 20 | 21 | Getting the product list: `GET - http://localhost:5000/api/products`. 22 | 23 | Getting the contents of a basket: `GET - http://localhost:5000/api/basket/1`. 24 | 25 | Add an item to the basket: `PUT - http://localhost:5000/api/basket/1/items` with JSON body: 26 | 27 | ```json 28 | { 29 | "productId": 1000, 30 | "amount": 1 31 | } 32 | ``` 33 | 34 | Remove an item from the basket: `DELETE - http://localhost:5000/api/basket/1/items/{basketItemGUID}`. 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ 3 | "src", 4 | "test" 5 | ] 6 | } -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /publish.cmd: -------------------------------------------------------------------------------- 1 | cd src/BasketService 2 | dotnet publish -c release 3 | cd ../.. 4 | docker build . -------------------------------------------------------------------------------- /src/BasketService/Baskets/Basket.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace BasketService.Baskets { 4 | public class Basket { 5 | 6 | public List Items { get; set; } = new List(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/BasketService/Baskets/BasketActor.Events.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BasketService.Baskets 4 | { 5 | public partial class BasketActor 6 | { 7 | public abstract class BasketEvent {} 8 | 9 | public class ItemAdded : BasketEvent 10 | { 11 | public readonly Guid BasketItemId; 12 | 13 | public ItemAdded(Guid basketItemId = new Guid()) 14 | { 15 | this.BasketItemId = basketItemId; 16 | } 17 | } 18 | 19 | public class NotInStock : BasketEvent {} 20 | public class ProductNotFound : BasketEvent {} 21 | 22 | public class ItemRemoved : BasketEvent {} 23 | 24 | public class ItemNotFound : BasketEvent {} 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/BasketService/Baskets/BasketActor.Messages.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BasketService.Core.Messaging; 3 | 4 | namespace BasketService.Baskets 5 | { 6 | public partial class BasketActor 7 | { 8 | public class GetBasket : MessageWithCustomerId { 9 | public GetBasket(int customerId = 0) : base(customerId) { } 10 | } 11 | 12 | public class AddItemToBasket : MessageWithCustomerId { 13 | public readonly int ProductId; 14 | public readonly int Amount; 15 | 16 | public AddItemToBasket(int customerId = 0, int productId = 0, int amount = 0) : base(customerId) 17 | { 18 | this.ProductId = productId; 19 | this.Amount = amount; 20 | } 21 | } 22 | 23 | public class RemoveItemFromBasket : MessageWithCustomerId { 24 | public readonly Guid BasketItemId; 25 | 26 | public RemoveItemFromBasket(int customerId = 0, Guid basketItemId = new Guid()) : base(customerId) 27 | { 28 | this.BasketItemId = basketItemId; 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/BasketService/Baskets/BasketActor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Akka.Actor; 4 | 5 | using BasketService.Products; 6 | 7 | namespace BasketService.Baskets 8 | { 9 | public partial class BasketActor : ReceiveActor 10 | { 11 | public Basket BasketState { get; set; } 12 | private IActorRef ProductsActorRef { get; set; } 13 | public BasketActor(IActorRef productsActor) 14 | { 15 | this.BasketState = new Basket(); 16 | this.ProductsActorRef = productsActor; 17 | 18 | Receive(_ => Sender.Tell(this.BasketState)); 19 | ReceiveAsync(m => AddItemToBasketAction(m).PipeTo(Sender), m => m.Amount > 0); 20 | Receive(m => Sender.Tell(RemoveItemToBasketAction(m))); 21 | } 22 | 23 | public static Props Props(IActorRef productsActor) 24 | { 25 | return Akka.Actor.Props.Create(() => new BasketActor(productsActor)); 26 | } 27 | 28 | public async Task AddItemToBasketAction(AddItemToBasket message) 29 | { 30 | var productActorResult = await this.ProductsActorRef.Ask( 31 | new ProductsActor.UpdateStock( 32 | productId: message.ProductId, 33 | amountChanged: -message.Amount 34 | ) 35 | ); 36 | 37 | if (productActorResult is ProductsActor.StockUpdated) 38 | { 39 | var product = ((ProductsActor.StockUpdated)productActorResult).Product; 40 | return AddToBasket(product, message.Amount) as ItemAdded; 41 | } 42 | else if (productActorResult is ProductsActor.ProductNotFound) 43 | { 44 | return new ProductNotFound(); 45 | } 46 | else if (productActorResult is ProductsActor.InsuffientStock) 47 | { 48 | return new NotInStock(); 49 | } 50 | else 51 | { 52 | throw new NotImplementedException($"Unknown response: {productActorResult.GetType().ToString()}"); 53 | } 54 | } 55 | 56 | public BasketEvent RemoveItemToBasketAction(RemoveItemFromBasket message) 57 | { 58 | var basketItem = this.BasketState.Items.Find(item => item.Id == message.BasketItemId); 59 | if (basketItem is BasketItem) 60 | { 61 | this.BasketState.Items.Remove(basketItem); 62 | return new ItemRemoved(); 63 | } 64 | else 65 | { 66 | return new ItemNotFound(); 67 | } 68 | } 69 | 70 | private ItemAdded AddToBasket(Product productToAdd, int amount) 71 | { 72 | var existingBasketItemWithProduct = this.BasketState.Items.Find(item => item.ProductId == productToAdd.Id); 73 | if (existingBasketItemWithProduct is BasketItem) 74 | { 75 | // Add to existing basket item 76 | existingBasketItemWithProduct.Amount += amount; 77 | return new ItemAdded( 78 | basketItemId: existingBasketItemWithProduct.Id 79 | ); 80 | } 81 | else 82 | { 83 | // Create a new basket item 84 | var basketItemId = Guid.NewGuid(); 85 | this.BasketState.Items.Add(new BasketItem { 86 | Id = basketItemId, 87 | ProductId = productToAdd.Id, 88 | Title = productToAdd.Title, 89 | Brand = productToAdd.Brand, 90 | PricePerUnit = productToAdd.PricePerUnit, 91 | Amount = amount 92 | }); 93 | 94 | return new ItemAdded(basketItemId); 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/BasketService/Baskets/BasketItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BasketService.Baskets { 4 | public class BasketItem { 5 | public Guid Id { get; set; } 6 | 7 | public int ProductId { get ; set; } 8 | 9 | public string Title { get; set; } 10 | 11 | public string Brand { get; set; } 12 | 13 | public int PricePerUnit { get; set; } 14 | 15 | public int Amount { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/BasketService/Baskets/BasketsActor.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | 3 | using BasketService.Core.Messaging; 4 | 5 | namespace BasketService.Baskets 6 | { 7 | public class BasketsActor : ReceiveActor 8 | { 9 | private IActorRef ProductActor { get; } 10 | 11 | public BasketsActor(IActorRef productActor) 12 | { 13 | this.ProductActor = productActor; 14 | 15 | ReceiveAny(m => { 16 | if (m is MessageWithCustomerId) 17 | { 18 | var envelope = m as MessageWithCustomerId; 19 | var basketActor = Context.Child(envelope.CustomerId.ToString()) is Nobody ? 20 | Context.ActorOf(BasketActor.Props(this.ProductActor), envelope.CustomerId.ToString()) : 21 | Context.Child(envelope.CustomerId.ToString()); 22 | basketActor.Forward(m); 23 | } 24 | }); 25 | } 26 | public static Props Props(IActorRef productsActor) 27 | { 28 | return Akka.Actor.Props.Create(() => new BasketsActor(productsActor)); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/BasketService/Baskets/BasketsActorProvider.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | using BasketService.Products; 3 | 4 | namespace BasketService.Baskets 5 | { 6 | public class BasketsActorProvider 7 | { 8 | private IActorRef BasketsActorInstance { get; set; } 9 | 10 | public BasketsActorProvider(ActorSystem actorSystem, ProductsActorProvider provider) 11 | { 12 | var productsActor = provider.Get(); 13 | this.BasketsActorInstance = actorSystem.ActorOf(BasketsActor.Props(productsActor), "baskets"); 14 | } 15 | 16 | public IActorRef Get() 17 | { 18 | return this.BasketsActorInstance; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/BasketService/Baskets/Routes/AddItemToBasket.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Akka.Actor; 4 | using System; 5 | using System.Threading.Tasks; 6 | 7 | namespace BasketService.Baskets.Routes 8 | { 9 | public class AddItemToBasket 10 | { 11 | private ILogger Logger { get; set; } 12 | private IActorRef BasketsActor { get; set; } 13 | 14 | public AddItemToBasket(BasketsActorProvider provider, ILogger logger) 15 | { 16 | this.Logger = logger; 17 | this.BasketsActor = provider.Get(); 18 | } 19 | 20 | public async Task Execute(int customerId, int productId, int amount) 21 | { 22 | Logger.LogInformation($"Adding {amount} of product '{productId}' to basket for customer '{customerId}'"); 23 | var result = await this.BasketsActor.Ask(new BasketActor.AddItemToBasket( 24 | customerId, 25 | productId, 26 | amount 27 | )); 28 | 29 | if (result is BasketActor.ItemAdded) 30 | { 31 | var e = result as BasketActor.ItemAdded; 32 | return new CreatedResult($"/api/baskets/{customerId}/", e.BasketItemId); 33 | } 34 | else if (result is BasketActor.ProductNotFound || result is BasketActor.NotInStock) 35 | { 36 | return new BadRequestResult(); 37 | } 38 | else 39 | { 40 | throw new NotSupportedException(); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/BasketService/Baskets/Routes/ApiDsl.cs: -------------------------------------------------------------------------------- 1 | namespace BasketService.Baskets.Routes 2 | { 3 | public class ApiDsl 4 | { 5 | public class AddItem 6 | { 7 | public int ProductId { get; set; } 8 | public int Amount { get; set; } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/BasketService/Baskets/Routes/BasketApiController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace BasketService.Baskets.Routes 6 | { 7 | [Route("/api/basket")] 8 | public class BasketApiController 9 | { 10 | private GetBasket GetBasket { get; } 11 | private AddItemToBasket AddItemToBasket { get; } 12 | private RemoveItemFromBasket RemoveItemFromBasket { get; } 13 | 14 | public BasketApiController(GetBasket getBasket, AddItemToBasket addItemToBasket, RemoveItemFromBasket removeItemFromBasket) 15 | { 16 | this.GetBasket = getBasket; 17 | this.AddItemToBasket = addItemToBasket; 18 | this.RemoveItemFromBasket = removeItemFromBasket; 19 | } 20 | [HttpGet("{customerId}")] public async Task Get(int customerId) 21 | { 22 | var result = await this.GetBasket.Execute(customerId); 23 | return result; 24 | } 25 | 26 | [HttpPut("{customerId}/items")] public async Task PutItem(int customerId, [FromBody] ApiDsl.AddItem item) 27 | { 28 | var result = await this.AddItemToBasket.Execute(customerId, item.ProductId, item.Amount); 29 | return result; 30 | } 31 | 32 | [HttpDelete("{customerId}/items/{basketItemId}")] public async Task DeleteItem(int customerId, Guid basketItemId) 33 | { 34 | var result = await this.RemoveItemFromBasket.Execute(customerId, basketItemId); 35 | return result; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/BasketService/Baskets/Routes/GetBasket.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Akka.Actor; 3 | using System.Threading.Tasks; 4 | 5 | namespace BasketService.Baskets.Routes 6 | { 7 | public class GetBasket 8 | { 9 | private ILogger Logger { get; set; } 10 | private IActorRef BasketsActor { get; set; } 11 | 12 | public GetBasket(BasketsActorProvider provider, ILogger logger) 13 | { 14 | this.Logger = logger; 15 | this.BasketsActor = provider.Get(); 16 | } 17 | 18 | public async Task Execute(int customerId) { 19 | Logger.LogInformation($"Requesting basket of customer '{customerId}'"); 20 | return await this.BasketsActor.Ask(new BasketActor.GetBasket(customerId)); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/BasketService/Baskets/Routes/RemoveItemFromBasket.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Akka.Actor; 4 | using System; 5 | using System.Threading.Tasks; 6 | 7 | namespace BasketService.Baskets.Routes 8 | { 9 | public class RemoveItemFromBasket 10 | { 11 | private ILogger Logger { get; set; } 12 | private IActorRef BasketsActor { get; set; } 13 | 14 | public RemoveItemFromBasket(BasketsActorProvider provider, ILogger logger) 15 | { 16 | this.Logger = logger; 17 | this.BasketsActor = provider.Get(); 18 | } 19 | 20 | public async Task Execute(int customerId, Guid basketItemId) 21 | { 22 | Logger.LogInformation($"Removing item {basketItemId} from basket of customer {customerId}"); 23 | var result = await this.BasketsActor.Ask(new BasketActor.RemoveItemFromBasket( 24 | customerId, 25 | basketItemId 26 | )); 27 | 28 | if (result is BasketActor.ItemRemoved) 29 | { 30 | return new OkResult(); 31 | } 32 | else if (result is BasketActor.ItemNotFound) 33 | { 34 | return new BadRequestResult(); 35 | } 36 | else 37 | { 38 | throw new NotSupportedException(); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/BasketService/Baskets/Services.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace BasketService.Baskets 4 | { 5 | public static class Services 6 | { 7 | public static void AddBasketServices(this IServiceCollection services) 8 | { 9 | services.AddSingleton(); 10 | 11 | services.AddSingleton(); 12 | services.AddSingleton(); 13 | services.AddSingleton(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/BasketService/Core/Messaging/MessageWithCustomerId.cs: -------------------------------------------------------------------------------- 1 | namespace BasketService.Core.Messaging 2 | { 3 | public abstract class MessageWithCustomerId 4 | { 5 | public readonly int CustomerId; 6 | 7 | public MessageWithCustomerId(int customerId = 0) 8 | { 9 | this.CustomerId = customerId; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/BasketService/Products/Product.cs: -------------------------------------------------------------------------------- 1 | namespace BasketService.Products { 2 | public class Product { 3 | public int Id { get; set; } 4 | public string Title { get; set; } 5 | public string Brand { get; set; } 6 | public int PricePerUnit { get; set; } 7 | public int InStock { get; set; } 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/BasketService/Products/ProductsActor.Events.cs: -------------------------------------------------------------------------------- 1 | namespace BasketService.Products 2 | { 3 | public partial class ProductsActor 4 | { 5 | public abstract class ProductEvent {} 6 | public class ProductFound : ProductEvent 7 | { 8 | public readonly Product Product; 9 | 10 | public ProductFound(Product product) 11 | { 12 | this.Product = product; 13 | } 14 | } 15 | 16 | public class ProductNotFound : ProductEvent {} 17 | public class StockUpdated : ProductEvent 18 | { 19 | public readonly Product Product; 20 | 21 | public StockUpdated(Product product) 22 | { 23 | this.Product = product; 24 | } 25 | } 26 | public class InsuffientStock : ProductEvent {} 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/BasketService/Products/ProductsActor.Messages.cs: -------------------------------------------------------------------------------- 1 | namespace BasketService.Products 2 | { 3 | public partial class ProductsActor 4 | { 5 | public class GetAllProducts {} 6 | 7 | public class UpdateStock 8 | { 9 | public readonly int ProductId; 10 | public readonly int AmountChanged; 11 | 12 | public UpdateStock(int productId = 0, int amountChanged = 0) 13 | { 14 | this.ProductId = productId; 15 | this.AmountChanged = amountChanged; 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/BasketService/Products/ProductsActor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | using System.Linq; 4 | using Akka.Actor; 5 | 6 | namespace BasketService.Products 7 | { 8 | public partial class ProductsActor : ReceiveActor 9 | { 10 | private IList Products { get; set; } 11 | public ProductsActor(IList products) 12 | { 13 | this.Products = products; 14 | 15 | Receive(_ => Sender.Tell(new ReadOnlyCollection(this.Products))); 16 | Receive(m => Sender.Tell(UpdateStockAction(m))); 17 | } 18 | 19 | public ProductEvent UpdateStockAction(UpdateStock message) 20 | { 21 | var product = this.Products 22 | .FirstOrDefault(p => p.Id == message.ProductId); 23 | 24 | if (product is Product) 25 | { 26 | if (product.InStock + message.AmountChanged >= 0) 27 | { 28 | product.InStock += message.AmountChanged; 29 | return new StockUpdated(product); 30 | } 31 | else 32 | { 33 | return new InsuffientStock(); 34 | } 35 | } 36 | 37 | return new ProductNotFound(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/BasketService/Products/ProductsActorProvider.cs: -------------------------------------------------------------------------------- 1 | using Akka.Actor; 2 | 3 | namespace BasketService.Products 4 | { 5 | public class ProductsActorProvider 6 | { 7 | private IActorRef ProductsActor { get; set; } 8 | 9 | public ProductsActorProvider(ActorSystem actorSystem) 10 | { 11 | var products = SampleData.Get(); // set sample products 12 | this.ProductsActor = actorSystem.ActorOf(Props.Create(products), "products"); 13 | } 14 | 15 | public IActorRef Get() 16 | { 17 | return ProductsActor; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/BasketService/Products/Routes/GetAllProducts.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Akka.Actor; 3 | using System.Threading.Tasks; 4 | using System.Collections.Generic; 5 | 6 | namespace BasketService.Products.Routes 7 | { 8 | public class GetAllProducts 9 | { 10 | private ILogger Logger { get; set; } 11 | private IActorRef ProductsActor { get; set; } 12 | 13 | public GetAllProducts(ProductsActorProvider provider, ILogger logger) 14 | { 15 | this.Logger = logger; 16 | this.ProductsActor = provider.Get(); 17 | } 18 | 19 | public async Task> Execute() { 20 | Logger.LogInformation("Requesting all products"); 21 | return await this.ProductsActor.Ask>( 22 | new ProductsActor.GetAllProducts() 23 | ); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/BasketService/Products/Routes/ProductApiController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | 5 | namespace BasketService.Products.Routes 6 | { 7 | [Route("/api/products")] 8 | public class ProductApiController 9 | { 10 | private GetAllProducts GetAllProducts { get; } 11 | public ProductApiController(GetAllProducts getAllProducts) 12 | { 13 | this.GetAllProducts = getAllProducts; 14 | } 15 | 16 | [HttpGet()] public async Task> Get() 17 | { 18 | var result = await this.GetAllProducts.Execute(); 19 | return result; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/BasketService/Products/SampleData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace BasketService.Products 4 | { 5 | public class SampleData { 6 | public static IEnumerable Get() 7 | { 8 | return new List { 9 | new Product { 10 | Id = 1000, 11 | Title = "Playstation 4 500GB", 12 | Brand = "Sony", 13 | PricePerUnit = 29900, 14 | InStock = 5 15 | }, 16 | new Product { 17 | Id = 1001, 18 | Title = "Playstation 4 Pro 1TB", 19 | Brand = "Sony", 20 | PricePerUnit = 39900, 21 | InStock = 2 22 | }, 23 | new Product { 24 | Id = 1002, 25 | Title = "XBOX One", 26 | Brand = "Microsoft", 27 | PricePerUnit = 26700, 28 | InStock = 10 29 | }, 30 | new Product { 31 | Id = 1003, 32 | Title = "XBOX One Scorpio", 33 | Brand = "Microsoft", 34 | PricePerUnit = 499000, 35 | InStock = 1 36 | }, 37 | new Product { 38 | Id = 1004, 39 | Title = "Wii U", 40 | Brand = "Nintendo", 41 | PricePerUnit = 19900, 42 | InStock = 8 43 | }, 44 | }; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/BasketService/Products/Services.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace BasketService.Products 4 | { 5 | public static class Services 6 | { 7 | public static void AddProductServices(this IServiceCollection services) 8 | { 9 | services.AddSingleton(); 10 | services.AddSingleton(); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/BasketService/Program.cs: -------------------------------------------------------------------------------- 1 | namespace BasketService 2 | { 3 | using System.IO; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting; 6 | 7 | public class Program 8 | { 9 | public static void Main(string[] args) 10 | { 11 | var host = new WebHostBuilder() 12 | .UseContentRoot(Directory.GetCurrentDirectory()) 13 | .UseKestrel() 14 | .UseStartup() 15 | .Build(); 16 | 17 | host.Run(); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/BasketService/Startup.cs: -------------------------------------------------------------------------------- 1 | namespace BasketService 2 | { 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Serilog; 9 | using Akka.Actor; 10 | 11 | using Baskets; 12 | using Products; 13 | 14 | public class Startup 15 | { 16 | public IConfiguration Configuration { get; set; } 17 | 18 | public Startup(IHostingEnvironment env) 19 | { 20 | // Setup logging 21 | Log.Logger = new LoggerConfiguration() 22 | .Enrich.FromLogContext() 23 | .WriteTo.LiterateConsole() 24 | .CreateLogger(); 25 | } 26 | 27 | public void ConfigureServices(IServiceCollection services) { 28 | services.AddMvcCore() 29 | .AddJsonFormatters(); 30 | 31 | services.AddSingleton(_ => ActorSystem.Create("basketservice")); 32 | 33 | services.AddBasketServices(); 34 | services.AddProductServices(); 35 | } 36 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, 37 | ILoggerFactory loggerFactory) 38 | { 39 | app.UseMvc(); 40 | loggerFactory.AddSerilog(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/BasketService/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Verbose", 6 | "BasketService": "Verbose", 7 | "System": "Information", 8 | "Microsoft": "Information" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/BasketService/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-*", 3 | "buildOptions": { 4 | "debugType": "portable", 5 | "emitEntryPoint": true 6 | }, 7 | 8 | "frameworks": { 9 | "netcoreapp1.0": { 10 | "dependencies": { 11 | "Akka": "1.2.0-alpha1", 12 | "Microsoft.AspNetCore.Hosting": "1.0.0", 13 | "Microsoft.AspNetCore.Server.Kestrel": "1.0.1", 14 | "Microsoft.AspNetCore.Owin": "1.0.0", 15 | "Microsoft.Extensions.Configuration.Json": "1.0.0", 16 | "Microsoft.Extensions.Configuration.Binder": "1.0.0", 17 | "Microsoft.AspNetCore.Mvc.Core": "1.0.1", 18 | "Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.1", 19 | "Serilog.Extensions.Logging": "1.2.0", 20 | "Serilog.Sinks.Literate": "2.0.0", 21 | "xunit": "2.2.0-beta3-build3402", 22 | "Microsoft.NETCore.App": { 23 | "type": "platform", 24 | "version": "1.0.1" 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/BasketServiceTests/Baskets/BasketActorTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using BasketService.Baskets; 4 | using BasketService.Products; 5 | using Akka.TestKit.Xunit2; 6 | using Akka.Actor; 7 | using System.Collections.Generic; 8 | 9 | namespace BasketServiceTests.Baskets 10 | { 11 | public class BasketActorTests : TestKit 12 | { 13 | private BasketActor.AddItemToBasket AddToBasketMessage() 14 | { 15 | return new BasketActor.AddItemToBasket( 16 | customerId: 1, 17 | productId: 1001, 18 | amount: 1 19 | ); 20 | } 21 | 22 | public BasketActorTests() : base("akka.suppress-json-serializer-warning = on") { } 23 | 24 | [Fact] 25 | public void Return_Empty_Basket_After_Init() 26 | { 27 | var productActor = CreateTestProbe("products"); 28 | var actorUnderTest = ActorOf(Props.Create(productActor.Ref)); 29 | actorUnderTest.Tell( 30 | new BasketActor.GetBasket() 31 | ); 32 | 33 | var result = ExpectMsg().Items; 34 | Assert.Empty(result); 35 | } 36 | 37 | [Fact] 38 | public void Return_Filled_Basket() 39 | { 40 | var basketItem = new BasketItem { Id = Guid.NewGuid(), Title = "SomeProduct", ProductId = 1001 }; 41 | var productActor = CreateTestProbe("products"); 42 | var actorUnderTest = ActorOfAsTestActorRef(Props.Create(productActor.Ref)); 43 | actorUnderTest.UnderlyingActor.BasketState.Items.Add(basketItem); 44 | 45 | actorUnderTest.Tell( 46 | new BasketActor.GetBasket() 47 | ); 48 | var result = ExpectMsg().Items; 49 | Assert.Equal(new List { basketItem }, result); 50 | } 51 | 52 | [Fact] 53 | public void Add_Item_To_Basket_Correctly() 54 | { 55 | var product = new Product { Id = 1001, Title = "SomeProduct", InStock = 1 }; 56 | var productActor = CreateTestProbe("products"); 57 | var actorUnderTest = ActorOfAsTestActorRef(Props.Create(productActor.Ref)); 58 | actorUnderTest.Tell(AddToBasketMessage()); 59 | 60 | var updateMessage = productActor.ExpectMsg(); 61 | Assert.Equal(-1, updateMessage.AmountChanged); 62 | Assert.Equal(1001, updateMessage.ProductId); 63 | productActor.Reply(new ProductsActor.StockUpdated(product)); 64 | 65 | var result = ExpectMsg(); 66 | Assert.NotNull(result.BasketItemId); 67 | Assert.Equal(1, actorUnderTest.UnderlyingActor.BasketState.Items.Count); 68 | } 69 | 70 | [Fact] 71 | public void Addition_Fails_Because_Insuffient_Stock() 72 | { 73 | var productActor = CreateTestProbe("products"); 74 | var actorUnderTest = ActorOf(Props.Create(productActor.Ref)); 75 | actorUnderTest.Tell(AddToBasketMessage()); 76 | 77 | var updateMessage = productActor.ExpectMsg(); 78 | productActor.Reply(new ProductsActor.InsuffientStock()); 79 | 80 | var result = ExpectMsg(); 81 | } 82 | 83 | [Fact] 84 | public void Addition_Fails_Because_Product_Not_Found() 85 | { 86 | var productActor = CreateTestProbe("products"); 87 | var actorUnderTest = ActorOf(Props.Create(productActor.Ref)); 88 | actorUnderTest.Tell(AddToBasketMessage()); 89 | 90 | var updateMessage = productActor.ExpectMsg(); 91 | productActor.Reply(new ProductsActor.ProductNotFound()); 92 | 93 | var result = ExpectMsg(); 94 | } 95 | 96 | [Fact] 97 | public void Remove_Item_From_Basket_Correctly() 98 | { 99 | var basketItemId = new Guid("0fe5c840-ecc2-4c7c-b3fe-034f9a23bec2"); 100 | var basketItem = new BasketItem { Id = basketItemId, Title = "SomeProduct", ProductId = 1001 }; 101 | var productActor = CreateTestProbe("products"); 102 | var actorUnderTest = ActorOfAsTestActorRef(Props.Create(productActor.Ref)); 103 | actorUnderTest.UnderlyingActor.BasketState.Items.Add(basketItem); 104 | 105 | actorUnderTest.Tell(new BasketActor.RemoveItemFromBasket(customerId: 0, basketItemId: basketItemId)); 106 | var result = ExpectMsg(); 107 | Assert.Equal(0, actorUnderTest.UnderlyingActor.BasketState.Items.Count); 108 | } 109 | 110 | [Fact] 111 | public void Remove_Item_Fails_Because_Item_Not_In_Basket() 112 | { 113 | var basketItemId = new Guid("0fe5c840-ecc2-4c7c-b3fe-034f9a23bec2"); 114 | var basketItem = new BasketItem { Id = basketItemId, Title = "SomeProduct", ProductId = 1001 }; 115 | var productActor = CreateTestProbe("products"); 116 | var actorUnderTest = ActorOfAsTestActorRef(Props.Create(productActor.Ref)); 117 | actorUnderTest.UnderlyingActor.BasketState.Items.Add(basketItem); 118 | 119 | var otherBasketItemId = new Guid("51b9ad1c-d1f2-4d61-a8e1-4dbf961b590f"); 120 | actorUnderTest.Tell(new BasketActor.RemoveItemFromBasket(customerId: 0, basketItemId: otherBasketItemId)); 121 | var result = ExpectMsg(); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /test/BasketServiceTests/Products/ProductsActorTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | using BasketService.Products; 3 | using Akka.TestKit.Xunit2; 4 | using Akka.Actor; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace BasketServiceTests.Products 9 | { 10 | public class ProductsActorTests : TestKit 11 | { 12 | private IEnumerable Products { get; } = GetProductData(); 13 | 14 | public ProductsActorTests() : base("akka.suppress-json-serializer-warning = on") { } 15 | 16 | [Fact] 17 | public void Return_All_Products() 18 | { 19 | var actorUnderTest = ActorOf(Props.Create(this.Products)); 20 | actorUnderTest.Tell( 21 | new ProductsActor.GetAllProducts() 22 | ); 23 | 24 | var result = ExpectMsg>(); 25 | Assert.Equal(this.Products, result); 26 | } 27 | 28 | [Fact] 29 | public void Update_Stock_When_Product_With_Enough_Stock_Is_Found() 30 | { 31 | var actorUnderTest = ActorOf(Props.Create(this.Products)); 32 | actorUnderTest.Tell(new ProductsActor.UpdateStock( 33 | productId: 1000, 34 | amountChanged: 1 35 | )); 36 | 37 | var result = ExpectMsg(); 38 | Assert.Equal(this.Products.Single(p => p.Id == 1000), result.Product); 39 | } 40 | 41 | [Fact] 42 | public void Cannot_Update_Stock_Because_Insufficient_Stock() 43 | { 44 | var actorUnderTest = ActorOf(Props.Create(this.Products)); 45 | actorUnderTest.Tell(new ProductsActor.UpdateStock( 46 | productId: 1000, 47 | amountChanged: -2 48 | )); 49 | 50 | var result = ExpectMsg(); 51 | } 52 | 53 | [Fact] 54 | public void Cannot_Update_Stock_Because_Not_Found() 55 | { 56 | var actorUnderTest = ActorOf(Props.Create(this.Products)); 57 | actorUnderTest.Tell(new ProductsActor.UpdateStock( 58 | productId: 9999, 59 | amountChanged: -1 60 | )); 61 | 62 | var result = ExpectMsg(); 63 | } 64 | 65 | private static IEnumerable GetProductData() 66 | { 67 | return new List { 68 | new Product { 69 | Id = 1000, 70 | Title = "Product 1", 71 | InStock = 1 72 | }, 73 | new Product { 74 | Id = 1001, 75 | Title = "Product 2", 76 | InStock = 2 77 | } 78 | }; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/BasketServiceTests/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-*", 3 | "buildOptions": { 4 | "debugType": "portable" 5 | }, 6 | "dependencies": { 7 | "System.Runtime.Serialization.Primitives": "4.1.1", 8 | "Akka.TestKit.Xunit2": "1.2.0-alpha1", 9 | "xunit": "2.2.0-beta3-build3402", 10 | "dotnet-test-xunit": "2.2.0-preview2-build1029", 11 | "BasketService": { 12 | "target": "project" 13 | } 14 | }, 15 | "testRunner": "xunit", 16 | "frameworks": { 17 | "netcoreapp1.0": { 18 | "dependencies": { 19 | "Microsoft.NETCore.App": { 20 | "type": "platform", 21 | "version": "1.0.1" 22 | } 23 | } 24 | } 25 | } 26 | } 27 | --------------------------------------------------------------------------------