3. Relate your slices
An author exists to have written things. So our second feature is adding a book to an author’s catalog — and it’s the first time two slices in our app relate to each other. A book belongs to an author; we’ll model that with a plain foreign key — the author’s id on the book — and read a catalog back with a filtered query.
Here’s the shape:
The book slice
Section titled “The book slice”-
Strong types for the book. Same discipline as before:
public record BookId(Guid Value) : ConceptAs<Guid>(Value){public static BookId New() => new(Guid.NewGuid());}public record BookTitle(string Value) : ConceptAs<string>(Value){public static implicit operator BookTitle(string value) => new(value);} -
The command writes a book, tagged with its author.
AddBookis keyed byAuthorId— the author it belongs to — and writes aBookcarrying that id:[Command]public record AddBook([Key] AuthorId AuthorId, BookId BookId, BookTitle Title){public Task Handle(IMongoCollection<Book> books) =>books.InsertOneAsync(new Book(BookId, AuthorId, Title));}[Command]public record AddBook([Key] AuthorId AuthorId, BookId BookId, BookTitle Title){public async Task Handle(LibraryDbContext db){db.Books.Add(new Book(BookId, AuthorId, Title));await db.SaveChangesAsync();}}
Read a catalog back, filtered
Section titled “Read a catalog back, filtered”The read model is just the book document — it carries the AuthorId it belongs to. The query takes the author’s id (Arc binds it from the route via [Key]) and returns only that author’s books, live:
[ReadModel]public record Book([property: Key] BookId Id, AuthorId AuthorId, BookTitle Title){ public static ISubject<IEnumerable<Book>> BooksForAuthor([Key] AuthorId authorId, IMongoCollection<Book> books) => books.Observe(b => b.AuthorId == authorId);}[ReadModel]public record Book([property: Key] BookId Id, AuthorId AuthorId, BookTitle Title){ public static ISubject<IEnumerable<Book>> BooksForAuthor([Key] AuthorId authorId, LibraryDbContext db) => db.Books.Observe(b => b.AuthorId == authorId);
// add the DbSet to LibraryDbContext: public DbSet<Book> Books => Set<Book>();}Showing the catalog
Section titled “Showing the catalog”BooksForAuthor takes an author id, so it’s a query per author. The list of authors comes from chapter 1’s AllAuthors; each row renders its own live catalog:
import { AllAuthors } from './Authors/Author';import { BooksForAuthor } from './Books/Book'; // generated proxies
const AuthorBooks = ({ authorId }: { authorId: string }) => { const [books] = BooksForAuthor.use(authorId); // live, scoped to this author return <ul>{books.data.map(b => <li key={String(b.id)}>{b.title}</li>)}</ul>;};
export const Catalog = () => { const [authors] = AllAuthors.use(); return ( <ul> {authors.data.map(a => ( <li key={String(a.id)}>{a.name}<AuthorBooks authorId={String(a.id)} /></li> ))} </ul> );};Adding a book is a CommandDialog<AddBook>, exactly like RegisterAuthor — the only difference is that it carries the authorId of the author you’re adding to. Pass that in as an initial value:
<CommandDialog<AddBook> command={AddBook} title="Add book" okLabel="Add" initialValues={{ authorId, bookId: Guid.create() }}> <InputTextField<AddBook> value={i => i.title} title="Title" /></CommandDialog>What you built
Section titled “What you built”- A second slice —
AddBook— that tags each book with the author it belongs to. - A relationship as a foreign key + a filtered query:
BooksForAuthorreads exactly one author’s catalog, live, with no join. - A React screen that composes the two: a list of authors, each rendering its own live catalog.
Two features, related, both live. We keep saying “it stays live” — next we’ll make that concrete and watch a screen update itself the instant the data changes. Let’s make it live →