Your first stream
In this tutorial, you store a shopping cart as a stream of events. You add items, check the cart out, and load its state back. It takes about 10 minutes.
You need the .NET 10 SDK (or .NET 8) and a Postgres or SQL Server database. Docker is the fastest way to get one.
-
Create a web app and add the packages for your database.
Terminal window dotnet new web -o Shopcd Shopdotnet add package Deedbox.Postgres --prereleasedocker run -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres:17-alpineTerminal window dotnet new web -o Shopcd Shopdotnet add package Deedbox.SqlServer --prereleasedocker run -d -p 1433:1433 -e ACCEPT_EULA=Y -e MSSQL_SA_PASSWORD='yourStrong(!)Password' mcr.microsoft.com/mssql/server:2022-latest -
Write the events. An event is a plain record that states what happened.
// Events: plain records. No marker interface, no base class.public record ItemAdded(string Sku, int Qty);public record CheckedOut(DateTimeOffset At); -
Write the state.
Initialis the state of a new cart.Evolveapplies one event.// State: Initial and Evolve. Nothing else.public record Cart(ImmutableDictionary<string, int> Items, bool IsCheckedOut) : IState<Cart>{public static Cart Initial { get; } = new(ImmutableDictionary<string, int>.Empty, false);public static Cart Evolve(Cart s, object e) => e switch{ItemAdded x => s with { Items = s.Items.SetItem(x.Sku, s.Items.GetValueOrDefault(x.Sku) + x.Qty) },CheckedOut => s with { IsCheckedOut = true },_ => s,};} -
Write the decisions. A decision is a pure function: it takes the current state and returns new events. It never writes anything.
// Decisions: pure functions from state to new events.public static class CartDecider{public static IEnumerable<object> Add(Cart cart, string sku, int qty) =>cart.IsCheckedOut? throw new InvalidOperationException("The cart is checked out."): [new ItemAdded(sku, qty)];public static IEnumerable<object> CheckOut(Cart cart, DateTimeOffset now) =>cart.IsCheckedOut || cart.Items.IsEmpty ? [] : [new CheckedOut(now)];} -
Register the stream in
Program.cs.ApplySchemaOnStartupcreates the Deedbox tables when the app starts.builder.Services.AddDeedbox(es => es.UsePostgres(connStr).ApplySchemaOnStartup().Stream<Cart>(s => s // stream type "cart".Events<ItemAdded, CheckedOut>())); // cart.item_added, cart.checked_outbuilder.Services.AddDeedbox(es => es.UseSqlServer(connStr).ApplySchemaOnStartup().Stream<Cart>(s => s.Events<ItemAdded, CheckedOut>()));Deedbox names the stream type
cartand the eventscart.item_addedandcart.checked_out. These names are stored with every event, so a class rename later does not break stored events. -
Append events. Get
IEventStorefrom dependency injection.Executeloads the cart, runs your decision, and appends the new events in one transaction.// Load, decide, evolve and append in one transaction.var result = await store.Execute<Cart>(cartId, cart => CartDecider.Add(cart, sku, qty));// result.State is the new state; result.Version the new version; result.Events the appended envelopes. -
Or do each part yourself.
Loadreturns the state and the version.Appendwrites only if the stream is still at that version.var (cart, version) = await store.Load<Cart>(cartId);var events = CartDecider.CheckOut(cart, now).ToList();if (events.Count > 0)await store.Append(cartId, ExpectedVersion.Exact(version), events);
What you have now
Section titled “What you have now”- A
deedboxschema with astreamstable and aneventstable. - One stream,
cart-1, with its events in order, and its current state stored next to it. - No mediator, no base class and no changes to the rest of your app.
- Add a projection to build a read model.
- Add Deedbox to an existing EF Core app.
- Read streams and state to see what Deedbox stores.