- Notifications
You must be signed in to change notification settings - Fork19
A by the book DDD application with React/Redux and .NET Core. It features CQRS, event-sourcing, functional programming, TDD, Docker and much more.
License
dnikolovv/cafe
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Café is an example application demonstrating a combination between domain-driven design and functional programming. It is continuously deployed tohttps://cafeapi.devadventures.net/openapi andhttps://cafe.devadventures.net. (if those are unavailable it's because I've stopped paying my Azure subscription, sorry)
- DDD done by the book
- REST withHATEOAS
- CQRS
- Functional style command/query handlers
- Event-sourcing
- A complete integration tests suite (~100% coverage) + coverage reports
- Docker CI setup with multiple data sources + CD on Azure
- Real-time communications through SignalR
As a whole, this projects aims to stand out from the other examples by being as complete as possible code/structure wise. It should serve as an example of how a real enterprise project built using these principles would look like.
This project can also serve as an example for myIntegration Testing SignalR Websockets article. Seesome hub tests for examples.
- AuthContext
- BaristaContext
- CashierContext
- ManagerContext
- MenuContext
- OrderContext
- TabContext
- TableContext
- WaiterContext
SeeCafe.Business
orCafe.Core
for a more in-depth look.
Menu - The café menu. Contains Menu Items that can be ordered either for a Tab or for a To-go Order.
Menu Item - An item from the menu, identified by a number.
Tab - An open bill on a table.
To-go order/Order - An order that is not linked to a particular table. Customers make these orders by going to the Cashier. When they get paid, the Cashier will issue them to the Barista for completion.
Customer - A person that opened a tab/made an order.
Table - Represents a physical table in the café. Tables are assigned to Waiters who are responsible for managing the tab.
Manager - The café manager. Manages the menu items. Manages the tables. Hires Waiters/Cashiers/Baristas. Assigns waiters to tables.
Cashier - Takes to-go orders.
Barista - Waits for confirmed to-go orders to complete them.
Waiter - Serves a fixed set of tables, takes orders and delivers/rejects menu items.
User/Account - An account with which you can login into the web portal. On its own, the account can do nothing, it must be assigned to some employee by the Admin. (e.g. you can link an account to a waiter)
Admin - The web portal administrator. Manages the links between the accounts and the employees. Has rights to do pretty much everything.
TheTab
is an event-sourced aggregate that is constructed by the variousdomain events that are published by the command handlers using theEventBus.
For when you need to share data between contexts, use the shared kernel.
SeeCafe.Core
andCafe.Business
Each handler is implemented as a chain of functions (usingOptional.Async
). Each function represents an operation that can either pass (continue the execution) or fail (return anError
to the consumer).
Examples:
// OpenTabHandler.cspublicoverrideTask<Option<Unit,Error>>Handle(OpenTabcommand)=>TabShouldNotExist(command.Id).FlatMapAsync(tab=>TableShouldNotBeTaken(command.TableNumber).FlatMapAsync(tableNumber=>TheTableShouldHaveAWaiterAssigned(tableNumber).MapAsync(waiter=>PublishEvents(tab.Id,tab.OpenTab(command.CustomerName,waiter.ShortName,command.TableNumber)))));// OrderMenuItemsHandler.cspublicoverrideTask<Option<Unit,Error>>Handle(OrderMenuItemscommand)=>TabShouldNotBeClosed(command.TabId).FlatMapAsync(tab=>MenuItemsShouldExist(command.ItemNumbers).MapAsync(items=>PublishEvents(command.TabId,tab.OrderMenuItems(items))));// CallWaiterHandler.cspublicoverrideTask<Option<Unit,Error>>Handle(CallWaitercommand)=>TableShouldExist(command.TableNumber).FlatMapAsync(table=>TableShouldHaveAWaiterAssigned(table).MapAsync(waiter=>PublishEvents(table.Id,newWaiterCalled{TableNumber=table.Number,WaiterId=waiter.Id})));
The command/query validation is handled byFluentValidation
and happens at the handler level rather than at the API level.
The chain itself contains all of the business validations such as checking whether the tab is closed, checking whether you're not serving beverages that haven't been ordered, etc.
Each handler is in a separate file to avoid the classes getting too big.
Most of the functionality is implemented using TDD, therefore the project has nearly 100% tests coverage, most of which are integration tests.
Examples:
[Theory][CustomizedAutoData]publicasyncTaskCanOpenTab(OpenTabopenTabCommand,HireWaiterhireWaiterCommand,AddTableaddTableCommand){// Arrangeawait_helper.SetupWaiterWithTable(hireWaiterCommand,addTableCommand);// Make sure we're trying to open a tab on the added tableopenTabCommand.TableNumber=addTableCommand.Number;// Actvarresult=await_fixture.SendAsync(openTabCommand);// Assertawait_helper.AssertTabExists(openTabCommand.Id, t=>t.IsOpen==true&&t.WaiterName==hireWaiterCommand.ShortName&&t.CustomerName==openTabCommand.CustomerName);}[Theory][CustomizedAutoData]publicasyncTaskCanOpenTabOnARecentlyFreedTable(GuidtabId,inttableNumber){// Arrangeawait_helper.OpenTabOnTable(tabId,tableNumber);await_helper.CloseTab(tabId,1);varcommandToTest=newOpenTab{Id=Guid.NewGuid(),CustomerName="Customer",TableNumber=tableNumber};// Actvarresult=await_fixture.SendAsync(commandToTest);// Assertawait_helper.AssertTabExists(commandToTest.Id, t=>t.IsOpen==true&&t.TableNumber==tableNumber);}[Theory][CustomizedAutoData]publicasyncTaskCanAddMenuItems(MenuItemView[]itemsToAdd){// Arrangevarcommand=newAddMenuItems{MenuItems=itemsToAdd};// Actvarresult=await_fixture.SendAsync(command);// AssertvaritemsInDb=await_fixture.ExecuteDbContextAsync(dbContext=>dbContext.MenuItems.ToListAsync());itemsInDb.ShouldAllBe(i=>itemsToAdd.Any(addedItem=>i.Number==addedItem.Number&&i.Description==addedItem.Description&&i.Price==addedItem.Price));}[Theory][CustomizedAutoData]publicasyncTaskCannotAddConflictingMenuItemsWhenAllAreConflicting(MenuItemView[]itemsToAdd){// Arrangevarcommand=newAddMenuItems{MenuItems=itemsToAdd};await_fixture.SendAsync(command);// Actvarresult=await_fixture.SendAsync(command);// Assertresult.ShouldHaveErrorOfType(ErrorType.Conflict);}
You can find out more by taking a look at thetests assembly.
Issue orders:
Call waiter/ request bill:
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
If you have Docker installed you can just double-clickrun-app.sh
.
If not, you'll need to havePostgreSql either installed locally or at least have some instance available to set up the connection strings.
You'll also need at least version2.2
of the.NET Core SDK
.
Note that you can point both the event-store and the relational connection to the same database
- Execute
run-app.sh
. You should have a client running athttp://localhost:3000 and api athttp://localhost:5000
- Open the
.sln
file using Visual Studio - Set up the connection strings inside
Cafe.Api/appsettings.json
- Execute
Update-Database
inside thePackage Manager Console
- Run the application
- Open the project folder inside your favorite editor
- Set up the connection strings inside
Cafe.Api/appsettings.json
- Execute
dotnet ef database update
inside theCafe.Api
folder - Execute
dotnet run
- Go to
http://localhost:5000
(or whatever port you're running it on)
- Execute
run-app.sh
. You should have a client running athttp://localhost:3000 and API athttp://localhost:5000
- While in the context of the
./client
folder - Run
npm i
- Run
npm start
Note that you can point both the event-store and the relational connection to the same database
- Simply run
run-integration-tests.sh
.
- Set up the connection strings inside
Bar.Tests/appsettings.json
to a valid database. (if you point it to an unexisting one, the app will create it for you) - Either run them through the
Test Explorer
in Visual Studio or usingdotnet test
If you feel like contributing, PRs are welcome!
This project is licensed under the MIT License.