|
| 1 | +# Simple, practical EventSourcing with EventStoreDB and EntityFramework |
| 2 | + |
| 3 | +The PR is adding a new sample that contains the simple Event Sourcing setup with EventStoreDB. For the Read Model, Postgres and Entity Framework are used. |
| 4 | + |
| 5 | +You can watch the webinar on YouTube when I'm explaining the details of the implementation: |
| 6 | + |
| 7 | +<a href="https://www.youtube.com/watch?v=rqYPVzjoxqI" target="_blank"><img src="https://img.youtube.com/vi/rqYPVzjoxqI/0.jpg" alt="Practical introduction to Event Sourcing with EventStoreDB" width="320" height="240" border="10" /></a> |
| 8 | + |
| 9 | +or read the article explaining the read model part: ["How to build event-driven projections with Entity Framework"](https://event-driven.io/en/how_to_do_events_projections_with_entity_framework/) |
| 10 | + |
| 11 | +## Main assumptions: |
| 12 | +- explain basics of Event Sourcing, both from the write model (EventStoreDB) and read model part (Postgres and EntityFramework), |
| 13 | +- CQRS architecture sliced by business features, keeping code that changes together at the same place. Read more in [How to slice the codebase effectively?](https://event-driven.io/en/how_to_slice_the_codebase_effectively/) |
| 14 | +- no aggregates, just data (records) and functions, |
| 15 | +- clean, composable (pure) functions for command, events, projections, query handling instead of marker interfaces (the only one used internally is `IEventHandler`). Thanks to that testability and easier maintenance. |
| 16 | +- easy to use and self-explanatory fluent API for registering commands and projections with possible fallbacks, |
| 17 | +- registering everything into regular DI containers to integrate with other application services. |
| 18 | +- pushing the type/signature enforcement on edge, so when plugging to DI. |
| 19 | + |
| 20 | +## Overview |
| 21 | + |
| 22 | +It uses: |
| 23 | +- pure data entities, functions and handlers, |
| 24 | +- Stores events from the command handler result EventStoreDB, |
| 25 | +- Builds read models using [Subscription to `$all`](https://developers.eventstore.com/clients/grpc/subscribing-to-streams/#subscribing-to-all). |
| 26 | +- Read models are stored to Postgres relational tables with [Entity Framework](https://docs.microsoft.com/en-us/ef/core/). |
| 27 | +- App has Swagger and predefined [docker-compose](./docker/docker-compose.yml) to run and play with samples. |
| 28 | + |
| 29 | +## Write Model |
| 30 | +- Sample [ShoppingCart](./ECommerce/ShoppingCarts/ShoppingCart.cs#L34) entity and [events](./ECommerce/ShoppingCarts/ShoppingCart.cs#L6) represent the business workflow. All are stored in the same file to be able to understand flow without jumping from one file to another. It also contains [When](./ECommerce/ShoppingCarts/ShoppingCart.cs#L42) method defining how to apply events to get the entity state. It uses the C#9 switch syntax with records deconstruction. |
| 31 | +- Example [ProductItemsList](./ECommerce/ShoppingCarts/ProductItems/ProductItemsList.cs) value object wrapping the list of product items in the shopping carts. It simplified the main state apply logic and offloaded some of the invariants checks. |
| 32 | +- All commands by convention should be created using the [factory method](./ECommerce/ShoppingCarts/AddingProductItem/AddProductItemToShoppingCart.cs#L13) to enforce the types, |
| 33 | +- Command handlers are defined as static methods in the same file as command definition. Usually, they change together. They are pure functions that take command and/or state and create new events based on the business logic. See sample [Adding Product Item to ShoppingCart](./ECommerce/ShoppingCarts/AddingProductItem/AddProductItemToShoppingCart.cs#L25). This example also shows that you can inject external services to handlers if needed. |
| 34 | +- [Added syntax for self-documenting command handlers registration](./ECommerce/ShoppingCarts/Configuration.cs#L22). See the details of registration in [CommandHandlerExtensions](./ECommerce.Core/Commands/CommandHandler.cs). They differentiate case when [a new entity/stream is created](./Ecommerce.Core/Commands/CommandHandler.cs#L11) from the [update case](./Ecommerce.CoreECommerce.Core/Commands/CommandHandler.cs#L25). Update has to support optimistic concurrency. |
| 35 | +- Added simple [EventStoreDBRepository](./ECommerce.Core/Entities/EventStoreDBRepository.cs) repository to load entity state and store event created by business logic, |
| 36 | +- [New, simplified Core infrastructure](./ECommerce.Core/) |
| 37 | + |
| 38 | +## Read Model |
| 39 | +- Read models are rebuilt with eventual consistency using subscribe to $all stream EventStoreDB feature, |
| 40 | +- Used Entity Framework to store projection data into Postgres tables, |
| 41 | +- Added sample projection for [Shopping cart details](./ECommerce/ShoppingCarts/GettingCartById/ShoppingCartDetails.cs) and slimmed [Shopping cart short info](./ECommerce/ShoppingCarts/GettingCarts/ShoppingCartShortInfo.cs) as an example of different interpretations of the same events. Shopping cart details also contain a nested collection of product items to show more advanced use case. All event handling is done by functions. It enables easier unit and integration testing. |
| 42 | +- [Added syntax for self-documenting projection handlers registration](./ECommerce/ShoppingCarts/Configuration.cs#L49). See the details of registration in [EntityFrameworkProjectionBuilder](./ECommerce.Core/Projections/EntityFrameworkProjection.cs#L28). They differentiate case when [a new read model is created](./ECommerce.Core/Projections/EntityFrameworkProjection.cs#L83) from the [update case](./ECommerce.Core/Projections/EntityFrameworkProjection.cs#L108). Update has to support optimistic concurrency. |
| 43 | +- [example query handlers](./ECommerce/ShoppingCarts/GettingCarts/GetCarts.cs#25) for reading data together with [registration helpers](./ECommerce.Core/Queries/QueryHandler.cs) for EntityFramework querying. |
| 44 | +- Added service [EventStoreDBSubscriptionToAll](./ECommerce.Core/Subscriptions/EventStoreDBSubscriptionToAll.cs) to handle subscribing to all. It handles checkpointing and simple retries when the connection is dropped. Added also general [BackgroundWorker](./ECommerce.Api/Core/BackgroundWorker.cs) to wrap the general `IHostedService` handling |
| 45 | +- Added [ISubscriptionCheckpointRepository](./ECommerce.Core/Subscriptions/ISubscriptionCheckpointRepository.cs) for handling Subscription checkpointing. |
| 46 | +- Added checkpointing to EventStoreDB stream with [EventStoreDBSubscriptionCheckpointRepository](./ECommerce.Core/Subscriptions/EventStoreDBSubscriptionCheckpointRepository.cs), |
| 47 | +- Added custom [EventBus](./ECommerce.Core/Events/EventBus.cs) implementation to not take an additional dependency on external frameworks like MediatR. It's not needed as no advanced pipelining is used here. |
| 48 | + |
| 49 | +## Tests |
| 50 | +API integration tests for: |
| 51 | +- [Initiating shopping cart](./ECommerce.Api.Tests/ShoppingCarts/Initializing/InitializeShoppingCartTests.cs) as an example of creating a new entity, |
| 52 | +- [Confirming shopping cart](./ECommerce.Api.Tests/ShoppingCarts/Confirming/ConfirmShoppingCartTests.cs) as an example of updating an existing entity, |
| 53 | + |
| 54 | + |
| 55 | +## Prerequisities |
| 56 | + |
| 57 | +1. Install git - https://git-scm.com/downloads. |
| 58 | +2. Install .NET Core 5.0 - https://dotnet.microsoft.com/download/dotnet/5.0. |
| 59 | +3. Install Visual Studio 2019, Rider or VSCode. |
| 60 | +4. Install docker - https://docs.docker.com/docker-for-windows/install/. |
| 61 | +5. Open `ECommerce.sln` solution. |
| 62 | + |
| 63 | +## Running |
| 64 | + |
| 65 | +1. Go to [docker](./docker) and run: `docker-compose up`. |
| 66 | +2. Wait until all dockers got are downloaded and running. |
| 67 | +3. You should automatically get: |
| 68 | + - EventStoreDB UI (for event store): http://localhost:2113/ |
| 69 | + - Postgres DB running (for read models) |
| 70 | + - PG Admin - IDE for postgres. Available at: http://localhost:5050. |
| 71 | + - Login: `[email protected]`, Password: `admin` |
| 72 | + - To connect to server Use host: `postgres`, user: `postgres`, password: `Password12!` |
| 73 | +4. Open, build and run `ECommerce.sln` solution. |
| 74 | + - Swagger should be available at: http://localhost:5000/index.html |
0 commit comments