├── .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 |
--------------------------------------------------------------------------------