Build a full-stack feature
This is where the three products meet. You’ll build one vertical slice of a library app — registering an author and listing authors — end to end: a command and event on the backend (Arc + Chronicle), a read model built by a projection, and a React screen (Components) that consumes the generated, type-safe proxies. No hand-written API client, no DTO duplication.
Everything for the feature lives in one folder — that’s the vertical-slice idea: you navigate by feature, not by technical layer.
Here’s the slice as an event model — read left to right, the user acts on a screen, a command records a fact, that fact is projected into a read model, and the next screen reads it:
The blocks are the slice, one per product: the ui screens are Components, the cmd and the rmo’s query are Arc, the evt is Chronicle, and the build generates the typed proxies that connect them. Frames 01–03 are the command pattern (intent → fact); 03–05 are the view pattern (fact → read model → screen).
Before you start
Section titled “Before you start”Scaffold a full-stack app and have it running — dotnet new cratis -o Library from the Chronicle getting started. You’ll add the slice below into a Features/Authors/ folder.
The host
Section titled “The host”dotnet new cratis generates the host for you — the whole stack in two calls. AddCratis brings up Arc and Chronicle together; UseCratis activates both:
var builder = WebApplication.CreateBuilder(args);
builder.AddCratis( configureArcBuilder: arc => arc.WithMongoDB(), configureChronicleOptions: chronicle => chronicle.EventStore = "Library", configureChronicleBuilder: chronicle => chronicle.WithCamelCaseNamingPolicy());
var app = builder.Build();
app.UseCratis();app.Run();You won’t touch this again for the slice below — it’s here so you can see where the command, event, read model, and query you’re about to write get wired in. The Cratis package reference covers what AddCratis configures and how to adjust it.
1. Model the slice (backend)
Section titled “1. Model the slice (backend)”-
A strongly-typed id. Never pass a raw
Guidaround — wrap it:public record AuthorId(Guid Value) : ConceptAs<Guid>(Value){public static AuthorId New() => new(Guid.NewGuid());public static implicit operator EventSourceId(AuthorId id) => new(id.Value.ToString());} -
The command and the event, together in one file. The command is a record with
Handle()on it — no separate handler class.Handle()returns the event that happened:[Command]public record RegisterAuthor(AuthorId Id, string Name){public AuthorRegistered Handle() => new(Name);}[EventType]public record AuthorRegistered(string Name); -
The read model and its projection. Declare the shape you want to query and which event feeds it — AutoMap matches
AuthorRegistered.NametoName. A static method exposes the query as an observable so the UI updates live:[ReadModel][FromEvent<AuthorRegistered>]public record Author([property: Key] AuthorId Id, string Name){public static ISubject<IEnumerable<Author>> AllAuthors(IMongoCollection<Author> collection) =>collection.Observe();}
That’s the whole backend for the feature — one file, three records. Run dotnet build.
2. Build the screen (frontend)
Section titled “2. Build the screen (frontend)”-
A dialog that executes the command.
CommandDialoginstantiates and runs the generated command and renders the OK/Cancel footer for you — you just supply the fields:import { CommandDialog } from '@cratis/components/CommandDialog';import { InputTextField } from '@cratis/components/CommandForm';import { DialogProps } from '@cratis/arc.react/dialogs';import { RegisterAuthor } from './RegisterAuthor';import { Guid } from '@cratis/fundamentals';export const AddAuthor = ({ closeDialog }: DialogProps) => (<CommandDialog<RegisterAuthor>command={RegisterAuthor}title="Add author"okLabel="Add"onBeforeExecute={(values) => { values.id = Guid.create(); return values; }}><InputTextField<RegisterAuthor> value={i => i.name} title="Name" /></CommandDialog>); -
A table that reads the observable query. The generated
AllAuthorsproxy is observable —.use()re-renders when the projection changes:import { AllAuthors } from './Author';export const Authors = () => {const [authors] = AllAuthors.use();return (<ul>{authors.data.map(a => <li key={String(a.id)}>{a.name}</li>)}</ul>);};
What you built
Section titled “What you built”- One vertical slice: command, event, read model, projection, and UI — all for a single behavior, in one place.
- Full-stack type safety: the React code calls proxies generated from your C# types. There is no second source of truth to drift.
- The CQRS loop: the command appends a fact, a projection builds the read model, and the observable query streams it to the UI live.
Go deeper
Section titled “Go deeper”- Backend: Commands and Queries in Arc, Projections in Chronicle.
- Frontend: Dialogs and data tables in Components.
- The reasoning: Why developers choose Cratis and Why Arc.