Skip to content

CQRS in C# and .NET with Arc and Chronicle

CQRS (Command Query Responsibility Segregation) means commands change state and queries read it, each with its own model. In C# and .NET, Cratis Arc is an open-source (MIT) CQRS application framework for ASP.NET Core: a command is a record with a Handle() method, a query is a static method on a read model, and Arc generates the HTTP endpoints and typed TypeScript proxies from them. Cratis Chronicle is optional; add it when you also want the write side stored as events.

This page is for .NET developers who search for a CQRS framework that is not a mediator library. If you are still deciding whether CQRS fits, read CQRS explained first.

CapabilityHow it looks in ArcMaturity
Commands[Command] record with Handle(); endpoint generatedReleased
Queries and read models[ReadModel] record with static query methodsReleased
Live queriesA query returning ISubject<T> streams changes to the clientReleased
ValidationData annotations or FluentValidation CommandValidator<T>, run before Handle()Released
Authorization[Roles] and [Authorize] on commands and query methodsReleased
Typed frontendTypeScript proxies generated from the same typesReleased
Event sourcingChronicle: Handle() returns an event; a projection fills the read modelReleased, optional

“Released” means the packages are published. The capability matrix shows each row per language, checked against source and tests. Arc runs on ASP.NET Core and on a lighter standalone Arc.Core host; the integrations for MongoDB and Entity Framework Core are separate packages.

A library application registers authors and lists them. The command is a record. Its parameters are its input and its logic lives in Handle(). This version writes a document to MongoDB and needs no Chronicle:

using Cratis.Arc.Commands.ModelBound;
using MongoDB.Driver;
[Command]
public record RegisterAuthor(Guid Id, string Name)
{
public Task Handle(IMongoCollection<Author> authors) =>
authors.InsertOneAsync(new Author(Id, Name));
}

The query side is a read model with a static method. Arc resolves IMongoCollection<Author> from the container, serves the method over HTTP, and generates a proxy for the frontend:

using Cratis.Arc.Queries.ModelBound;
using MongoDB.Driver;
[ReadModel]
public record Author(Guid Id, string Name)
{
public static IEnumerable<Author> AllAuthors(IMongoCollection<Author> authors) =>
authors.Find(_ => true).ToList();
}

These are excerpts. A runnable host also needs the setup from Arc’s .NET getting started guide. Neither type has a controller or a route attribute. The command never returns the author list and the query never changes anything, so you can test, secure and change each on its own.

Validation, authorization and live queries

Section titled “Validation, authorization and live queries”

Validation and authorization sit at the boundary, so Handle() stays about behavior. Annotate the record’s properties and Arc rejects bad input before Handle() runs. This example is from the validation guide:

[Command]
public record AllocateLabeledIdentifier(
[property: Required] string Label,
[property: Range(1, 100)] int Copies)
{
public Guid Handle() => Guid.NewGuid();
}

Put [Roles(...)] on a command or on a query method and Arc checks the caller’s roles first. Authorization applies when the call goes through Arc’s pipelines; calling Handle() directly from C# bypasses it. See authorizing commands and queries.

For a live read model, return ISubject<T>. The client receives the current result and then each change, and the generated React proxy exposes it as a hook:

[ReadModel]
public record Author([property: Key] AuthorId Id, AuthorName Name)
{
[Roles(nameof(UserRole.Librarian))]
public static ISubject<IEnumerable<Author>> AllAuthors(IMongoCollection<Author> collection) =>
collection.Observe();
}
const [authors] = AllAuthors.use();

CQRS does not need an event log. When you want one, the command changes and the query does not. Handle() returns the fact, and Arc appends it to Chronicle:

using Cratis.Arc.Commands.ModelBound;
using Cratis.Chronicle.Events;
[Command]
public record RegisterAuthor(EventSourceId Id, string Name)
{
public AuthorRegistered Handle() => new(Name);
}
[EventType]
public record AuthorRegistered(string Name);

A projection then fills the read model. With [FromEvent<AuthorRegistered>], Chronicle maps the event’s Name onto the read model by convention, and the query keeps its signature:

[ReadModel]
[FromEvent<AuthorRegistered>]
public record Author(AuthorId Id, AuthorName Name)
{
public static ISubject<IEnumerable<Author>> AllAuthors(IMongoCollection<Author> collection) =>
collection.Observe();
}

The generated proxies and React screens do not change. Moving an existing slice needs more than these type changes: a running Chronicle server, a sink that matches the collection the query reads, and a plan for existing rows. The add event sourcing guide covers each. In an application that uses both products, the Cratis package wires Arc and Chronicle with AddCratis and UseCratis.

Chronicle also provides reducers, reactors and projections for the read side. Projections and read models explains the choice between them. For the storage side, see event sourcing in .NET.

Use it when reads and writes have different shapes, load or rules; when the frontend needs typed, live data; or when you expect to add event sourcing later and want the contract between commands and queries to stay stable.

It is not worth it for a single form over a single table, where plain CRUD is cheaper. It also adds types: a command, a read model and often a validator per behavior. Reads that follow a write can lag when the read model is built asynchronously, as it is with Chronicle projections, so design the screen for that. CQRS explained lists more trade-offs.

Arc is not a mediator library. It owns hosting, endpoints and proxies as well as dispatch. If you only want in-process command handlers behind your own controllers, a smaller library fits better. Coming from MediatR and MVC maps the two.

It depends on how much you want a framework to own. You can write handlers by hand or use a mediator library. Arc is the option where commands, queries, validation, authorization and frontend proxies share one model. Try its getting started guide and compare against what you have.

No. CQRS is a design choice, not a library. Arc dispatches commands and queries itself and exposes them over generated HTTP endpoints.

Can I use CQRS without event sourcing in .NET?

Section titled “Can I use CQRS without event sourcing in .NET?”

Yes. Arc works with MongoDB or Entity Framework Core and has no Chronicle dependency. See CQRS without event sourcing.

How does the frontend call commands and queries?

Section titled “How does the frontend call commands and queries?”

Arc generates typed TypeScript proxies from your C# commands and queries after the build. React components call them as typed methods and hooks. See understanding the proxy boundary.

Does Arc work with languages other than C#?

Section titled “Does Arc work with languages other than C#?”

Yes. See CQRS in Kotlin and Java and CQRS in TypeScript and Node.js. They do not have full parity with .NET.