Skip to content

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:

UI/A: AuthorsUI/A: AuthorsC/RM: AuthorsC/RM: AuthorsStream: Authors
AddAuthor
RegisterAuthor



id: uuid, name: string
AuthorRegistered



name: string
Author
Authors

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).

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.

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. A strongly-typed id. Never pass a raw Guid around — 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());
    }
  2. 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);
  3. The read model and its projection. Declare the shape you want to query and which event feeds it — AutoMap matches AuthorRegistered.Name to Name. 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.

  1. A dialog that executes the command. CommandDialog instantiates 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>
    );
  2. A table that reads the observable query. The generated AllAuthors proxy 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>
    );
    };
  • 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.