This is the full developer documentation for Cratis
# Software gets built differently now
> In the age of AI, software gets built differently. Cratis is one connected, purposeful ecosystem — an event store, an application framework, a UI library, developer tooling, and AI skills — designed together, from one company, so the pieces fit instead of being assembled. It is database-agnostic (MongoDB by default, PostgreSQL, SQL Server, or SQLite) with clients for .NET, TypeScript, Kotlin/Java, and Elixir. Everything Cratis publishes today is MIT licensed and free to use.
One Ecosystem Event Sourcing CQRS Application Framework The Stack Any Language Any Database AI-Native Studio Screenplay Prologue
## Why this matters
[Section titled “Why this matters”](#why-this-matters)
Most stacks are assembled: a database here, a framework there, a UI kit from somewhere else, and glue code everywhere in between. Every seam is something a team — and now every AI assistant — has to learn, maintain, and keep in sync.
Cratis takes the opposite bet. One company designs the whole path — from the facts your system records, through the application that shapes them, to the screens people use and the tools that operate it — around one shared model and one set of conventions. We believe software built this way is better, more robust, faster to deliver, higher quality, and more cost-effective, because the seams you would normally hand-write and babysit simply are not there.
That same coherence is what makes the ecosystem AI-ready: conventions that are explicit and consistent are conventions an AI agent can follow. Cratis ships its conventions as [AI skills, rules, and diagnostics](/ai/), so assistants build with the grain of the platform instead of guessing at it.
## One connected ecosystem — adopt it one piece at a time
[Section titled “One connected ecosystem — adopt it one piece at a time”](#one-connected-ecosystem--adopt-it-one-piece-at-a-time)
Designed together does not mean all-or-nothing. Each product stands on its own, and you choose what you need: run Chronicle alone as your event store, build with Arc without event sourcing at all, or take the whole stack and let value flow through the entire chain. Start with the piece that solves today’s problem — the rest are there when you want them, not before:
[Chronicle — the event store ](/chronicle/)Record what happened as immutable facts and derive read models from them. Database-agnostic — MongoDB (default), PostgreSQL, SQL Server, or SQLite — so it runs on the database you already operate.
[Arc — the application framework ](/arc/)Turn commands and queries into a full-stack CQRS application with validation, authorization, and generated TypeScript proxies — no hand-written API layer.
[Components — the UI library ](/components/)React components that render Arc's generated contracts as forms, dialogs, and live tables, so screens follow the model.
[CLI and Workbench — the tooling ](/cli/)See what the running system is doing — inspect events, observers, and read models, replay, and diagnose — from the terminal or the browser.
[AI skills — the accelerant ](/ai/)The same conventions developers use, packaged for AI agents: skills, editor rules, analyzers, and an MCP server into the running store.
[Clients for your language ](/chronicle-clients/)Use Chronicle from .NET (first-class), TypeScript, Kotlin/Java (JVM), or Elixir — with a Python client coming soon.
## Built to deliver outcomes, not just features
[Section titled “Built to deliver outcomes, not just features”](#built-to-deliver-outcomes-not-just-features)
Cratis is opinionated on purpose. The opinions are what carry the value:
* **Built to keep history you can trust.** Events are immutable facts, and read models derive from them — audit and replay are part of the design, not an afterthought.
* **Built to remove glue.** One C# model becomes typed frontend contracts; drift between backend and frontend becomes a build error instead of a production incident.
* **Built to fit your infrastructure.** A database-agnostic event store and multiple language clients mean the ecosystem adapts to you — not the other way around.
* **Built to be adopted piecemeal.** Every product is independently adoptable. Chronicle works without Arc, Arc works without event sourcing, Components is optional on top of Arc — you never buy the whole stack to get one piece, and nothing is wasted when you later add the next one.
* **Built for people and AI together.** Predictable, convention-driven codebases are easier for new developers to join and for AI agents to extend.
We believe this adds up to software that is more robust, faster to build, and more cost-effective to own. [Why developers choose Cratis](/why-cratis/) walks through the reasoning; [The Cratis Stack](/cratis-stack/) shows the whole journey end to end.
## Use one piece — or the whole thing
[Section titled “Use one piece — or the whole thing”](#use-one-piece--or-the-whole-thing)
Modularity is a design commitment, not a marketing line. The products compose, but none of them holds the others hostage — each seam below is an intentional, supported adoption boundary:
* Chronicle stands alone. Use it purely as an event store — from .NET, TypeScript, Kotlin/Java, or Elixir — with no Arc, no Components, and no other Cratis product involved.
* Arc does not require event sourcing. Use application services, MongoDB, or Entity Framework Core for current-state behavior, and add Chronicle only where an event-sourced path fits.
* Arc does not require Components. Generated TypeScript clients can be consumed directly or through Arc’s React packages.
* Chronicle is not .NET-only. The [client landing page](/chronicle-clients/) maps each shipped client to its documentation and packages; the protocol details live in the [Chronicle architecture](/chronicle/architecture/) pages.
* Chronicle and its bundled local Workbench are available as MIT-licensed self-hosted software; authorized local use is separate from paid Cratis support, hosted coordination, or managed operational responsibility.
These boundaries do not establish compatibility with every package, provider, browser, runtime, topology, or product version. Check the documentation and package manifests for the combination you use.
## Go deeper
[Section titled “Go deeper”](#go-deeper)
Technical depth lives in each product’s own documentation:
[Start Chronicle locally ](/chronicle/get-started/)Run the development image, wait for readiness, and open the bundled Workbench.
[Understand Chronicle architecture ](/chronicle/architecture/)Locate protocol surfaces, the kernel, event stores, namespaces, processing, and subscriptions.
[Run Chronicle on your database ](/chronicle/hosting/configuration/storage/)Configure the storage provider — MongoDB (default), PostgreSQL, SQL Server, or SQLite.
[Pick a Chronicle client ](/chronicle-clients/)Start from .NET, TypeScript, Kotlin/Java, or Elixir, with links to each client's documentation and packages.
[Explore Arc ](/arc/)Choose commands, queries, observable-query APIs, frontend contracts, identity, tenancy, persistence, or testing.
[Browse Components ](/components/)Find package setup, component areas, styling boundaries, examples, and migration guidance.
[Use the CLI ](/cli/)Install the CLI, connect to Chronicle, inspect events and read models, and diagnose observer failures.
[Run a sample ](/samples/)Use Cratis-owned sample applications to evaluate one bounded technical path.
## About this site
[Section titled “About this site”](#about-this-site)
Product repositories own their technical documentation, examples, packages, and released contracts. This site aggregates that material into one searchable set of tutorials, guides, explanations, and references.
* [Browse the Cratis repositories](https://github.com/Cratis)
* [Join the Cratis community](https://discord.gg/kt4AMpV8WV)
* [Report a documentation problem](https://github.com/Cratis/Documentation/issues)
* [Read company, fit, and trust information](https://cratis.no/)
# 404
> That page wandered off. Let's get you back to something useful.
Looking for something specific? Try the **search** in the top bar, or jump to a product:
* [Chronicle](/chronicle/) — the event sourcing platform
* [Arc](/arc/) — the full-stack application framework
* [Components](/components/) — the React component library
# Adopting Cratis
> Where to start with Cratis — default to the full event-sourced stack for new information systems, or adopt one product at a time in existing systems.
Getting oriented
## Where to start
Cratis products stand on their own and compose when you want them to. For a new information system, our default is the full loop: Chronicle for event sourcing, Arc for CQRS, Components for React, and AuthProxy at the edge. For an existing system or a bounded current-state slice, you can adopt one piece at a time without losing the path back to the full stack.
## Pick your entry point
[Section titled “Pick your entry point”](#pick-your-entry-point)
Two things decide where you start: **what you’re building on** — a new codebase or an existing one — and **which boundary you are changing first**. Event sourcing is the default architecture we recommend for information systems; CQRS, generated contracts, and edge identity can still be adopted independently.
```
New information system
Bounded CRUD slice
Existing
Frontend/backend glue
Audit & history
New information system
or existing?
Default stack:
Arc + Chronicle + Components
Arc + Components
over MongoDB / EF Core
What hurts
most?
Add Arc to your app
Introduce Chronicle,
one slice at a time
```
| Your situation | Start with | Why |
| ----------------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| New information system | **Arc + Chronicle + Components** | The default Cratis loop: CQRS at the boundary, event history underneath, React generated from C# — the [full-stack capstone](/build-a-full-app/) builds the shape. |
| Bounded slice where current state is genuinely enough | **Arc + Components** over a database | Typed commands, queries, and a generated React client with CQRS but no event log — [CQRS without event sourcing](/arc/arc-without-event-sourcing/). |
| Existing .NET API, frontend glue is the pain | **Arc**, added to your app | A typed TS client for new endpoints, without rewriting what you have. |
| Existing system, you need history or audit | **Chronicle**, incrementally | Event-source one bounded context; leave the rest as it is. |
| Need to separate CQRS from event sourcing | Read first | [CQRS without event sourcing](/arc/arc-without-event-sourcing/), then [Why developers choose Cratis](/why-cratis/). |
## Starting from scratch (greenfield)
[Section titled “Starting from scratch (greenfield)”](#starting-from-scratch-greenfield)
A new project is the easy case — nothing to migrate, no constraints. For an information system, start with the whole loop unless you have a clear reason not to:
* **Default to the full experience.** Scaffold the stack and follow [getting started](/chronicle/get-started/), then the [full-stack capstone](/build-a-full-app/). You’ll have the command → event → projection → query → React loop running, with history available from the event log. Scaffolding wires Arc and Chronicle into one host through the [Cratis package](/arc/backend/chronicle/cratis-package/)’s `AddCratis`/`UseCratis`.
* **Use Arc-only for deliberate current-state slices.** If a bounded part of the app is just reference data, settings, or an adoption step, [Arc over a database](/arc/arc-without-event-sourcing/) still gives you CQRS, generated proxies, and live queries. Because adopting Chronicle is a write-side change, that slice can move to events later without touching your queries or frontend.
CQRS and event sourcing are independent
Arc gives you CQRS: commands for information entering the system, queries for information leaving it. Chronicle gives you event sourcing: facts as the durable source of truth. They fit together extremely well, but either one can stand without the other.
## Adding to an existing system (brownfield)
[Section titled “Adding to an existing system (brownfield)”](#adding-to-an-existing-system-brownfield)
You rarely get to start clean. Cratis is built to be adopted a slice at a time, alongside code that already works — you don’t rewrite, you grow into it.
### Add Arc to an existing ASP.NET Core app
[Section titled “Add Arc to an existing ASP.NET Core app”](#add-arc-to-an-existing-aspnet-core-app)
If your pain is the **frontend-to-backend boundary** — hand-written controllers, DTOs duplicated in TypeScript, fetch wrappers that drift out of sync — Arc slots into an app you already have. Register it on your existing host and start expressing *new* endpoints as commands and queries; Arc generates their typed proxies while your existing controllers keep running untouched.
Arc’s persistence meets your data where it already lives: its [MongoDB](/arc/backend/mongodb/) and [Entity Framework](/arc/backend/entity-framework/) integrations read and write the database you already use — EF Core even has a [direct-registration path](/arc/backend/entity-framework/getting-started/) designed for slotting into an existing application without taking over the whole framework. For controllers and MediatR handlers, the [MediatR, MVC, and Arc](/arc/coming-from-mediatr-and-mvc/) bridge maps familiar concepts onto Arc’s model.
### Introduce Chronicle into an existing domain
[Section titled “Introduce Chronicle into an existing domain”](#introduce-chronicle-into-an-existing-domain)
If your pain is **history** — you need an audit trail, a “how did this order get into this state?” view, or a new read model the current schema can’t serve — you don’t have to event-source the whole system in one move. Pick the bounded context where the model is clearest and event-source that first:
1. Model the facts for that area as [events](/chronicle/concepts/event/), and append them when the corresponding things happen — for new behavior, append the event; for existing writes, append alongside the current write.
2. Build [projections](/chronicle/projections/) that fold those events into exactly the read models that context needs. They can sit right next to your existing tables.
3. Let the rest of the system keep working as it is. Event sourcing earns its place one context at a time, not as a big-bang rewrite.
The [CRUD, EF Core, and Chronicle](/chronicle/coming-from-crud/) guide maps tables and `SaveChanges` onto events — that’s the mental shift that makes this click.
Keep the boundary honest
Event sourcing is the default for information systems, not a mandate that every settings table needs a stream. Use Chronicle for the facts and processes that define the domain, and leave genuinely current-state-only corners on Arc-over-a-database. [When to use event sourcing](/chronicle/concepts/when-to-use-event-sourcing/) is the honest filter.
## Grow into the full stack
[Section titled “Grow into the full stack”](#grow-into-the-full-stack)
However you start, the pieces are designed to be *added*, not swapped out. The common growth paths:
* **Arc-over-a-database → add Chronicle.** Move a slice’s write side from a direct insert to appending an event with a projection behind it. The query and the React don’t change — see the side-by-side in [CQRS without event sourcing](/arc/arc-without-event-sourcing/).
* **Chronicle-only service → add Arc and a frontend.** Already event-sourcing from a worker or service? Put Arc in front to expose typed commands and queries, and Components to render them — without changing how your events are stored.
* **Any backend → add Components.** The React library consumes Arc’s generated proxies, so adding it is a frontend-only step.
## Next steps
[Section titled “Next steps”](#next-steps)
[ Why developers choose Cratis](/why-cratis/)
[How the three products stand alone and compose — the map this page navigates.](/why-cratis/)
[ CQRS without event sourcing](/arc/arc-without-event-sourcing/)
[The standalone Arc shape, and the write-side change that moves a slice to Chronicle.](/arc/arc-without-event-sourcing/)
[ Build the full loop](/build-a-full-app/)
[The full-stack capstone — command to event to live React screen, with history.](/build-a-full-app/)
# AI-native development
> Cratis combines framework-aware AI workflows with documented CLI and MCP operating tools. Learn what maintainers can use today, what is still in distribution preview, and where each agent ecosystem fits.
The Cratis Stack
## Build and operate with AI agents
Cratis is designed to give an AI assistant the same domain vocabulary, framework boundaries, and verification gates your team uses. Cratis maintainers use framework-aware workflows in Cratis repositories today; external portable coding skills remain in distribution preview. The CLI and Chronicle MCP provide the documented operating side now.
## Why “AI-native”
[Section titled “Why “AI-native””](#why-ai-native)
An AI assistant is only as good as what it knows about your framework and your system. Drop a general-purpose agent into a Cratis codebase and it will *guess* — inventing handler classes, missing the vertical-slice conventions, hand-writing the API client you don’t need. Point it at a running event store and it has no idea how to read the log or recover a stuck observer.
Cratis closes the operating gap with documented CLI and MCP setup today. For coding workflows, it separates the repository-local setup used by Cratis maintainers from a generated portable distribution that remains gated until its first real target and release are approved.
## Build with AI: skills that know the Cratis way
[Section titled “Build with AI: skills that know the Cratis way”](#build-with-ai-skills-that-know-the-cratis-way)
The **Cratis AI configuration** teaches an assistant the conventions—vertical slices, `[Command]` with `Handle()` on the record, model-bound projections, and `ConceptAs` instead of raw primitives.
Cratis-owned repositories currently carry repository-local rules and host adapters, so maintainers can use these workflows today. The public portable skills are being generated for Agent Skills, Claude Code, Codex, GitHub Copilot, Cursor, Kiro, Junie, Gemini CLI, and Pi, but they are not released for external installation yet. The public distribution repository remains fixture-only until source, target, canary, and release gates pass.
Among the workflows available to Cratis maintainers:
* **`new-vertical-slice`** — scaffold a whole feature end to end: command, events, projection, query, React, and specs.
* **`cratis-command`**, **`cratis-readmodel`**, **`add-projection`**, **`add-reactor`**, **`add-concept`** — build one artifact correctly, by convention.
* **`scaffold-feature`**, **`write-specs`**, **`review-code`** — set up a feature folder, cover it with BDD specs, and review the result against the project’s standards.
Because the skills encode the conventions, an agent that uses them produces slices that look like the rest of your codebase — not a layered approximation of it. (The configuration is the canonical `.ai/` source in the Cratis AI repository, dropped into a project as its `.claude/` and `.github/` folders.)
Start with [AI setup and current availability](/ai/getting-started/), then use the [ecosystem support matrix](/ai/ecosystems/) to see what is verified and what remains blocked. [Plugins](/plugins/) explains the shared skill model, while the [code-analysis gates](/code-analysis/) enforce framework contracts during the build.
Model first, then generate
Pair this with [Studio](/studio/) or [Screenplay](/screenplay/): model the feature on the canvas or as a declarative `.play` script, generate the C# shapes, then let an agent flesh out the slices around them. Design → generate → build, with AI at each step.
## Operate with AI: teach your assistant your store
[Section titled “Operate with AI: teach your assistant your store”](#operate-with-ai-teach-your-assistant-your-store)
Building is only half the loop. The other half is *operating* what you built — and Cratis makes the running store legible to an assistant two ways.
### `cratis init` — the CLI, made AI-aware
[Section titled “cratis init — the CLI, made AI-aware”](#cratis-init--the-cli-made-ai-aware)
Run it once inside your project:
```bash
cratis init
```
It writes a **`CHRONICLE.md`** describing every command the CLI can run, installs instruction files for **Claude Code, GitHub Copilot, Cursor, and Windsurf**, and adds a **`chronicle-diagnose`** slash command. From then on your assistant knows how to browse events, watch observers, and diagnose a stuck partition through the [CLI](/cli/) — because the whole command catalog is in its context. Refresh it after a CLI upgrade with `cratis init --refresh`. The [CLI getting started](/cli/getting-started/) guide walks through it in full.
### The Chronicle MCP server — an agent, connected to the store
[Section titled “The Chronicle MCP server — an agent, connected to the store”](#the-chronicle-mcp-server--an-agent-connected-to-the-store)
For tools that speak the **Model Context Protocol**, Cratis publishes a containerized MCP server that connects straight to a running Chronicle store. Point your tool at it with an `mcp.json`:
```json
{
"servers": {
"Chronicle": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm",
"-eCratis__Chronicle__Mcp__ConnectionString=chronicle://host.docker.internal:35000",
"cratis/chronicle-mcp"]
}
}
}
```
With it connected, your assistant can — in plain language — do things like:
* **Explore** — list event stores and sequences, show event types and their schemas, and read the event log or the events for a single event source.
* **Run observers** — list observers, replay an observer or one partition, and recover a failed partition.
* **Act on recommendations** — list, perform, or ignore the store’s recommendations.
* **Manage jobs** — resume, stop, or delete a running job.
It needs a Chronicle server running; everything it does, it does against the live store.
Operate, not mutate
The MCP server and the CLI are *operate-and-inspect* tools — the agent reads the log and manages observers and jobs. To change application state you still go through commands and events. History stays honest.
## The whole loop, AI-accelerated
[Section titled “The whole loop, AI-accelerated”](#the-whole-loop-ai-accelerated)
```
Operate with AI
Build with AI
scaffold slices
run it
inspect & diagnose
browse · replay · recover
Cratis AI skills
model · build · review
cratis init
CLI command catalog
Chronicle MCP server
connected to the store
Your Cratis app
Running Chronicle store
```
## Where to go next
[Section titled “Where to go next”](#where-to-go-next)
[ Get started with AI](/ai/getting-started/)
[Set up the operating tools today and understand the coding-skills release status.](/ai/getting-started/)
[ Ecosystem support](/ai/ecosystems/)
[See generated formats and verification status across supported AI hosts.](/ai/ecosystems/)
[ Trust and distribution](/ai/trust-and-distribution/)
[Learn how approvals, generated bytes, canaries, rollback, and project context fit together.](/ai/trust-and-distribution/)
[ The Cratis Stack](/cratis-stack/)
[How design, build, and operate fit together end to end — AI accelerates every step.](/cratis-stack/)
[ CLI getting started](/cli/getting-started/)
[Install the CLI, connect to your store, and run `cratis init` to set up AI tooling.](/cli/getting-started/)
[ Plugins](/plugins/)
[The agents, skills, and coding rules in detail — and how each AI assistant loads them.](/plugins/)
[ Code analysis](/code-analysis/)
[The Roslyn analyzers and ESLint rules that enforce the same conventions during the build.](/code-analysis/)
[ Vertical slices](/arc/vertical-slices/)
[The convention the build-side skills follow — everything for a feature in one folder.](/arc/vertical-slices/)
# Use AI as a Cratis maintainer
> Follow the Cratis repository profile, project context, skill, verification, review, and shipping workflow without copying the shared AI corpus between repositories.
Cratis maintainers do not ask a general assistant to guess the framework. Each Cratis repository already carries the instructions and host adapters needed for its current development workflow. Your job is to start in the right repository, state the outcome, and require fresh verification before shipping.
## Work from the repository root
[Section titled “Work from the repository root”](#work-from-the-repository-root)
1. **Start the assistant in the repository you intend to change**
The root `AGENTS.md` identifies whether the repository is a Cratis application, framework, client, documentation site, or another supported profile. Profile selection matters: application vertical-slice rules do not apply to Chronicle, Arc, Components, or client-library source repositories.
2. **Read project-owned context**
If the repository has project-specific context, preserve it. Current repositories may still use `.agents/PROJECT.md`; the controlled migration design uses `.cratis/PROJECT.md` with small host bootstraps. Never replace, concatenate, or copy another repository’s project context.
3. **Ask for the outcome, not an implementation pattern**
Good requests name the behavior and constraints:
* “Model the registration flow before implementation.”
* “Add a strongly typed invoice number.”
* “Diagnose why this projection stopped updating.”
* “Review this change for Cratis architecture and security.”
The matching skill chooses the Cratis workflow. If you know the skill, you can invoke it explicitly using your host’s skill picker or mention syntax.
4. **Let the repository answer routine questions**
The assistant should inspect local rules, source, generated patterns, and specs before interrupting you. Stop only for product choices, unavailable authority, credentials, risky production actions, or genuinely conflicting evidence.
5. **Require fresh gates**
Compilation is not behavior evidence. Run the repository’s current build, specs, lint, generated-file, and documentation gates. Re-run the failing gate after a correction.
6. **Ship through a reviewed pull request**
Use logical commits, green CI, and the repository’s merge policy. When a task creates a new Cratis repository, register it through a linked `Cratis/Strategy` issue so Strategy can establish ownership, metadata, and AI setup with its own rules.
## Common maintainer workflows
[Section titled “Common maintainer workflows”](#common-maintainer-workflows)
| Intent | Skill or capability |
| ------------------------------------------------------------------- | --------------------------------------- |
| Design unsettled commands, facts, streams, views, or reactions | `event-modeling` |
| Build a complete application behavior | `new-vertical-slice` |
| Add a command, read model, projection, reactor, reducer, or concept | The matching focused Cratis skill |
| Diagnose source behavior | `diagnose-slice` |
| Inspect a running Chronicle store | `inspect-running-chronicle` and the CLI |
| Review correctness, architecture, security, or performance | The matching review skill |
| Write product or site documentation | The Cratis documentation skills |
| Commit, push, open, monitor, and merge a PR | `ship-changes` |
Skills guide the work; analyzers still enforce contracts
The AI workflow does not replace Arc or Chronicle analyzers, compiler errors, specifications, CI, or human review. It helps an agent reach the same gates a maintainer would use.
## Do not propagate the corpus
[Section titled “Do not propagate the corpus”](#do-not-propagate-the-corpus)
Do **not** copy shared `.ai`, `.claude`, `.github`, `.agents`, or `.pi` trees from one repository to another. The old all-to-all propagation model is frozen because it created mixed versions, ownership ambiguity, and accidental fan-out.
The replacement model is:
1. author and approve canonical capabilities in `Cratis/AI`;
2. generate immutable, host-native artifacts into `Cratis/AI.Distribution`;
3. install or pin one reviewed version through the host or organization scope;
4. keep only project facts and minimal host bootstraps in the consuming repository;
5. canary updates, observe failures, and roll back by version.
Existing repository-local AI files remain in place until the replacement has real canary and rollback evidence. Do not delete them early, and do not restart legacy propagation.
## Keep shared and project knowledge separate
[Section titled “Keep shared and project knowledge separate”](#keep-shared-and-project-knowledge-separate)
Shared skills own Cratis conventions and reusable workflows. The consuming repository owns architecture decisions, commands, environments, credentials, fixtures, and verification details. That separation lets a shared package update without overwriting the facts that make one project different from another.
Read [Trust and distribution](/ai/trust-and-distribution/) for the complete boundary and [Start using AI with Cratis](/ai/getting-started/) for the public setup status.
# AI ecosystem support
> See how Cratis AI skills map to Agent Skills, Claude Code, Codex, GitHub Copilot, Cursor, Kiro, Junie, Gemini CLI, Pi, and npm—and which gates remain before public installation.
Cratis authors one passive skill tree and generates the wrapper each host expects. A wrapper changes packaging and discovery—not the behavior of the skill. This keeps a projection workflow or event-modeling workflow consistent when your team uses different assistants.
No supported public coding-skills release yet
The formats below have fixture generation evidence. The public distribution repository is intentionally marked fixture-only, and no real Cratis skill target is approved for installation. Do not treat generated `main` as a release.
## Current matrix
[Section titled “Current matrix”](#current-matrix)
| Ecosystem | Generated form | Current evidence | Public install status |
| ---------------------- | ----------------------------------------- | ---------------------------------------------------------- | --------------------------------------------- |
| Agent Skills | `skills//SKILL.md` | Format and name/frontmatter validation | Blocked pending an approved target |
| Claude Code | Skills-only plugin and marketplace | Strict validation plus isolated install/list/remove smoke | Blocked pending an immutable release |
| OpenAI Codex / ChatGPT | Skills-only Codex plugin and marketplace | Manifest generation and Codex marketplace add/remove smoke | Blocked; ChatGPT desktop review remains |
| GitHub Copilot | Skills-only plugin and marketplace | Isolated marketplace install/list/remove smoke | Blocked pending an immutable release |
| Cursor | Skills-only Cursor plugin and marketplace | Manifest and canonical-byte parity | Blocked; Cursor host and vendor review remain |
| Kiro | Agent Plugins 1.0 skills-only power | Agent Plugin manifest and canonical-byte parity | Blocked; Kiro host review remains |
| Junie | Skills-only Junie extension | Extension manifest and canonical-byte parity | Blocked; Junie host review remains |
| Gemini CLI | Skills-only extension | Isolated link/list/remove smoke | Blocked pending an immutable release |
| Pi | Passive `pi-package` npm layout | Isolated install/list/remove smoke | Blocked pending package ownership and release |
| npm | `@cratis/ai` passive tarball | Pack/install/uninstall with scripts disabled | Blocked pending trusted publishing |
## What every adapter must preserve
[Section titled “What every adapter must preserve”](#what-every-adapter-must-preserve)
Every generated target must contain the same approved `SKILL.md`, references, assets, and required licenses. Target-native manifests may add names, versions, and discovery roots, but they may not redefine workflow behavior.
The passive package excludes:
* lifecycle scripts and executable extensions;
* MCP implementations and credentials;
* engineering rules, agents, hooks, prompts, evals, and authoring tooling;
* project-specific context;
* private product or Strategy content.
## Why installation is still closed
[Section titled “Why installation is still closed”](#why-installation-is-still-closed)
Packaging is working, but packaging is not approval. Public installation opens only after one Cratis product owner approves exact source bytes and claims, the target passes behavior and collision evidence, an immutable generated release is reviewed, and a real consuming repository proves install, update, rollback, and emergency disable.
The manual gates are tracked in:
* [`Cratis/AI#148`](https://github.com/Cratis/AI/issues/148) — first approved public target and source contract;
* [`Cratis/Workflows#72`](https://github.com/Cratis/Workflows/issues/72) — PR/release-capable distribution bot;
* [`Cratis/Workflows#70`](https://github.com/Cratis/Workflows/issues/70) — npm package ownership and stage-only trusted publishing;
* [`Cratis/Workflows#71`](https://github.com/Cratis/Workflows/issues/71) — first real consumer canary and rollback;
* [`Cratis/AI#147`](https://github.com/Cratis/AI/issues/147) — vendor marketplace submissions after release approval.
## What to do now
[Section titled “What to do now”](#what-to-do-now)
Use [`cratis init`](/cli/getting-started/) and Chronicle MCP for documented operating workflows today. Cratis maintainers can use the existing repository- local AI setup as described in [Using AI as a Cratis maintainer](/ai/cratis-maintainers/). External application teams should wait for the first immutable release rather than copying internal repository trees.
# Start using AI with Cratis
> Set up the Cratis CLI and agent-facing operating tools today, understand the coding-skills release status, and choose the right next step for your team.
A general coding assistant can read C#, but it does not automatically understand Cratis commands, facts, event sources, projections, or vertical slices. Cratis closes that gap in two places: it gives the assistant documented operating tools today, and it is preparing portable coding skills for supported agent ecosystems.
Coding-skills distribution is still a preview
The public [`Cratis/AI.Distribution`](https://github.com/Cratis/AI.Distribution) repository currently contains **fixture-only generated bytes**. It is not a supported installation or release target. Do not install its `main` branch into a project. This page separates what you can use now from what is still gated.
## Set up the operating tools
[Section titled “Set up the operating tools”](#set-up-the-operating-tools)
1. **Install the Cratis CLI**
Follow the [CLI getting-started guide](/cli/getting-started/) for your platform, then open a terminal in your Cratis project.
2. **Generate the agent-facing CLI context**
```bash
cratis init
```
The command writes the CLI context and supported host instructions for the project. Your assistant can now discover documented Chronicle inspection and diagnosis commands instead of inventing shell syntax.
3. **Refresh after a CLI upgrade**
```bash
cratis init --refresh
```
Refreshing keeps the assistant’s command catalog aligned with the CLI version you actually run.
4. **Connect Chronicle MCP when your host supports MCP**
Use the [Chronicle MCP setup guide](/chronicle-mcp/) to connect an approved host to a running Chronicle store. Review every requested capability before enabling mutating operational tools.
You now have the operating side of Cratis AI: the assistant can use documented CLI or MCP surfaces to inspect Chronicle with the same vocabulary you use in the application.
## Use coding skills in a Cratis-owned repository
[Section titled “Use coding skills in a Cratis-owned repository”](#use-coding-skills-in-a-cratis-owned-repository)
Cratis maintainers can use the repository-local AI setup already present in Cratis repositories. Start your agent from the repository root so it reads `AGENTS.md` and the repository’s current host adapter. Ask for the outcome in plain language—for example, “add a projection,” “model this workflow,” or “review these changes”—and let the matching skill apply the repository profile and quality gates.
See [Using AI as a Cratis maintainer](/ai/cratis-maintainers/) for the complete workflow and the rules for project context, verification, and shipping.
## Set up coding skills in your own repository
[Section titled “Set up coding skills in your own repository”](#set-up-coding-skills-in-your-own-repository)
Portable public skills are not released yet. Cratis is validating one canonical skill tree across Agent Skills, Claude Code, Codex, GitHub Copilot, Cursor, Kiro, Junie, Gemini CLI, Pi, and npm packaging before opening installation.
Until an immutable release is published:
* do not copy `.ai`, `.claude`, `.github`, or skill folders from another repository;
* do not install the fixture-only distribution repository;
* keep your project-specific facts in your repository rather than in shared skills;
* use `cratis init` and Chronicle MCP for the currently documented operating workflow.
The [ecosystem support matrix](/ai/ecosystems/) shows the generated format and current verification level for every host. The [trust and distribution model](/ai/trust-and-distribution/) explains why Cratis uses versioned generated artifacts instead of copying one repository into another.
## Choose your next step
[Section titled “Choose your next step”](#choose-your-next-step)
[ Cratis maintainers](/ai/cratis-maintainers/)
[Work effectively with the repository-local rules and skills already used by the Cratis team.](/ai/cratis-maintainers/)
[ Agent ecosystems](/ai/ecosystems/)
[See Claude, Codex, Copilot, Cursor, Kiro, Junie, Gemini, Pi, and npm status in one place.](/ai/ecosystems/)
[ Trust and distribution](/ai/trust-and-distribution/)
[Understand generated packages, approvals, canaries, rollback, and why propagation stays frozen.](/ai/trust-and-distribution/)
# Trust and distribution for Cratis AI
> Understand why Cratis generates host-native AI packages from one approved source, how project context stays local, and which approval, canary, rollback, and publication gates protect users.
An AI skill can influence every file an assistant reads or writes. Treating it like a copied snippet makes updates hard to audit and rollback. Cratis therefore separates authoring, generated distribution, host packaging, and project-owned context.
## One source, generated packages
[Section titled “One source, generated packages”](#one-source-generated-packages)
```
approved exact bytes
manifest + checksums + provenance
Cratis/AI
author and evaluate
empty generated stage
Cratis/AI.Distribution
Claude / Codex / Copilot / Cursor
Kiro / Junie / Gemini / Pi
version-pinned project
project-owned context
```
`Cratis/AI` is the canonical authoring and approval repository. A generator selects an exact positive allowlist into an empty stage, verifies byte parity, and emits target-native manifests. The generated repository contains no hand-authored behavior.
`Cratis/AI.Distribution` is public but currently fixture-only. Its protected `main` branch demonstrates the generated boundary; it does not grant public installation or release status.
## Why Cratis does not propagate folders
[Section titled “Why Cratis does not propagate folders”](#why-cratis-does-not-propagate-folders)
The previous model copied shared AI folders across repositories. That creates three problems:
1. repositories silently drift to different corpus versions;
2. a target can accidentally become another distribution source;
3. rollback means reconstructing overwritten files instead of changing a pin.
The replacement is ordinary versioned distribution. One source produces one release manifest. Consumers install or pin that version through their host. Workflows can canary, update, disable, and roll it back without rewriting the project’s own facts.
## Project context remains project-owned
[Section titled “Project context remains project-owned”](#project-context-remains-project-owned)
Shared packages teach reusable Cratis concepts and workflows. They do not own a project’s architecture, environment names, commands, credentials, test fixtures, or product decisions.
The controlled context design uses `.cratis/PROJECT.md` as canonical project content, with minimal host bootstraps where a host cannot discover it directly. Some current repositories still use `.agents/PROJECT.md` during migration. A resolver chooses one; it never merges, overwrites, or deletes either file.
Uninstall must leave the project intact
Removing a shared AI package must remove shared capabilities only. It must not delete `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `.cratis/PROJECT.md`, legacy project context, or repository-local overrides.
## The release gates
[Section titled “The release gates”](#the-release-gates)
A public capability moves through separate gates:
| Gate | Evidence required |
| ----------------- | ------------------------------------------------------------------------------------------------- |
| Source authority | Owning product repository, immutable revision, owner, permission, claims, digest, and expiry |
| Target approval | Behavior, trigger, negative trigger, collision, security, originality, and portability evidence |
| Materialization | Exact file closure, native manifests, byte parity, checksums, and provenance |
| Package lifecycle | Pack, install, discovery, smoke, update, uninstall, and rollback |
| Canary | One approved consuming repository with observable version-bound results |
| Publication | Protected environment, machine identity, reviewer, immutable release, and vendor/npm approval |
| Retirement | Fleet visibility, rollback evidence, emergency disable, and proof the old topology cannot restart |
A green build at one gate never implies the next gate passed.
## Current state
[Section titled “Current state”](#current-state)
Cratis has fixture evidence for native generation, package lifecycle, checksums, local and hosted canary/rollback simulation, and a generated-only protected Git repository. The one-time initialization credential was removed after use.
Still blocked:
* no real public skill target or product-source contract is approved;
* no PR/release-capable distribution bot is provisioned;
* `@cratis/ai` package ownership and trusted publishing are not configured;
* no real consuming-repository canary has run;
* marketplace review and listing are not complete.
That boundary is deliberate. Follow the [ecosystem support matrix](/ai/ecosystems/) for status and [Using AI as a Cratis maintainer](/ai/cratis-maintainers/) for the current internal workflow.
# API reference
> Where to find the generated .NET and TypeScript API reference, and how it's produced.
The narrative documentation — guides, concepts, tutorials — is what you’re reading here. The **API reference** is the exhaustive, generated description of every public type and member, produced directly from the source so it never drifts.
## .NET / C\#
[Section titled “.NET / C#”](#net--c)
The .NET API reference is generated with **DocFX** from the XML documentation comments across the Chronicle client SDK, Arc, and Fundamentals assemblies.
**[Browse the .NET API reference →](/api/)** — every public type and member, organized per library (Chronicle clients, Arc + MongoDB, Fundamentals).
While you’re coding, the same XML docs power **IntelliSense** in your IDE — so the reference is right there as you type. On NuGet:
* [`Cratis.Chronicle`](https://www.nuget.org/packages/Cratis.Chronicle) — the Chronicle client SDK
* [`Cratis.Arc`](https://www.nuget.org/packages/Cratis.Arc) — the Arc application framework
## TypeScript
[Section titled “TypeScript”](#typescript)
The TypeScript API reference is generated with **TypeDoc** from the `@cratis/*` packages and surfaced alongside the rest of the site:
* [`@cratis/arc`](/api/arc/javascript/arc/) — the Arc client core
* [`@cratis/arc` React bindings](/api/arc/javascript/arc.react/) — hooks and components
* [`@cratis/arc` MVVM](/api/arc/javascript/arc.react.mvvm/) — the MVVM layer
* [`@cratis/arc` Vite plugin](/api/arc/javascript/arc.vite/) — build-time proxy generation
* [`@cratis/fundamentals`](/api/fundamentals/javascript/) — shared utilities and concepts
## How it’s produced (for contributors)
[Section titled “How it’s produced (for contributors)”](#how-its-produced-for-contributors)
We deliberately **combine tooling**: a modern site for the narrative docs, and the best generator for each kind of API reference. The reference is built in the documentation pipeline from the product source — there’s nothing to hand-maintain. See the documentation site’s `README.md` for the build details.
## When to reach for what
[Section titled “When to reach for what”](#when-to-reach-for-what)
* **Learning or solving a problem?** Stay in the guides, [tutorial](/chronicle/tutorial/), and [scenarios](/chronicle/scenarios/) — they explain the *why* and the *how*.
* **Looking up an exact signature?** The API reference and your IDE’s IntelliSense are the fastest path.
# Arc
> Commands, queries, validation, authorization, and TypeScript proxy generation for ASP.NET Core.
Arc
## CQRS application framework for ASP.NET Core
Arc is an opinionated CQRS application framework for ASP.NET Core with commands, queries, validation, authorization, and TypeScript proxy generation. Event sourcing is not required.
## Start here
[Section titled “Start here”](#start-here)
When commands, queries, validation, authorization, frontend contracts, and persistence each grow their own conventions, Arc gives them one application boundary. Start with current-state persistence or application services. Add Chronicle only where event sourcing fits, and add Components only where its React compositions fit.
[ Start an Arc host](#start-an-arc-host)
[Add the Arc package and configure an ASP.NET Core host.](#start-an-arc-host)
[ Commands and queries](#commands-and-queries)
[Execute changes, expose reads, and generate the client contracts for both.](#commands-and-queries)
[ Frontend contracts](#frontend-contracts)
[Use the generated TypeScript runtime directly or compose it with React.](#frontend-contracts)
[ Persistence choices](#persistence-choices)
[Keep current state in application services, MongoDB, or EF Core, or add Chronicle.](#persistence-choices)
[Why Arc ](/arc/why-arc/)The problem Arc solves, why it leans on CQRS and proxy generation, and how it keeps a full-stack app type-safe end to end.
[Coming from MediatR and MVC ](/arc/coming-from-mediatr-and-mvc/)Map controllers, DTOs, and MediatR handlers onto Arc's command and query model.
[Samples ](/samples/)Runnable event sourcing and CQRS samples for the Cratis stack, including full Arc applications.
## Start an Arc host
[Section titled “Start an Arc host”](#start-an-arc-host)
Use the .NET SDK declared by Arc’s current `global.json`.
1. Install the umbrella host package.
```bash
dotnet add package Cratis.Arc
```
2. Configure and run the host.
```csharp
using Cratis.Arc;
var builder = ArcApplication.CreateBuilder(args);
builder.AddCratisArc();
var app = builder.Build();
app.UseCratisArc();
await app.RunAsync();
```
3. Confirm the application starts on the selected SDK/package profile.
Starting the host verifies the setup path only. Commands, queries, generated proxies, persistence integrations, and frontend packages each have additional configuration and version requirements.
## Commands and queries
[Section titled “Commands and queries”](#commands-and-queries)
Arc supports model-bound records and methods as well as controller-based endpoints. The command and query pipelines handle dependency injection, validation, authorization, filters, execution scopes, result conversion, and HTTP exposure.
Queries can return one result or use Arc’s observable-query APIs. The TypeScript generator creates clients for recognized commands, queries, referenced types, validation rules, and identity details. See the detailed product documentation for the exact transport, subscription, and lifecycle behavior.
[Command pipeline ](/arc/backend/commands/)Model intentions, validate and authorize them, provide dependencies, execute handlers, and convert typed results into HTTP responses.
[Query pipeline ](/arc/backend/queries/)Expose purpose-shaped reads with paging, sorting, filtering, result rendering, and diagnostics.
[Observable-query APIs ](/arc/backend/queries/observable-query-demultiplexer/)Work with documented query subscriptions, collection changes, subscription guards, and query health information.
## Frontend contracts
[Section titled “Frontend contracts”](#frontend-contracts)
`@cratis/arc` supplies the TypeScript command, query, validation, identity, and messaging runtime used by generated proxies. `@cratis/arc.react` adds hooks, command forms, dialogs, identity composition, and query components. The optional React/MVVM package adds view models, dependency injection, route parameters, and browser abstractions.
[Explore Arc’s frontend packages](/arc/frontend/) to choose the TypeScript, React, or React/MVVM path. Components is separate: applications may use Arc without Components, and may use the generated TypeScript clients without Arc’s React packages.
## Identity and tenancy
[Section titled “Identity and tenancy”](#identity-and-tenancy)
Arc includes [pluggable identity and access behavior](/arc/understanding-identity-and-access/), identity-detail providers, server-side authorization and role checks, and frontend identity context. [Tenant resolution](/arc/backend/tenancy/) can use a header, query parameter, claim, subdomain, fixed value, or development setting before provider-specific database or namespace mapping.
Frontend visibility helpers do not replace server-side authorization. Tenant resolution also does not by itself establish isolation for an application or storage topology.
## Persistence choices
[Section titled “Persistence choices”](#persistence-choices)
Arc.Core does not depend on Chronicle. Commands and queries can use application services or current-state persistence without an event log.
[MongoDB ](/arc/backend/mongodb/)Use Arc's serialization conventions, tenant-aware database resolution, resilient collection access, and observable change streams.
[Entity Framework Core ](/arc/backend/entity-framework/)Discover read models, configure value conversions and mappings, run migrations, and observe supported SQL Server, PostgreSQL, or SQLite changes.
[Chronicle ](/arc/backend/chronicle/)Add event-sourced commands, projections, reducers, aggregates, reactors, concurrency behavior, tenant namespaces, and Chronicle-specific testing.
## Inspection, contracts, and testing
[Section titled “Inspection, contracts, and testing”](#inspection-contracts-and-testing)
Arc can describe registered commands, queries, identity schemas, and OpenAPI operations at runtime. Repository packages also include analyzers, source generators, command-scenario testing, proxy-generation checks, Vite helpers, and Arc-specific ESLint rules.
* [Inspect registered application contracts](/arc/backend/introspection/)
* [Generate OpenAPI descriptions](/arc/backend/open-api/)
* [Test command behavior](/arc/backend/testing/)
* [Review analyzer rules](/arc/backend/code-analysis/)
Check the combination you use
These are separate packages and integration surfaces. Their existence does not establish compatibility with every frontend, runtime, persistence provider, browser, or product version. Check the package manifests and the evidence for the combination you evaluate.
## Continue
[Section titled “Continue”](#continue)
[ Arc source and README](https://github.com/Cratis/Arc)
[Repository structure, package roles, contribution guidance, and current limits.](https://github.com/Cratis/Arc)
[ Arc packages](https://www.nuget.org/packages/Cratis.Arc)
[Published .NET package metadata and versions.](https://www.nuget.org/packages/Cratis.Arc)
[ Chronicle](/chronicle/)
[The optional event-sourced runtime integration for Arc application behavior.](/chronicle/)
[ Components](/components/)
[Optional React components aligned with Arc application patterns.](/components/)
# CQRS without event sourcing
> Arc's CQRS model can run over MongoDB or EF Core without Chronicle. This page shows the boundary between CQRS and event sourcing, not an argument against event sourcing.
It’s easy to assume Arc and [Chronicle](/chronicle/) are a package deal, because they work well together. We think event sourcing is the default architecture for information systems, and Chronicle is the Cratis event-sourcing platform. But Arc itself is CQRS: **commands, queries, and the generated C# → TypeScript proxies that keep your React frontend in lockstep with your backend.** Where the data actually lives is a separate decision.
This page shows Arc on its own — the same typed full-stack experience, backed by a plain database instead of an event log — so the line between CQRS and event sourcing is explicit. CQRS and event sourcing fit naturally together, but neither depends on the other.
## The line between Arc and Chronicle
[Section titled “The line between Arc and Chronicle”](#the-line-between-arc-and-chronicle)
Arc is a layer that can sit *on top of* Chronicle; Chronicle never depends on Arc. That direction is the whole point — it’s why a bounded current-state slice can keep everything Arc gives you without storing events. In the backend docs, Chronicle, [MongoDB](/arc/backend/mongodb/), and [Entity Framework](/arc/backend/entity-framework/) are integrations: each gives commands and queries somewhere to read and write, while Chronicle adds the event-sourced backbone.
```
Arc.Core — commands · queries · generated proxies
MongoDB
EF Core / SQL
Chronicle — event sourcing
```
Pick MongoDB or EF Core and you have a complete, fully-typed CQRS app without an event log. Pick Chronicle and the same Arc boundary records facts, builds projections, and keeps history.
## A standalone slice, end to end
[Section titled “A standalone slice, end to end”](#a-standalone-slice-end-to-end)
Here’s the whole thing — register an author, and list authors live — with the data stored straight in a MongoDB collection.
**The read model is just a document.** Mark it `[ReadModel]` and Arc exposes its query methods. A static method *is* the query, and returning an `ISubject<>` makes it live:
```csharp
[ReadModel]
public record Author(AuthorId Id, AuthorName Name)
{
// This static method is the query — served over HTTP, and live.
public static ISubject> AllAuthors(IMongoCollection authors) =>
authors.Observe();
}
```
**The command writes the document directly.** Inject the collection and insert — `Handle()` returns nothing, because there’s no event to record:
```csharp
[Command]
public record RegisterAuthor(AuthorId Id, AuthorName Name)
{
public Task Handle(IMongoCollection authors) =>
authors.InsertOneAsync(new Author(Id, Name));
}
```
**A command can take the read model it acts on.** Mark the property holding the key and Arc loads the document, so the command decides against current state instead of fetching it first:
```csharp
using System.ComponentModel.DataAnnotations;
[Command]
public record RenameAuthor([property: Key] AuthorId Id, AuthorName NewName)
{
public Task Handle(Author author, IMongoCollection authors) =>
authors.ReplaceOneAsync(_ => _.Id == author.Id, author with { Name = NewName });
}
```
This works the same for a read model held in MongoDB and one carried by an Entity Framework `ReadOnlyDbContext`. [Read models in commands](/arc/backend/chronicle/read-models/injecting-into-commands/) covers what a nullable parameter means, and [Read models from other providers](/arc/backend/chronicle/read-models/other-providers/#declaring-the-key-without-chronicle) how to declare the key when it is not a single property.
That’s the backend. Register MongoDB once at startup:
```csharp
var builder = WebApplication.CreateBuilder(args);
builder.AddCratisArc();
builder.UseCratisMongoDB();
var app = builder.Build();
app.UseCratisArc();
app.Run();
```
Build, and Arc generates the typed proxies for `RegisterAuthor` and `AllAuthors` exactly as it would for an event-sourced slice. The React side is unchanged from any other Arc app:
```tsx
const [authors] = AllAuthors.use(); // live — re-renders when the collection changes
i.name} title="Name" />
```
Note
`AllAuthors` is live with no event sourcing involved. `IMongoCollection.Observe()` watches MongoDB’s change stream, so the moment the command inserts a document, every subscribed browser re-renders. Entity Framework gets the same treatment through [observed DbSets](/arc/backend/entity-framework/observing/).
## Test the slice the same way
[Section titled “Test the slice the same way”](#test-the-slice-the-same-way)
Arc’s command testing does not depend on Chronicle either. Start with `Cratis.Arc.Testing`, drive the command through a `CommandScenario`, and assert the `CommandResult` exactly as you would in an event-sourced slice. The only difference is what you assert after the command runs: a current-state slice checks the database or application service it wrote to, while a Chronicle-backed slice can also assert the appended events.
That keeps the CQRS boundary testable before you decide whether the slice needs an event log. See [Arc testing](/arc/backend/testing/) and [command scenarios](/arc/backend/testing/command-scenario/) for the base testing loop; add the [Chronicle testing extension](/arc/backend/testing/chronicle/) only when the command appends events.
## What actually changes when you add Chronicle
[Section titled “What actually changes when you add Chronicle”](#what-actually-changes-when-you-add-chronicle)
Set this slice next to the same slice with [Chronicle added later](/arc/backend/chronicle/add-event-sourcing/). The query and the React are **identical**. The only thing that differs is the command’s write path:
| | Standalone (this page) | With Chronicle |
| ------------------------- | ---------------------- | ---------------------------------- |
| What `Handle()` does | inserts a document | appends an event |
| What fills the read model | the command, directly | a projection over the event |
| What you can read | current state | current state **and full history** |
So adopting Chronicle later is a *write-side* change. Your queries, your generated proxies, and your screens don’t move.
## The trade-off
[Section titled “The trade-off”](#the-trade-off)
Storing current state directly is simpler for bounded CRUD surfaces, reference data, settings, and adoption steps. What you don’t get is everything an event log buys you: an audit trail, the ability to rebuild a read model a brand-new way from history, temporal queries, and reactors that fire on facts. For information systems, we prefer starting with Chronicle because those needs show up often. [Why Event Sourcing](/chronicle/why-event-sourcing/) explains the default.
The reassuring part, from the table above: the boundary stays clean. If a direct-database slice later belongs in the event-sourced model, move the write side to Chronicle — the read side and the entire frontend come along unchanged. [Adopting Cratis](/adopting-cratis/) walks through doing exactly that, one step at a time.
## Go deeper
[Section titled “Go deeper”](#go-deeper)
* [MongoDB integration](/arc/backend/mongodb/) — setup, serializers, class mapping, and [observing collections](/arc/backend/mongodb/observing-collections/) for live queries.
* [Entity Framework integration](/arc/backend/entity-framework/getting-started/) — DbContexts, read-only contexts, and [observing DbSets](/arc/backend/entity-framework/observing/).
* [Commands](/arc/backend/commands/) and [Queries](/arc/backend/queries/) — the full model-bound and controller-based reference.
* [Why Arc](/arc/why-arc/) — the problem Arc solves, and how CQRS relates to event sourcing.
# Backend
The backend is where you express *what your application does* — the commands that change state and the queries that read it. Arc’s job is to make that expression the only thing you write: you define a command or query as a plain record, and Arc handles the HTTP endpoint, validation, authorization, and a typed TypeScript proxy for the frontend. CQRS without the ceremony. In the full Cratis loop those commands append events through Chronicle; in bounded current-state slices they can write MongoDB or EF Core directly.
```
Arc discovers
proxy generator
commands write
queries read
Command / query record
HTTP endpoint
Typed TS proxy
MongoDB / EF Core
Read models
```
## Start here
[Section titled “Start here”](#start-here)
New to the backend? Walk through [Getting started](/arc/backend/getting-started/) to build your first command and query end to end. Then the two pillars:
* [Commands](/arc/backend/commands/) — intents that change state through `Handle()`.
* [Queries](/arc/backend/queries/) — reads, exposed to the frontend as typed proxies (including live, observable ones).
The magic that ties it to the frontend is [Proxy generation](/arc/backend/proxy-generation/) — read it early; it’s why the whole stack is type-safe.
## Integrations
[Section titled “Integrations”](#integrations)
Arc meets the rest of your stack:
| Topic | What it covers |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| [MongoDB](/arc/backend/mongodb/) | Document storage for read models and other data. |
| [Entity Framework](/arc/backend/entity-framework/) | EF Core integration for relational read models. |
| [Chronicle](/arc/backend/chronicle/) | Event sourcing integration — append events from commands, build read models, feed state back into business rules. |
| [ASP.NET Core](/arc/backend/asp-net-core/) | How Arc plugs into the ASP.NET Core pipeline. |
## Cross-cutting
[Section titled “Cross-cutting”](#cross-cutting)
| Topic | What it covers |
| -------------------------------------------- | --------------------------------------------------------------- |
| [Core](/arc/backend/core/) | Commands, queries, and dependency injection at the lower level. |
| [Identity](/arc/backend/identity/) | Who the user is — authentication and identity details. |
| [Tenancy](/arc/backend/tenancy/) | Multi-tenant isolation. |
| [Open API](/arc/backend/open-api/) | OpenAPI/Swagger generation. |
| [Code Analysis](/arc/backend/code-analysis/) | Analyzers and fixers that catch mistakes at compile time. |
Building the UI on top? Head to the [frontend](/arc/frontend/), which consumes everything here through the generated proxies.
# ASP.NET Core Integration
The Arc provides enhanced capabilities for ASP.NET Core applications, building upon the core Arc features with web-specific functionality. This integration offers powerful tools for API development including advanced model binding, validation, authorization, and automatic API documentation.
## Features
[Section titled “Features”](#features)
* **[Configuration](/arc/backend/asp-net-core/configuration/)** - Configure Arc through appsettings.json or programmatically
* **[Authorization](/arc/backend/asp-net-core/authorization/)** - Enhanced authorization with role-based access and policy support
* **[Microsoft Identity](/arc/backend/asp-net-core/microsoft-identity/)** - Integration with Microsoft Client Principal for Azure services
* **[FromRequest Attribute](/arc/backend/asp-net-core/from-request/)** - Advanced model binding combining multiple HTTP request sources
* **[Swagger](/arc/backend/asp-net-core/swagger/)** - Enhanced OpenAPI documentation with Arc-specific schema generation
* **[Validation](/arc/backend/asp-net-core/validation/)** - Comprehensive validation with FluentValidation support
* **[Without Wrappers](/arc/backend/asp-net-core/without-wrappers/)** - Control response wrapping behavior for specific endpoints
* **[Invariant Culture](/arc/backend/asp-net-core/invariant-culture/)** - Guarantee consistent culture-sensitive behavior across all environments
## When to Use ASP.NET Core Integration
[Section titled “When to Use ASP.NET Core Integration”](#when-to-use-aspnet-core-integration)
Use the ASP.NET Core integration when you need:
* Full web framework capabilities (Kestrel, middleware pipeline, static files)
* Razor views or MVC features
* Swagger UI for API documentation
* Advanced middleware scenarios
* Maximum HTTP throughput with Kestrel
* Traditional web application patterns
## When to Use Arc.Core Instead
[Section titled “When to Use Arc.Core Instead”](#when-to-use-arccore-instead)
Consider using [Arc.Core](/arc/backend/core/overview/) (without ASP.NET Core) when you need:
* Minimal dependencies and smaller binary size
* Faster startup times
* Lower memory footprint
* Native AOT compilation support
* Console applications or background services
* Scenarios where full web framework is unnecessary
## See Also
[Section titled “See Also”](#see-also)
* [Arc.Core Overview](/arc/backend/core/overview/) - Lightweight alternative without ASP.NET Core
* [Commands](/arc/backend/commands/) - Command handling patterns
* [Queries](/arc/backend/queries/) - Query patterns and conventions
* [Tenancy](/arc/backend/tenancy/) - Tenant isolation and management
# Authorization
The Arc provides enhanced authorization capabilities that build upon ASP.NET Core’s built-in authorization system. It offers role-based authorization through specialized attributes and integrates authorization state into command and query results across controllers, model-bound commands, and queries.
## Setup
[Section titled “Setup”](#setup)
Ensure that authentication and authorization are enabled in your application pipeline:
```csharp
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
```
> Note: If you’re interested in leveraging the Microsoft Identity way of working with identity, read more about [Microsoft Identity integration](/arc/backend/asp-net-core/microsoft-identity/)
## Protecting All Endpoints by Default
[Section titled “Protecting All Endpoints by Default”](#protecting-all-endpoints-by-default)
By default, ASP.NET Core endpoints are accessible to anonymous users unless explicitly protected with authorization attributes. You can change this behavior to require authentication for all endpoints by setting a fallback authorization policy.
### Using Fallback Policy
[Section titled “Using Fallback Policy”](#using-fallback-policy)
The fallback policy applies to all endpoints that don’t have an explicit authorization policy:
```csharp
builder.Services.AddAuthorizationBuilder()
.SetFallbackPolicy(new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build());
```
With this configuration:
* **All endpoints require authentication by default** - No anonymous access unless explicitly allowed
* **Use `[AllowAnonymous]`** to opt specific endpoints out of the requirement
* **Explicit `[Authorize]` attributes still work** - They override the fallback policy with their own requirements
> **Note**: Fallback policies are a standard ASP.NET Core authorization feature. For more details on authorization policies, policy requirements, and advanced scenarios, refer to the [ASP.NET Core authorization documentation](https://learn.microsoft.com/aspnet/core/security/authorization/policies).
### Allowing Anonymous Access with Fallback Policy
[Section titled “Allowing Anonymous Access with Fallback Policy”](#allowing-anonymous-access-with-fallback-policy)
When using a fallback policy, use `[AllowAnonymous]` to make specific endpoints publicly accessible:
```csharp
// This command requires authentication (from fallback policy)
[Command]
public record ProcessOrder(OrderId Id)
{
public void Handle(IOrderService orders) => orders.Process(Id);
}
// This command is publicly accessible despite the fallback policy
[Command]
[AllowAnonymous]
public record GetPublicCatalog()
{
public Catalog Handle(ICatalogService catalog) => catalog.GetPublic();
}
```
### AllowAnonymous Inheritance
[Section titled “AllowAnonymous Inheritance”](#allowanonymous-inheritance)
The `[AllowAnonymous]` attribute can be applied at different levels and follows specific inheritance rules:
| Scenario | Result |
| -------------------------------------------------------- | ---------------------------------------------- |
| `[AllowAnonymous]` on type | All methods inherit anonymous access |
| `[AllowAnonymous]` on method | Method allows anonymous access |
| `[Authorize]` on method with `[AllowAnonymous]` on type | Method requires authorization (overrides type) |
| Both `[AllowAnonymous]` and `[Authorize]` on same member | Error - throws `AmbiguousAuthorizationLevel` |
```csharp
// Type-level AllowAnonymous - all methods allow anonymous access
[AllowAnonymous]
public record PublicQueries
{
public static IEnumerable GetProducts() => /* ... */;
public static IEnumerable GetCategories() => /* ... */;
}
// Method-level authorization overrides type-level AllowAnonymous
[AllowAnonymous]
public record MixedQueries
{
// Inherits [AllowAnonymous] from type
public static IEnumerable GetPublicProducts() => /* ... */;
// Requires authorization despite type having [AllowAnonymous]
[Authorize]
public static IEnumerable GetInternalProducts() => /* ... */;
}
// ERROR: This will throw AmbiguousAuthorizationLevel at startup
[AllowAnonymous]
[Authorize] // Cannot have both on the same member!
public record InvalidCommand
{
public void Handle() { }
}
```
> **Warning**: Applying both `[AllowAnonymous]` and `[Authorize]` to the same type or method will result in an `AmbiguousAuthorizationLevel` exception. This prevents accidental security misconfigurations.
### Custom Fallback Policies
[Section titled “Custom Fallback Policies”](#custom-fallback-policies)
You can create more specific fallback policies with custom requirements:
```csharp
// Require a specific role for all endpoints by default
builder.Services.AddAuthorizationBuilder()
.SetFallbackPolicy(new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.RequireRole("User")
.Build());
```
Or create a named policy and set it as the fallback:
```csharp
builder.Services.AddAuthorizationBuilder()
.AddPolicy("RequireUserRole", policy => policy
.RequireAuthenticatedUser()
.RequireRole("User"))
.SetFallbackPolicy(new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build());
```
### Default Policy vs Fallback Policy
[Section titled “Default Policy vs Fallback Policy”](#default-policy-vs-fallback-policy)
ASP.NET Core distinguishes between two policies:
| Policy | Description |
| ------------------- | --------------------------------------------------------- |
| **Default Policy** | Applied when `[Authorize]` is used without parameters |
| **Fallback Policy** | Applied to endpoints without any authorization attributes |
```csharp
builder.Services.AddAuthorizationBuilder()
// Default policy: what [Authorize] means
.SetDefaultPolicy(new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build())
// Fallback policy: applied when no [Authorize] attribute is present
.SetFallbackPolicy(new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build());
```
> **Recommendation**: For most secure applications, set a fallback policy that requires authentication. This follows the principle of “secure by default” - developers must explicitly opt-in to anonymous access rather than accidentally leaving endpoints unprotected.
## Role-Based Authorization
[Section titled “Role-Based Authorization”](#role-based-authorization)
The Arc provides two convenient ways to implement role-based authorization:
1. **Standard ASP.NET Core `[Authorize]` attribute** - Works with all scenarios
2. **Convenient `[Roles]` attribute** - Simplifies multi-role scenarios with cleaner syntax
The `RolesAttribute` is a wrapper around ASP.NET Core’s `AuthorizeAttribute` that eliminates the need to manually format role strings. Instead of writing `[Authorize(Roles = "Admin,Manager")]`, you can use the more readable `[Roles("Admin", "Manager")]`.
### Using the Authorize Attribute
[Section titled “Using the Authorize Attribute”](#using-the-authorize-attribute)
Standard ASP.NET Core authorization works across all scenarios:
```csharp
using Microsoft.AspNetCore.Authorization;
// Single role
[Authorize(Roles = "Admin")]
public class AdminController : ControllerBase { }
// Multiple roles (user needs at least one)
[Authorize(Roles = "Admin,Manager")]
public record DeleteUser(string UserId);
```
### Using the Roles Attribute for Controllers
[Section titled “Using the Roles Attribute for Controllers”](#using-the-roles-attribute-for-controllers)
The `RolesAttribute` provides cleaner syntax for multiple roles:
```csharp
using Cratis.Arc.Authorization;
// Equivalent to [Authorize(Roles = "Admin,Manager")]
[Roles("Admin", "Manager")]
public class UserManagementController : ControllerBase
{
[HttpPost("create")]
public async Task CreateUser(CreateUserCommand command)
{
// Only users with "Admin" or "Manager" roles can access this endpoint
// ...
}
[HttpDelete("{id}")]
[Roles("Admin")] // Override controller-level roles for specific actions
public async Task DeleteUser(string id)
{
// Only users with "Admin" role can delete users
// ...
}
}
```
Users must have at least one of the specified roles to access the resource.
## Authorization in Controllers
[Section titled “Authorization in Controllers”](#authorization-in-controllers)
### Controller-Level Authorization
[Section titled “Controller-Level Authorization”](#controller-level-authorization)
Apply authorization to an entire controller to protect all actions:
```csharp
[Roles("Admin")]
public class AdminController : ControllerBase
{
// All actions in this controller require "Admin" role
}
```
### Action-Level Authorization
[Section titled “Action-Level Authorization”](#action-level-authorization)
Apply authorization to specific actions for fine-grained control:
```csharp
public class ProductController : ControllerBase
{
[HttpGet]
public async Task GetProducts()
{
// No authorization required - public endpoint
}
[HttpPost]
[Roles("Editor", "Admin")]
public async Task CreateProduct(CreateProductCommand command)
{
// Requires "Editor" or "Admin" role
}
[HttpDelete("{id}")]
[Roles("Admin")]
public async Task DeleteProduct(string id)
{
// Requires "Admin" role only
}
}
```
### Overriding Controller-Level Authorization
[Section titled “Overriding Controller-Level Authorization”](#overriding-controller-level-authorization)
Action-level authorization overrides controller-level settings:
```csharp
[Route("api/management")]
[Roles("Manager")]
public class ManagementController : ControllerBase
{
[HttpGet("reports")]
public async Task GetReports()
{
// Requires "Manager" role (from controller)
}
[HttpGet("sensitive-data")]
[Roles("Admin")] // Overrides controller-level authorization
public async Task GetSensitiveData()
{
// Requires "Admin" role only, not "Manager"
}
}
```
## Authorization in Model-Bound Commands
[Section titled “Authorization in Model-Bound Commands”](#authorization-in-model-bound-commands)
Model-bound commands support authorization through both standard ASP.NET Core authorization attributes and the convenient `[Roles]` attribute.
### Using Standard Authorization
[Section titled “Using Standard Authorization”](#using-standard-authorization)
```csharp
[Command]
[Authorize]
public record DeleteUser(string UserId)
{
public void Handle(IUserService userService)
{
userService.DeleteUser(UserId);
}
}
```
For role-based authorization with the standard attribute:
```csharp
[Command]
[Authorize(Roles = "Admin,Manager")]
public record ApproveRequest(int RequestId)
{
public void Handle(IRequestService requestService)
{
requestService.ApproveRequest(RequestId);
}
}
```
### Using the Roles Attribute for Commands
[Section titled “Using the Roles Attribute for Commands”](#using-the-roles-attribute-for-commands)
The `[Roles]` attribute provides cleaner syntax for model-bound commands:
```csharp
[Command]
[Roles("Admin", "Manager")]
public record ApproveRequest(int RequestId)
{
public void Handle(IRequestService requestService)
{
requestService.ApproveRequest(RequestId);
}
}
[Command]
[Roles("System", "Admin")]
public record CreateUser(
string Name,
string Email,
int Age)
{
public void Handle(IUserService userService)
{
// Command implementation
}
}
```
### Authorization Results for Commands
[Section titled “Authorization Results for Commands”](#authorization-results-for-commands)
When authorization fails, the command pipeline automatically returns an unauthorized result. The command’s `Handle()` method will not be executed:
```csharp
public class Users(ICommandPipeline commandPipeline)
{
public async Task DeleteUser(string user)
{
var result = await commandPipeline.Execute(new DeleteUserCommand(user));
if (!result.IsAuthorized)
{
// Handle unauthorized access - command was not executed
}
if (result.IsSuccess)
{
// Command executed successfully
}
}
}
```
## Authorization in Model-Bound Queries
[Section titled “Authorization in Model-Bound Queries”](#authorization-in-model-bound-queries)
Queries also support both authorization approaches for data protection:
### Using Standard Authorization for Queries
[Section titled “Using Standard Authorization for Queries”](#using-standard-authorization-for-queries)
```csharp
[ReadModel]
[Authorize(Roles = "Admin,Manager")]
public record UserAuditLog(string UserId, DateTime Occurred, string Action)
{
public static IEnumerable GetUserAuditLog(
IMongoCollection collection,
string userId,
DateTime fromDate,
DateTime toDate) =>
collection.Find(entry =>
entry.UserId == userId &&
entry.Occurred >= fromDate &&
entry.Occurred <= toDate).ToList();
}
```
### Using the Roles Attribute for Queries
[Section titled “Using the Roles Attribute for Queries”](#using-the-roles-attribute-for-queries)
```csharp
[ReadModel]
[Roles("Manager", "Admin", "Auditor")]
public record UserAuditLog(string UserId, DateTime Occurred, string Action)
{
public static IEnumerable GetUserAuditLog(
IMongoCollection collection,
string userId,
DateTime fromDate,
DateTime toDate) =>
collection.Find(entry =>
entry.UserId == userId &&
entry.Occurred >= fromDate &&
entry.Occurred <= toDate).ToList();
}
[ReadModel]
[Roles("Viewer", "Editor", "Admin")]
public record ProductDetails(string ProductId, string Name, decimal Price)
{
public static ProductDetails? GetProductDetails(
IMongoCollection collection,
string productId) =>
collection.Find(product => product.ProductId == productId).FirstOrDefault();
}
```
### Authorization Results for Queries
[Section titled “Authorization Results for Queries”](#authorization-results-for-queries)
Model-bound queries are invoked by calling their static method directly. The authorization attributes are enforced by the query pipeline before the method runs, so a caller that lacks the required roles never reaches the query logic:
```csharp
var auditLog = UserAuditLog.GetUserAuditLog(
collection,
"user123",
DateTime.Now.AddDays(-30),
DateTime.Now);
// Use the returned data
foreach (var entry in auditLog)
{
// Process each audit log entry
}
```
## Authorization Integration
[Section titled “Authorization Integration”](#authorization-integration)
The Arc integrates authorization state into command and query results, allowing you to handle authorization failures gracefully.
## Policy-Based Authorization
[Section titled “Policy-Based Authorization”](#policy-based-authorization)
For more complex authorization scenarios, you can use standard ASP.NET Core policy-based authorization alongside the Arc:
```csharp
[Command]
[Authorize(Policy = "RequireAdminOrOwner")]
public record UpdateResource(string ResourceId, ResourceData Data)
{
public void Handle()
{
// Custom policy can check multiple claims, roles, and requirements
}
}
```
You can define custom authorization policies in your service configuration:
```csharp
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdminOrOwner", policy =>
policy.RequireAssertion(context =>
context.User.IsInRole("Admin") ||
context.User.HasClaim("resource", "owner")));
});
```
## Custom Authorization
[Section titled “Custom Authorization”](#custom-authorization)
### Authorization Filters
[Section titled “Authorization Filters”](#authorization-filters)
The Arc includes authorization filters that integrate with the command and query pipeline:
```csharp
// Custom authorization logic can be implemented through command filters
public class CustomAuthorizationFilter : ICommandFilter
{
public Task OnExecution(CommandContext context)
{
// Custom authorization logic
if (!IsAuthorized(context))
{
return Task.FromResult(CommandResult.Error(context.CorrelationId, "Unauthorized"));
}
return Task.FromResult(CommandResult.Success(context.CorrelationId));
}
}
```
For queries, you can implement custom authorization through query filters:
```csharp
public class QueryAuthorizationFilter : IQueryFilter
{
public Task OnPerform(QueryContext context)
{
// Custom authorization logic for queries
if (!IsAuthorized(context))
{
return Task.FromResult(QueryResult.Unauthorized(context.CorrelationId));
}
return Task.FromResult(QueryResult.Success(context.CorrelationId));
}
}
```
## Built-in Authorization Filter
[Section titled “Built-in Authorization Filter”](#built-in-authorization-filter)
The Arc provides a built-in `AuthorizationFilter` that automatically handles both `[Authorize]` and `[Roles]` attributes for commands and queries:
* **Authentication**: Verifies user is authenticated
* **Role-based authorization**: Checks required roles if specified
* **Policy-based authorization**: Evaluates custom policies
* **Automatic result handling**: Returns appropriate unauthorized results
This filter is automatically registered and executes before command handlers and query renderers.
## Best Practices
[Section titled “Best Practices”](#best-practices)
### Role Naming
[Section titled “Role Naming”](#role-naming)
* Use descriptive role names that reflect business functions (e.g., “AccountManager”, “ContentEditor”)
* Avoid generic names like “User1”, “Level2”
* Consider using a consistent naming convention across your application
### Granular Permissions
[Section titled “Granular Permissions”](#granular-permissions)
* Apply authorization at the appropriate level (controller vs. action vs. command/query)
* Use action-level and command/query-level authorization for fine-grained control
* Consider the principle of least privilege
### Error Handling
[Section titled “Error Handling”](#error-handling)
* Always check authorization status in your command/query results
* Provide meaningful error messages while avoiding information disclosure
* Log authorization failures for security monitoring
### Authorization Architecture
[Section titled “Authorization Architecture”](#authorization-architecture)
* Use controller-level authorization for protecting entire API surfaces
* Use model-bound command/query authorization for business logic protection
* Combine both approaches when you need different authorization rules for different access patterns
## Integration with Identity
[Section titled “Integration with Identity”](#integration-with-identity)
Authorization works seamlessly with the [Identity](/arc/backend/identity/) system. User roles are automatically extracted from the identity token and made available for authorization decisions. The identity provider context includes role information that can be used for authorization:
```csharp
public class IdentityDetailsProvider : IProvideIdentityDetails
{
public Task Provide(IdentityProviderContext context)
{
var userRoles = context.Claims
.Where(c => c.Key == ClaimTypes.Role)
.Select(c => c.Value)
.ToList();
var isAuthorized = userRoles.Contains("Admin") || userRoles.Contains("User");
return Task.FromResult(new IdentityDetails(isAuthorized, new { Roles = userRoles }));
}
}
```
## Frontend Integration
[Section titled “Frontend Integration”](#frontend-integration)
Authorization attributes work seamlessly with the [proxy generator](/arc/backend/proxy-generation/), which automatically creates TypeScript proxies for your commands and queries. The generated proxies provide:
* Authorization status handling in command and query results
* Consistent error handling for unauthorized access
* Integration with frontend authentication systems
* Type-safe authorization checking
## See Also
[Section titled “See Also”](#see-also)
* [Commands](/arc/backend/commands/) - Command documentation including authorization
* [Model-Bound Commands](/arc/backend/commands/model-bound/) - Model-bound command authorization
* [Queries](/arc/backend/queries/) - Query documentation
* [Command Filters](/arc/backend/commands/command-filters/) - Including the AuthorizationFilter
* [Identity](/arc/backend/identity/) - Identity and authentication setup
* [Microsoft Identity](/arc/backend/asp-net-core/microsoft-identity/) - Microsoft Identity integration
# Configuration
Cratis Arc can be configured both through `appsettings.json` and programmatically to customize its behavior. The main configuration is handled through the `ArcOptions` class.
This page covers the ASP.NET Core host specifically. For the lightweight Arc.Core host and its listen URL configuration, see [Arc.Core Getting Started](/arc/backend/core/getting-started/).
## Default Configuration Section
[Section titled “Default Configuration Section”](#default-configuration-section)
By default, Arc looks for configuration under the `Cratis:Arc` section in your `appsettings.json` file. The same keys can be supplied through environment variables — .NET maps the `__` separator onto nested keys, so `Cratis:Arc:GeneratedApis:RoutePrefix` becomes `Cratis__Arc__GeneratedApis__RoutePrefix`.
## Configuration Options
[Section titled “Configuration Options”](#configuration-options)
### Configuration example
[Section titled “Configuration example”](#configuration-example)
Here’s an example covering the most common options, bound from `appsettings.json` under `Cratis:Arc`:
```json
{
"Cratis": {
"Arc": {
"CorrelationId": {
"HttpHeader": "X-Correlation-ID"
},
"Tenancy": {
"ResolverType": "Header",
"HttpHeader": "x-cratis-tenant-id"
},
"GeneratedApis": {
"RoutePrefix": "api",
"SegmentsToSkipForRoute": 0,
"IncludeCommandNameInRoute": true,
"IncludeQueryNameInRoute": true
},
"Query": {
"KeepAliveInterval": "00:00:30"
}
}
}
}
```
Note
`ArcOptions.Hosting` (the listen URL) applies only to **Arc.Core** hosts — in an ASP.NET Core app the URL comes from Kestrel and `launchSettings.json`, not from Arc. For the Arc.Core hosting shape, see [Arc.Core Getting Started](/arc/backend/core/getting-started/).
### Configuration Properties
[Section titled “Configuration Properties”](#configuration-properties)
#### CorrelationId
[Section titled “CorrelationId”](#correlationid)
Controls how correlation IDs are handled in HTTP requests.
* **HttpHeader** (string, default: `"X-Correlation-ID"`): The HTTP header name to use for correlation ID tracking.
#### Tenancy
[Section titled “Tenancy”](#tenancy)
Controls how the active tenant is resolved on each request.
* **ResolverType** (`TenantResolverType`, default: `Header`): How to resolve the tenant — `Header`, `Query`, `Claim`, `Subdomain`, `Development`, or `Fixed`.
* **HttpHeader** (string, default: `"x-cratis-tenant-id"`): The HTTP header used when `ResolverType` is `Header`, and the fallback header when it is `Subdomain`.
* **QueryParameter** (string, default: `"tenantId"`): The query-string parameter used when `ResolverType` is `Query`.
* **ClaimType** (string, default: `"tenant_id"`): The claim used when `ResolverType` is `Claim`.
* **FixedTenantId** (string, default: `"development"`): The tenant every request resolves to when `ResolverType` is `Fixed` or `Development`. Prefer `Fixed` for a single-tenant production deployment — `Development` resolves identically but is named for an environment it does not check.
* **DevelopmentTenantId** (string, default: `"development"`): The same value under its original name — reading or writing either key sets both. Supply only one; if both are present the binder’s property order decides.
#### GeneratedApis
[Section titled “GeneratedApis”](#generatedapis)
Controls how automatically generated API endpoints are configured for commands and queries.
* **RoutePrefix** (string, default: `"api"`): The base route prefix for all generated API endpoints.
* **SegmentsToSkipForRoute** (int, default: `0`): Number of namespace segments to skip when constructing routes from type namespaces.
* **IncludeCommandNameInRoute** (bool, default: `true`): Whether to include the command type name as the last segment of the route for command endpoints.
* **IncludeQueryNameInRoute** (bool, default: `true`): Whether to include the query type name as the last segment of the route for query endpoints.
#### Query
[Section titled “Query”](#query)
Controls observable (real-time) queries.
* **KeepAliveInterval** (`TimeSpan`, default: `00:00:30`): How often a keep-alive frame is sent on an open observable-query connection.
#### IdentityDetailsProvider
[Section titled “IdentityDetailsProvider”](#identitydetailsprovider)
* **IdentityDetailsProvider** (Type, default: `null`): Specifies a custom identity details provider type. If not specified, the system will use type discovery to find one automatically.
## Setup Methods
[Section titled “Setup Methods”](#setup-methods)
The ASP.NET Core host bootstraps with `WebApplication.CreateBuilder`, registers Arc with `AddCratisArc`, and activates it with `UseCratisArc`.
### Using Configuration File
[Section titled “Using Configuration File”](#using-configuration-file)
The most common approach is to use the configuration file with the default section:
```csharp
var builder = WebApplication.CreateBuilder(args);
// Bind the default configuration section (Cratis:Arc)
builder.AddCratisArc();
var app = builder.Build();
app.UseCratisArc();
app.Run();
```
### Using Custom Configuration Section
[Section titled “Using Custom Configuration Section”](#using-custom-configuration-section)
You can specify a custom configuration section path:
```csharp
var builder = WebApplication.CreateBuilder(args);
// Bind a custom configuration section
builder.AddCratisArc(configSectionPath: "MyApp:CratisConfig");
var app = builder.Build();
app.UseCratisArc();
app.Run();
```
### Programmatic Configuration
[Section titled “Programmatic Configuration”](#programmatic-configuration)
You can configure the options through code with the `configureOptions` callback:
```csharp
var builder = WebApplication.CreateBuilder(args);
builder.AddCratisArc(options =>
{
// Configure correlation ID
options.CorrelationId.HttpHeader = "X-My-Correlation-ID";
// Configure tenancy
options.Tenancy.HttpHeader = "X-Custom-Tenant";
// Configure generated APIs
options.GeneratedApis.RoutePrefix = "myapi";
options.GeneratedApis.SegmentsToSkipForRoute = 2;
options.GeneratedApis.IncludeCommandNameInRoute = false;
options.GeneratedApis.IncludeQueryNameInRoute = false;
// Set a custom identity details provider
options.IdentityDetailsProvider = typeof(MyCustomIdentityDetailsProvider);
});
var app = builder.Build();
app.UseCratisArc();
app.Run();
```
### Hybrid Configuration
[Section titled “Hybrid Configuration”](#hybrid-configuration)
`AddCratisArc` binds `appsettings.json` first and then applies the `configureOptions` callback, so the callback acts as a programmatic override on top of file-based settings:
```csharp
var builder = WebApplication.CreateBuilder(args);
// Values come from Cratis:Arc; the callback overrides specific settings
builder.AddCratisArc(options =>
{
options.GeneratedApis.RoutePrefix = "v1/api";
});
var app = builder.Build();
app.UseCratisArc();
app.Run();
```
## Environment-Specific Configuration
[Section titled “Environment-Specific Configuration”](#environment-specific-configuration)
You can use different configurations for different environments using the standard ASP.NET Core configuration pattern:
**appsettings.json** (base configuration):
```json
{
"Cratis": {
"Arc": {
"GeneratedApis": {
"RoutePrefix": "api"
}
}
}
}
```
**appsettings.Development.json** (development overrides):
```json
{
"Cratis": {
"Arc": {
"CorrelationId": {
"HttpHeader": "X-Dev-Correlation-ID"
}
}
}
}
```
**appsettings.Production.json** (production overrides):
```json
{
"Cratis": {
"Arc": {
"GeneratedApis": {
"RoutePrefix": "v1"
}
}
}
}
```
## Route Generation Examples
[Section titled “Route Generation Examples”](#route-generation-examples)
The `GeneratedApis` configuration affects how routes are generated for your commands and queries. Here are some examples:
Given a command class `MyApp.Sales.Commands.CreateOrderCommand`:
### Default Configuration
[Section titled “Default Configuration”](#default-configuration)
```json
{
"GeneratedApis": {
"RoutePrefix": "api",
"SegmentsToSkipForRoute": 0,
"IncludeCommandNameInRoute": true,
"IncludeQueryNameInRoute": true
}
}
```
**Generated route**: `/api/MyApp/Sales/Commands/CreateOrderCommand`
### Skip Namespace Segments
[Section titled “Skip Namespace Segments”](#skip-namespace-segments)
```json
{
"GeneratedApis": {
"RoutePrefix": "api",
"SegmentsToSkipForRoute": 2,
"IncludeCommandNameInRoute": true,
"IncludeQueryNameInRoute": true
}
}
```
**Generated route**: `/api/Sales/Commands/CreateOrderCommand`
### Exclude Type Names
[Section titled “Exclude Type Names”](#exclude-type-names)
```json
{
"GeneratedApis": {
"RoutePrefix": "api",
"SegmentsToSkipForRoute": 3,
"IncludeCommandNameInRoute": false,
"IncludeQueryNameInRoute": false
}
}
```
**Generated route**: `/api/Commands` (for commands) or `/api/Queries` (for queries)
**Note**: When `IncludeCommandNameInRoute` or `IncludeQueryNameInRoute` is set to `false`, the system automatically detects route conflicts. If multiple commands or queries exist in the same namespace (after skipping segments), the type name will be automatically included in the route to prevent conflicts. This ensures that:
* Single command/query in a namespace: Route remains clean without the type name
* Multiple commands/queries in the same namespace: Type names are automatically added to prevent route collisions
* Both runtime endpoint mapping and proxy generation apply this logic consistently
For example, with the configuration above:
* If you have only `MyApp.Sales.Commands.CreateOrder`, the route will be `/api/commands`
* If you have both `MyApp.Sales.Commands.CreateOrder` and `MyApp.Sales.Commands.UpdateOrder`, the routes will be `/api/commands/create-order` and `/api/commands/update-order` respectively (type names added automatically to avoid conflict)
Note
The runtime `GeneratedApis` settings and the build-time proxy-generation settings must agree. If you change route generation here, mirror it in the `CratisProxies*` MSBuild properties so the generated TypeScript clients call the same routes. See [Proxy Generation Configuration](/arc/backend/proxy-generation/configuration/).
## JSON Serialization
[Section titled “JSON Serialization”](#json-serialization)
Arc provides a centralized `JsonSerializerOptions` configuration through `ArcOptions`. This ensures consistent JSON serialization across your entire application, including controller actions, manual serialization, and generated API endpoints.
### Default Configuration
[Section titled “Default Configuration”](#default-configuration-1)
Arc configures `JsonSerializerOptions` with the following defaults:
* **Property Naming**: Camel case with acronym-friendly handling (e.g., `XMLParser` becomes `xmlParser`)
* **Null Handling**: Null values are ignored when writing JSON
* **Enums**: Serialized as integers (not strings)
* **Concepts**: Full support for Cratis Concepts (strongly-typed primitives)
* **Date/Time**: Support for `DateOnly` and `TimeOnly` types
* **Types**: Support for `System.Type` and `System.Uri` serialization
* **Derived Types**: Polymorphic serialization support when derived types are discovered
### Accessing JsonSerializerOptions
[Section titled “Accessing JsonSerializerOptions”](#accessing-jsonserializeroptions)
The configured `JsonSerializerOptions` is available through dependency injection:
```csharp
public class MyService
{
public MyService(JsonSerializerOptions jsonOptions)
{
// Use the Arc-configured options
var json = JsonSerializer.Serialize(myObject, jsonOptions);
}
}
```
Or through `ArcOptions`:
```csharp
public class MyService
{
public MyService(IOptions arcOptions)
{
var jsonOptions = arcOptions.Value.JsonSerializerOptions;
}
}
```
### Customizing JSON Serialization
[Section titled “Customizing JSON Serialization”](#customizing-json-serialization)
You can add custom converters or modify the configuration through the options pattern:
```csharp
builder.AddCratisArc(options =>
{
// Add a custom converter
options.JsonSerializerOptions.Converters.Add(new MyCustomConverter());
});
```
Any customizations made to `ArcOptions.JsonSerializerOptions` will automatically be applied to ASP.NET Core controller actions as well, ensuring consistency throughout your application.
## Best Practices
[Section titled “Best Practices”](#best-practices)
1. **Use Configuration Files**: For most scenarios, use `appsettings.json` configuration as it allows easy environment-specific overrides without code changes.
2. **Environment-Specific Settings**: Leverage `appsettings.{Environment}.json` files for environment-specific configurations.
3. **Programmatic Configuration**: Use programmatic configuration when you need to:
* Set configuration based on runtime conditions
* Use custom identity providers
* Override specific settings that can’t be easily expressed in JSON
4. **Route Planning**: Consider your API route structure carefully when configuring `GeneratedApis` options, especially in public-facing APIs where route stability is important.
5. **Header Standardization**: Use standard HTTP header names for correlation IDs and tenant IDs that align with your organization’s conventions and any API gateways or load balancers in use.
6. **JSON Consistency**: Always use the injected `JsonSerializerOptions` when manually serializing/deserializing JSON to maintain consistency with controller actions and generated APIs.
# FromRequest Attribute
The `[FromRequest]` attribute is a powerful model binding feature that allows you to combine data from multiple sources of an HTTP request into a single model. Unlike the standard ASP.NET Core model binding attributes that bind from a single source, `[FromRequest]` intelligently merges data from the request body with data from other parts of the request (route parameters, query strings, etc.). This creates a unified object that’s perfect for comprehensive validation with frameworks like FluentValidation, enables cross-parameter validation rules, and simplifies controller methods by reducing multiple parameters into a single, well-structured request model.
## Overview
[Section titled “Overview”](#overview)
When you decorate a parameter with `[FromRequest]`, the Arc will:
1. First attempt to bind the entire model from the request body (JSON)
2. Then attempt to bind the same model from other request sources (route, query, headers)
3. Merge the results, with request body taking precedence for non-default values
4. Use values from other sources only when the corresponding property in the body-bound model has a default value
This enables powerful scenarios where you can have a JSON payload that gets enhanced with additional data from the URL or query parameters.
## How It Works
[Section titled “How It Works”](#how-it-works)
The `FromRequestModelBinder` performs the following steps:
1. **Body Binding**: Uses the standard body model binder to deserialize the request body into your model
2. **Complex Binding**: Uses the complex model binder to bind from route, query, and other sources
3. **Intelligent Merging**: For each property:
* If the body-bound value is not the default value for that type, it keeps the body value
* If the body-bound value is the default value but the complex-bound value is not, it uses the complex value
* This allows selective overriding of JSON properties with URL/query parameters
## Usage Examples
[Section titled “Usage Examples”](#usage-examples)
### Basic Usage
[Section titled “Basic Usage”](#basic-usage)
```csharp
public class UserUpdateRequest
{
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
[FromRoute] public int UserId { get; set; }
[FromQuery] public bool NotifyUser { get; set; }
}
[HttpPut("users/{userId}")]
public async Task UpdateUser([FromRequest] UserUpdateRequest request)
{
// request.UserId will be populated from the route
// request.NotifyUser will be populated from query string
// request.Name and request.Email will come from JSON body
// If Name or Email are not provided in JSON (or are empty/null),
// they could be overridden by query parameters if present
return Ok();
}
```
**Request Example:**
```http
PUT /users/123?notifyUser=true
Content-Type: application/json
{
"name": "John Doe",
"email": "john@example.com"
}
```
In this case:
* `UserId` = 123 (from route)
* `NotifyUser` = true (from query)
* `Name` = “John Doe” (from JSON body)
* `Email` = `"john@example.com"` (from JSON body)
### Fallback Scenarios
[Section titled “Fallback Scenarios”](#fallback-scenarios)
```csharp
public class SearchRequest
{
public string Query { get; set; } = string.Empty;
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 10;
[FromRoute] public string Category { get; set; } = string.Empty;
}
[HttpPost("search/{category}")]
public async Task Search([FromRequest] SearchRequest request)
{
return Ok();
}
```
**Request with partial JSON:**
```http
POST /search/electronics?page=2&pageSize=20
Content-Type: application/json
{
"query": "laptop"
}
```
Result:
* `Query` = “laptop” (from JSON body)
* `Page` = 2 (from query, since JSON didn’t provide it and default is 1)
* `PageSize` = 20 (from query, since JSON didn’t provide it and default is 10)
* `Category` = “electronics” (from route)
**Request with complete JSON:**
```http
POST /search/electronics?page=2&pageSize=20
Content-Type: application/json
{
"query": "laptop",
"page": 5,
"pageSize": 50
}
```
Result:
* `Query` = “laptop” (from JSON body)
* `Page` = 5 (from JSON body, overrides query parameter)
* `PageSize` = 50 (from JSON body, overrides query parameter)
* `Category` = “electronics” (from route)
## Benefits
[Section titled “Benefits”](#benefits)
1. **Flexible API Design**: Allows clients to provide data in the most convenient way
2. **Backward Compatibility**: Existing APIs can be enhanced without breaking changes
3. **RESTful Patterns**: Supports having resource identifiers in the URL while allowing detailed data in the body
4. **Progressive Enhancement**: Start with simple query parameters and optionally move to JSON for complex scenarios
5. **Unified Object for Validation**: Creates a single, complete object that can be validated using frameworks like FluentValidation
### Validation Scenarios
[Section titled “Validation Scenarios”](#validation-scenarios)
One of the key advantages of `[FromRequest]` is that it creates a complete object that contains all the data from your request, regardless of where it came from. This is particularly valuable when using validation frameworks like FluentValidation, which work with objects rather than individual parameters.
**Traditional approach (multiple parameters):**
```csharp
[HttpPut("users/{userId}")]
public async Task UpdateUser(
[FromRoute] int userId,
[FromQuery] bool notifyUser,
[FromBody] UserUpdateData data)
{
// Validation is fragmented across multiple objects
// FluentValidation can't easily validate cross-parameter rules
// Manual validation logic becomes complex
}
```
**With \[FromRequest]:**
```csharp
public class UserUpdateRequest
{
[FromRoute] public int UserId { get; set; }
[FromQuery] public bool NotifyUser { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
public class UserUpdateRequestValidator : AbstractValidator
{
public UserUpdateRequestValidator()
{
RuleFor(x => x.UserId).GreaterThan(0);
RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
RuleFor(x => x.Email).EmailAddress();
// Cross-property validation is now possible
RuleFor(x => x.NotifyUser)
.Equal(false)
.When(x => string.IsNullOrEmpty(x.Email))
.WithMessage("Cannot notify user without email address");
}
}
[HttpPut("users/{userId}")]
public async Task UpdateUser([FromRequest] UserUpdateRequest request)
{
// Single object validation with complete context
// FluentValidation can validate the entire request as one unit
// Cross-parameter validation rules are straightforward
}
```
This approach enables:
* **Cross-parameter validation**: Rules that depend on multiple values from different request sources
* **Unified validation logic**: All validation rules in one place for the complete request
* **Cleaner controllers**: Single parameter instead of multiple individual parameters
* **Better testability**: Test validators with complete request objects rather than parameter combinations
## Integration with Other Features
[Section titled “Integration with Other Features”](#integration-with-other-features)
### Swagger/OpenAPI
[Section titled “Swagger/OpenAPI”](#swaggeropenapi)
The Arc includes a `FromRequestOperationFilter` that automatically updates your Swagger documentation to correctly represent `[FromRequest]` parameters as request body schemas rather than individual parameters.
### Proxy Generation
[Section titled “Proxy Generation”](#proxy-generation)
The TypeScript proxy generator understands `[FromRequest]` parameters and generates appropriate client code that handles the combination of route, query, and body parameters correctly.
## Best Practices
[Section titled “Best Practices”](#best-practices)
1. **Use for Hybrid Scenarios**: Best suited when you need both URL-based parameters (IDs, filters) and complex body data
2. **Default Values**: Ensure your model properties have appropriate default values to enable the fallback behavior
3. **Documentation**: Clearly document which properties can come from which sources for API consumers
4. **Validation**: Apply validation attributes as needed - they work seamlessly with `[FromRequest]`
## Limitations
[Section titled “Limitations”](#limitations)
* Properties must have appropriate default values for the merging logic to work correctly
* The merging is based on default value comparison, so be mindful of what constitutes a “default” for your types
* Complex nested scenarios may require careful consideration of the binding order
# Invariant Culture
Arc provides a `.UseInvariantCulture()` extension method that configures your application to use invariant culture throughout, ensuring consistent and predictable behavior regardless of the host machine’s regional settings.
## Why Invariant Culture?
[Section titled “Why Invariant Culture?”](#why-invariant-culture)
Modern distributed applications run across different machines, containers, and cloud regions—each potentially configured with different regional settings. Without explicit culture configuration, operations like number parsing, date formatting, or string comparisons may produce inconsistent results depending on where the code runs.
Using invariant culture guarantees:
* **DateTime formatting and parsing** behaves identically everywhere
* **Number formatting and parsing** produces consistent results (e.g., decimal separator is always `.`)
* **String comparisons and sorting** are culture-independent
* **Serialization and deserialization** never depends on machine locale
## Configuration
[Section titled “Configuration”](#configuration)
Call `UseInvariantCulture()` on the `WebApplicationBuilder` before building the application, and on the resulting `WebApplication` to activate the request localization middleware:
```csharp
var builder = WebApplication.CreateBuilder(args);
builder.UseInvariantCulture();
builder.AddCratisArc();
var app = builder.Build();
app.UseInvariantCulture();
app.UseCratisArc();
await app.RunAsync();
```
> **Important**: Call `app.UseInvariantCulture()` before any middleware that processes request data to ensure invariant culture is applied to all incoming requests.
## What Gets Configured
[Section titled “What Gets Configured”](#what-gets-configured)
Calling `UseInvariantCulture()` on the builder:
* Sets `CultureInfo.DefaultThreadCurrentCulture` to `CultureInfo.InvariantCulture`
* Sets `CultureInfo.DefaultThreadCurrentUICulture` to `CultureInfo.InvariantCulture`
* Configures `RequestLocalizationOptions` to use only `InvariantCulture`
* Removes all request culture providers, preventing clients from overriding the culture via `Accept-Language` headers or query strings
Calling `UseInvariantCulture()` on the application:
* Registers the ASP.NET Core request localization middleware with the invariant culture settings
## Practical Example
[Section titled “Practical Example”](#practical-example)
Consider an API that accepts a price value. Without invariant culture, `"1,5"` might parse as `1.5` on a European machine but fail on an English-locale machine. With invariant culture, parsing is always based on the standard `"1.5"` format:
```csharp
var builder = WebApplication.CreateBuilder(args);
builder.UseInvariantCulture();
builder.AddCratisArc();
var app = builder.Build();
app.UseInvariantCulture();
app.MapPost("/products", (ProductRequest request) =>
{
// Price is always parsed using invariant culture rules
return Results.Ok(new { Price = request.Price });
});
await app.RunAsync();
record ProductRequest(decimal Price, DateOnly ExpiryDate);
```
## When to Use
[Section titled “When to Use”](#when-to-use)
Invariant culture is recommended for:
* **APIs and services** that exchange data between systems
* **Applications deployed in multiple regions** or cloud environments
* **Data processing pipelines** where consistency is critical
* **Any scenario** where culture-sensitive behavior could lead to bugs or data inconsistencies
It is generally safe to apply invariant culture in back-end services, as the locale-specific formatting should be handled on the client side (e.g., a frontend application) rather than in the API itself.
# Microsoft Identity
Cratis’ Arc provides a way to easily work with providing an object that represents properties the application finds important for describing the logged in user. The purpose of this is to provide details about the logged in user on the ingress level of an application and letting it provide the details on the request going in. Having it on the ingress level lets you expose the details to all microservices behind the ingress.
The values provided by the provider are values that are typically application specific and goes beyond what is already found in the token representing the user. This is optimized for working with Microsoft Azure well known HTTP headers passed on by the different app services, such as Azure ContainerApps or WebApps. Internally, it is based on the following HTTP headers to be present.
| Header | Description |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| x-ms-client-principal | The token holding all the details, base64 encoded [Microsoft Client Principal Data definition](https://learn.microsoft.com/en-us/azure/static-web-apps/user-information?tabs=csharp#client-principal-data) |
| x-ms-client-principal-id | The unique identifier from the identity provider for the identity |
| x-ms-client-principal-name | The name of the identity, typically resolved from claims within the token |
> Important note: Since local development is not configured with the identity provider, but you still need a way to test that both the backend and the frontend deals with the identity in the correct way. This can be achieved by creating the correct token and injecting it as request headers using a browser extension. Read more about [generating principal tokens for local development](/arc/general/generating-principal/).
The token in the `x-ms-client-principal` should be a base64 encoded [Microsoft Client Principal Data definition](https://learn.microsoft.com/en-us/azure/static-web-apps/user-information?tabs=csharp#client-principal-data).
## Authentication / Authorization
[Section titled “Authentication / Authorization”](#authentication--authorization)
To get the Microsoft Client Principal supported in your backend, the Arc offers an `AuthenticationHandler` that supports the HTTP headers and does the right thing to put ASP.NET Core and every `HttpContext` in the right state.
You can add this by calling the `AddMicrosoftIdentityPlatformIdentityAuthentication()` method on your services.
```csharp
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMicrosoftIdentityPlatformIdentityAuthentication();
```
The above code will then also call the `.AddAuthentication()` with the default scheme name (**MicrosoftIdentityPlatform**) and register the appropriate `AuthenticationHandler` for that scheme.
You can override the scheme name on the extension method by passing your own string as an argument.
For it to be appropriately setup, you’ll need to enable the default authentication and authorization on your app, like below:
```csharp
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
```
## Knowing which identity provider signed the caller in
[Section titled “Knowing which identity provider signed the caller in”](#knowing-which-identity-provider-signed-the-caller-in)
The `x-ms-client-principal` payload carries an `identityProvider` field describing the provider the ingress authenticated the caller with. Without it, everything downstream sees is a set of claims that look the same whether they came from Entra ID or GitHub — so the application has no way to tell one federation apart from another.
Arc keeps that value on the reconstructed principal as a claim, so both normal request authorization and the `/.cratis/me` identity resolution can read it:
```csharp
using Cratis.Arc.Identity;
using Microsoft.AspNetCore.Http;
public class IdentityProviderReader(IHttpContextAccessor httpContextAccessor)
{
public string? Current =>
httpContextAccessor.HttpContext?.User.FindFirst(MicrosoftIdentityPlatformClaims.IdentityProvider)?.Value;
}
```
| Aspect | Detail |
| ----------- | -------------------------------------------------------------------------------------------------------- |
| Claim type | `urn:cratis:arc:identity:provider` — use the `MicrosoftIdentityPlatformClaims.IdentityProvider` constant |
| Value | The exact `identityProvider` value the ingress forwarded, for example `aad` or `github` |
| When absent | The forwarded principal carried no `identityProvider` field, or the field held only blank characters |
The claim type is **reserved for Arc**. The `x-ms-client-principal` header is base64, not a signature, so any caller that can reach the application can put whatever it likes in the serialized payload — including a claim of this very type. Arc therefore removes every claim of the reserved type from the deserialized payload, ignoring casing, *before* writing its own value.
Caution
**What the strip guarantees is single provenance, not authenticity.** The claim always carries exactly one value, and that value always comes from one place — the `identityProvider` field of the forwarded principal — so a claim of the reserved type passed through by the ingress or by the identity provider can never displace it. It does **not** make the value trustworthy. The same unsigned header carries that field too, and Arc does not check who sent the header, so a caller that can reach the application authors the whole document; the strip only decides which of its fields wins. **Trust this claim exactly as far as you trust the `x-ms-client-principal` header itself — that is, only insofar as your ingress is the only thing that can set it.** Terminate the header at the ingress and reject or overwrite an inbound one; nothing inside Arc can do that for you.
Read the claim with `FindFirst` or `FindAll`, as in the example above, and **never normalize the claim type yourself**. Those lookups compare the claim type the same way the strip does, so what they return is exactly what Arc wrote. Enumerating `User.Claims` and folding the type with `ToUpperInvariant()` or `Trim()` widens the match beyond what was removed — a forged type differing only by Unicode case folding (`urn:cratiſ:arc:identity:provider`, U+017F) or by trailing whitespace then matches, and because forwarded claims are added before Arc’s own, a `FirstOrDefault()` over that widened set returns the forgery.
Note
The value is the exact `identityProvider` field the ingress forwarded, verbatim and untrimmed — Arc neither interprets nor normalizes it. What it *means* is therefore the ingress’s choice, not Arc’s. Cratis AuthProxy forwards the canonical provider key, the same value it publishes as its own `urn:cratis:identity:provider-key` claim, so in a canonical AuthProxy deployment the two carry identical values. Another ingress may forward an authentication scheme name or a provider display name that changes when the provider is renamed. **Arc guarantees neither**, so use this claim for telling federations apart, for diagnostics, and for provider-aware behavior. If you need a durable provider key, read the claim the ingress publishes for that purpose — `urn:cratis:identity:provider-key` in an AuthProxy deployment — rather than this one. Arc copies such ingress-authored claims through untouched.
## Identity Details
[Section titled “Identity Details”](#identity-details)
For information about providing additional identity details for logged-in users, including authorization checks and custom identity information, see the [Identity documentation](/arc/backend/identity/).
The Microsoft Identity integration works seamlessly with the generic identity system to provide domain-specific information beyond what’s available in identity provider tokens.
# Swagger
Cratis Arc provides enhanced Swagger/OpenAPI support through the `Cratis.Arc.Swagger` package. This extension automatically configures Swagger to properly handle the Arc’s specific features and conventions.
## Overview
[Section titled “Overview”](#overview)
The Swagger extension adds several filters and enhancements to provide accurate API documentation for:
* **Concepts** - Properly represents concept types as their underlying primitive types
* **Commands** - Adds correct response schemas with validation and error handling
* **Queries** - Includes pagination/sorting parameters and proper response schemas
* **FromRequest attributes** - Correctly handles complex model binding scenarios
* **Model-bound endpoints** - Supports minimal API endpoints for commands and queries
## Setup
[Section titled “Setup”](#setup)
To use the Swagger enhancements, add the extension to your Swagger configuration:
```csharp
builder.Services.AddSwaggerGen(options =>
{
options.AddConcepts();
});
```
The `AddConcepts()` method adds all the necessary filters and operation filters automatically.
## Features
[Section titled “Features”](#features)
### Concept Schema Filter
[Section titled “Concept Schema Filter”](#concept-schema-filter)
Automatically maps concept types (types inheriting from `ConceptAs`) to their underlying primitive types in the Swagger schema. This ensures that concepts appear as their actual data types (string, int, Guid, etc.) rather than complex objects in the API documentation.
**Example:**
```csharp
public class UserId : ConceptAs;
// In Swagger, UserId parameters will appear as string (UUID format)
// instead of a complex object with a Value property
```
### Command Result Operation Filter
[Section titled “Command Result Operation Filter”](#command-result-operation-filter)
Enhances command endpoints by:
* Adding proper `CommandResult` or `CommandResult` response schemas
* Including standard HTTP status codes (200, 400, 403, 500) with appropriate error schemas
* Handling void/Task return types correctly
* Supporting concept return types
### Query Result Operation Filter
[Section titled “Query Result Operation Filter”](#query-result-operation-filter)
Enhances query endpoints by:
* Adding `QueryResult` response schemas
* Including standard HTTP status codes with error handling
* Automatically adding pagination and sorting parameters for enumerable results
* Supporting concept return types
### FromRequest Operation Filter
[Section titled “FromRequest Operation Filter”](#fromrequest-operation-filter)
Properly handles the `[FromRequest]` attribute by:
* Removing the parameter from the query string/path parameters
* Adding it as a request body with the correct JSON schema
* Supporting complex model binding scenarios
### Model-Bound Operation Filters
[Section titled “Model-Bound Operation Filters”](#model-bound-operation-filters)
Provides support for minimal API endpoints that use model binding for commands and queries, ensuring they appear correctly in the Swagger documentation.
### Pagination and Sorting Parameters
[Section titled “Pagination and Sorting Parameters”](#pagination-and-sorting-parameters)
For query endpoints that return enumerable results, the following query parameters are automatically added to the Swagger documentation:
| Parameter | Type | Description |
| --------------- | ------- | -------------------------------- |
| `sortBy` | string | Field name to sort by |
| `sortDirection` | string | Sort direction (`asc` or `desc`) |
| `pageSize` | integer | Number of items per page |
| `page` | integer | Page number (0-based) |
## Enum Schema Filter
[Section titled “Enum Schema Filter”](#enum-schema-filter)
Provides proper schema generation for enum types, ensuring they are documented correctly in the API specification.
## Response Schemas
[Section titled “Response Schemas”](#response-schemas)
The Swagger extension automatically adds consistent response schemas for all command and query endpoints:
### Success Responses (200)
[Section titled “Success Responses (200)”](#success-responses-200)
* Commands: `CommandResult` or `CommandResult`
* Queries: `QueryResult` with the actual data type
### Error Responses
[Section titled “Error Responses”](#error-responses)
* **400 Bad Request**: Validation errors or malformed requests
* **403 Forbidden**: Authorization failures
* **500 Internal Server Error**: Unexpected server errors
All error responses use the same result schema as success responses but with error information populated.
## Integration with Arc Features
[Section titled “Integration with Arc Features”](#integration-with-arc-features)
The Swagger extension seamlessly integrates with other Arc features:
* **[FromRequest](/arc/backend/asp-net-core/from-request/)**: Properly documents complex model binding
* **[Commands](/arc/backend/commands/)**: Accurate documentation of command endpoints and responses
* **[Queries](/arc/backend/queries/)**: Complete documentation including pagination for collection results
* **[Validation](/arc/backend/asp-net-core/validation/)**: Error responses include validation failure information
* **[Without Wrappers](/arc/backend/asp-net-core/without-wrappers/)**: Works correctly with unwrapped responses
This ensures that your API documentation accurately reflects the actual behavior and capabilities of your Arc-based API.
# Validation
The concept of validation is to make sure all your inputs are in a valid form before hitting your logic. Validation is different from business rules in the sense that it is focused on user input and sanitizing the values coming as input. While business rules tend to be stateful based on state of your system to be able to validate for correctness.
## Default behavior
[Section titled “Default behavior”](#default-behavior)
The default behavior when using the `CommandActionFilter` is to stop the execution of a command coming in if it is invalid. It will automatically look at the `ModelState` of the currently executing controller action and stop execution if things are invalid. The result will then be put on the `CommandResult` as validation errors for any consumers to react to.
## Value based validators
[Section titled “Value based validators”](#value-based-validators)
The value based validation leverages what is already in the ASP.NET Core pipelines [custom attributes](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-6.0#custom-attributes).
You can leverage this by using attributes such as the `[Required]` attribute:
```csharp
public record OpenDebitAccount(
[Required] AccountId AccountId,
AccountDetails Details);
```
Attributes are compile time and just represent metadata, the downside of this is that if you’re having context dependent rules or need to inject localized strings or similar you won’t be able to get to it.
## Fluent Validation
[Section titled “Fluent Validation”](#fluent-validation)
Another alternative approach to validation is to use [FluentValidation](https://docs.fluentvalidation.net). Cratis comes with this all setup and automatically hooks up different types of validators for specific purposes.
### Base Validator
[Section titled “Base Validator”](#base-validator)
The formalized validator types (`CommandValidator`, `QueryValidator` and `ConceptValidator`) all derive from a type called `BaseValidator`. This base type provides methods for defining validation rules for known `ConceptAs<>` primitives and will unwrap the inner `Value` property automatically, providing you a clean way of validating the concepts inner primitive type without considering the `Value` property.
However, if you’re hooking up validators for the actual concept, you need to use the method `RuleForConcept()` to work directly with the concept. The `IRuilBuilderInitial` type returned would then be for the actual concept and not its primitive type its encapsulating.
### Discoverable validators
[Section titled “Discoverable validators”](#discoverable-validators)
Validators can be automatically discovered. This done through the discovery of anything that implements the marker interface `IDiscoverableValidator<>`. The formalized types for Commands, Queries and Concepts all implement this. There is also a base type that can be used, which inherits from `BaseValidator` to give you the additional functionality it provides. All you need to do is inherit from `DiscoverableValidator<>`.
### Command Validator
[Section titled “Command Validator”](#command-validator)
To write command validators, all you need to do is implement the `CommandValidator<>` class and create rules for your properties.
Lets say you have a command as follows:
```csharp
public record OpenDebitAccount(
AccountId AccountId,
AccountDetails Details);
```
A validator for this could then be as follows:
```csharp
using Cratis.Arc.Commands;
public class OpenDebitAccountValidator : CommandValidator
{
public OpenDebitAccountValidator()
{
RuleFor(_ => _.Details.Name).NotEmpty().WithMessage("Name is required");
RuleFor(_ => _.Details.Owner).NotNull().WithMessage("Owner is required");
RuleFor(_ => _.Details.IncludeCard).NotNull().WithMessage("Include card should be specified");
}
}
```
### Concept Validator
[Section titled “Concept Validator”](#concept-validator)
When one is using [domain concepts](/arc/general/), you have the opportunity to create a validator for the concept that will automatically be used as part of the ASP.NET Core validation pipeline.
The benefit of this approach is that you can reuse validation rules and they will automatically implicitly be hooked up, leading to not have to remember to explicitly add every rule for reused concepts.
The tradeoff of this is obviously that your rules are scattered around.
Say you have a concept as follows:
```csharp
public record AccountName(string Value) : ConceptAs(Value);
```
By inheriting the `ConceptValidator<>` type you can create rules for the concept:
```csharp
using Cratis.Arc.Validation;
public class AccountNameValidator : ConceptValidator
{
public AccountNameValidator()
{
RuleFor(_ => _).Length(0, 16).WithMessage("Account name has to be less than 16 characters");
}
}
```
### Conditional validation
[Section titled “Conditional validation”](#conditional-validation)
FluentValidation supports the concept of [conditions](https://docs.fluentvalidation.net/en/latest/conditions.html) for validation, in the API you can see `When()` for different levels. In Cratis Arc you’ll find that every validator that is a discoverable validator (Concept, Command…) have methods on the base type that offers convenience conditions for whether or not the request is a **command** or a **query** called `WhenCommand()` or `WhenQuery()`. With this you can build rule-sets that are specific the scenario of a command or a query given a specific object you’re validating.
The use case for this is most relevant for **concepts** where the concept type is used by both commands and queries.
Following is an example for an RBAC system where you have system users that can’t be modified and you want to cross cuttingly apply the validation for all command operations that work on the `UserId` concept but not for any queries that has the same type as an argument.
```csharp
using Cratis.Arc.Validation;
public class UserIdValidator : ConceptValidator
{
public UserIdValidator()
{
RuleFor(userId => userId)
.NotNull()
.UserMustExist().WithMessage("User does not exist.");
RuleFor(userId => userId)
.UserMustNotBeSystem().WithMessage("Operation is not allowed on a system user.")
.WhenCommand();
}
}
```
> Note: The rules `UserMustExist()`and `UserMustNotBeSystem()` are an example of extension methods that you could implement. The implementation is irrelevant for the example.
The code sets up a rule that is general without a condition, then it applies a rule for when it is a **command**. With the `WhenCommand()` extension you can also specify whether or not you want it to apply for the entire validation chain or just the current. All validators are default, you can then use `ApplyConditionTo.CurrentValidator` for only the current validator.
With the `ApplyConditionTo.CurrentValidator` you can merge into one rule-set:
```csharp
using Cratis.Arc.Validation;
public class UserIdValidator : ConceptValidator
{
public UserIdValidator()
{
RuleFor(userId => userId)
.NotNull()
.UserMustExist().WithMessage("User does not exist.");
.UserMustNotBeSystem().WithMessage("Operation is not allowed on a system user.")
.WhenCommand(ApplyCondition.CurrentValidator);
}
}
```
If you have multiple rules that should only apply when it is a **command** or **query** you can use the action method:
```csharp
using Cratis.Arc.Validation;
public class UserIdValidator : ConceptValidator
{
public UserIdValidator()
{
RuleFor(userId => userId)
.NotNull()
.UserMustExist().WithMessage("User does not exist.");
WhenCommand(() =>
{
RuleFor(userId => userId)
.UserMustNotBeSystem().WithMessage("Operation is not allowed on a system user.")
.UserMustNotBeAdministrator().WithMessage("Operation is not allowed on an administrator user.")
});
}
}
```
# Without wrappers (Controller-based Commands and Queries)
When using controller-based [Commands](/arc/backend/commands/controller-based/) or [Queries](/arc/backend/queries/controller-based/), the result is wrapped in a more descriptive structure that will include the result returned by your controller action. This behavior is not an opt-in on a per controller or action level, making it a cross cutting always on behavior.
If you need controller-based endpoints that do not have the encapsulation of the `CommandResult` or the `QueryResult` structures, you can use an attribute called `[AspNetResult]`.
The attribute can be used on a controller or a specific action. Once used, the result is kept as is from the action called.
> Important note: The behavior of not allowing an invalid state is still kept intact, meaning that if you have validation that kicks in and makes the `ModelState`invalid, both the command and the query action filters will not call the action.
To keep the original result on a controller level, place the attribute before the controller:
```csharp
[Route("/api/accounts/debit")]
[AspNetResult] // <-
public class Accounts : Controller
{
[HttpGet("starting-with")]
public async Task> StartingWith([FromQuery] string? filter)
{
/* Code that gets the data and returns it */
}
[HttpGet("latest-transactions/{accountId}")]
public DebitAccountLatestTransactions LatestTransactions([FromRoute] AccountId accountId)
{
/* Code that gets the data and returns it */
}
}
```
To keep the original result on an action level, place the attribute before the action:
```csharp
[Route("/api/accounts/debit")]
public class Accounts : Controller
{
[HttpGet("starting-with")]
[AspNetResult] // <-
public async Task> StartingWith([FromQuery] string? filter)
{
/* Code that gets the data and returns it */
}
[HttpGet("latest-transactions/{accountId}")]
public DebitAccountLatestTransactions LatestTransactions([FromRoute] AccountId accountId)
{
/* Code that gets the data and returns it */
}
}
```
## Proxy generator
[Section titled “Proxy generator”](#proxy-generator)
The proxy generator will exclude commands or queries that are marked with the `[AspNetResult]` attribute and as a consequence not generate any artifacts for the affect command or query.
# Authorizing commands and queries
> Restrict who can run a command or read a query with role-based authorization, applied at the boundary.
**Goal:** only certain users may perform an action or see certain data — “only a librarian can register an author,” “only an admin sees the audit list.” You want that enforced declaratively, not with `if` checks scattered through your logic.
## Authorize at the boundary, not in the logic
[Section titled “Authorize at the boundary, not in the logic”](#authorize-at-the-boundary-not-in-the-logic)
Authorization is a cross-cutting concern: it belongs at the edge, applied as an attribute, so your `Handle()` methods and read models stay focused on behavior. Arc enforces role attributes on **both** commands and query methods.
## Protect a command
[Section titled “Protect a command”](#protect-a-command)
Put `[Roles(...)]` on the `[Command]` record. Arc checks the caller’s roles before the command runs:
```csharp
[Command]
[Roles(nameof(UserRole.Librarian))]
public record RegisterAuthor(AuthorId Id, AuthorName Name)
{
public Task Handle(IMongoCollection authors) =>
authors.InsertOneAsync(new Author(Id, Name));
}
```
## Protect a query
[Section titled “Protect a query”](#protect-a-query)
Query methods on a read model take the same attribute, so the read side is gated too:
```csharp
[ReadModel]
public record Author([property: Key] AuthorId Id, AuthorName Name)
{
[Roles(nameof(UserRole.Librarian))]
public static ISubject> AllAuthors(IMongoCollection collection) =>
collection.Observe();
}
```
## Who the user is
[Section titled “Who the user is”](#who-the-user-is)
Roles come from the authenticated identity. Arc integrates with standard ASP.NET Core authentication and can enrich the identity with application-specific details (roles, tenant, preferences) through `IProvideIdentityDetails`. See the [Identity](/arc/backend/identity/) section for setting that up, and for generating a principal during local development so you can exercise authorized endpoints without a full login.
## Notes
[Section titled “Notes”](#notes)
* **Multi-tenancy** narrows access further: combine roles with [tenancy](/arc/backend/tenancy/) so a user only ever sees their tenant’s data.
* The generated TypeScript proxies respect the same rules — an unauthorized call fails the same way it would from any client.
## See also
[Section titled “See also”](#see-also)
* [Identity](/arc/backend/identity/) — authentication, identity details, and local-dev principals.
* [Tenancy](/arc/backend/tenancy/) — isolating data per tenant.
* [Commands](/arc/backend/commands/) and [Queries](/arc/backend/queries/) — the full model.
# Integrate with Chronicle
> Add Chronicle's event-sourced write path to an Arc application while keeping Arc's commands, queries, identity, tenancy, and generated proxies.
`Cratis.Arc.Chronicle` is the integration package that extends Arc with [Cratis Chronicle](https://github.com/Cratis/Chronicle) capabilities. It wires the two frameworks together so that Arc’s application model — commands, queries, identity, tenancy, and code generation — works seamlessly with Chronicle’s event sourcing infrastructure.
Arc does not require Chronicle, and Chronicle does not require Arc. That independence is useful for adoption and bounded current-state slices. In a full Cratis information system, though, this integration is the natural pairing: Arc gives the CQRS boundary and Chronicle keeps the event-sourced facts underneath it.
## How the two fit together
[Section titled “How the two fit together”](#how-the-two-fit-together)
The frameworks meet at one seam: an Arc **command** appends a Chronicle **event**, a Chronicle **projection** folds events into a **read model**, and an Arc **query** serves that read model back — with the generated TypeScript proxy carrying both ends to React.
```
command
returns an event
projection
query
React
[Command] record · Handle()
Chronicle event log
[ReadModel] · materialized to the sink
```
Reading that loop in code:
* **A command writes by *returning*.** A `[Command]` record carries the command’s inputs as its **properties**, and the decision lives in a `Handle()` method on the record. Whatever `Handle()` **returns** is what Chronicle does with it — return an `[EventType]` event and it’s appended; the [return signature](/arc/backend/chronicle/commands/events/) (a single event, several, a tuple, a `Result<,>`, or nothing) decides the outcome. You never touch an event log directly.
* **The event source id picks the stream.** Every event belongs to one **event source** — one entity’s stream of history. Chronicle resolves that id from the command: a `[Key]` parameter, a property whose type converts to `EventSourceId` (typically a `ConceptAs` with an `implicit operator EventSourceId`), or `ICanProvideEventSourceId`. See [Resolving EventSourceId](/arc/backend/chronicle/resolving-event-source-id/).
* **The read model is projected, then queried.** A `[ReadModel]` record declares the shape you want; `[FromEvent]`, `[SetFrom]`, and `[SetValue]` map events onto its properties and Chronicle keeps it **materialized** in the configured sink (MongoDB by default). An Arc query — a static method on the read model, often returning an observable so the UI stays live — serves it through the generated proxy. See [Read Models](/arc/backend/chronicle/read-models/).
* **The loop closes: a command can read the state it helped build.** When a decision depends on what’s already true, the command doesn’t query for it — Arc resolves the read model for the command’s key and hands it to the validator, `Provide()`, or `Handle()` as a parameter. See [Use current state in a command](/arc/scenarios/use-current-state-in-a-command/).
So the round-trip is: a fact happens (command → event), it’s folded into state (projection → read model), and the UI reads it (query → proxy) — and the next command can read that same state to decide. Each piece is one of the topics below.
## What it provides
[Section titled “What it provides”](#what-it-provides)
Without this package, Arc and Chronicle are independent. With it:
* **Commands return events** — `Handle()` methods on commands can return event records directly; the package appends them to the correct event log automatically.
* **Event source resolution** — the command context (current user identity, tenant, route parameters) is used to resolve the event source id without manual plumbing.
* **Read models backed by projections** — Arc’s read model conventions drive Chronicle projections so that query responses always reflect the current projected state.
* **Tenant-aware event stores** — each tenant’s event log and projections are namespaced automatically, matching Arc’s tenancy model.
* **Compliance integration** — PII-annotated properties are decrypted transparently before read models are served, and the compliance subject is set on commands from the current identity.
* **Aggregate support** — aggregate roots are discoverable and invocable via the standard command pipeline, with Chronicle managing the event stream and rehydration.
* **Current state as a command dependency** — a command’s `CommandValidator<>`, `Provide()`, and `Handle()` can each take the read model Chronicle projected for the command’s key as an ordinary parameter, so a state-dependent decision needs no query round-trip.
## Topics
[Section titled “Topics”](#topics)
| Topic | Description |
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| [Aggregates](/arc/backend/chronicle/aggregates/) | Working with aggregate roots and event sourcing. |
| [Add event sourcing to an Arc slice](/arc/backend/chronicle/add-event-sourcing/) | Move one database-backed slice to Chronicle while keeping its query and React screen in place. |
| [Cratis Package](/arc/backend/chronicle/cratis-package/) | The convenience package for Arc + Chronicle applications. |
| [React to an event](/arc/backend/chronicle/react-to-an-event/) | Run side effects or follow-up commands from Chronicle events with reactors. |
| [Commands](/arc/backend/chronicle/commands/) | Returning events from commands, event source id resolution, and concurrency scoping. |
| [Resolving EventSourceId](/arc/backend/chronicle/resolving-event-source-id/) | How Chronicle resolves aggregate and read model identity from commands and query arguments. |
| [Read Models](/arc/backend/chronicle/read-models/) | What makes a read model injectable into a command, how it is resolved by key, and what happens when it does not exist. |
| [Tenancy](/arc/backend/chronicle/tenancy/) | Tenant-aware namespaces for event stores and projections. |
| [Validation](/arc/backend/chronicle/validation/) | Validating a command against the state Chronicle already projected for its key. |
| [Compliance](/arc/backend/chronicle/compliance/) | PII decryption on read models and compliance subject resolution on commands. |
| [Code Analysis](/arc/backend/chronicle/code-analysis/) | Diagnostics and analyzers specific to the Chronicle integration. |
# Add event sourcing to an Arc slice
> You built an Arc slice over a plain database. Here's how adding Chronicle is a write-side change that leaves every query, proxy, and React screen exactly where it is.
You have a complete, typed, live full-stack app from the Arc tutorial — and so far it used a database directly. This page shows the write-side move that puts [Chronicle](/chronicle/) underneath the same CQRS boundary. For information systems, this is the direction we usually recommend: keep the Arc command/query shape, and let Chronicle store the facts.
The reassuring part: adopting it is a **write-side change**. Your queries, your generated proxies, and your React screens don’t move.
If you drew the database-backed slice as an **[event model](/event-modeling/)**, the model barely changes here. The screen, the command, the `AuthorRegistered` fact, the read model, and the query all stay put. What changes is *where the fact lives* — a database row becomes a real, stored event — and that unlocks one genuinely new block: a **reactor** (frame `06`), automation that fires *when a fact happens*.
```
UI/A: AuthorsUI/A: AuthorsUI/A: AuthorsC/RM: AuthorsC/RM: AuthorsStream: Authors
AddAuthor
RegisterAuthor
id: uuid, name: string
AuthorRegistered
name: string
Author
Authors
WelcomeNewAuthors
```
Frames `01`–`05` are unchanged from the intro — the fact at `03` is just stored differently now. Frame `06` is the new part: a reactor, which you couldn’t have before because there were no stored events to react to. Let’s prove it on the very first slice.
## What changes, and what doesn’t
[Section titled “What changes, and what doesn’t”](#what-changes-and-what-doesnt)
Set the two versions of the author slice side by side.
**The command stops writing a document and starts recording a fact.** `Handle()` returns the event that happened instead of inserting:
```csharp
[Command]
public record RegisterAuthor(AuthorId Id, AuthorName Name)
{
public AuthorRegistered Handle() => new(Name); // returns the fact, doesn't write
}
[EventType]
public record AuthorRegistered(AuthorName Name);
```
The event is an immutable, past-tense fact. `[EventType]` carries **no name argument** — Chronicle uses the type name, `AuthorRegistered`, as its identity. For Chronicle to use an author’s id as the key of their event stream, give the concept an implicit conversion to `EventSourceId`:
```csharp
public record AuthorId(Guid Value) : ConceptAs(Value)
{
public static AuthorId New() => new(Guid.NewGuid());
public static implicit operator EventSourceId(AuthorId id) => new(id.Value.ToString());
}
```
**The read model stops being filled by the command and starts being filled by a projection.** Mark it `[FromEvent]` and Chronicle folds the event onto it — **AutoMap** matches `AuthorRegistered.Name` straight onto `Author.Name`, so you write no update code:
```csharp
[ReadModel]
[FromEvent]
public record Author([property: Key] AuthorId Id, AuthorName Name)
{
// The query is UNCHANGED from chapter 1.
public static ISubject> AllAuthors(IMongoCollection collection) =>
collection.Observe();
}
```
That’s the whole change for this slice. Lined up:
| | Over a database | With Chronicle |
| ----------------------------- | --------------------- | ---------------------------------- |
| What `Handle()` does | writes a document | returns an event |
| What fills the read model | the command, directly | a projection over the event |
| What you can read | current state | current state **and full history** |
| The query method | `AllAuthors` | `AllAuthors` — *identical* |
| The generated proxies & React | as built | *identical* |
## The frontend doesn’t notice
[Section titled “The frontend doesn’t notice”](#the-frontend-doesnt-notice)
The query is the same method, so the generated proxy is the same type, so the screen you wrote in chapter 1 keeps working untouched — it’s still subscribed to the read model, which a projection now keeps current instead of the command:
* C# — backend
```csharp
// the read model is now event-sourced — the query signature is unchanged
public static ISubject> AllAuthors(IMongoCollection collection) =>
collection.Observe();
```
* TypeScript — frontend
```tsx
// not one character changes
const [authors] = AllAuthors.use();
```
## What the events unlock
[Section titled “What the events unlock”](#what-the-events-unlock)
Now that state changes are recorded as facts, you get things a plain database can’t give you:
* **A free audit trail** — every change, in order, forever.
* **Rebuild read models from history** — model a brand-new view of the past by replaying the events into it.
* **Reactors** — automate a follow-up *when a fact happens*. A reactor is a class marked `IReactor`; the method’s first parameter is the event it reacts to:
```csharp
public class WelcomeNewAuthors(INotificationService notifications) : IReactor
{
public Task AuthorRegistered(AuthorRegistered @event, EventContext context) =>
notifications.Notify($"Welcome aboard, {@event.Name}!");
}
```
Note
This is why the live-screen chapter was just *observable queries* and not “make it react” — reactions fire on **events**, and you didn’t have any until now. Reactors are an event-sourcing feature.
## Why this is usually the better backbone
[Section titled “Why this is usually the better backbone”](#why-this-is-usually-the-better-backbone)
Storing current state directly is simpler for bounded CRUD surfaces and adoption steps. Event sourcing earns its keep for information systems because history, auditability, replay, integration, and process insight show up over time. [Why Event Sourcing](/chronicle/why-event-sourcing/) explains why we treat it as the default, and [When to use event sourcing](/chronicle/concepts/when-to-use-event-sourcing/) names the exceptions.
You also saw why the boundary matters: the read side and the entire frontend came along unchanged.
* **The event-sourcing side, in depth** — the [Chronicle tutorial](/chronicle/tutorial/) builds the same library model one event-sourcing concept at a time.
* **The two together, end to end** — the [Cratis Stack tour](/cratis-stack/) and the [full-stack capstone](/build-a-full-app/) put Arc, Chronicle, and Components together on a real feature.
* **Adopt it incrementally** — [Adopting Cratis](/adopting-cratis/) walks through moving an existing Arc app’s write side to Chronicle, one slice at a time.
That’s the library slice — built full-stack and type-safe, with CQRS at the boundary and Chronicle underneath when the slice belongs in the event-sourced model. You have the model; go build your own.
# Aggregates
> Use an aggregate root when a command's decision must hold against the entity's own history — Arc resolves it from the command's key and commits what it applies.
Most commands can decide from what they carry plus a projected read model. Some can’t. “Withdraw 200” has to be checked against *this* account’s actual history, and it has to stay correct when two withdrawals arrive at once — a read model that lags by a few milliseconds will happily approve both.
That is what an aggregate root is for. It rehydrates from the entity’s own event stream, applies new events under its own rules, and commits them as one unit. Where a read model is a *snapshot you read*, an aggregate root is *the thing that decides and records*.
## The difference in one line
[Section titled “The difference in one line”](#the-difference-in-one-line)
```csharp
[Command]
public record WithdrawFunds([Key] Guid AccountId, decimal Amount)
{
public Task Handle(Account account) => account.Withdraw(Amount);
}
```
`Account` is an aggregate root. Arc resolved it for `AccountId` and replayed its events to rebuild current state before `Handle()` ran. Whatever the aggregate applies is enrolled in the command’s transaction and committed when the command succeeds — or rolled back when it fails. You never fetch it, never call `Commit()`, and never touch the event log.
## Which one do I reach for
[Section titled “Which one do I reach for”](#which-one-do-i-reach-for)
| | Read model | Aggregate root |
| ----------------- | ------------------------------------------- | -------------------------------------------- |
| Answers | “what does this look like now?” | “is this change allowed, and what happened?” |
| Built from | events, materialized to a sink | events, replayed per command |
| Consistency | eventual | consistent within the aggregate boundary |
| Can emit events | no | yes |
| Reach for it when | gating on projected state, computing inputs | an invariant must hold under concurrency |
They compose. Validate against a read model to give the user a fast, specific message, and let the aggregate enforce the invariant that actually must not break. See [Read models](/arc/backend/chronicle/read-models/).
## How Arc wires it up
[Section titled “How Arc wires it up”](#how-arc-wires-it-up)
The same key resolution that picks a read model picks the aggregate — a `[Key]` property, a property that converts to `EventSourceId`, or `ICanProvideEventSourceId`. See [Resolving EventSourceId](/arc/backend/chronicle/resolving-event-source-id/).
* **Discovered automatically** — every type implementing `IAggregateRoot` is registered without configuration.
* **Resolved per command** — the instance is command-scoped and bound to that command’s event source id, rehydrated from its stream on resolution.
* **Committed for you** — applied events are enrolled in the command’s transaction and committed on success, rolled back on failure.
If the command carries no usable key, resolution fails with `UnableToResolveAggregateRootFromCommandContext`.
## Topics
[Section titled “Topics”](#topics)
| Topic | Description |
| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| [Defining an aggregate root](/arc/backend/chronicle/aggregates/defining-an-aggregate-root/) | Writing the class itself — applying events, `On` methods, and how state is rebuilt. |
| [Aggregate roots in commands](/arc/backend/chronicle/aggregates/injecting-into-commands/) | Taking one as a `Handle()` dependency, key resolution, and lifetime. |
# Defining an aggregate root
> Write the aggregate root class itself — applying events, On methods, how state is rebuilt from the stream, and committing changes.
The concept of an Aggregate Root comes from [Domain Driven Design](https://martinfowler.com/bliki/DDD_Aggregate.html). Its role is to govern the interaction of domain objects that should be treated as a single unit. With event sourcing, an aggregate root typically is responsible for applying events as it sees fit according to its domain logic and rules.
Said in another way, Aggregate Root objects are responsible for managing the domain transaction and governs the integrity of the state changes that goes together.
## Overview
[Section titled “Overview”](#overview)
In Arc, an aggregate root is represented by implementing the `IAggregateRoot` interface or inheriting from the `AggregateRoot` base class. The framework provides two main types of aggregate roots:
1. **Stateless Aggregate Roots** - Simple aggregate roots that don’t maintain internal state
2. **Stateful Aggregate Roots** - Aggregate roots that maintain state using reducers, projections, or event handler methods
## Basic Structure
[Section titled “Basic Structure”](#basic-structure)
All aggregate roots in Chronicle inherit from the `AggregateRoot` base class:
```csharp
public class MyAggregateRoot : AggregateRoot
{
// Your domain logic here
}
```
## Working with Events
[Section titled “Working with Events”](#working-with-events)
### Applying Events
[Section titled “Applying Events”](#applying-events)
To apply events within an aggregate root, use the `Apply` method:
```csharp
public class UserAggregateRoot : AggregateRoot
{
public async Task CreateUser(string firstName, string lastName, string email)
{
// Validation logic here
await Apply(new UserCreated
{
FirstName = firstName,
LastName = lastName,
Email = email
});
}
}
```
### Committing Changes
[Section titled “Committing Changes”](#committing-changes)
After applying events, you need to commit the changes:
```csharp
public async Task HandleCreateUserCommand(CreateUserCommand command)
{
var aggregateRoot = await _aggregateRootFactory.Get(command.UserId);
await aggregateRoot.CreateUser(command.FirstName, command.LastName, command.Email);
await aggregateRoot.Commit();
}
```
## Event Handlers (On Methods)
[Section titled “Event Handlers (On Methods)”](#event-handlers-on-methods)
Chronicle automatically discovers event handler methods in your aggregate root. These methods are called when events are applied or when rehydrating the aggregate from the event store.
### Method Naming Convention
[Section titled “Method Naming Convention”](#method-naming-convention)
Event handler methods can be named with any prefix you prefer, but commonly use `On` or `Handle`:
```csharp
public class UserAggregateRoot : AggregateRoot
{
public void OnUserCreated(UserCreated @event)
{
// Handle the event
}
public Task OnUserNameChanged(UserNameChanged @event, EventContext context)
{
// Async handler with event context
return Task.CompletedTask;
}
}
```
### Handler Method Signatures
[Section titled “Handler Method Signatures”](#handler-method-signatures)
Event handlers can have different signatures:
* `void OnEvent(MyEvent @event)` - Synchronous handler
* `Task OnEvent(MyEvent @event)` - Asynchronous handler
* `void OnEvent(MyEvent @event, EventContext context)` - With event context
* `Task OnEvent(MyEvent @event, EventContext context)` - Async with context
## State Management
[Section titled “State Management”](#state-management)
Chronicle provides multiple approaches for managing state in aggregate roots. For stateful aggregates, you can take a dependency on a read model in the constructor. If there is a projection or reducer for that read model, it will be automatically used to realize the state. The read model will be resolved using the same key as the aggregate root.
### 1. Using Read Models with Projections
[Section titled “1. Using Read Models with Projections”](#1-using-read-models-with-projections)
Take a dependency on a read model in your aggregate root’s constructor. The framework will automatically resolve and populate it based on the aggregate’s event stream:
```csharp
public record UserState(string FirstName, string LastName, bool IsActive);
public class UserProjection : IProjection
{
public void On(UserCreated @event, UserState model, EventContext context)
{
model.FirstName = @event.FirstName;
model.LastName = @event.LastName;
model.IsActive = true;
}
public void On(UserNameChanged @event, UserState model, EventContext context)
{
model.FirstName = @event.FirstName;
model.LastName = @event.LastName;
}
public void On(UserDeactivated @event, UserState model, EventContext context)
{
model.IsActive = false;
}
}
public class UserAggregateRoot : AggregateRoot
{
readonly UserState _state;
public UserAggregateRoot(UserState state)
{
_state = state;
}
public async Task ChangeName(string firstName, string lastName)
{
if (!_state.IsActive)
throw new InvalidOperationException("Cannot change name of inactive user");
await Apply(new UserNameChanged
{
FirstName = firstName,
LastName = lastName
});
}
}
```
### 2. Using Read Models with Reducers
[Section titled “2. Using Read Models with Reducers”](#2-using-read-models-with-reducers)
Similarly, you can use a reducer to build state from events:
```csharp
public record UserState(string FirstName, string LastName, bool IsActive);
public class UserReducer : IReducer
{
public UserState? Reduce(UserState? previous, object @event) => @event switch
{
UserCreated created => new UserState(created.FirstName, created.LastName, true),
UserNameChanged nameChanged => previous with { FirstName = nameChanged.FirstName, LastName = nameChanged.LastName },
UserDeactivated => previous with { IsActive = false },
_ => previous
};
}
public class UserAggregateRoot : AggregateRoot
{
readonly UserState _state;
public UserAggregateRoot(UserState state)
{
_state = state;
}
public async Task ChangeName(string firstName, string lastName)
{
if (!_state.IsActive)
throw new InvalidOperationException("Cannot change name of inactive user");
await Apply(new UserNameChanged
{
FirstName = firstName,
LastName = lastName
});
}
}
```
### 3. Manual State Management (On Methods)
[Section titled “3. Manual State Management (On Methods)”](#3-manual-state-management-on-methods)
You can still manually manage state by handling events in `On` methods. These methods are automatically discovered and called when events are applied or when rehydrating the aggregate:
```csharp
public class UserAggregateRoot : AggregateRoot
{
private string _firstName = string.Empty;
private string _lastName = string.Empty;
private bool _isActive;
public void OnUserCreated(UserCreated @event)
{
_firstName = @event.FirstName;
_lastName = @event.LastName;
_isActive = true;
}
public void OnUserDeactivated(UserDeactivated @event)
{
_isActive = false;
}
public async Task ChangeName(string firstName, string lastName)
{
if (!_isActive)
throw new InvalidOperationException("Cannot change name of inactive user");
await Apply(new UserNameChanged
{
FirstName = firstName,
LastName = lastName
});
}
}
```
> **Note:** The `On` methods are still fully supported and work alongside the read model approach. You can use them for additional side effects or internal state management even when using read models.
## Aggregate Root Factory
[Section titled “Aggregate Root Factory”](#aggregate-root-factory)
Use the `IAggregateRootFactory` to create and retrieve aggregate root instances:
```csharp
public class UserService
{
private readonly IAggregateRootFactory _aggregateRootFactory;
public UserService(IAggregateRootFactory aggregateRootFactory)
{
_aggregateRootFactory = aggregateRootFactory;
}
public async Task CreateUser(EventSourceId userId, string firstName, string lastName, string email)
{
var userAggregate = await _aggregateRootFactory.Get(userId);
await userAggregate.CreateUser(firstName, lastName, email);
await userAggregate.Commit();
}
}
```
## Best Practices
[Section titled “Best Practices”](#best-practices)
1. **Keep aggregates focused** - Each aggregate should represent a single business concept
2. **Validate within aggregates** - Business rules and validation should be enforced in the aggregate
3. **Emit meaningful events** - Events should represent business events, not technical operations
4. **Use appropriate state management** - Choose between read models (with projections/reducers) or manual state management (`On` methods) based on your needs
5. **Handle invariants** - Use the current state to enforce business rules before applying new events
6. **Keep aggregates small** - Large aggregates can lead to performance and concurrency issues
7. **Leverage dependency injection** - Take dependencies on read models to get automatically resolved state based on the aggregate’s event stream
## Error Handling
[Section titled “Error Handling”](#error-handling)
### Reporting Errors from Aggregate Root Methods
[Section titled “Reporting Errors from Aggregate Root Methods”](#reporting-errors-from-aggregate-root-methods)
Aggregate root methods can report structured validation failures using the `Failed` method. Failures are accumulated and included in the `AggregateRootCommitResult` when `Commit` is called.
```csharp
public class Order : AggregateRoot
{
int _quantity;
public async Task AddItem(Guid productId, int quantity)
{
if (quantity <= 0)
{
Failed("Quantity must be greater than zero.");
return;
}
if (_quantity + quantity > 100)
{
Failed("Adding this item would exceed the maximum order quantity.", ValidationResultSeverity.Warning);
}
await Apply(new ItemAdded(productId, quantity));
}
}
```
`Failed` accepts:
* A **message** — the human-readable description of the problem.
* An optional **severity** (`ValidationResultSeverity.Error`, `Warning`, or `Information`). Defaults to `Error`.
**Error** severity failures **prevent the commit** — `Commit()` returns without persisting any events and `IsSuccess` is `false`.
**Warning** and **Information** severity failures are informational only — they are included in the result but do not block the commit.
### Handling the Commit Result
[Section titled “Handling the Commit Result”](#handling-the-commit-result)
```csharp
var result = await order.Commit();
if (!result.IsSuccess)
{
foreach (var validation in result.ValidationResults)
{
Console.WriteLine($"[{validation.Severity}] {validation.Message}");
}
}
```
### Model-Bound Commands
[Section titled “Model-Bound Commands”](#model-bound-commands)
When using the model-bound command pattern, return the `AggregateRootCommitResult` from the `Handle` method. The `AggregateRootCommitResultCommandResponseValueHandler` will automatically convert it to a `CommandResult`, propagating all validation results, constraint violations, and errors back to the API caller.
```csharp
[Command]
public record AddItemToOrder([Key] Guid OrderId, Guid ProductId, int Quantity)
{
public async Task Handle(Order order)
{
await order.AddItem(ProductId, Quantity);
return await order.Commit();
}
}
```
Chronicle provides built-in support for handling validation errors and constraint violations during event application and commit operations. The `AggregateRootCommitResult` contains information about any errors that occurred during the commit process.
```csharp
var result = await aggregateRoot.Commit();
if (!result.IsSuccess)
{
// Handle validation errors or constraint violations
foreach (var error in result.ValidationResults)
{
// Process validation errors
}
}
```
# Aggregate roots in commands
> Take an aggregate root as a Handle() dependency — Arc resolves it from the command's key, rehydrates it from its event stream, and commits what it applies.
A command takes an aggregate root the same way it takes a read model: declare it as a parameter. Arc resolves it from the event source id in the [Command Context](/arc/backend/commands/command-context/), supplied by [Resolving EventSourceId](/arc/backend/chronicle/resolving-event-source-id/).
For when to reach for an aggregate root rather than a read model, see the [Aggregates overview](/arc/backend/chronicle/aggregates/).
## Overview
[Section titled “Overview”](#overview)
Arc automatically registers every aggregate root type as a **command-scoped** service. Resolving one reads the event source id from the current command context and rehydrates the instance from that entity’s event stream, so `Handle()` receives an aggregate whose state already reflects its history.
## Automatic Registration
[Section titled “Automatic Registration”](#automatic-registration)
Aggregate roots are automatically discovered and registered when you configure Arc.
This will scan for all aggregate root types and register them with the dependency injection container.
## Taking Dependencies on Aggregate Roots
[Section titled “Taking Dependencies on Aggregate Roots”](#taking-dependencies-on-aggregate-roots)
You can inject aggregate roots directly into your commands through the Handle method signature:
```csharp
public record AddItemToOrderCommand([Key] Guid OrderId, Guid ProductId, int Quantity, decimal Price)
{
public object Handle(Order order, ILogger logger)
{
order.AddItem(ProductId, Quantity, Price);
// The changes are automatically tracked and will be committed
// when the command handler completes successfully
return new ItemAddedToOrder { ProductId = ProductId, Quantity = Quantity, Price = Price };
}
}
```
## Event Source ID Resolution
[Section titled “Event Source ID Resolution”](#event-source-id-resolution)
The aggregate root resolution depends entirely on the event source ID being available in the command context. [Resolving EventSourceId](/arc/backend/chronicle/resolving-event-source-id/) supplies this value through the [Command Context Values](/arc/backend/commands/command-context/#command-context-values) pipeline. The resolution process works as follows:
1. **Command Context Lookup**: The system retrieves the event source ID from the current `CommandContext`
2. **Validation**: If no event source ID is found, an `UnableToResolveAggregateRootFromCommandContext` exception is thrown
3. **Factory Invocation**: The `IAggregateRootFactory.Get()` method is called with the resolved event source ID
4. **Instance Return**: The loaded aggregate root instance is returned
### Event Source ID Requirements
[Section titled “Event Source ID Requirements”](#event-source-id-requirements)
For aggregate root resolution to work, the command must provide an event source ID through one of these methods:
* Implement `ICanProvideEventSourceId`
* Have a property of type `EventSourceId`
* Have a property marked with `[Key]` attribute
* Be part of a tuple that contains an `EventSourceId`
## Example Usage
[Section titled “Example Usage”](#example-usage)
### Basic Command Handler
[Section titled “Basic Command Handler”](#basic-command-handler)
```csharp
public record CreateUserCommand(EventSourceId UserId, string Email, string Name)
{
public UserCreated Handle(User user)
{
// The 'user' aggregate root is automatically loaded using the UserId
// from the command as the event source ID
user.Create(Email, Name);
return new UserCreated
{
Email = Email,
Name = Name
};
}
}
```
### Update Command Handler
[Section titled “Update Command Handler”](#update-command-handler)
```csharp
public record UpdateUserEmailCommand([Key] Guid UserId, string NewEmail)
{
public UserEmailUpdated Handle(User user)
{
// The 'user' aggregate root is loaded using UserId as event source ID
user.UpdateEmail(NewEmail);
return new UserEmailUpdated { NewEmail = NewEmail };
}
}
```
## Multiple Aggregate Roots
[Section titled “Multiple Aggregate Roots”](#multiple-aggregate-roots)
Note that with the current pattern, you can only automatically resolve one aggregate root per command (based on the event source ID). For scenarios involving multiple aggregates, you’ll need to load additional ones manually:
````csharp
public record TransferFundsCommand(Guid FromAccountId, Guid ToAccountId, decimal Amount) : ICanProvideEventSourceId
{
// This command uses FromAccountId as the primary event source
public EventSourceId GetEventSourceId() => FromAccountId.ToString();
public FundsTransferred Handle(Account fromAccount, IAccountRepository accountRepository)
{
// Load the target account manually since we can only auto-resolve one
var toAccount = accountRepository.GetById(ToAccountId).GetAwaiter().GetResult();
fromAccount.TransferTo(toAccount, Amount);
return new FundsTransferred
{
ToAccountId = ToAccountId,
Amount = Amount
};
}
}
```## Error Handling
### UnableToResolveAggregateRootFromCommandContext
This exception is thrown when:
- No event source ID is available in the command context
- The event source ID is `EventSourceId.Unspecified`
```csharp
public record InvalidCommand(string SomeProperty);
// No event source ID property or interface implementation
// This will fail because no event source ID can be resolved
````
## Lifecycle Management
[Section titled “Lifecycle Management”](#lifecycle-management)
### Command scope
[Section titled “Command scope”](#command-scope)
Aggregate roots are registered as command-scoped services, meaning:
* The instance is resolved once per command and shared for the rest of that command
* It is tied to the specific event source ID from the command context, and rehydrated from that stream on resolution
* Changes made to the aggregate root are automatically tracked
* The aggregate root is released when the command completes
### Automatic commit
[Section titled “Automatic commit”](#automatic-commit)
When using aggregate roots through dependency injection:
* Changes are automatically tracked by Chronicle’s change tracking system
* Events applied by the aggregate root are enrolled in the command’s transaction and committed when the command succeeds
* If the command fails — an exception, a validation failure, or a rejected append — the transaction is rolled back
## Best Practices
[Section titled “Best Practices”](#best-practices)
### Single Responsibility
[Section titled “Single Responsibility”](#single-responsibility)
Keep command handlers focused on a single aggregate root when possible:
```csharp
// Good: Single aggregate root
public record AddItemCommand([Key] Guid OrderId, Guid ProductId, int Quantity)
{
public object Handle(Order order) =>
order.AddItem(ProductId, Quantity);
}
// Consider refactoring: Multiple concerns would require manual loading
```
### Event Source ID Clarity
[Section titled “Event Source ID Clarity”](#event-source-id-clarity)
Make it clear which property serves as the event source ID:
```csharp
// Clear and explicit
public record UpdateOrderCommand(EventSourceId OrderId, string Status); // Obviously the event source ID
// Also clear with Key attribute
public record UpdateOrderCommand([Key] Guid OrderId, string Status); // Marked as the key
```
### Validation
[Section titled “Validation”](#validation)
Validate that the event source ID is meaningful before processing:
````csharp
public record UpdateOrderCommand([Key] Guid OrderId, string Status)
{
public object Handle(Order order)
{
if (order.IsDeleted)
{
throw new OrderAlreadyDeletedException(OrderId);
}
order.UpdateStatus(Status);
return new OrderUpdated { Status = Status };
}
}
```## Advanced Scenarios
### Custom Aggregate Root Resolution
If you need custom resolution logic, you can bypass the automatic injection and use `IAggregateRootFactory` directly:
```csharp
public record SomeCommand(string SomeProperty)
{
public object Handle(IAggregateRootFactory aggregateRootFactory, CommandContext commandContext)
{
var customEventSourceId = DetermineCustomEventSourceId();
var aggregate = aggregateRootFactory.Get(customEventSourceId).GetAwaiter().GetResult();
// Process with custom-loaded aggregate
return new SomeEvent();
}
private EventSourceId DetermineCustomEventSourceId() => EventSourceId.New();
}
```### Conditional Aggregate Loading
```csharp
public record ConditionalCommand([Key] Guid OrderId, bool ShouldProcessOrder)
{
public object Handle(IServiceProvider serviceProvider)
{
if (ShouldProcessOrder)
{
// Only resolve Order when needed
var order = serviceProvider.GetRequiredService();
order.Process();
return new OrderProcessed();
}
return new CommandIgnored();
}
}
````
# Code analysis
> The analyzers that catch Chronicle integration mistakes at build time rather than at runtime.
This section documents the code analysis rules provided by the Chronicle code analyzer for .NET.
Chronicle includes Roslyn analyzers that validate aggregate root event handler signatures at compile time to catch errors early and enforce best practices.
All rules follow the identifier format `ARCCHR####` where the numbers are sequential without gaps.
## Rules Overview
[Section titled “Rules Overview”](#rules-overview)
| Rule ID | Title | Severity | Description |
| -------------------------------------------------------------- | ---------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- |
| [ARCCHR0001](/arc/backend/chronicle/code-analysis/arcchr0001/) | Incorrect aggregate root event handler signature | Error | Aggregate root event handlers must follow allowed `On` method signatures. |
| [ARCCHR0003](/arc/backend/chronicle/code-analysis/arcchr0003/) | Reactor must not reach the default event log | Warning | A reactor appends to the default event log directly instead of returning the events. |
| [ARCCHR0005](/arc/backend/chronicle/code-analysis/arcchr0005/) | Chronicle is used but not wired up | Warning | A project uses Chronicle features but sets up Arc without `WithChronicle()` or `AddCratis()`. |
| [ARCCHR0008](/arc/backend/chronicle/code-analysis/arcchr0008/) | Command key marked with the data annotations Key attribute | Warning | A command marks its key with an attribute Chronicle does not resolve keys from. |
| [ARCCHR0009](/arc/backend/chronicle/code-analysis/arcchr0009/) | Command property reads as a secret and should be marked `[NotAudited]` | Warning | A command carries a property whose name reads like a secret, whose value is written to the causation of every event it appends. |
## Quick Fixes
[Section titled “Quick Fixes”](#quick-fixes)
| Rule ID | Quick fix |
| -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| [ARCCHR0008](/arc/backend/chronicle/code-analysis/arcchr0008/) | **Use the Chronicle Key attribute** — rewrites the attribute to the one Chronicle resolves keys from. |
The other rules have no automatic code fix.
## Installation
[Section titled “Installation”](#installation)
The analyzer is automatically included when you reference Chronicle in your project. No additional configuration is required.
```xml
```
# ARCCHR0001: Incorrect aggregate root event handler signature
> Aggregate root event handlers must follow one of the allowed On method signatures to be discovered.
## Rule
[Section titled “Rule”](#rule)
Event handler methods (typically named `On`) on aggregate roots must accept an event parameter and optionally an `EventContext` parameter, and return `void` or `Task`.
## Severity
[Section titled “Severity”](#severity)
Error
## Allowed Signatures
[Section titled “Allowed Signatures”](#allowed-signatures)
```csharp
void On(TEvent @event)
Task On(TEvent @event)
void On(TEvent @event, EventContext context)
Task On(TEvent @event, EventContext context)
```
## Example
[Section titled “Example”](#example)
### Violation
[Section titled “Violation”](#violation)
```csharp
public class UserAggregateRoot : AggregateRoot
{
// ARCCHR0001: Invalid return type
public string OnUserCreated(UserCreated @event)
{
return "not allowed";
}
// ARCCHR0001: Task is not allowed
public Task OnUserNameChanged(UserNameChanged @event)
{
return Task.FromResult(42);
}
// ARCCHR0001: Too many parameters
public void OnUserUpdated(UserUpdated @event, EventContext context, string extra)
{
}
}
```
### Fix
[Section titled “Fix”](#fix)
```csharp
public class UserAggregateRoot : AggregateRoot
{
public void OnUserCreated(UserCreated @event)
{
// Handle event.
}
public Task OnUserNameChanged(UserNameChanged @event)
{
return Task.CompletedTask;
}
public void OnUserUpdated(UserUpdated @event, EventContext context)
{
// Handle event with context.
}
}
```
## Why This Rule Exists
[Section titled “Why This Rule Exists”](#why-this-rule-exists)
Chronicle discovers event handler methods based on naming and signature conventions. Standardized signatures ensure:
* Handlers are discovered consistently.
* Event processing remains predictable.
* Asynchronous handlers integrate cleanly with the runtime.
## Related Rules
[Section titled “Related Rules”](#related-rules)
* None
# ARCCHR0003: Reactor must not reach the default event log
> A reactor appends to the default event log directly instead of returning the events from its handler method.
## Rule
[Section titled “Rule”](#rule)
A reactor produces side-effect events by **returning** them from its handler method — Chronicle appends what a handler returns. Reaching the default event log yourself performs the same write, but outside the side-effect pipeline.
This rule fires on every way of reaching it:
| Shape | What it looks like |
| ------------------------------- | -------------------------------------------------------------- |
| Injected event log | `MyReactor(IEventLog eventLog) : IReactor` |
| Event log through the store | `eventStore.EventLog.Append(...)` |
| Default log named explicitly | `eventStore.GetEventSequence(EventSequenceId.Log).Append(...)` |
| Enlisted in a unit of work | `eventStore.EventLog.Transactional.Append(...)` |
| Reached with a null-conditional | `eventStore?.EventLog.Append(...)` |
`Transactional` hands back the same sequence enlisted in a unit of work, so the write is identical — the chain is one member longer. Every `Append*` overload counts, `AppendMany` included, and `?.` anywhere in the chain changes nothing.
## Severity
[Section titled “Severity”](#severity)
Warning
## Example
[Section titled “Example”](#example)
### Violation
[Section titled “Violation”](#violation)
```csharp
[Reactor]
public class IncomingInvitationReactor(IEventStore eventStore) : IReactor
{
[OnceOnly]
public Task On(UserInvited @event, EventContext context) =>
// ARCCHR0003: appends to the sequence the return type already targets
eventStore.EventLog.Append(context.EventSourceId, new InvitationIssued(@event.Email));
}
```
### Fix
[Section titled “Fix”](#fix)
```csharp
[Reactor]
public class IncomingInvitationReactor : IReactor
{
[OnceOnly]
public InvitationIssued On(UserInvited @event) => new(@event.Email);
}
```
Return a single event, an `IEnumerable