{"id":20023676,"url":"https://github.com/fredimachado/ncafe","last_synced_at":"2025-05-05T02:30:45.795Z","repository":{"id":136243141,"uuid":"447249846","full_name":"fredimachado/NCafe","owner":"fredimachado","description":"Minimal .NET microservices implementation in the context of a cafe","archived":false,"fork":false,"pushed_at":"2025-04-13T23:01:56.000Z","size":680,"stargazers_count":29,"open_issues_count":0,"forks_count":4,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-05-02T14:09:30.946Z","etag":null,"topics":["asp-net-core","aspnet","aspnetcore","blazor-webassembly","c-sharp","clean-architecture","cqrs","csharp","dotnet","dotnet-aspire","event-sourcing","eventstore","eventstoredb","helm","helmfile","kubernetes","microservices","minimal-api","rabbitmq","signalr"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fredimachado.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-01-12T14:30:07.000Z","updated_at":"2025-04-13T23:02:00.000Z","dependencies_parsed_at":"2024-06-02T12:42:06.599Z","dependency_job_id":"244b8c39-7483-47fe-88a4-17053cfc032c","html_url":"https://github.com/fredimachado/NCafe","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fredimachado%2FNCafe","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fredimachado%2FNCafe/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fredimachado%2FNCafe/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fredimachado%2FNCafe/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fredimachado","download_url":"https://codeload.github.com/fredimachado/NCafe/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252427647,"owners_count":21746252,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asp-net-core","aspnet","aspnetcore","blazor-webassembly","c-sharp","clean-architecture","cqrs","csharp","dotnet","dotnet-aspire","event-sourcing","eventstore","eventstoredb","helm","helmfile","kubernetes","microservices","minimal-api","rabbitmq","signalr"],"created_at":"2024-11-13T08:47:34.138Z","updated_at":"2025-05-05T02:30:45.266Z","avatar_url":"https://github.com/fredimachado.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# \u003cimg src=\"images/logo.svg?raw=true\" alt=\"Logo\" width=\"28\" /\u003e NCafe\n\nMinimal .NET microservices implementation in the context of a cafe.\n\nHeavily inspired on the [microcafe](https://github.com/rbanks54/microcafe) project by [Richard Banks](https://github.com/rbanks54).\n\nMore information about the application domain, please check the links below:\n\nhttps://www.slideshare.net/rbanks54/architecting-microservices-in-net\nhttps://www.enterpriseintegrationpatterns.com/ramblings/18_starbucks.html\n\n### Tech stack:\n\n- **C# 12/.NET 8**\n- **ASP.NET Core Minimal APIs**\n- **.NET Aspire**\n- **Docker**\n- **Kubernetes**\n- **EventStore**: Database for Event Sourcing where we store events as the source of truth instead of current state\n- **RabbitMQ**: Message broker used for asynchronous messaging\n\n### Warning\n\nThis code should be treated as sample code. It's a sandbox for practicing microservice ideas,\nCQRS, Event Sourcing, Kubernetes/helm deployment and related things.\n\nI wrote some documentation below for those who are starting on this journey. I hope it helps! :smile:\n\n### TODO\n\n- [x] Use SignalR to update the Barista page in real time as new orders are placed\n- [x] Kubernetes/helm/helmfile deployment to my internal K3S cluster\n- [x] Add observability with OpenTelemetry (Thanks to .NET Aspire)\n- [x] Cashier should be able to add the customer name to the order\n- [x] Cashier should be able to add multiple products to the same order\n- [x] Barista should be able to see the name of the customer and products/quantities\n- [x] Add timestamp to orders so barista sees in correct order\n- [x] Cashier should be able to remove products from the order\n- [x] Admin should be able to delete products\n- [ ] Cashier should be able to cancel order\n- [ ] Make sure we always publish OrderPlaceMessage when Cashier places an order (Outbox?)\n- [ ] Use a database for the read models (projections)\n- [ ] Move projection services to their own microservice so reads and writes are separate\n- [ ] Admin should be able to edit products\n\n## Content\n\n- [NCafe Solution](#ncafe-solution)\n  - [Core (Shared Abstractions)](#core-shared-abstractions)\n  - [Application Domain](#application-domain)\n  - [Web API](#web-api)\n  - [Web UI](#web-ui)\n  - [Infrastructure](#infrastructure)\n- [NCafe's CQRS + Event Sourcing implementation](#ncafes-cqrs-event-sourcing-implementation)\n  - [Command](#command)\n  - [Query](#query)\n  - [Projections](#projections)\n- [Run with .NET Aspire](#run-with-net-aspire)\n- [Run with docker](#run-with-docker)\n  - [Starting the infrastructure containers](#starting-the-infrastructure-containers)\n  - [Starting the microservices](#starting-the-microservices)\n  - [Swagger](#swagger)\n- [NCafe in action](#ncafe-in-action)\n  - [EventStore](#eventstore)\n  - [RabbitMQ](#rabbitmq)\n  - [Stopping everything](#stopping-everything)\n\n## NCafe Solution\n\nThe NCafe Solution is based on Clean Architecture, so dependencies only point torwards the center.\n\n![Clean Architecture](images/architecture.png?raw=true)\n\n#### Core (Shared Abstractions)\n\nAll common interfaces and abstractions are here. For example:\n\n- **AggregateRoot**: Abstract base class for our domain entities\n- **IEvent and Event**: Base for events that represent domain entity state changes\n- **IRepository**: Contract used to fetch and save domain entities\n- **IBusPublisher**: Contract used for publishing integration events\n- **MessageBus Events**: Integration events used to communicate between microservices\n- **IProjectionService**: Contract used for building projections based on domain entity events\n- **Read Model**: Contract used to fetch and save read models (projections)\n- **Basic exceptions**\n\nSo, basically, this project is the core of our solution and will only contain the\nabstractions used by other layers. This project doesn't have any microservice specific code.\n\n#### Shared\n\nThere is a shared project (`NCafe.Shared`), that contains code that don't require any core\nabstraction or logic. For example, types that define SignalR objects for real-time functionality.\n\nThe Web UI project doesn't really need a reference to `NCafe.Core` (`NCafe.Shared` is enough).\n\n### Application Domain\n\nDepends only on the Core project in order to define domain entities (aggregates),\nevents, commands, queries, their respective handlers and business logic. So it might have:\n\n- **Entities**: Their current state is defined by a stream of events stored in EventStore.\nProvide methods to make it do something (ex.: `CompletePreparation`).\nThese methods will raise events (after doing some validation if necessary), that will be appended\nto its event stream in EventStore\n- **Events**: Represent something that has happened in the domain (ex.: `OrderPlaced`). These events\nwill be raised by the entities as described above\n- **Commands/Handlers**: Commands and handlers will perform some kind of action. Most probably\nchanging the state of an entity. Must use\nabstractions/interfaces (ex.: `IRepository`) instead of implementations\n- **Queries/Handlers**: Used to return data from read models. Must use abstractions/interfaces\n(ex.: `IReadModelRepository`) instead of implementations\n- **Read Models**: To be used by projection services and query handlers\n\n### Web API\n\nThe entry points (runners) of our microservices. They simply register the required dependencies using methods\nfrom the Infrastructure project and map endpoints, which use `MediatR` to invoke a handler (see `Program.cs`).\n\nThese projects have a reference to its Domain, which should actually be called Application, to conform with Clean Architecture\nsince it contains use cases. The APIs also have a reference to the Infrastructure project.\n\nProjection services can also be in the Web API project (Find more about projections below).\n\nIn case the microservice needs to consume integration events, a Consumer service can be created\n(see `OrdersConsumerService` in `Barista.Api`). Basically, this service implements .NET's `IHostedService`, subscribes\nto a RabbitMQ stream specifying a queue, a topic and a callback, which in case will use `MediatR` to invoke a domain command.\n\n### Web UI\n\nThe Web UI project is a Blazor WebAssembly project that interacts with the Admin, Barista and Cashier microservices.\nIt stores the base address of each microservice in `wwwroot/appsettings.json`.\n\nIt should work out of the box locally when running with .NET Aspire and docker compose, due to the port configuration\nin `services-compose.yaml` and `applicationUrl` (inside each microservice's `launchSettings.json`).\n\nFor production, the base address should be set in `wwwroot/appsettings.json`. Since the Web UI project is a static site,\nI decided to deploy it inside a container running `nginx`. In the Dockerfile, I added a step to copy `prepare-appsettings.sh` to\nthe `/docker-entrypoint.d/` folder. When nginx starts, it will run scripts inside this folder. The `prepare-appsettings.sh` script\nwill replace the base addresses in `wwwroot/appsettings.json` with values from environment variables (`ADMIN_BASE_ADDRESS`,\n`CASHIER_BASE_ADDRESS` and `BARISTA_BASE_ADDRESS`).\n\nFor my deployment, since I'm using a local Kubernetes cluster and Nginx Proxy Manager, I hardcoded the base addresses in the\n`appspec.yaml`.\n\n### Infrastructure\n\nImplementations for all external dependencies are defined in this project. Like:\n\n- EventStore Repository and Projection Service\n- RabbitMQ publisher\n\nThere are some other implementations in here as well, like a Logging decorator and\nread model repositories (only in-memory for now).\n\nThis project also contains methods for registering all the implementations for interfaces defined\nin the Core project.\n\nAll the implementation types should be marked as internal, because our Application/Domain should only depend on abstractions.\nThe Application/Domain has no idea about the actual implementations. This is the Dependency Inversion Principle in action.\n\n## NCafe's CQRS + Event Sourcing implementation\n\n![CQRS and Event Sourcing in NCafe](images/ncafe-cqrs-event-sourcing.png?raw=true)\n\nInspired by https://codeopinion.com/cqrs-event-sourcing-code-walk-through/.\n\n### Command\n\nDepending on the command, the handler will instantiate the domain model (entity/aggregate), for example in `PlaceOrderHandler`.\nThen the entity will be saved using `IRepository`.\n\nFor other commands, the handler will first fetch the entity by id from the repository,\nthen tell it to do something (ex.: `CompletePreparation`) and then save it using the repository.\n\nWhen fetching an entity, the repository will actually get all events for the specific aggregate and apply them in order. This will\nre-build the current state of the entity based on the events.\n\nThe Save method in the repository sends pending events to EventStore, for example, the `OrderPrepared` event\nafter `CompletePreparation` is called (see the Barista.Domain project).\n\n### Query\n\nThis is the simplest part of the system. The handlers will use a read model repository to return one or more items from the\nquery database (in-memory for now). The data from this database comes from a projection service, described below.\n\n### Projections\n\nAll Projection services subscribed to EventStore event streams (ex.: `baristaOrder`), will receive new events\nfrom EventStore and use it to update the query database (read model) if necessary.\n\nIn NCafe, Projection services are implemented using .NET's `BackgroundService` running in the API projects, as the read models are\nbeing stored using an in-memory repository for the time being. But when switch to a proper database, we can easily move the\nprojection specific code to it's own microservice. In fact, I think we will have to move the code, otherwise we need a way to run\nonly one projection service in case there's a need to scale out (create more instances of) the API microservice. In case you're\nwondering why, it's because I'm imagining multiple projection services subscribed to the same streams and also using the same\ndatabase. So they would all try to do the same inserts/updates. It looks like trouble.\n\nAlso, when we move to a database, we'll need to save checkpoints, so when we restart the projection service we can tell EventStore\nthat we only care about events from a specific position in the stream. Saving the version should be enough.\n\n## Run with .NET Aspire\n\n.NET Aspire is an opinionated, cloud ready stack for building observable, production ready, distributed applications.\n\nMake sure to set `NCafe.AppHost` as Startup Project in the IDE and run. If you prefer CLI, just run the `NCafe.AppHost` project.\nIf you run through the CLI, you will see a link to the dashboard.\n\n![image](https://github.com/fredimachado/NCafe/assets/29800/a2899e7d-b52f-4e93-ae91-e6c2215cce51)\n\n## Run with docker\n\nIn order to run the solution, you need the following:\n\n- .NET 8 SDK\n- Docker\n\n### Starting the infrastructure containers\n\nRun the following command (repo's root folder):\n\n    docker compose -f infrastructure-compose.yaml up -d\n\n### Starting the microservices\n\nYou can start the microservices via the dotnet CLI or your favorite IDE/code editor.\nIf you're using Visual Studio you can also set multiple startup projects by going to solution properties.\n\nIf you prefer docker, run the build script in the root folder:\n\n    chmod +x build.sh\n    ./build.sh\n\nIf you're on Windows:\n\n    .\\Build.ps1 \n\nRun the following command to build and start all microservices containers:\n\n    docker compose -f services-compose.yaml up -d\n\n### Starting the Web UI in Visual Studio\n\nSometimes, when working on the Web UI project, it might be worth to run the infrastructure and other\nservices using docker compose and then run the Web UI project in Visual Studio. This way can be easier to debug.\n\nRun this command to start all services except web-ui:\n\n    docker compose -f services-compose.yaml up -d --scale web-ui=0\n\n### Swagger\n\n- **Admin**: [http://localhost:5010/swagger/index.html](http://localhost:5010/swagger/index.html)\n- **Cashier**: [http://localhost:5020/swagger/index.html](http://localhost:5020/swagger/index.html)\n- **Barista**: [http://localhost:5030/swagger/index.html](http://localhost:5030/swagger/index.html)\n\n## NCafe in action\n\n1. Start the NCafe.Web project (Blazor WebAssembly)\n2. Create a new product in the Admin page\n3. Place an order in the Cashier page\n4. Complete the order in the Barista page\n\n![ncafe](https://github.com/fredimachado/NCafe/assets/29800/16f01fb9-15ef-445c-afd8-0883c0e53775)\n\n\nOr, you can use Swagger for example:\n\n1. Create a product via the POST `/products` endpoint in **Admin**\n2. Get a product Id using the GET `/products` endpoint\n3. Create an order via the POST `/orders` endpoint in **Cashier**, it will return the order Id\n4. Add item(s) to the order via the POST `/orders/add-item` in **Cashier**\n5. Place the order via the POST `/orders/place` endpoint in **Cashier**\n6. Complete the order via the POST `/orders/prepared` endpoint in **Barista**\n\n### EventStore\n\nYou can check all the events in EventStore by going to the `Stream Browser`\nin [http://localhost:2113/](http://localhost:2113/).\n\n![EventStore Screenshot](images/eventstore.png?raw=true)\n\n### RabbitMQ\n\nYou can check the message queue in [http://localhost:15672/](http://localhost:15672/).\n\n### Stopping everything\n\nRun the following command:\n\n    docker compose -f services-compose.yaml -f infrastructure-compose.yaml down\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffredimachado%2Fncafe","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffredimachado%2Fncafe","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffredimachado%2Fncafe/lists"}