2. Building a read model
We can record that a book arrived, but the librarian can’t see the catalog yet — the books live only as history in the log. In this chapter we’ll fix that: we’ll build a Books read model that always reflects the current state of every book, and — here’s the part that surprises people coming from CRUD — we’ll do it without writing a single line that updates anything.
In event-modeling terms that’s the view pattern — events fold into a read model the UI can query. It’s the slice of the model we build in this chapter:
First, a couple more facts
Section titled “First, a couple more facts”A book doesn’t just arrive; it gets borrowed and brought back. Those are facts too, so they’re events:
[EventType]public record BookBorrowed(string MemberName);
[EventType]public record BookReturned;Notice BookReturned has no data at all — and that’s fine. The fact that it happened, on a particular book’s stream, at a particular time, is the whole story. Not every event needs a payload.
Declare what you want to read
Section titled “Declare what you want to read”Here’s the shift. In a database you’d write code to keep a Books table in sync — insert on add, update a flag on borrow, update it back on return. In Chronicle you instead declare the shape you want and tell it which events feed it. Chronicle does the keeping-in-sync for you. That declaration is a projection:
using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[ReadModel]public record Book( [Key] BookId Id,
[SetFrom<BookAdded>(nameof(BookAdded.Title))] string Title,
[SetFrom<BookAdded>(nameof(BookAdded.Isbn))] string Isbn,
[SetValue<BookAdded>(false)] [SetValue<BookBorrowed>(true)] [SetValue<BookReturned>(false)] bool OnLoan,
[SetFrom<BookBorrowed>(nameof(BookBorrowed.MemberName))] string? BorrowedBy);Read the attributes as a sentence: a book’s Title and Isbn are taken from BookAdded; OnLoan is false when the book is added, true when it’s borrowed, and false again when it’s returned; BorrowedBy is set to whoever borrowed it. You’re declaring how each fact maps onto the view — not writing imperative updates, not worrying about ordering. Chronicle replays the events in order and applies your mapping.
Query it
Section titled “Query it”The projection writes the Book read model to a store (MongoDB by default), so reading it is just a query — exactly what you’re used to:
public class Books(IMongoCollection<Book> collection){ public IEnumerable<Book> OnLoan() => collection.Find(b => b.OnLoan).ToList();}Now exercise it. Append a BookBorrowed for your book and query again — OnLoan is true, and BorrowedBy has the member’s name. Append a BookReturned and it flips back. You never wrote an UPDATE. The projection did it, by re-deriving the book from its events.
What you did
Section titled “What you did”- Added the events that make up a book’s life (
BookBorrowed,BookReturned). - Declared a
Booksread model and how events map onto it — no update code anywhere. - Queried it like ordinary data, and watched it stay correct on its own.
You can now see the catalog. The last piece is to make the library do something when the world changes — when a book comes back, tell the next person waiting for it. That’s a job for a reactor, and it’s the final chapter. Let’s finish the tour →