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`, or `EventForEventSourceId` wrappers when the events belong to a different event source. To trigger work in another slice, inject `ICommandPipeline` and [execute a command](/arc/backend/chronicle/reactors/command-side-effects/). ## When It Does Not Fire [Section titled “When It Does Not Fire”](#when-it-does-not-fire) **Injecting `IEventStore` is not itself a violation.** The rule reports the *append*, not the dependency — a reactor that injects the store to read from it, to reach `IReadModels`, or to route to a different sequence is left alone. Routing to another sequence is the case that matters most: ```csharp [Reactor] public class AcceptanceOutbox(IEventStore eventStore) : IReactor { [OnceOnly] public Task On(InvitationAccepted @event, EventContext context) => eventStore.GetEventSequence(EventSequenceId.Outbox) .Append(context.EventSourceId, @event); } ``` A returned side-effect event is always appended to the **default event log** — neither a bare event nor an `EventForEventSourceId` carries an `EventSequenceId` — so the outbox is not expressible as a return value and the store is the only way to reach it. The rule stays silent whenever `GetEventSequence` names anything other than the default log, including a sequence resolved at runtime. **Another event store is the same kind of exception.** A returned event goes to *this* reactor’s own store, in *this* namespace. A store the reactor obtains at runtime targets a different one, so the rule’s advice would send the event somewhere else entirely: ```csharp [Reactor] public class AuthorReplicator(IChronicleClient client) : IReactor { [OnceOnly] public async Task On(AuthorRegistered @event, EventContext context) { var other = await client.GetEventStore("Reporting", "tenant-x"); await other.EventLog.Append(context.EventSourceId, @event); } } ``` The rule reports an append only through the event store the reactor **holds** — a constructor parameter, a field, or a property. Anything else is treated as a store the reactor went and found. ## What The Rule Cannot See [Section titled “What The Rule Cannot See”](#what-the-rule-cannot-see) The analyzer reads the call in front of it. It does not follow values, so an append the rule would otherwise report goes unreported whenever the thing being appended to arrives from somewhere else in the method: | Shape | Why it is missed | | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | `var log = eventStore.EventLog; log.Append(...)` | The sequence is held in a local. | | `var store = eventStore; store.EventLog.Append(...)` | The store is held in a local, and a local can hold another store as readily as this one. | | `GetEventSequence(sequenceId)` where `sequenceId` is a parameter or a field | The sequence is only known at run time; it may or may not be the default log. | | `GetEventSequence(flag ? EventSequenceId.Log : EventSequenceId.Outbox)` | Same — the analyzer cannot pick a branch. | | An append in a base class that does not itself implement `IReactor` | The rule decides from the type the append sits in, and that type is not a reactor. | | An append in a helper type the reactor delegates to | Same — the helper is not a reactor. | None of this is a suppression mechanism to reach for. It is the honest boundary of a syntactic rule: **it catches the shape you write by accident, not the one you write to get around it.** Every one of these still costs you `ReactorScenario.Produced` at spec time, which is where the absence really bites. ## Existing Code May Now Warn [Section titled “Existing Code May Now Warn”](#existing-code-may-now-warn) ARCCHR0003 previously matched only an injected `IEventLog`. It now also matches an append through an injected `IEventStore`, so a reactor with that shape starts warning on the first build after you upgrade — and if you build with warnings as errors, that is a build break rather than a warning. **The migration is to return the event.** Change the handler from `Task` to the event type and drop the store: ```csharp // Before — warns, and ReactorScenario.Produced sees nothing public Task On(UserInvited @event, EventContext context) => eventStore.EventLog.Append(context.EventSourceId, new InvitationIssued(@event.Email)); // After — Chronicle appends the returned event, and ShouldHaveProduced() works public InvitationIssued On(UserInvited @event) => new(@event.Email); ``` If the append targets a different event source, return `EventForEventSourceId(id, @event)` — or an `IEnumerable` mixing bare events and wrappers. If it targets a different sequence or a different store, the rule does not fire in the first place. To get the build green while you work through it, suppress at the narrowest scope that unblocks you: ```csharp #pragma warning disable ARCCHR0003 eventStore.EventLog.Append(context.EventSourceId, new InvitationIssued(@event.Email)); #pragma warning restore ARCCHR0003 ``` Or turn it off for a folder or the whole project from `.editorconfig`: ```ini [*.cs] dotnet_diagnostic.ARCCHR0003.severity = none ``` `suggestion` and `silent` are the middle settings — the rule keeps reporting in the IDE without failing the build. Caution Suppressing the diagnostic does not make the shape work. The handler still returns nothing Chronicle recognizes as a side effect, so `ReactorScenario.Produced` stays empty and `ShouldHaveProduced()` still throws. Treat a suppression as a note to come back, not as a resolution. ## Why This Rule Exists [Section titled “Why This Rule Exists”](#why-this-rule-exists) The two shapes write the same event to the same sequence, so the rule’s reason applies to both: replay and side-effect semantics stay Chronicle’s concern only while the append goes through the return type. The cost shows up later, in the testing surface. `ReactorScenario.Produced` is *the side effects the reactor returned from its handler methods* — a handler that returns bare `Task` and appends through an injected store produces nothing by that definition, so `ShouldHaveProduced()` throws and the sanctioned assertion surface is simply unavailable. Nothing at authoring time tells you that you have left the contract; this rule does. ## Related Rules [Section titled “Related Rules”](#related-rules) * [ARCCHR0005](/arc/backend/chronicle/code-analysis/arcchr0005/) — Chronicle is used but not wired up # ARCCHR0005: Chronicle is used but not wired up > A project uses Chronicle features but sets Arc up without WithChronicle() or AddCratis(). ## Rule [Section titled “Rule”](#rule) Running Arc **without** Chronicle is a valid, supported setup — `AddCratisArc` on its own backs commands and queries with MongoDB or EF Core and never needs an event store. So a missing `WithChronicle()` is only a problem when the project actually **uses** Chronicle. This rule fires when a project calls `AddCratisArc` (without `WithChronicle()` or `AddCratis()`) yet does one of the following in the same project: * declares an **aggregate root**; * declares a **reactor** or a **reducer**; * declares a **projection** — fluent (`IProjectionFor<>`) or model-bound (a read model with `[FromEvent]`, `[SetFrom]`, `[SetValue]`, or any other `Cratis.Chronicle.Projections` attribute); * declares an **`[EventType]`** event; * injects a Chronicle service such as **`IEventLog`** or **`IEventStore`** (appending events, returning events from a command handler, reading the event store). Detection is by namespace: implementing any interface, or applying any attribute (on the type or its properties), from the `Cratis.Chronicle` namespace counts as Chronicle usage — so new Chronicle features are covered automatically. In any of those cases the event store is required, so a command, query, reactor, or reducer that touches Chronicle fails to resolve at runtime. The rule only reports when the setup call and the Chronicle usage live in the **same project**. When Arc is set up in a separate host project, it stays silent — and when the project genuinely doesn’t use Chronicle, it never fires. ## Severity [Section titled “Severity”](#severity) Warning ## Example [Section titled “Example”](#example) ### Violation [Section titled “Violation”](#violation) ```csharp using Cratis.Chronicle.Events; [EventType] public record AuthorRegistered(string Name); var builder = WebApplication.CreateBuilder(args); // ARCCHR0005: Chronicle artifacts exist, but Chronicle is never wired up builder.AddCratisArc(); var app = builder.Build(); app.UseCratisArc(); app.Run(); ``` ### Fix [Section titled “Fix”](#fix) Add the event store with `WithChronicle()` on the Arc builder: ```csharp builder.AddCratisArc(configureBuilder: arc => arc.WithChronicle()); var app = builder.Build(); app.UseCratisArc(); app.UseCratisChronicle(); app.Run(); ``` Or use the all-in-one `AddCratis()`, which wires Arc, the Chronicle client, and identity together: ```csharp builder.AddCratis(); var app = builder.Build(); app.UseCratis(); app.Run(); ``` ## Why This Rule Exists [Section titled “Why This Rule Exists”](#why-this-rule-exists) `AddCratisArc` deliberately supports [running Arc without an event store](/arc/arc-without-event-sourcing/), backed by MongoDB or EF Core. That flexibility means the framework cannot assume Chronicle is wanted — so forgetting `WithChronicle()` is a silent mistake that only surfaces the first time an event is appended or read. This rule catches it at compile time, before the application runs. ## Related Rules [Section titled “Related Rules”](#related-rules) * None # ARCCHR0008: Command key marked with the data annotations Key attribute > A command in a Chronicle application marks its key with System.ComponentModel.DataAnnotations.KeyAttribute, which Chronicle does not resolve keys from. ## Rule [Section titled “Rule”](#rule) Two attributes are spelled `[Key]`, and which one a command needs depends on whether the application has Chronicle: | Attribute | Resolved by | | ---------------------------------------------------- | ------------------------------------------------------ | | `Cratis.Chronicle.Keys.KeyAttribute` | Chronicle, as the command’s event source id | | `System.ComponentModel.DataAnnotations.KeyAttribute` | Arc — but only in an application with **no** Chronicle | This rule fires when a `[Command]` type marks a property with the data annotations attribute in a project that references Chronicle. It stays silent when the property also carries Chronicle’s attribute, on anything that is not a command, and in a project without Chronicle — where the data annotations attribute is the right one. ## Severity [Section titled “Severity”](#severity) Warning ## Example [Section titled “Example”](#example) ### Violation [Section titled “Violation”](#violation) ```csharp using System.ComponentModel.DataAnnotations; using Cratis.Arc.Commands.ModelBound; [Command] public record RenameCustomer([property: Key] Guid CustomerId, string NewName) { // ARCCHR0008: Chronicle does not resolve keys from this attribute public CustomerRenamed Handle(Customer customer) => new(customer.Id, NewName); } ``` ### Fix [Section titled “Fix”](#fix) ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Chronicle.Keys; [Command] public record RenameCustomer([property: Key] Guid CustomerId, string NewName) { public CustomerRenamed Handle(Customer customer) => new(customer.Id, NewName); } ``` A command whose key is not one property implements `ICanProvideEventSourceId` instead. ## Quick Fix [Section titled “Quick Fix”](#quick-fix) **Use the Chronicle Key attribute** rewrites the attribute for you. It writes the attribute out in full rather than adding a using: the file already has one for the data annotations namespace, and with both in scope a bare `[Key]` is ambiguous (`CS0104`). Remove the data annotations using yourself if nothing else in the file needs it, and the name shortens to `[Key]`. ## Why This Rule Exists [Section titled “Why This Rule Exists”](#why-this-rule-exists) The mistake is invisible without it. The code compiles, and it reads exactly like a command that declares its key — but Chronicle finds no key property, invents a fresh event source id for the command, and every read model keyed by it resolves to nothing. What reaches the client is [`ReadModelDoesNotExistForCommand`](/arc/backend/chronicle/read-models/failures/#readmodeldoesnotexistforcommand) — “The command targets an entity that does not exist” — which points at the data rather than at the attribute, on a request whose entity exists perfectly well. The two attributes are also easy to reach for by accident. The data annotations one is what an Entity Framework Core read model marks its primary key with, so it is already imported in projects that use both, and an editor completing `[Key]` offers whichever namespace is in scope. ## Related Rules [Section titled “Related Rules”](#related-rules) * [ARCCHR0005](/arc/backend/chronicle/code-analysis/arcchr0005/) — Chronicle is used but not wired up ## See also [Section titled “See also”](#see-also) * [Read models from other providers](/arc/backend/chronicle/read-models/other-providers/#declaring-the-key-without-chronicle) — declaring a command’s key with and without Chronicle # ARCCHR0009: Command property reads as a secret and should be marked [NotAudited] > A command carries a property whose name reads like a secret, and its value will be written to the causation of every event the command appends. ## Rule [Section titled “Rule”](#rule) A command’s property values are recorded on the [causation chain](/arc/backend/chronicle/commands/causation/) of every event it appends. This rule fires when a `[Command]` type has a public property whose name contains a word that reads as a secret — `Password`, `Token`, `ApiKey`, `Credential`, `Pin`, `Cvv` and the like — and neither the property, its positional parameter, nor the command is marked `[NotAudited]` or `[PII]`. It matches whole words, not fragments: `PasswordPolicyId` is reported, `Passenger` and `Subtotal` are not. It also judges the type, because a name is weak evidence on its own — `AccessTokenExpiresAt` holds a `DateTimeOffset` and is a timestamp, not a secret, so a property whose type is a date, a duration, a number, a bool, a `Guid` or an enum is never reported however it is named. A concept is judged by the value it wraps, since that is what gets recorded. It stays silent on anything that is not a command, and in a project without Chronicle, where nothing is written to a causation chain at all. ## Severity [Section titled “Severity”](#severity) Warning ## Example [Section titled “Example”](#example) ### Violation [Section titled “Violation”](#violation) ```csharp using Cratis.Arc.Commands.ModelBound; [Command] public record ChangePassword(Guid UserId, string Password) { // ARCCHR0009: 'Password' is written to the causation of every event this command appends public PasswordChanged Handle(IPasswordHasher hasher) => new(hasher.Hash(Password)); } ``` ### Fix [Section titled “Fix”](#fix) ```csharp using Cratis.Arc.Chronicle.Commands; using Cratis.Arc.Commands.ModelBound; [Command] public record ChangePassword(Guid UserId, [property: NotAudited] string Password) { public PasswordChanged Handle(IPasswordHasher hasher) => new(hasher.Hash(Password)); } ``` Marking the positional parameter — `[NotAudited] string Password` — works the same way. A command that exists only to carry secrets is marked once, on the type: ```csharp [Command] [NotAudited] public record ResetCredentials(Guid UserId, string Password, string RecoveryCode); ``` If the value is personal data rather than a secret, mark it `[PII]` instead — Chronicle withholds that from the causation too, and encrypts it in the event. When the secret has its own concept, mark the concept once and every command that takes one is covered: ```csharp [NotAudited] public record ProviderApiKey(string Value) : ConceptAs(Value); ``` ## Quick Fix [Section titled “Quick Fix”](#quick-fix) None. The right response depends on what the value is: `[NotAudited]` for a secret, `[PII]` for personal data, [a suppression](#when-the-rule-is-wrong) for a false positive. The two attributes are not interchangeable — `[PII]` also encrypts the value and enrolls it in erasure, which is wrong for a password, and `[NotAudited]` does nothing for a GDPR request, which is wrong for a name. ## Why This Rule Exists [Section titled “Why This Rule Exists”](#why-this-rule-exists) A name-based guess is normally a poor basis for a diagnostic. It earns its place here because of what is at the other end of it: the causation is written into the event log, the event log is immutable, and a secret recorded there cannot be taken back out by changing code. Fixing it after the fact means redacting events. The cost of a false positive is one attribute; the cost of a miss is permanent. The mistake is also easy to make silently. Adding a property to a command is an ordinary edit, nothing about it says “this is now in the audit trail forever”, and the value only appears somewhere a person would notice — the Workbench, a replay, an export — long after the commit that introduced it. ## When the Rule Is Wrong [Section titled “When the Rule Is Wrong”](#when-the-rule-is-wrong) A property can read as a secret and hold something you do want recorded. **Suppress the diagnostic — do not mark it `[NotAudited]`:** ```csharp [SuppressMessage("Arc.Chronicle", "ARCCHR0009", Justification = "The token's scope names, not the token")] public string TokenScopes { get; init; } ``` `[NotAudited]` would silence the warning by withholding the value, which is a behavior change dressed up as a suppression — the audit trail quietly loses a value you wanted in it. A suppression says “recorded on purpose” and keeps it. ## What It Does Not Catch [Section titled “What It Does Not Catch”](#what-it-does-not-catch) The rule reads names, so it only sees secrets whose names say so. A property called `Value` or `Payload` holding an API key is invisible to it. Treat a clean build as “nothing obvious was missed”, not as “no secrets are recorded” — the decision about what belongs in a permanent audit record is still yours to make when you add the property. ## Related Rules [Section titled “Related Rules”](#related-rules) * [ARCCHR0008](/arc/backend/chronicle/code-analysis/arcchr0008/) — Command key marked with the data annotations Key attribute ## See also [Section titled “See also”](#see-also) * [Causation](/arc/backend/chronicle/commands/causation/) — what a command records, and how to keep a value out of it * [Compliance](/arc/backend/chronicle/compliance/pii/) — marking personal data # Commands > How an Arc command becomes appended events — return shapes, event source identity, subject, and concurrency scoping. Chronicle extends the [Arc command pipeline](/arc/backend/commands/) with event sourcing behavior. The same [model-bound](/arc/backend/commands/model-bound/) command patterns work here — `[Command]` records, `Handle()` methods, `CommandValidator` — with Chronicle adding automatic event appending, event source identity resolution, and metadata-driven concurrency control. ## What Chronicle Adds [Section titled “What Chronicle Adds”](#what-chronicle-adds) A model-bound command handler in the Chronicle context can return events directly from `Handle()`, and Chronicle appends those events to the event log automatically. This keeps command handlers focused on decisions and domain logic rather than event log plumbing. Chronicle also resolves the event source identity, event stream metadata, and concurrency scope from the command record itself — either by convention or via explicit attributes and interfaces. This means the same record that defines your command’s shape also carries all the information Chronicle needs to append events correctly. ## What `Handle()` returns decides what happens [Section titled “What Handle() returns decides what happens”](#what-handle-returns-decides-what-happens) Chronicle inspects the value `Handle()` returns and appends accordingly. Anything it doesn’t recognize as an event (or event metadata) becomes the command’s **response** to the caller — so a tuple lets you append a fact *and* hand something back without ever calling the event log yourself. | `Handle()` returns | What Chronicle does | | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `void` / `Task` | Nothing is appended — the command just ran. | | An `[EventType]` event | Appended to the resolved event source’s log. | | `IEnumerable` of events | Each is appended, in order. | | `EventForEventSourceId` (or a collection of them) | Appended to the event source id carried *in the value*, overriding the resolved one — for writing to a different or several streams. | | `EventsWithConcurrencyScopes` | Appends ordered cross-source events with exact, independently labeled concurrency scopes in the command transaction. | | Tuple `(event, result)` | The event is appended; the other element is returned to the caller as the response. | | Tuple `(EventSourceId, event)` | The `EventSourceId` sets the stream; the event is appended. See [Returning EventSourceId](/arc/backend/chronicle/commands/returning-event-source-id/). | | Tuple `(event, Subject)` | The event is appended; the `Subject` is attached as [compliance metadata](/arc/backend/chronicle/commands/subject/), not returned. | | `Result` (e.g. `Result`) | A failure short-circuits the command (nothing appended); a success is unwrapped and handled like the rows above. | The rule of thumb: **return the fact that happened.** The event source id (see [Resolving EventSourceId](/arc/backend/chronicle/commands/returning-event-source-id/)) decides which stream it lands on. ## Topics [Section titled “Topics”](#topics) | Topic | Description | | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | [Events](/arc/backend/chronicle/commands/events/) | Returning events from commands and how Chronicle appends them automatically, including stream metadata. | | [Setting Subject](/arc/backend/chronicle/commands/subject/) | Supplying a compliance subject on the command or by returning it from `Handle()`. | | [Returning EventSourceId](/arc/backend/chronicle/commands/returning-event-source-id/) | Explicitly deciding the event source id by returning it from a command tuple. | | [Concurrency](/arc/backend/chronicle/commands/concurrency/) | Declaring a concurrency scope on commands using metadata attributes and interfaces. | # Causation > A command's values are recorded on the causation of every event it appends, permanently — and how to keep a value out of that record. Every event a command appends carries a causation chain saying how the work arrived: an HTTP request came in, a command ran, an aggregate root committed. Arc adds a link naming the command, and records **the values that command was asked to act on** alongside the name. Naming the command alone answers “which command produced this event”. Recording the values answers “which invocation” — two purchase orders raised by the same command are otherwise indistinguishable on the chain. Note The causation is written into the event log, and the event log is immutable. A value recorded there stays there for as long as the events do, is read by everything that ever replays them, and **cannot be taken back out by changing code**. Before adding a property to a command, decide whether its value belongs in a permanent audit record — and mark it [`[NotAudited]`](#keeping-a-value-out-of-the-record) if it does not. ## What gets recorded [Section titled “What gets recorded”](#what-gets-recorded) For a command like this: ```csharp [Command] public record RaisePurchaseOrder(PurchaseOrderId OrderId, SupplierId Supplier, decimal Amount) { public PurchaseOrderRaised Handle() => new(Supplier, Amount); } ``` the causation link carries: | Property | Value | | --------------------- | -------------------------------------- | | `commandType` | `RaisePurchaseOrder` | | `commandTypeFullName` | `Acme.Purchasing.RaisePurchaseOrder` | | `eventSequenceId` | `event-log` | | `orderId` | `00000026-0000-0000-0000-0000000000b2` | | `supplier` | `ACME` | | `amount` | `1234.56` | Every readable public instance property is recorded, keyed by its camel-cased name. The values render as you would expect them to read: * A [concept](/fundamentals/csharp/concepts/) records the value it wraps, not the wrapper — `orderId` above is the id, not `{"Value":"…"}`. * Numbers and dates are written invariantly, dates round-trippably (`2026-02-26T11:03:00.0000000+00:00`), so a chain written in one locale reads the same in another. * A nested object or a collection is written as compact JSON. * A property that is not set is left out entirely, rather than recorded as empty. ## Keeping a value out of the record [Section titled “Keeping a value out of the record”](#keeping-a-value-out-of-the-record) Two markings keep a value off the chain. ### Personal data — `[PII]` [Section titled “Personal data — \[PII\]”](#personal-data--pii) Anything Chronicle already treats as personal data is withheld automatically. Nothing extra is needed: mark it as you would anywhere else and it stays out of the causation as well as out of the event. The marking is honored wherever it is written — on the property, on the command, on the positional parameter, and **on the concept**, so a concept marked once carries the marking to every command that uses it: ```csharp [PII("The name of a person")] public record ClaimantName(string Value) : ConceptAs(Value); [Command] public record SubmitClaim(ClaimId ClaimId, ClaimantName Claimant); // claimant is never recorded ``` See [Compliance](/arc/backend/chronicle/compliance/pii/) for the full picture. ### Secrets — `[NotAudited]` [Section titled “Secrets — \[NotAudited\]”](#secrets--notaudited) A password, a token, an API key or a card number is not personal data, so `[PII]` does not describe it and would not keep it out. `[NotAudited]` does: ```csharp [Command] public record ChangePassword( UserId User, [property: NotAudited] string OldPassword, [property: NotAudited] string NewPassword) { public PasswordChanged Handle(IPasswordHasher hasher) => new(hasher.Hash(NewPassword)); } ``` Written on a positional parameter — `[NotAudited] string OldPassword` — it works the same way. Applied to a concept it travels with the value, the same way `[PII]` does — mark it once and every command that takes one is covered: ```csharp [NotAudited] public record ProviderApiKey(string Value) : ConceptAs(Value); ``` Applied to the command itself it excludes every property at once, which is the right answer when a command exists only to carry secrets, and stays right as properties are added to it later: ```csharp [Command] [NotAudited] public record ResetCredentials(UserId User, string Password, string RecoveryCode); ``` The command is still **named** on the chain either way. What is withheld is the values, never the fact that the command ran — an audit trail that hides which commands executed would not be an audit trail. ## The analyzer [Section titled “The analyzer”](#the-analyzer) [ARCCHR0009](/arc/backend/chronicle/code-analysis/arcchr0009/) reports a command property whose name reads like a secret and which is not marked. It is a name-based guess, which is normally a poor basis for a diagnostic — it earns its place here because the cost of a false positive is one attribute and the cost of a miss is a password in the event log forever. It judges the type as well as the name, so a `DateTimeOffset` called `AccessTokenExpiresAt` is left alone — it is a timestamp, not a secret. When it is wrong the other way and a value that reads as a secret should be recorded, [suppress the diagnostic](/arc/backend/chronicle/code-analysis/arcchr0009/#when-the-rule-is-wrong) rather than marking it `[NotAudited]`, which would silence the warning by withholding a value you wanted. It will not catch a secret whose name does not say so. A property called `Value` holding an API key is invisible to it, and to any reviewer reading the model. The analyzer narrows the problem; it does not remove your judgment from it. ## Size [Section titled “Size”](#size) A recorded value is cut short at 1024 characters, marked with `…` where it was cut. The causation travels on **every** event the command appends, so an unbounded value is written once per event — a value long enough to be truncated has stopped being an audit note and become a payload. ## Reading the chain [Section titled “Reading the chain”](#reading-the-chain) In the [Chronicle Workbench](/chronicle/workbench/), an event’s **Context** tab shows its causation. Open `causation`, then an entry’s `properties`, to see what the command was asked to do. ## See also [Section titled “See also”](#see-also) * [ARCCHR0009](/arc/backend/chronicle/code-analysis/arcchr0009/) — the analyzer for unmarked secrets * [Compliance](/arc/backend/chronicle/compliance/pii/) — marking personal data * [Events](/arc/backend/chronicle/commands/events/) — what a command returns # Concurrency > Build a concurrency scope from command metadata so appends participate in optimistic concurrency checks. Chronicle’s [concurrency control](/chronicle/events/concurrency/) prevents conflicting operations from appending events to the same event source simultaneously. A `ConcurrencyScope` defines the boundaries for that check — which stream type, stream id, and event source type form the concurrency boundary. On model-bound commands, you declare concurrency intent directly on the command record using attributes and interfaces. Chronicle then builds the `ConcurrencyScope` automatically when appending the events returned by `Handle()`. No manual scope construction is required. That automatic path is the default. If the decision must remain bound to a revision it already read, return [`EventsWithConcurrencyScopes`](/arc/backend/chronicle/commands/events/#events-with-exact-concurrency-scopes) and supply that exact revision with the returned events. ## Concurrency Metadata Attributes [Section titled “Concurrency Metadata Attributes”](#concurrency-metadata-attributes) Three attributes control concurrency scope declaration on a command. Each attribute serves a dual purpose: it tags the appended events with metadata *and*, when `concurrency: true` is set, contributes that metadata to the concurrency scope. ### `[EventStreamId]` [Section titled “\[EventStreamId\]”](#eventstreamid) Scopes concurrency to a specific event stream id within a stream type. Use this when independent streams within the same stream type should not interfere with each other. ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Chronicle.Events; [Command] [EventStreamId("customer-profile", concurrency: true)] public record UpdateCustomerProfile(EventSourceId CustomerId, string DisplayName) { public CustomerDisplayNameChanged Handle() => new(CustomerId, DisplayName); } [EventType] public record CustomerDisplayNameChanged(EventSourceId CustomerId, string DisplayName); ``` ### `[EventStreamType]` [Section titled “\[EventStreamType\]”](#eventstreamtype) Scopes concurrency to a named stream type. Stream types group related streams — for example, separating `Onboarding` events from `Transactions` for the same customer. ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Chronicle.Events; [Command] [EventStreamType("Transactions", concurrency: true)] public record ProcessPayment(EventSourceId AccountId, decimal Amount) { public PaymentProcessed Handle() => new(AccountId, Amount); } [EventType] public record PaymentProcessed(EventSourceId AccountId, decimal Amount); ``` ### `[EventSourceType]` [Section titled “\[EventSourceType\]”](#eventsourcetype) Scopes concurrency to a named event source type. This is the overarching concept the event source belongs to — for example `Customer` or `BankAccount`. ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Arc.Chronicle.Commands; using Cratis.Chronicle.Events; [Command] [EventSourceType("Customer", concurrency: true)] public record RegisterCustomer(EventSourceId CustomerId, string Email) { public CustomerRegistered Handle() => new(CustomerId, Email); } [EventType] public record CustomerRegistered(EventSourceId CustomerId, string Email); ``` ## Combining Attributes [Section titled “Combining Attributes”](#combining-attributes) You can combine multiple concurrency attributes to build a precise scope. Only the attributes with `concurrency: true` contribute to the scope the command declares — but the others still tag the appended events, and the fallback strategy narrows by whatever tags an append carries, so they are not concurrency-inert either. See [what a routing-only tag already does](#what-a-routing-only-tag-already-does) below. ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Arc.Chronicle.Commands; using Cratis.Chronicle.Events; [Command] [EventStreamId("customer-profile", concurrency: true)] [EventStreamType("Profile", concurrency: true)] [EventSourceType("Customer", concurrency: true)] public record UpdateCustomerProfile(EventSourceId CustomerId, string DisplayName, string Email) { public IEnumerable Handle() => [ new CustomerDisplayNameChanged(CustomerId, DisplayName), new CustomerEmailChanged(CustomerId, Email) ]; } [EventType] public record CustomerDisplayNameChanged(EventSourceId CustomerId, string DisplayName); [EventType] public record CustomerEmailChanged(EventSourceId CustomerId, string Email); ``` If no attribute has `concurrency: true`, the command contributes no scope of its own and the append is left to the concurrency strategy configured on the event sequence — by default the optimistic one, which resolves the expected tail for the event source being appended to, **narrowed by whatever routing metadata the command carries**. ## What a routing-only tag already does [Section titled “What a routing-only tag already does”](#what-a-routing-only-tag-already-does) A metadata attribute declared *without* `concurrency: true` still narrows the concurrency check. Its value reaches the append regardless of the flag, and the fallback strategy resolves the expected tail with the same narrowing — so the flag governs whether the command **declares** a scope, while the tag governs what the check is **narrowed by**. Both are true at once. Three consequences follow, and the third is the surprising one: * **A routing-only tag silently narrows every concurrency check on that command.** `[EventStreamType("Attachments")]` with no flag restricts the expected tail to `Attachments` events, so a concurrent append to the same event source under a different stream type is invisible to the check. * **Declaring `concurrency: true` on *every* metadata attribute a command carries is behaviorally identical to declaring it on none.** The declared scope passes the context values; the fallback passes the same values, with a sentinel standing in for anything absent — and a sentinel adds no filter. Same filter set, same expected tail. * **Declaring it on a *subset* produces a strictly broader scope than declaring it on none.** The declared scope passes `null` for every dimension that did not opt in, while the fallback would have passed its real value. Declaring it on `[EventStreamType]` alone, on a command that also carries `[EventSourceType("X")]`, **drops** the `EventSourceType == "X"` filter and widens the check. Note The practical reading: reach for `concurrency: true` to state intent and to pin which dimensions bound the check, not because its absence leaves the check unbounded. If you want a check bounded by the whole event source, do not tag the command at all. ## Dynamic Event Stream Id [Section titled “Dynamic Event Stream Id”](#dynamic-event-stream-id) When the event stream id is determined at runtime rather than as a constant, implement `ICanProvideEventStreamId` and return the id from `GetEventStreamId()`. ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Arc.Chronicle.Commands; using Cratis.Chronicle.Events; [Command] [EventStreamType("Reporting", concurrency: true)] public record GenerateMonthlyReport(EventSourceId AccountId, string MonthKey) : ICanProvideEventStreamId { public EventStreamId GetEventStreamId() => MonthKey; public MonthlyReportGenerated Handle() => new(AccountId, MonthKey); } [EventType] public record MonthlyReportGenerated(EventSourceId AccountId, string MonthKey); ``` > **Note**: If both a non-empty `[EventStreamId]` value and `ICanProvideEventStreamId` are present on the same command, Chronicle throws an `AmbiguousEventStreamId` exception. Choose one approach, or set the attribute value to `null` to defer to the interface. ## Event Source Id [Section titled “Event Source Id”](#event-source-id) The event source id used when appending is resolved from the command by convention — not from the concurrency scope. See [Event Source Id Resolution](/arc/backend/chronicle/commands/events/#event-source-id-resolution) for the full resolution order, including `ICanProvideEventSourceId`. ## How the Scope Is Built [Section titled “How the Scope Is Built”](#how-the-scope-is-built) When `Handle()` returns events, Chronicle inspects the command type for the three concurrency attributes. It reads the resolved metadata values from the command context and builds a `ConcurrencyScope` covering only the metadata where `concurrency: true` was set. Two properties of that scope decide whether the check actually happens, and both are resolved per append rather than once per command: * **It carries an expected sequence number**, resolved by the same concurrency strategy an unscoped append would use. A scope without one is skipped by the kernel — there is nothing to compare against — so the append would proceed unchecked. * **It is bound to the event source being appended to.** A command that appends across streams gets a scope per target, because an expected tail belongs to exactly one stream; applying one stream’s tail to another would be wrong for both. ## Carrying the revision used by the decision [Section titled “Carrying the revision used by the decision”](#carrying-the-revision-used-by-the-decision) Automatic optimistic concurrency resolves the expected tail while Arc handles the returned value. A concurrent append that lands after `Handle()` read its state but before Arc handles the response is therefore part of the new tail, and the automatic strategy accepts it. When that newer tail would invalidate the decision, capture the revision during the read and return it in `EventsWithConcurrencyScopes`. Arc passes it unchanged into the same command transaction as the ordered events. Interference after the read then produces a concurrency validation failure at commit, with no partial append. This is opt-in. Returning ordinary events or `EventForEventSourceId` values keeps the automatic strategy and its existing behavior. # Events > What a command Handle() can return — a single event, several, a tuple, a Result, or nothing — and what Chronicle does with each. When a [model-bound](/arc/backend/commands/model-bound/) command handler returns an event (or a collection of events), Chronicle appends those events to the event log automatically. This lets you keep command handlers focused on decisions and domain rules instead of event log plumbing. The appends are part of the command’s transaction: all events commit atomically when the command succeeds, and none are appended when it fails — see [Transactional Commands](/arc/backend/commands/transactional-commands/). ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Chronicle.Events; [Command] public record RegisterCustomer(EventSourceId CustomerId, string Email) { public CustomerRegistered Handle() { return new CustomerRegistered(CustomerId, Email); } } [EventType] public record CustomerRegistered(EventSourceId CustomerId, string Email); ``` You can also return multiple events as a collection: ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Chronicle.Events; [Command] public record UpdateCustomerProfile(EventSourceId CustomerId, string DisplayName, string Email) { public IEnumerable Handle() { return new object[] { new CustomerDisplayNameChanged(CustomerId, DisplayName), new CustomerEmailChanged(CustomerId, Email) }; } } [EventType] public record CustomerDisplayNameChanged(EventSourceId CustomerId, string DisplayName); [EventType] public record CustomerEmailChanged(EventSourceId CustomerId, string Email); ``` Chronicle uses the command context to resolve the event source identity and event stream metadata before appending events. ## Event Source Id Resolution [Section titled “Event Source Id Resolution”](#event-source-id-resolution) Chronicle resolves the event source id for commands using a small set of conventions. This value is stored in the command context and is required for event appending. Chronicle resolves the event source id in this order: 1. Implement `ICanProvideEventSourceId` on the command and return the id from `GetEventSourceId()`. 2. Add a property of type `EventSourceId` to the command. 3. Mark a property with `[Key]` and let Chronicle use its value as the event source id. If none of these are present, Chronicle creates a new `EventSourceId` so the command still has a valid identity for event appends. ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Chronicle.Events; using Cratis.Chronicle.Keys; [Command] public record OpenAccount(Guid AccountId, string OwnerName) : ICanProvideEventSourceId { public EventSourceId GetEventSourceId() => AccountId.ToString(); } [Command] public record RenameAccount(EventSourceId AccountId, string NewName); [Command] public record CloseAccount([Key] Guid AccountId); ``` For the full reference, including how Chronicle uses the same identity conventions for query arguments and how you can override the command value by returning `EventSourceId` from `Handle()`, see [Resolving EventSourceId](/arc/backend/chronicle/resolving-event-source-id/) and [Returning EventSourceId](/arc/backend/chronicle/commands/returning-event-source-id/). ## Event Stream Metadata [Section titled “Event Stream Metadata”](#event-stream-metadata) Chronicle supports additional metadata that can be attached to commands and used when appending events. This metadata tags the appended events with the specified stream identity, making them easier to query and react to. ### EventStreamId [Section titled “EventStreamId”](#eventstreamid) Use `[EventStreamId]` to assign a specific event stream id to a command, or implement `ICanProvideEventStreamId` to supply it dynamically. ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Chronicle.Events; [Command] [EventStreamId("customer-profile")] public record UpdateCustomerProfile(EventSourceId CustomerId, string DisplayName, string Email); [Command] public record UpdateCustomerPreferences(EventSourceId CustomerId, string PreferenceKey, string PreferenceValue) : ICanProvideEventStreamId { public EventStreamId GetEventStreamId() => "customer-preferences"; } ``` If both a non-empty `[EventStreamId]` value and `ICanProvideEventStreamId` are used, Chronicle treats this as ambiguous and throws an `AmbiguousEventStreamId` exception. Choose one approach, or set the attribute value to `null` to defer to the interface. ### EventStreamType [Section titled “EventStreamType”](#eventstreamtype) Use `[EventStreamType]` to categorize events under a named stream type. This is useful for grouping related streams, such as separating onboarding events from transaction events for the same event source. ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Arc.Chronicle.Commands; using Cratis.Chronicle.Events; [Command] [EventStreamType("Onboarding")] public record RegisterCustomer(EventSourceId CustomerId, string Email); ``` ### EventSourceType [Section titled “EventSourceType”](#eventsourcetype) Use `[EventSourceType]` to tag events with a specific event source type when they are appended. ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Arc.Chronicle.Commands; using Cratis.Chronicle.Events; [Command] [EventSourceType("Customer")] public record RegisterCustomer(EventSourceId CustomerId, string Email); ``` These metadata attributes categorize and identify the appended events. Because the append carries them, the concurrency strategy configured on the event sequence resolves its expected tail with the same narrowing — so a routing-only tag already bounds the concurrency check, without any attribute opting in. Setting `concurrency: true` chooses which dimensions bound it explicitly; see [concurrency scoping](/arc/backend/chronicle/commands/concurrency/). ## Events for Specific Event Sources [Section titled “Events for Specific Event Sources”](#events-for-specific-event-sources) Sometimes a single command needs to append events to multiple different event sources. The standard approach appends all events to the same event source resolved from the command context, which is fine for the common case. When you need finer control — for example, a fund transfer that debits one account and credits another — use `EventForEventSourceId`. `EventForEventSourceId` is a record that pairs an event with an explicit `EventSourceId`. Chronicle appends each event to its specified event source, independently of the event source id in the command context. Because the command is a [transactional scope](/arc/backend/commands/transactional-commands/), the appends across all the event sources are atomic — if any of them is rejected, none of them land. Return a single `EventForEventSourceId` when only one cross-source event is needed: ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Arc.Chronicle.Commands; using Cratis.Chronicle.Events; [Command] public record MigrateCustomerToNewId(EventSourceId OldCustomerId, EventSourceId NewCustomerId) { public EventForEventSourceId Handle() => new(NewCustomerId, new CustomerMigrated(OldCustomerId, NewCustomerId)); } [EventType] public record CustomerMigrated(EventSourceId OldCustomerId, EventSourceId NewCustomerId); ``` Return an `IEnumerable` to append events to several different event sources in one command: ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Arc.Chronicle.Commands; using Cratis.Chronicle.Events; [Command] public record TransferFunds(EventSourceId FromAccountId, EventSourceId ToAccountId, decimal Amount) { public IEnumerable Handle() => [ new EventForEventSourceId(FromAccountId, new FundsDebited(Amount)), new EventForEventSourceId(ToAccountId, new FundsCredited(Amount)) ]; } [EventType] public record FundsDebited(decimal Amount); [EventType] public record FundsCredited(decimal Amount); ``` Chronicle enrolls the events in order in the command transaction. They commit through one atomic append when the command succeeds. A constraint violation, concurrency conflict, or append error rejects the whole batch and becomes an ordinary failed `CommandResult`; no event from the returned batch lands. You can mix `EventForEventSourceId` values with regular events in a tuple return, letting some events use the command’s own event source while others target specific event sources: ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Arc.Chronicle.Commands; using Cratis.Chronicle.Events; [Command] public record AcceptOrder(EventSourceId OrderId, EventSourceId CustomerId) { public (OrderAccepted, EventForEventSourceId) Handle() => ( new OrderAccepted(OrderId), new EventForEventSourceId(CustomerId, new CustomerOrderAccepted(OrderId)) ); } [EventType] public record OrderAccepted(EventSourceId OrderId); [EventType] public record CustomerOrderAccepted(EventSourceId OrderId); ``` > `EventForEventSourceId` does not share one concurrency scope across targets — a scope carries a single stream’s expected tail, so it cannot be reused for another stream. The command’s concurrency declaration still applies: one scope is built per target event source, with that target’s own expected tail. Each append also uses the stream metadata from the command (stream id, stream type, event source type) while targeting the event source id you supply explicitly. ## Events with exact concurrency scopes [Section titled “Events with exact concurrency scopes”](#events-with-exact-concurrency-scopes) The automatic strategy resolves a target’s expected tail after `Handle()` returns. That is right for ordinary optimistic concurrency. When a command makes its decision from an exact revision it already read, return `EventsWithConcurrencyScopes` to carry that revision with the events instead of resolving a newer tail later. The response contains two values: * the `EventForEventSourceId` values, in append order; and * the exact concurrency scopes the decision depended on, keyed by labels you choose. A label can name an event target, but it does not have to. An independent label lets a command protect a broader fact — for example, the tail of all active-administrator events — while writing to member and invitation streams. Exact revisions that govern authorization or another invariant must never come from request input. Resolve the authoritative revision on the server while handling the command, and construct any independent scope label from a deterministic server-owned value. Otherwise, a caller could choose which version of the protected fact the command validates. ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Chronicle.Events; using Cratis.Chronicle.EventSequences; using Cratis.Chronicle.EventSequences.Concurrency; [Command] public record InviteFirstAdministrator( EventSourceId MemberId, EventSourceId InvitationId) { static readonly EventSourceId AdministratorScope = "active-administrators"; public async Task Handle(IEventLog eventLog) { var activeAdministratorEvent = typeof(AdministratorActivated).GetEventType(); var expectedAdministratorRevision = await eventLog.GetTailSequenceNumber( filterEventTypes: [activeAdministratorEvent]); return new EventsWithConcurrencyScopes( [ new(MemberId, new MemberInvited()), new(InvitationId, new InvitationIssued(MemberId)) ], [ new( AdministratorScope, new ConcurrencyScope( expectedAdministratorRevision, EventTypes: [activeAdministratorEvent])) ]); } } [EventType] public record AdministratorActivated; [EventType] public record MemberInvited; [EventType] public record InvitationIssued(EventSourceId MemberId); ``` Arc enrolls the response in the command’s existing unit of work. The event order, exact scope labels, and exact scope values are passed to Chronicle together; the command does not append immediately. If the protected fact changes between the decision and commit, the command returns a concurrency validation failure and none of its returned events land. Use an exact scope only for a revision the command actually read. `ConcurrencyScope.NotSet` retains the event sequence’s configured strategy for an event-target label, while `ConcurrencyScope.None` deliberately disables checking for that label. An independent label must carry a concrete exact scope or `ConcurrencyScope.None` because there is no target from which Chronicle can infer a scope. # Returning EventSourceId from a command > Hand the generated identity back to the caller when the command is what creates the entity. Return `EventSourceId` from `Handle()` when the command decides which event source to append to at runtime. This is the pattern to use when the command does not already carry the final identity as part of its input. ## Return EventSourceId together with the event [Section titled “Return EventSourceId together with the event”](#return-eventsourceid-together-with-the-event) When `Handle()` returns a tuple that contains both an event and an `EventSourceId`, Chronicle uses the returned id for the automatic append. ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Chronicle.Events; [Command] public record RegisterCustomer(string Email, string DisplayName) { public (EventSourceId, CustomerRegistered) Handle() { var customerId = EventSourceId.New(); return (customerId, new CustomerRegistered(Email, DisplayName)); } } [EventType] public record CustomerRegistered(string Email, string DisplayName); ``` The tuple order does not matter. Chronicle looks for the `EventSourceId` value anywhere in the tuple. ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Chronicle.Events; [Command] public record RegisterCustomer(string Email, string DisplayName) { public (CustomerRegistered, EventSourceId) Handle() { var customerId = EventSourceId.New(); return (new CustomerRegistered(Email, DisplayName), customerId); } } [EventType] public record CustomerRegistered(string Email, string DisplayName); ``` ## Use typed ids [Section titled “Use typed ids”](#use-typed-ids) If your solution uses a type that derives from `EventSourceId`, you can return that type in the tuple as well. ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Chronicle.Events; public record CustomerId(Guid Value) : EventSourceId(Value); [Command] public record RegisterCustomer(string Email) { public (CustomerRegistered, CustomerId) Handle() { var customerId = new CustomerId(Guid.NewGuid()); return (new CustomerRegistered(Email), customerId); } } [EventType] public record CustomerRegistered(string Email); ``` ## Important behavior [Section titled “Important behavior”](#important-behavior) * Chronicle treats the returned `EventSourceId` as append metadata and as the command response value. * If the command record already had an event source id, the returned tuple value wins. * If you need to target several different event sources from one command, use [Events](/arc/backend/chronicle/commands/events/#events-for-specific-event-sources) and return `EventForEventSourceId` values instead. # Setting Subject on commands > Tell Chronicle which compliance identity a command writes under, so PII is encrypted under the right key. Use `Subject` on a Chronicle command when the compliance identity for appended events is different from the event source id. Chronicle passes the resolved subject to the EventStore when it appends events automatically, and Arc uses the same resolved subject when it releases dependent read models injected into the command handler or validator. If you do not provide a subject, Chronicle does not send one explicitly and the EventStore falls back to its normal event source id behavior. ## Set Subject on the command [Section titled “Set Subject on the command”](#set-subject-on-the-command) When the subject is already part of the command, put it on the record itself. Implement `ICanProvideSubject` when you want the subject to be computed: ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Arc.Chronicle.Commands; using Cratis.Chronicle; using Cratis.Chronicle.Events; [Command] public record PlaceOrder(EventSourceId OrderId, CustomerId CustomerId, decimal Amount) : ICanProvideSubject { public Subject GetSubject() => new(CustomerId.Value.ToString()); public OrderPlaced Handle() => new(OrderId, CustomerId, Amount); } [EventType] public record OrderPlaced(EventSourceId OrderId, CustomerId CustomerId, decimal Amount); ``` Use a `Subject` property directly when the command already has the final compliance identity: ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Chronicle; using Cratis.Chronicle.Events; [Command] public record ImportCustomer(EventSourceId CustomerId, Subject Subject, string Email) { public CustomerImported Handle() => new(CustomerId, Email); } [EventType] public record CustomerImported(EventSourceId CustomerId, string Email); ``` Use `[Subject]` when the source value is not already a `Subject`: ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Chronicle; using Cratis.Chronicle.Events; [Command] public record RegisterCustomer(EventSourceId CustomerId, [Subject] Guid PersonId, string Email) { public CustomerRegistered Handle() => new(CustomerId, Email); } [EventType] public record CustomerRegistered(EventSourceId CustomerId, string Email); ``` Chronicle converts the `[Subject]` value to `Subject` by calling `ToString()`. ## Override Subject from Handle() [Section titled “Override Subject from Handle()”](#override-subject-from-handle) Return `Subject` in the tuple from `Handle()` when the subject is decided inside the handler. A returned subject overrides any subject that was resolved from the command itself. ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Chronicle; using Cratis.Chronicle.Events; [Command] public record RegisterDependent(EventSourceId HouseholdId, Guid PersonId, string Name) { public (DependentRegistered, Subject) Handle() => ( new DependentRegistered(HouseholdId, PersonId, Name), new Subject(PersonId.ToString()) ); } [EventType] public record DependentRegistered(EventSourceId HouseholdId, Guid PersonId, string Name); ``` The `Subject` value is append metadata. Chronicle does not treat it as the command response. ## When to use this page [Section titled “When to use this page”](#when-to-use-this-page) This page focuses on how to set subject values on command appends and command-side read model dependencies. For the compliance background and how subject affects PII encryption and decryption, see [Subject](/arc/backend/chronicle/compliance/subject/). # Compliance > PII is encrypted at the event log boundary and decrypted transparently on the way out, so commands and queries stay unaware of it. Event sourcing and the right to erasure look like a contradiction. Events are immutable facts — that is the whole point — and yet someone can demand their personal data be deleted. You cannot rewrite history, but you must be able to make personal data unreadable. Chronicle resolves this by encrypting `[PII]`-annotated properties at the event log boundary with a **per-subject key**. Erasure then means destroying that one key: the events stay exactly where they are, and the personal data inside them becomes permanently unrecoverable. The history is intact, the person is forgotten. Arc’s job is to make this invisible to your code. You never encrypt, never decrypt, never fetch a key. ## What that looks like in practice [Section titled “What that looks like in practice”](#what-that-looks-like-in-practice) Two things have to happen, and both are automatic: * **On the way in**, the command has to carry the compliance *subject* — the identity whose key encrypts the data. Arc resolves it from the command and puts it in the command context. See [Subject](/arc/backend/chronicle/compliance/subject/). * **On the way out**, read models have to be decrypted before a client sees them. Arc’s read model interception pipeline does this for every query type — controller-based, model-bound, and observable. See [PII](/arc/backend/chronicle/compliance/pii/). The same release happens for a read model injected into a command, so a validator sees decrypted values under the same identity that encrypted them. Decryption never breaks a response If the key is gone — after an erasure request — the encrypted value is returned as-is and the failure is logged. Queries keep working; the data is simply unreadable, which is exactly what erasure means. ## Topics [Section titled “Topics”](#topics) | Topic | Description | | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | [PII](/arc/backend/chronicle/compliance/pii/) | Automatic decryption of PII-annotated properties on read models before they are served to clients. | | [Subject](/arc/backend/chronicle/compliance/subject/) | Setting the compliance subject on a command so Chronicle keys PII encryption to the correct identity. | ## How Chronicle compliance works underneath [Section titled “How Chronicle compliance works underneath”](#how-chronicle-compliance-works-underneath) Chronicle encrypts properties annotated with `[PII]` at the event log boundary using a per-subject encryption key. The *subject* is the compliance identity — typically a person rather than an aggregate. When events are projected into read models, encrypted values are stored as-is. Before a read model reaches a client, those values are decrypted with the subject’s key. For the full explanation of annotating types, managing encryption keys, and honoring erasure requests, see the [Chronicle compliance guide](/chronicle/compliance/). # PII > PII-annotated properties on read models are decrypted transparently before any query response, across controller-based, model-bound, and observable queries. Arc automatically decrypts `[PII]`-annotated properties on read models before they are served to clients. This builds on top of the [Read Model Interception](/arc/backend/queries/read-model-interception/) pipeline so decryption is applied consistently across all query types — controller-based, model-bound, and observable (WebSocket and SSE). For the full Chronicle-level guide on annotating types, identifying subjects, and honoring erasure requests, see [Chronicle compliance](/chronicle/compliance/). ## How It Works [Section titled “How It Works”](#how-it-works) Chronicle encrypts `[PII]` properties at the event log boundary under the subject’s encryption key. Arc’s interception pipeline calls `Release()` on `IReadModels` before each query response, decrypting those values transparently — no changes to query methods are needed. Given a read model with a PII-annotated property: ```csharp public record CustomerProfile( [Subject] CustomerId CustomerId, string CompanyName, [PII] string ContactEmail); ``` All query endpoints that return `CustomerProfile` serve decrypted values automatically. ## Behavior on Failure [Section titled “Behavior on Failure”](#behavior-on-failure) `Release()` is intentionally non-breaking: * If the read model has **no PII-annotated properties**, the original instance is returned immediately without contacting the server. * If the encryption key **no longer exists** (e.g. after a right-to-erasure request), the original encrypted instance is returned and an error is logged. No exception is thrown. # Subject > Set the compliance subject on a command so Chronicle encrypts PII under the right identity. The `Subject` is Chronicle’s compliance identity — the value used to key per-subject material such as PII encryption keys. When you append an event that contains `[PII]`-annotated properties, Chronicle encrypts those properties under the subject’s key. Selecting the correct subject on the command ensures that events land under the right encryption key so they can be decrypted later. When no explicit subject is supplied, Chronicle defaults to the `EventSourceId`. Setting a subject on the command is only necessary when the compliance identity differs from the aggregate identity — for example, when a command mutates an *order* aggregate but the PII belongs to the *customer*. ## Resolution Order [Section titled “Resolution Order”](#resolution-order) Arc resolves the subject from the command in this order: 1. **Return a `Subject`** from `Handle()` as part of the response tuple. 2. **Implement `ICanProvideSubject`** on the command record and return the subject from `GetSubject()`. 3. **Add a property of type `Subject`** to the command. 4. **Decorate a property with `[Subject]`** from `Cratis.Chronicle` — Arc reads its value and converts it to a `Subject`. If none of these are present, no subject is passed to Chronicle and it falls back to using the `EventSourceId`. ## Returning Subject from Handle [Section titled “Returning Subject from Handle”](#returning-subject-from-handle) The simplest approach when the subject is computed inside the handler: ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Chronicle; using Cratis.Chronicle.Events; [Command] public record PlaceOrder(EventSourceId OrderId, CustomerId CustomerId, decimal Amount) { public (OrderPlaced, Subject) Handle() => (new OrderPlaced(OrderId, CustomerId, Amount), new Subject(CustomerId.Value.ToString())); } [EventType] public record OrderPlaced(EventSourceId OrderId, CustomerId CustomerId, decimal Amount); ``` Arc detects the `Subject` in the tuple response and passes it to `Append` automatically. The `Subject` is treated as append metadata, so it does not become the command response even when you also return another response value in the same tuple. ## ICanProvideSubject Interface [Section titled “ICanProvideSubject Interface”](#icanprovidesubject-interface) Use `ICanProvideSubject` when the subject is derived from command properties and you want an explicit, discoverable contract: ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Arc.Chronicle.Commands; using Cratis.Chronicle; using Cratis.Chronicle.Events; [Command] public record PlaceOrder(EventSourceId OrderId, CustomerId CustomerId, decimal Amount) : ICanProvideSubject { public Subject GetSubject() => new(CustomerId.Value.ToString()); public OrderPlaced Handle() => new(OrderId, CustomerId, Amount); } ``` ## \[Subject] Attribute on a Property [Section titled “\[Subject\] Attribute on a Property”](#subject-attribute-on-a-property) When a command property directly represents the compliance identity, mark it with `[Subject]` from `Cratis.Chronicle`. Arc reads the property value and converts it to a `Subject`: ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Chronicle; using Cratis.Chronicle.Events; [Command] public record PlaceOrder(EventSourceId OrderId, [Subject] CustomerId CustomerId, decimal Amount) { public OrderPlaced Handle() => new(OrderId, CustomerId, Amount); } ``` If the property is already of type `Subject`, Arc uses it directly even without `[Subject]`. Any other `[Subject]`-annotated type is converted via its `ToString()` representation. ## Relationship to PII Decryption [Section titled “Relationship to PII Decryption”](#relationship-to-pii-decryption) Chronicle uses the subject as the lookup key for PII encryption keys. When a read model contains `[PII]`-annotated properties, Arc uses the same subject in two places: * query responses, where Arc’s [PII](/arc/backend/chronicle/compliance/pii/) interceptor calls `Release()` before the read model is served to the client * command-side read model dependencies, where Arc resolves the subject from the current command context and releases the injected read model before your handler or validator uses it The subject set here at append time must therefore match the subject used on the read model’s `[Subject]`-marked property. # Cratis package > One dependency and two calls that bring up Arc, Chronicle, MongoDB, and identity already agreeing on tenancy, serialization, and hosting. Wiring an event-sourced application by hand means bringing up Arc for commands and queries, Chronicle for the event store, MongoDB for read models, and identity for authentication — and making sure they all agree on tenancy, serialization, and hosting. The `Cratis` package collapses that into one dependency and two calls. ## What is the Cratis Package? [Section titled “What is the Cratis Package?”](#what-is-the-cratis-package) The `Cratis` package is a convenience package that bundles the whole stack: * **Arc Application Framework** — CQRS commands and queries, validation, multi-tenancy, proxy generation * **Chronicle Event Sourcing** — the event store **client** (connecting to a separately running Chronicle server), aggregates, projections, reactors, and reducers * **Swagger/OpenAPI** — automatic API documentation It exists to get you to a running, end-to-end event-sourced application without wiring each component yourself. ## Installation [Section titled “Installation”](#installation) Add the Cratis package to your ASP.NET Core project: ```bash dotnet add package Cratis ``` ## Basic Setup [Section titled “Basic Setup”](#basic-setup) Configure Cratis in your `Program.cs` with one call on the builder and one on the app: ```csharp var builder = WebApplication.CreateBuilder(args); // Add Cratis (Arc + Chronicle) with default configuration builder.AddCratis(); var app = builder.Build(); // Wire up Cratis middleware and endpoints app.UseCratis(); app.Run(); ``` `AddCratis()` registers Arc’s command and query infrastructure, the Chronicle client (which connects to a separately running event store — see [what AddCratis sets up for you](#what-addcratis-sets-up-for-you)), Swagger, and validation/model binding. `UseCratis()` activates both halves — it calls `UseCratisArc()` and `UseCratisChronicle()` for you. ## What AddCratis sets up for you [Section titled “What AddCratis sets up for you”](#what-addcratis-sets-up-for-you) `AddCratis` is opinionated — it makes a few decisions so you don’t have to. Knowing them up front avoids surprises: * **It adds a Chronicle *client* — not the Chronicle engine.** `AddCratis` calls `AddCratisArc` and then `WithChronicle`, and `WithChronicle` registers the Chronicle **client**: a gRPC client that connects to a Chronicle **server running as its own separate process** — the `cratis/chronicle` container you deploy. Your application never runs the event store; it connects to one over gRPC using the connection string from configuration. When you read “Arc and Chronicle in one host,” it’s the *client* that shares your host — the engine runs elsewhere. * **Microsoft Identity Platform authentication is wired automatically** (`AddMicrosoftIdentityPlatformIdentityAuthentication`). If you don’t want identity baked in, wire Arc and Chronicle separately with `AddCratisArc` + `WithChronicle` instead of `AddCratis` — see [Running Arc or Chronicle on their own](#running-arc-or-chronicle-on-their-own). * **Chronicle is tenant-aware by default.** `WithChronicle` resolves the event store namespace per tenant (via `TenantNamespaceResolver`), so every event store is automatically scoped to the active tenant. See [Namespaces](/chronicle/namespaces/) for how the namespace becomes the tenancy boundary. There are always **two processes**: your application (Arc plus the Chronicle client) and the Chronicle server (the event store). `AddCratis` sets up the first and connects it to the second — it never starts the second for you. ``` cratis/chronicle — separate process / container Your application — one host gRPC (connection string) Arc — commands and queries Chronicle client Chronicle engine — the event store ``` Note The same `AddCratis` code connects to a local `cratis/chronicle` container in development and a shared Chronicle instance in production — only the connection string changes between environments. There is no in-process Chronicle to run, and you should not see event-store traffic served from inside your app. If you haven’t started a `cratis/chronicle` container, your app has nothing to connect to. ## How Arc and Chronicle fit together [Section titled “How Arc and Chronicle fit together”](#how-arc-and-chronicle-fit-together) The two halves connect at a single seam: an Arc **command** appends a Chronicle **event**, a Chronicle **projection** turns events into a **read model**, and an Arc **query** serves that read model back to the client. ``` command appends projection query Client Arc command Chronicle event Read model ``` Because Arc and the Chronicle client run in the same host, your application shares the things that would otherwise need to be kept in sync by hand: the **MongoDB** connection that stores read models, the **identity** that authenticates requests and scopes tenancy, and the **hosting** (one Kestrel server, one configuration). That shared wiring is exactly what the `Cratis` package assembles for you. ## Running Arc or Chronicle on their own [Section titled “Running Arc or Chronicle on their own”](#running-arc-or-chronicle-on-their-own) `AddCratis` is the batteries-included front door, but the pieces underneath are independent — take just the part you need: * **Arc without an event store.** Call `AddCratisArc()` on its own and back your commands and queries with MongoDB or EF Core instead of Chronicle. You keep the full CQRS and proxy-generation experience with no event log. See [CQRS without event sourcing](/arc/arc-without-event-sourcing/). * **Arc + Chronicle without the baked-in identity.** Call `AddCratisArc()` and add `WithChronicle()` yourself. This is exactly what `AddCratis` does, minus `AddMicrosoftIdentityPlatformIdentityAuthentication()` — reach for it when you bring your own authentication. ```csharp var builder = WebApplication.CreateBuilder(args); builder.AddCratisArc(configureBuilder: arc => arc.WithChronicle()); var app = builder.Build(); app.UseCratisArc(); app.UseCratisChronicle(); // UseCratis() calls both — wire both halves yourself when you split them app.Run(); ``` Note Running Arc without Chronicle is a valid setup — but only if you don’t use Chronicle. If you call `AddCratisArc()` **without** `WithChronicle()` yet the project uses Chronicle (an aggregate root, reactor, reducer, projection, `[EventType]` event, or a command that injects `IEventLog`), the [ARCCHR0005](/arc/backend/chronicle/code-analysis/arcchr0005/) analyzer flags it at **compile time**. Should it slip through (for example, setup lives in a separate host project), resolution then fails at runtime with a message that points at the same fix: add `WithChronicle()`, or switch to `AddCratis()`. ## Advanced Configuration [Section titled “Advanced Configuration”](#advanced-configuration) You can customize both Arc and Chronicle through the optional configuration callbacks: ```csharp builder.AddCratis( configureArcOptions: options => { // Configure Arc options (ArcOptions) }, configureArcBuilder: arcBuilder => { // Add additional Arc features arcBuilder.WithMongoDB(); }, configureChronicleOptions: options => { // Configure Chronicle options (ChronicleAspNetCoreOptions) options.EventStore = "my-store"; }, configureChronicleBuilder: chronicleBuilder => { // Configure Chronicle features chronicleBuilder.WithCamelCaseNamingPolicy(); }); ``` `options.EventStore` names the Chronicle event store the application connects to. The Chronicle options are bound from the `Cratis:Chronicle` section of `appsettings.json`, so the connection string and other settings come from configuration — see the [ChronicleOptions reference](/chronicle/configuration/chronicle-options/). ## Adding MongoDB Support [Section titled “Adding MongoDB Support”](#adding-mongodb-support) By default, the Cratis package doesn’t include MongoDB support. To use MongoDB with your application, add the MongoDB package separately: ```bash dotnet add package Cratis.Arc.MongoDB ``` Then configure MongoDB using the `WithMongoDB` extension method: ```csharp builder.AddCratis( configureArcBuilder: arcBuilder => { arcBuilder.WithMongoDB(); }); ``` ### MongoDB Configuration Options [Section titled “MongoDB Configuration Options”](#mongodb-configuration-options) You can customize MongoDB settings using the configuration callback: ```csharp builder.AddCratis( configureArcBuilder: arcBuilder => { arcBuilder.WithMongoDB( configureOptions: options => { options.Server = "mongodb://localhost:27017"; options.Database = "my-database"; }); }); ``` ### MongoDB Configuration from appsettings.json [Section titled “MongoDB Configuration from appsettings.json”](#mongodb-configuration-from-appsettingsjson) Alternatively, configure MongoDB settings in `appsettings.json`: ```json { "MongoDB": { "Server": "mongodb://localhost:27017", "Database": "my-database" } } ``` The `WithMongoDB` extension automatically reads these settings from the configuration section. ### Custom Configuration Section Path [Section titled “Custom Configuration Section Path”](#custom-configuration-section-path) If your MongoDB settings are in a different configuration section: ```csharp arcBuilder.WithMongoDB( mongoDBConfigSectionPath: "MyApp:Database:MongoDB"); ``` ## Adding Entity Framework Core Support [Section titled “Adding Entity Framework Core Support”](#adding-entity-framework-core-support) To use Entity Framework Core with your application, add the Entity Framework Core package: ```bash dotnet add package Cratis.Arc.EntityFrameworkCore ``` Once added, you can define and configure your `DbContext` classes as you normally would in Entity Framework Core. Arc automatically discovers and configures registered DbContexts with enhanced features like: * Automatic multi-tenancy support * Integration with Arc’s dependency injection * Streamlined configuration patterns See the [Entity Framework Core](/arc/backend/entity-framework/) documentation for detailed configuration options and best practices. ## A complete Program.cs [Section titled “A complete Program.cs”](#a-complete-programcs) Putting it together, a realistic full-stack host wires Arc + Chronicle with MongoDB read models and a named event store: ```csharp var builder = WebApplication.CreateBuilder(args); builder.AddCratis( configureArcBuilder: arc => arc.WithMongoDB(), configureChronicleOptions: chronicle => chronicle.EventStore = "my-store", configureChronicleBuilder: chronicle => chronicle.WithCamelCaseNamingPolicy()); var app = builder.Build(); app.UseCratis(); app.Run(); ``` This is the same shape the `dotnet new cratis` full-stack template scaffolds. ## Next Steps [Section titled “Next Steps”](#next-steps) Now that you have Cratis set up, you can: * Define [Commands](/arc/backend/commands/) to handle user actions * Create [Queries](/arc/backend/queries/) to retrieve data * Build [Aggregates](/arc/backend/chronicle/aggregates/) to model your domain * Configure [MongoDB](/arc/backend/mongodb/) for read models and projections * Set up [tenancy](/arc/backend/tenancy/) for your application For more advanced scenarios, explore the individual Arc and Chronicle components in the documentation. # React to an event > Run a side effect or trigger a follow-up command automatically when a Chronicle event is appended — using a reactor. **Goal:** when an event-sourced slice records something — a book is added, an author is registered — you want something *else* to happen automatically: a notification goes out, another system is told, a follow-up command runs. That’s a reactor. This is part of Arc’s Chronicle integration. Direct database-backed Arc slices use commands, queries, and ordinary services; reactors become available when the write side records events in Chronicle. ## A reactor does, a projection shows [Section titled “A reactor does, a projection shows”](#a-reactor-does-a-projection-shows) A projection builds queryable state; a reactor *acts*. Where you’d reach for a projection to display data, reach for a reactor to cause an effect. `IReactor` is a marker interface — there’s nothing to override. Arc dispatches to a method by the **type of its first parameter**, so adding an event type is all it takes to subscribe. ## Do it [Section titled “Do it”](#do-it) 1. **For a side effect, call a collaborator.** Inject whatever does the work and handle the event: ```csharp public class NewArrivalsAnnouncer(INewArrivalsFeed feed) : IReactor { public async Task BookAdded(BookAddedToCatalog @event, EventContext context) => await feed.Announce($"New on the shelf: {@event.Title}"); } ``` 2. **To change state, execute a command.** A reactor must never touch the event log directly. When a reaction needs to produce new events, inject `ICommandPipeline` and run a command — it goes through validation and `Handle()` like any other: ```csharp public class CatalogIndexer(ICommandPipeline commands) : IReactor { public Task BookAdded(BookAddedToCatalog @event, EventContext context) => commands.Execute(new IndexBookForSearch(@event.BookId, @event.Title)); } ``` This is how one slice triggers another without either knowing the other’s internals — they meet only at the event. Design for idempotency A reactor may be called more than once for the same event — during replay or recovery. Make its effect safe to run twice, and use the event’s own data rather than querying a read model that may not have caught up. ## See also [Section titled “See also”](#see-also) * [Returning Commands as Side Effects](/arc/backend/chronicle/reactors/) — execute commands as side effects from a reactor. * [Add event sourcing to an Arc slice](/arc/backend/chronicle/add-event-sourcing/) — where reactors enter the Arc model. * [Return a result or an error](/arc/scenarios/return-a-result-or-error/) — what the command you execute can return. # Reactors > Reactors turn a recorded fact into an effect — a notification, an external call, or a follow-up command in another slice. A projection answers “what does this look like now?” A reactor answers “what should happen *because* of this?” When an author is registered, something has to send the welcome email, tell the search index, or kick off the next step — and none of that belongs in the command, which should only record that the fact happened. [React to an event](/arc/backend/chronicle/react-to-an-event/) is the place to start: when to reach for a reactor, how method dispatch works by event type, and why idempotency matters. The topics here go deeper. ## Topics [Section titled “Topics”](#topics) | Topic | Description | | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | [Returning commands as side effects](/arc/backend/chronicle/reactors/command-side-effects/) | Let a reactor trigger follow-up commands by returning them — Arc executes them through the command pipeline automatically. | # Returning commands as side effects > Let a reactor trigger follow-up commands by returning them — Arc executes them through the command pipeline automatically. **Goal:** a reactor sees an event and needs to *change state* in response — index a book, archive a record, notify a downstream slice. The usual way is to inject `ICommandPipeline` and call `Execute` yourself. But when all a reaction does is fire one or more commands, that plumbing is noise. Instead, **return the command from the handler method** and Arc runs it for you. ## Return a command [Section titled “Return a command”](#return-a-command) A reactor method dispatches on the type of its first parameter, and its *return value* becomes the side effect. Return a `[Command]` and Arc executes it: ```csharp [Command] public record CreateSearchIndex(BookId BookId, string Title) { public Task Handle(/* dependencies */) => /* ... */; } public class CatalogIndexer : IReactor { public CreateSearchIndex BookAdded(BookAddedToCatalog @event) => new(@event.BookId, @event.Title); } ``` That is the whole reactor. No `ICommandPipeline`, no `Execute` call. Under the hood Arc detects that the returned value is a command, creates a dedicated service scope, and runs it through the pipeline — validation, authorization, and `Handle()` — exactly as if it had been sent from an HTTP endpoint. The method can also be asynchronous — return `Task` — if you need to compute the command with `await`. ## Return several commands [Section titled “Return several commands”](#return-several-commands) When one event should trigger a handful of commands, return them as a collection: ```csharp public class BookArchiver : IReactor { public IEnumerable BookRemoved(BookRemovedFromCatalog @event) => [ new ArchiveBookMetadata(@event.BookId), new RemoveFromSearchIndex(@event.BookId), new NotifySubscribers(@event.BookId) ]; } ``` The commands run **sequentially, in order, within a single service scope**. If one fails, execution stops there — the commands after it do not run. ## When a command fails [Section titled “When a command fails”](#when-a-command-fails) If a returned command comes back unsuccessful — a validation error, an authorization failure, or an exception thrown from its `Handle()` — the reactor fails for that event. Chronicle then pauses the event source partition for that reactor until the problem is resolved and processing is retried, the same failure behavior as any other reactor that throws. This is not a transaction across commands Returning several commands is a convenience, not an atomic unit of work. Each command is its own [transactional scope](/arc/backend/commands/transactional-commands/) — its events commit or roll back together — but there is no transaction *across* the commands. If the third command fails, the events committed by the first two **remain committed**. If you need all-or-nothing semantics, model the operation as a single command that appends every event together. ## When you need finer control [Section titled “When you need finer control”](#when-you-need-finer-control) Returning commands is deliberately simple: run them, fail the reactor if any fails. When you need to *decide* what happens on failure — log and carry on, run a compensating command, branch on the result — inject `ICommandPipeline` and execute the command yourself so you can inspect the `CommandResult`: ```csharp public class ResilientIndexer(ICommandPipeline commands, ILogger logger) : IReactor { public async Task BookAdded(BookAddedToCatalog @event) { var result = await commands.Execute(new IndexBookForSearch(@event.BookId, @event.Title)); if (!result.IsSuccess) { logger.LogWarning("Could not index book {BookId}", @event.BookId); } } } ``` A single handler method does one or the other — either return commands, or execute them manually and return `Task`. It can’t do both in the same invocation. Design for idempotency A reactor may be called more than once for the same event — during replay or recovery — so the commands it returns may run more than once. Make them safe to re-run, and derive their data from the triggering event rather than from a read model that may not have caught up. ## See also [Section titled “See also”](#see-also) * [React to an event](/arc/backend/chronicle/react-to-an-event/) — the fundamentals of reactors and when to reach for one. * [Commands](/arc/backend/chronicle/commands/) — how Arc commands are defined and executed. # Read models > How Chronicle read models become command-scoped dependencies in Arc — what makes one injectable, how it is resolved by key, and what its lifetime is. A Chronicle read model is current state folded out of events. Arc makes that state available to a command as an ordinary constructor or method parameter, resolved for the key the command already carries — no query, no repository, no manual lookup. This section explains the mechanism. To *use* it, start with [Use current state in a command](/arc/scenarios/use-current-state-in-a-command/) for the recipe, or [Read models in commands](/arc/backend/chronicle/read-models/injecting-into-commands/) for the full reference on each position. ## What makes a read model injectable [Section titled “What makes a read model injectable”](#what-makes-a-read-model-injectable) A read model can be injected into command-scoped code because it is **resolvable by key** — the event source id Arc resolved from the command — through Chronicle’s read model store. That resolvability comes from a Chronicle backing artifact, so a read model is registered when it has one of: * a fluent [`IProjectionFor`](/chronicle/projections/) projection * a model-bound projection (`[FromEvent]`, `[SetFrom]`, `[SetValue]`) * an [`IReducerFor`](/chronicle/reducers/) reducer You declare the dependency identically in every case — which artifact materializes the state is an implementation detail Arc hides. ### `[ReadModel]` alone is not enough [Section titled “\[ReadModel\] alone is not enough”](#readmodel-alone-is-not-enough) The `[ReadModel]` attribute does **not** make a type injectable into commands. `[ReadModel]` is an Arc concept used for queries, and it can be backed by stores other than Chronicle — Entity Framework Core, for example. Injection into command scope only makes sense for a type that is resolvable by key, and key resolution is owned by the backing provider. Chronicle registers the read models it can resolve; a read model backed by another provider is registered by that provider, not by Chronicle. The practical consequence: adding `[ReadModel]` to a record with no projection or reducer behind it will not make it appear in a validator. Add the backing artifact. ## How an instance is resolved [Section titled “How an instance is resolved”](#how-an-instance-is-resolved) ``` Validator · Provide() · Handle()Chronicle read modelsCommand contextCommandValidator · Provide() · Handle()Chronicle read modelsCommand contextCommandalt[instance exists][never projected orremoved][Key] · EventSourceId · ICanProvideEventSourceIdGetInstanceById(type, eventSourceId)release with compliance subjectread model instancenull ``` Step by step: 1. **Identity strategy** — Arc inspects the command to determine its key, using one of the strategies in [Resolving EventSourceId](/arc/backend/chronicle/resolving-event-source-id/). 2. **Command context lookup** — the resolved identity is read from the current `CommandContext`. 3. **Guard** — if no usable identity is available, resolution fails with [`UnableToResolveReadModelFromCommandContext`](/arc/backend/chronicle/read-models/failures/#unabletoresolvereadmodelfromcommandcontext). 4. **Store query** — Chronicle’s read model store is queried by the resolved identity. 5. **Subject release** — if the command context carries a compliance `Subject` and the instance exists, it is released with that subject, so `[PII]` properties decrypt under the same identity used for the events. 6. **Result** — the instance is returned, or `null` when the projection instance does not exist. Resolution happens exactly once per command. The same instance is handed to the validator, `Provide()`, and `Handle()`. ### The key does not prove existence [Section titled “The key does not prove existence”](#the-key-does-not-prove-existence) A `[Key]` or event source id tells Arc *which* instance to resolve; it does not prove that instance exists. A projection may never have been created, may have been removed, or may be mid-rebuild. Whether that is a normal business condition or a fault is yours to declare — see [nullable versus required](/arc/backend/chronicle/read-models/injecting-into-commands/#nullable-means-you-handle-absence). ## Lifetime and mutability [Section titled “Lifetime and mutability”](#lifetime-and-mutability) Read models are registered as **command-scoped** services: * The state is fetched once per command and shared across that command’s validator, `Provide()`, and `Handle()`. * The instance is tied to the identity resolved from the command context. * It is disposed when the command completes. And they are **read-only snapshots**: * **Immutable in practice** — changes made to an injected instance are not persisted anywhere. * **Eventually consistent** — the state reflects events processed so far, not necessarily every event appended. * **Current as of the command** — the fetch happens when the command runs. To *change* state, return events from `Handle()` or use an [aggregate root](/arc/backend/chronicle/aggregates/). A read model is an input to a decision, never the place a decision is recorded. ## Read models or aggregate roots [Section titled “Read models or aggregate roots”](#read-models-or-aggregate-roots) Both give a command access to current state, and they answer different questions. | Reach for | When | | ------------------ | --------------------------------------------------------------------------------------------------------------------------- | | **Read model** | You need projected, possibly denormalized state to validate against or compute from, and eventual consistency is acceptable | | **Aggregate root** | You need to emit events, enforce an invariant inside a consistency boundary, or work from source-of-truth stream state | | **Both** | Validate against projected state, then make the change through the aggregate | If correctness depends on source-of-truth state under concurrency, prefer aggregate or event-stream state over a read model — or enforce it with a Chronicle [constraint](/chronicle/constraints/), which is checked at append time. ## Topics [Section titled “Topics”](#topics) | Topic | Description | | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | | [Read models in commands](/arc/backend/chronicle/read-models/injecting-into-commands/) | Injecting into a validator, `Provide()`, and `Handle()`, and what nullability means. | | [Read models from other providers](/arc/backend/chronicle/read-models/other-providers/) | Injecting one backed by Entity Framework Core or MongoDB, and declaring the key without Chronicle. | | [When resolution fails](/arc/backend/chronicle/read-models/failures/) | Every failure mode, what it means, and how to fix it. | # When read model resolution fails > Every failure mode for a command-scoped Chronicle read model — what the client sees, what it means, and how to fix it. Resolving a read model for a command can fail in a small number of well-defined ways. Each one is a distinct type, and each one is deliberately surfaced as a **validation failure (HTTP 400)** rather than a server error, because every one of them is caused by the request rather than by the server. | Failure | Cause | Client sees | | --------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | [`UnableToResolveReadModelFromCommandContext`](#unabletoresolvereadmodelfromcommandcontext) | The command carries no usable key | HTTP 400 — “The command is missing the identifier required to load its current state.” | | [`ReadModelDoesNotExistForCommand`](#readmodeldoesnotexistforcommand) | Valid key, but a required (non-nullable) read model does not exist | HTTP 400 — “The command targets an entity that does not exist.” | | [`ReadModelValidatorRequiresCommandPipeline`](#readmodelvalidatorrequirescommandpipeline) | The validator ran through MVC model binding, before a command context existed | Request fails | | [`CannotResolveCommandDependency` / `CannotResolveValidatorDependency`](#cannotresolvecommanddependency-and-cannotresolvevalidatordependency) | A required non-nullable dependency could not be resolved | Depends on the dependency | In every case the detailed message — which names the read model type — goes to the server log only. The client sees a generic message, so the type never leaks over the wire. ## UnableToResolveReadModelFromCommandContext [Section titled “UnableToResolveReadModelFromCommandContext”](#unabletoresolvereadmodelfromcommandcontext) The command carried no usable key, so there is nothing to resolve a read model by. With Chronicle that means the resolved identity is `EventSourceId.Unspecified`; without it, that no rule recognized a key on the command. ```csharp // No key: nothing marks one, and the command composes none — // so there is nothing to resolve a read model by. [Command] public record InvalidCommand(string SomeProperty) { public SomethingHappened Handle(Customer customer) => new(); } ``` This is not “the entity does not exist” — it is a command that could never resolve one, for a nullable and a non-nullable parameter alike. Making the parameter nullable does **not** suppress it. **Fix:** give the command a key. What counts as one depends on whether the application has Chronicle: | Setup | Declare the key by | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | With Chronicle | marking a property with `Cratis.Chronicle.Keys.KeyAttribute`, using a property whose type converts to `EventSourceId` (typically a `ConceptAs` with an `implicit operator EventSourceId`), or implementing `ICanProvideEventSourceId` — see [Resolving EventSourceId](/arc/backend/chronicle/resolving-event-source-id/) | | Without Chronicle | marking a property with `System.ComponentModel.DataAnnotations.KeyAttribute`, or implementing `ICanProvideKeyForCommand` — see [Declaring the key without Chronicle](/arc/backend/chronicle/read-models/other-providers/#declaring-the-key-without-chronicle) | A command that marks the data annotations attribute in a Chronicle application fails this way while looking correct — [ARCCHR0008](/arc/backend/chronicle/code-analysis/arcchr0008/) reports it at build time. ## ReadModelDoesNotExistForCommand [Section titled “ReadModelDoesNotExistForCommand”](#readmodeldoesnotexistforcommand) The command carried a valid key, but no read model exists for it — and the dependency was declared **non-nullable**, so Arc cannot inject anything. This is the runtime counterpart of the choice [ARC0006](/arc/backend/code-analysis/arc0006/) asks you to make. Arc treats it as a rejected command rather than a server fault, because “you asked me to act on an entity that isn’t there” is invalid input. **Fix — pick the one that matches your intent:** * **Absence is a business condition.** Make the parameter nullable and write the rule around `null`: ```csharp public class RemoveContactValidator : CommandValidator { public RemoveContactValidator(Customer? customer) => RuleFor(_ => customer) .NotNull() .WithMessage("Customer is not registered"); } ``` You get a specific message instead of the generic one, which is almost always the better experience. * **The projection really is required.** Leave it non-nullable — the HTTP 400 is the intended behavior, and nothing needs to change. If neither fits, the read model may not have caught up yet: it is eventually consistent, so a command issued immediately after the event that creates the projection can arrive first. For an invariant that must hold regardless, use a Chronicle [constraint](/chronicle/constraints/) instead of projected state. ## ReadModelValidatorRequiresCommandPipeline [Section titled “ReadModelValidatorRequiresCommandPipeline”](#readmodelvalidatorrequirescommandpipeline) A `CommandValidator` that depends on a read model was constructed through the **MVC controller** model-validation path. MVC runs validation during model binding — before the command context, and therefore before the event source id, exists. The validator cannot be constructed, so the request fails. This affects MVC controllers only. Minimal-API command endpoints (the Arc default) and direct `ICommandPipeline` execution both establish the command context first and work correctly. **Fix:** expose the command through a minimal-API command endpoint, or move the read-model based check out of the validator and into the command’s `Handle()` method. ## CannotResolveCommandDependency and CannotResolveValidatorDependency [Section titled “CannotResolveCommandDependency and CannotResolveValidatorDependency”](#cannotresolvecommanddependency-and-cannotresolvevalidatordependency) The general case: Arc needed to invoke `Provide()`, `Handle()`, or construct a discoverable validator, and a required non-nullable parameter could not be resolved or resolved to `null`. For a registered read model with a valid event source id, Arc classifies the failure as `ReadModelDoesNotExistForCommand` instead — so if you are seeing these types for a read model parameter, the cause is usually one of: * **The read model has no Chronicle backing artifact.** `[ReadModel]` alone does not register a type for command-scope injection. Add an `IProjectionFor`, a model-bound projection, or an `IReducerFor`. See [what makes a read model injectable](/arc/backend/chronicle/read-models/#what-makes-a-read-model-injectable). * **The parameter is not a read model at all** — an ordinary service that is not registered. Arc deliberately leaves these as server errors rather than masking them as validation failures, so genuine misconfiguration is not hidden behind an HTTP 400. ## See also [Section titled “See also”](#see-also) * [Read models in commands](/arc/backend/chronicle/read-models/injecting-into-commands/) — declaring the dependency and choosing nullability. * [ARC0006](/arc/backend/code-analysis/arc0006/) — the analyzer that surfaces the nullability choice at build time. * [Resolving EventSourceId](/arc/backend/chronicle/resolving-event-source-id/) — how the key is found in the first place. # Read models in commands > Take a Chronicle read model as a dependency in a CommandValidator, a Provide method, or a Handle method — and declare what a missing instance means. A command can take the read model Arc resolved for its key in three places: the constructor of a `CommandValidator`, a `Provide()` method, and a `Handle()` method. All three resolve from the same command scope, so all three see the same instance. Which one to use is a question about *what the state is for*. | Position | Use it when | The state is… | | ---------------------------- | ------------------------------------------------------------------ | ----------------------- | | `CommandValidator` | The command should be rejected with a message | a gate | | `Handle()` | The event you produce is computed from the state | an input | | `Provide()` | The state has to be combined with fetched data before the decision | an input to acquisition | ## In a validator [Section titled “In a validator”](#in-a-validator) Validators run before the handler, which makes them the natural place for state-based rejection. The read model is an ordinary constructor dependency: ```csharp [Command] public record SettleLedger(EventSourceId LedgerId) { public LedgerSettled Handle(LedgerBalance balance) => new(balance.Balance); } public class SettleLedgerValidator : CommandValidator { public SettleLedgerValidator(LedgerBalance balance) => RuleFor(command => command.LedgerId) .Must(_ => balance.Balance > 0) .WithMessage("Ledger has no funds to settle."); } ``` Validators are discovered by convention — there is nothing to register. Their messages reach the client through `CommandResult` like any other validation error. Validators need the Arc command pipeline Read-model injection into a validator works for commands that run through the Arc command pipeline — minimal-API command endpoints (the default) and `ICommandPipeline` directly. It does **not** work through MVC controllers, because MVC model validation runs during model binding, before the command context exists and therefore before there is an event source id to resolve by. The request fails with [`ReadModelValidatorRequiresCommandPipeline`](/arc/backend/chronicle/read-models/failures/#readmodelvalidatorrequirescommandpipeline). Expose the command through a minimal-API endpoint, or move the check into `Handle()`. ## In `Handle()` [Section titled “In Handle()”](#in-handle) When the state is an input to the event rather than a gate on it, take it in the handler: ```csharp [Command] public record UseReducerReadModelInHandle(EventSourceId AccountId) { public BalanceRecorded Handle(ReducerAccountSummary summary) => new(summary.Balance); } ``` This works identically whether `ReducerAccountSummary` is materialized by a reducer, a fluent projection, or a model-bound projection. ## In `Provide()` [Section titled “In Provide()”](#in-provide) `Provide()` acquires the data `Handle()` needs, and its return value is passed to `Handle()` as an argument. A read model can be one of `Provide`’s own inputs: ```csharp [Command] public record ProvideReadModelDependencyCommand(EventSourceId AccountId) { public ProvidedAccountBalance Provide(AccountBalanceReadModel readModel) => new(readModel.Balance); public ReadModelDependencyProvided Handle(ProvidedAccountBalance balance) => new(balance.Value); } ``` Use this shape when the projected state has to be combined with something fetched — a rate, a policy, an external lookup — before `Handle()` can decide. For plain validation, prefer a validator; `Provide()` exists to keep IO out of the decision, not to host rules. See [Provide data to a command handler](/arc/scenarios/provide-data-to-a-command/). ## Nullable means you handle absence [Section titled “Nullable means you handle absence”](#nullable-means-you-handle-absence) The key on a command identifies *which* read model instance to resolve. It does not prove that instance exists. Nullability is how you declare what absence means, and Arc behaves differently for each choice. ### Nullable — absence is a business condition [Section titled “Nullable — absence is a business condition”](#nullable--absence-is-a-business-condition) Declare the parameter nullable when “does not exist” is a state your rule is written around. Arc injects `null` and your code decides: ```csharp [Command] public record RegisterCustomer([Key] Guid CustomerId, string Name); public class RegisterCustomerValidator : CommandValidator { public RegisterCustomerValidator(Customer? customer) => RuleFor(_ => customer) .Null() .WithMessage("Customer is already registered"); } ``` The mirror image — reject when the entity is *missing* — is the same shape with the rule inverted, and `When` guards the rules that dereference it: ```csharp public class AssignPersonToRoleValidator : CommandValidator { public AssignPersonToRoleValidator(RoleReadModel? role) { RuleFor(_ => role) .NotNull() .WithMessage("Role does not exist"); When(_ => role is not null, () => { RuleFor(command => command.PersonId) .Must(personId => !role!.AssignedPersonIds.Contains(personId)) .WithMessage("Person is already assigned to this role"); RuleFor(command => command) .Must(_ => role!.Status == RoleStatus.Active) .WithMessage("Cannot assign people to inactive roles"); }); } } ``` ### Non-nullable — the projection is required [Section titled “Non-nullable — the projection is required”](#non-nullable--the-projection-is-required) Keep the parameter non-nullable when the command genuinely requires the projection and its absence is a fault, not an outcome. Arc then fails the command with [`ReadModelDoesNotExistForCommand`](/arc/backend/chronicle/read-models/failures/#readmodeldoesnotexistforcommand) before your code runs, and you write rules against the state directly: ```csharp [Command] public record SubmitOrder([Key] Guid OrderId); public class SubmitOrderValidator : CommandValidator { public SubmitOrderValidator(OrderReadModel order) { RuleFor(_ => order.Status) .Equal(OrderStatus.ReadyForSubmission) .WithMessage("Only orders that are ready for submission can be submitted"); RuleFor(_ => order.Lines) .NotEmpty() .WithMessage("Order must have at least one line"); } } ``` A missing `OrderReadModel` here is not a validation outcome — it is a rejected command, because the validator declared the projection required. ### The analyzer makes the choice explicit [Section titled “The analyzer makes the choice explicit”](#the-analyzer-makes-the-choice-explicit) [ARC0006](/arc/backend/code-analysis/arc0006/) reports a warning on every non-nullable command-scoped read model parameter, in a validator, `Provide()`, or `Handle()`. It is not saying non-nullable is wrong — it is making sure the required-state choice was made deliberately rather than by default. The same nullability rules apply in all three positions: ```csharp [Command] public record UseNullableReducerReadModelInHandle(EventSourceId AccountId) { public ReadModelAbsenceRecorded Handle(ReducerAccountSummary? summary) => new(summary is null); } ``` ## Combining with an aggregate root [Section titled “Combining with an aggregate root”](#combining-with-an-aggregate-root) A command can take both — projected state as context, and the aggregate as the thing that changes: ```csharp [Command] public record AddItemToCart([Key] Guid CartId, Guid ProductId, int Quantity) { public ItemAddedToCart Handle( ShoppingCart cart, // aggregate root — emits the events ShoppingCartSummary? summary, // read model — projected context ILogger logger) { logger.LogAddingItem(summary?.TotalItems ?? 0); cart.AddItem(ProductId, Quantity); return new ItemAddedToCart(ProductId, Quantity); } } ``` Read models never emit events. If the decision must hold under concurrency, drive it from the aggregate or from a Chronicle [constraint](/chronicle/constraints/) rather than from projected state — read models are eventually consistent. ## Read models from other providers [Section titled “Read models from other providers”](#read-models-from-other-providers) Injection is not Chronicle-only. A read model backed by Entity Framework Core or MongoDB is injected into a command exactly the same way, and everything on this page — the three positions, and what nullability means — applies unchanged. What differs is where the read model is loaded from and what key loads it, including how a command declares its key when there is no Chronicle to resolve one. See [Read models from other providers](/arc/backend/chronicle/read-models/other-providers/). ## Testing [Section titled “Testing”](#testing) Seed the state the command should see with the `Given` builder — either the events behind it or a pinned instance — and execute through the real pipeline. See [Testing with Chronicle](/arc/backend/testing/chronicle/#testing-commands-that-take-read-model-dependencies). ## See also [Section titled “See also”](#see-also) * [Use current state in a command](/arc/scenarios/use-current-state-in-a-command/) — the short recipe. * [When resolution fails](/arc/backend/chronicle/read-models/failures/) — every error and what it means. * [Command validation](/arc/backend/commands/validation/) — the rest of Arc’s validation model. # Read models from other providers > Inject a read model backed by Entity Framework Core or MongoDB into a command, and declare the key it is loaded by when there is no Chronicle to resolve one. Injection is not Chronicle-only. Any provider that owns a read model’s storage can make its `[ReadModel]` types injectable into a command, resolved by the same key, so a validator, `Provide()`, or `Handle()` takes the read model exactly as it would a Chronicle-backed one. This page is about *where the read model comes from* and *what key loads it*. For where to put the parameter and what a nullable one means, see [Read models in commands](/arc/backend/chronicle/read-models/injecting-into-commands/). ## Entity Framework Core [Section titled “Entity Framework Core”](#entity-framework-core) A `[ReadModel]` entity carried by a `ReadOnlyDbContext` becomes injectable once the context is registered — there is nothing extra to wire up: ```csharp [ReadModel] public class Customer { public Guid Id { get; set; } public string Name { get; set; } = string.Empty; } public class CustomerDbContext(DbContextOptions options) : ReadOnlyDbContext(options) { public DbSet Customers => Set(); } ``` ```csharp [Command] public record RenameCustomer([Key] Guid CustomerId, string NewName) { public CustomerRenamed Handle(Customer customer) => new(customer.Id, NewName); } ``` `WithEntityFrameworkCore()` discovers the `ReadOnlyDbContext`, and the command’s resolved key (here the `[Key]` on `CustomerId`) loads the entity by its primary key. The primary key may be a `Guid`, `int`, `long`, `string`, or a `ConceptAs` wrapping one of those. The nullable rules are identical: a nullable `Customer?` receives `null` when no row exists, and a non-nullable `Customer` fails the command with [`ReadModelDoesNotExistForCommand`](/arc/backend/chronicle/read-models/failures/#readmodeldoesnotexistforcommand). ## MongoDB [Section titled “MongoDB”](#mongodb) `WithMongoDB()` does the same for the read models MongoDB holds. There is nothing to declare — a `[ReadModel]` becomes injectable, resolved by the document `_id`: ```csharp [ReadModel] public record Customer(Guid Id, string Name) { public static IEnumerable AllCustomers(IMongoCollection collection) => collection.Find(_ => true).ToList(); } ``` ```csharp [Command] public record RenameCustomer([Key] Guid CustomerId, string NewName) { public CustomerRenamed Handle(Customer customer) => new(customer.Id, NewName); } ``` The id member is whichever one MongoDB maps to `_id` — a member named `Id` by convention, or the one marked `[BsonId]`. Like the EF primary key it may be a `Guid`, `int`, `long`, `string`, or a `ConceptAs` wrapping one of those. A read model with no member mapped to `_id` cannot be resolved by key, and injecting it fails with `MissingIdMapping`. ## Which provider resolves a read model [Section titled “Which provider resolves a read model”](#which-provider-resolves-a-read-model) More than one provider can be able to load the same read model, and the order an application registers them in should not decide the outcome. What decides it is whether an artifact in the application says the provider *owns* the read model: | Provider | Owns a read model when | Claims it as | | --------------------- | ----------------------------------------------------------- | ------------ | | Chronicle | a projection, model-bound projection, or reducer targets it | declared | | Entity Framework Core | a `DbSet` on a `ReadOnlyDbContext` carries it | declared | | MongoDB | — a collection is served for any read model | fallback | A declaring provider always wins, in either registration order. MongoDB claims only what nothing else resolves, and it also leaves your own registration of a read model type alone. This matters beyond tidiness: Chronicle is the provider that releases a read model’s compliance-protected values, so a read model Chronicle projects has to be resolved by Chronicle. ### What else the winner decides [Section titled “What else the winner decides”](#what-else-the-winner-decides) The provider that claims a read model also decides which serialization boundary the injected instance crosses, and the three cross entirely different ones: | Provider | Materializes a command-side read model through | | --------------------- | ---------------------------------------------------- | | Chronicle | a JSON payload deserialized with `System.Text.Json` | | Entity Framework Core | its own entity model | | MongoDB | the driver’s `BsonClassMap` and convention machinery | So whatever customization belongs to one of those boundaries — a convention pack, a class-map customization, an element rename, a custom serializer, a JSON converter — reaches a command-side read model only when its own provider is the one that claimed it. Chronicle and Entity Framework Core both declare. In an application whose read models are owned by either, MongoDB never claims a command-side read model, and no MongoDB serialization customization reaches one — however the MongoDB integration is configured, and in whatever order anything is registered. The same customization can be plainly at work on the query side A convention registered through `ICanProvideMongoDBConventionPacks` goes into the driver’s global registry, so it applies wherever the driver materializes a read model — which includes queries served from an `IMongoCollection`. Seeing it work there says nothing about the command side, and this is the shape the failure takes: the customization looks discovered and correct, because the surface anybody checks first is the one it does reach. To contribute a provider of your own, implement `ICanResolveReadModelForCommand` — reporting the types it resolves, the `ReadModelForCommandOwnership` it claims them with, and how to load one by key — and register it with `services.AddReadModelsForCommand(...)`. ## Declaring the key without Chronicle [Section titled “Declaring the key without Chronicle”](#declaring-the-key-without-chronicle) Every provider loads a read model by the command’s key, and Chronicle is what resolves that key — from `ICanProvideEventSourceId`, from a property assignable to `EventSourceId`, or from one carrying `Cratis.Chronicle.Keys.KeyAttribute`. An application without Chronicle has none of those, so Arc reads the key from the command itself. Mark the property holding it with the data annotations `[Key]`: ```csharp using System.ComponentModel.DataAnnotations; [Command] public record RenameCustomer([property: Key] Guid CustomerId, string NewName) { public CustomerRenamed Handle(Customer customer) => new(customer.Id, NewName); } ``` The key may be a `Guid`, `int`, `long`, `string`, or a `ConceptAs` wrapping one of those — a concept resolves to the value it wraps rather than to its own `ToString()`. When the key is not one property — a composite of two, or a value derived from them — the command declares it: ```csharp [Command] public record MoveItem(Guid CartId, Guid ItemId) : ICanProvideKeyForCommand { public string GetKey() => $"{CartId}/{ItemId}"; } ``` Nothing is inferred from the shape of a command. One carrying two identifiers and marking neither resolves no key, and injection fails as a validation error rather than silently picking one of them. To key commands your own way across an application, implement `ICanResolveKeyForCommand`. It is discovered automatically and asked before the rule Arc ships, whichever order the two happen to be discovered in. Two attributes are spelled `[Key]` In an application **with** Chronicle, the data annotations `[Key]` does nothing. Chronicle resolves keys from `Cratis.Chronicle.Keys.KeyAttribute`, invents a fresh event source id when it finds no key property, and every read model keyed by that command then resolves to nothing. [ARCCHR0008](/arc/backend/chronicle/code-analysis/arcchr0008/) reports it, so this is a build warning rather than a puzzling “the entity does not exist” at runtime. ## See also [Section titled “See also”](#see-also) * [Read models in commands](/arc/backend/chronicle/read-models/injecting-into-commands/) — where to declare the dependency and what nullability means. * [When resolution fails](/arc/backend/chronicle/read-models/failures/) — every error and what it means. * [Arc without event sourcing](/arc/arc-without-event-sourcing/) — the whole slice, with the data stored straight in a collection. # Resolving EventSourceId > The conventions Chronicle uses to find an entity identity on a command or query argument — [Key], EventSourceId-convertible types, and ICanProvideEventSourceId. Chronicle resolves an `EventSourceId` anywhere it needs an identity for an aggregate, event append, or read model lookup. The same conventions work whether the value comes from a command record or from query arguments bound from the HTTP request. For details on how Chronicle stores resolved values in the command pipeline, see [Command Context Values](/arc/backend/commands/command-context/#command-context-values). ## Why Chronicle Resolves EventSourceId [Section titled “Why Chronicle Resolves EventSourceId”](#why-chronicle-resolves-eventsourceid) Chronicle needs an event source id to: * append events to the correct event source * load aggregate roots for model-bound commands * resolve read models used in command handlers and validators * match read model queries to a specific aggregate identity ## Resolution Order for Commands [Section titled “Resolution Order for Commands”](#resolution-order-for-commands) When Chronicle inspects a command, it resolves the event source id in this order: 1. Implement `ICanProvideEventSourceId` and return the id from `GetEventSourceId()`. 2. Add a property whose type is `EventSourceId` or derives from it. 3. Mark a property with `[Key]` and let Chronicle convert that value to `EventSourceId`. If none of these are present, Chronicle creates a new `EventSourceId` so automatic event appends still have a valid identity. ```csharp using Cratis.Arc.Commands.ModelBound; using Cratis.Arc.Chronicle.Commands; using Cratis.Chronicle.Events; using Cratis.Chronicle.Keys; [Command] public record OpenAccount(Guid AccountId, string OwnerName) : ICanProvideEventSourceId { public EventSourceId GetEventSourceId() => AccountId.ToString(); } [Command] public record RenameAccount(EventSourceId AccountId, string NewName); [Command] public record CloseAccount([Key] Guid AccountId); ``` ## Resolution for Query Arguments [Section titled “Resolution for Query Arguments”](#resolution-for-query-arguments) Chronicle uses the same identity conventions when you pass arguments to query methods that target a specific read model instance. In practice this usually means: * an argument of type `EventSourceId` * an argument of a type that derives from `EventSourceId` * an argument or bound property marked with `[Key]` Those arguments can come from route parameters, query string parameters, or request bodies through Arc’s normal query binding rules. ```csharp using Cratis.Arc.Queries.ModelBound; using Cratis.Chronicle.Events; using Cratis.Chronicle.Keys; using MongoDB.Driver; [ReadModel] public record CustomerOverview(EventSourceId Id, string Name) { public static CustomerOverview? ById( EventSourceId id, IMongoCollection collection) => collection.Find(_ => _.Id == id).FirstOrDefault(); public static CustomerOverview? ByLegacyId( [Key] Guid customerId, IMongoCollection collection) => collection.Find(_ => _.Id == customerId.ToString()).FirstOrDefault(); } ``` ## Read Models and Aggregate Roots [Section titled “Read Models and Aggregate Roots”](#read-models-and-aggregate-roots) When you inject a read model or aggregate root into a model-bound command, Chronicle uses the resolved event source id from the current command context to load the correct instance. For command-specific guidance, see: * [Events](/arc/backend/chronicle/commands/events/) * [Setting Subject](/arc/backend/chronicle/commands/subject/) * [Returning EventSourceId from a Command](/arc/backend/chronicle/commands/returning-event-source-id/) # Tenancy > Each tenant's events and projections land in their own Chronicle namespace automatically, resolved from Arc's tenant context with no per-query filtering. The usual way multi-tenancy goes wrong is a forgotten `WHERE TenantId = ...`. One query misses the filter and one tenant sees another’s data. The defense is discipline, applied everywhere, forever. Chronicle removes the filter instead of asking you to remember it. Each tenant gets its own **namespace** — a separate event store partition — so there is no shared table to accidentally read across. Arc wires the two together: whatever resolved Arc’s tenant for the current request also picks the Chronicle namespace, automatically. ## What you write [Section titled “What you write”](#what-you-write) Nothing. Adding the Chronicle integration registers `TenantNamespaceResolver`, and from then on every append, projection, and query runs in the current tenant’s namespace: ```csharp [Command] public record RegisterAuthor(AuthorId Id, AuthorName Name) { public AuthorRegistered Handle() => new(Name); } ``` That command is tenant-aware. There is no tenant parameter, no filter, and no namespace argument — the event lands in the namespace belonging to whoever made the request. ## The mapping rule [Section titled “The mapping rule”](#the-mapping-rule) `TenantNamespaceResolver` implements Chronicle’s `IEventStoreNamespaceResolver` and reads Arc’s current tenant: | Arc tenant context | Chronicle namespace | | ----------------------------------------- | -------------------------------------------------- | | A tenant is resolved | the tenant id, used verbatim as the namespace name | | No tenant is resolved (`TenantId.NotSet`) | `EventStoreNamespaceName.Default` | The fallback matters: a request with no tenant does not fail and does not leak across tenants — it uses the default namespace. Single-tenant applications therefore need no tenancy configuration at all; they simply always use the default. Isolation follows the tenant resolver, not Chronicle Chronicle isolates by whatever tenant id Arc hands it. If the resolver picks the wrong tenant — a spoofable header, say — Chronicle will faithfully write to the wrong namespace. The security boundary lives in how the tenant is resolved, so choose that resolver deliberately. ## Configure how the tenant is resolved [Section titled “Configure how the tenant is resolved”](#configure-how-the-tenant-is-resolved) Chronicle consumes Arc’s tenant resolution rather than defining its own, so the interesting decision — where the tenant comes from — is an Arc-level one: a claim, a header, a subdomain, or a custom resolver. See [Tenancy](/arc/backend/tenancy/) for the resolver choices and configuration, and [Namespaces](/chronicle/namespaces/) for what a Chronicle namespace is and how it partitions the event store. # Validation with read models > Validate an event-sourced command against the state Chronicle already projected for its key, without writing a query. Arc’s [validation model](/arc/backend/commands/validation/) — data annotations, `ConceptValidator`, and `CommandValidator` — works unchanged when Chronicle is in the picture. What the integration adds is one thing: a validator can take the **read model Chronicle projected for the command’s key** as a constructor dependency, and validate against current state without a query. ```csharp public class SettleLedgerValidator : CommandValidator { public SettleLedgerValidator(LedgerBalance balance) => RuleFor(command => command.LedgerId) .Must(_ => balance.Balance > 0) .WithMessage("Ledger has no funds to settle."); } ``` Arc resolves `LedgerBalance` for the same event source id the command appends events to, then constructs the validator with it. The flow is: 1. The command is bound, and its event source id is resolved — from `[Key]`, from a property that converts to `EventSourceId`, or from `ICanProvideEventSourceId`. See [Resolving EventSourceId](/arc/backend/chronicle/resolving-event-source-id/), which contributes the value to the [Command Context Values](/arc/backend/commands/command-context/#command-context-values). 2. The read model instance is loaded from Chronicle’s read model store by that id. 3. Validators are constructed with it and their rules run — before `Handle()` is invoked. Because the same command scope serves `Provide()` and `Handle()`, all three see the same instance. ## What to be aware of [Section titled “What to be aware of”](#what-to-be-aware-of) * **A key does not prove existence.** Declare the parameter nullable when a missing projection is a business condition, non-nullable when it is required. This is the central decision — see [nullable versus required](/arc/backend/chronicle/read-models/injecting-into-commands/#nullable-means-you-handle-absence). * **Read models are eventually consistent.** They are the right input for gating on projected state, and the wrong one for an invariant that must hold under concurrent commands. Use a Chronicle [constraint](/chronicle/constraints/) for those. * **Validators need the Arc command pipeline.** Read-model injection does not work through MVC controllers — see [`ReadModelValidatorRequiresCommandPipeline`](/arc/backend/chronicle/read-models/failures/#readmodelvalidatorrequirescommandpipeline). ## See also [Section titled “See also”](#see-also) * [Use current state in a command](/arc/scenarios/use-current-state-in-a-command/) — the recipe, covering validators, `Provide()`, and `Handle()`. * [Read models in commands](/arc/backend/chronicle/read-models/injecting-into-commands/) — the full reference for all three positions. * [Command validation](/arc/backend/commands/validation/) — Arc’s validation model in general. # Code Analysis Rules This section documents the code analysis rules provided by the Arc.Core code analyzer for .NET. Arc.Core includes Roslyn analyzers that validate Arc constructs at compile time to catch errors early and enforce best practices. All rules follow the identifier format `ARC####` where the numbers are sequential without gaps. ## Rules Overview [Section titled “Rules Overview”](#rules-overview) | Rule ID | Title | Severity | Description | | ---------------------------------------------- | --------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------- | | [ARC0001](/arc/backend/code-analysis/arc0001/) | Incorrect query method signature on ReadModel | Error | ReadModel query methods must return the ReadModel type or an allowed wrapper of it. | | [ARC0002](/arc/backend/code-analysis/arc0002/) | Missing \[Command] attribute on command-like type | Warning | Command-like types must be marked with `[Command]` to be recognized as commands. | | [ARC0003](/arc/backend/code-analysis/arc0003/) | Handle() must be on \[Command] type | Error | Public command `Handle()` methods must be declared on the command type itself. | | [ARC0004](/arc/backend/code-analysis/arc0004/) | \[Command] type must have public Handle() method | Error | Types marked with `[Command]` must declare a public instance `Handle()` method. | | [ARC0005](/arc/backend/code-analysis/arc0005/) | Value produced by Provide is not consumed by Handle | Warning | Every value a command’s `Provide()` method produces must be consumed by a `Handle()` parameter. | | [ARC0006](/arc/backend/code-analysis/arc0006/) | Command-scoped read model can be missing | Warning | Non-nullable command-scoped read model parameters should be a deliberate required-state choice. | ## Quick Fixes [Section titled “Quick Fixes”](#quick-fixes) No automatic code fixes are currently provided for these rules. ## Installation [Section titled “Installation”](#installation) The analyzer is automatically included when you reference Arc.Core in your project. No additional configuration is required. ```xml ``` # ARC0001: Incorrect query method signature on ReadModel ## Rule [Section titled “Rule”](#rule) Query methods on types with `[ReadModel]` must return the ReadModel type, a collection, `Task`, `IAsyncEnumerable`, or `ISubject` of the ReadModel type. ## Severity [Section titled “Severity”](#severity) Error ## Valid Return Types [Section titled “Valid Return Types”](#valid-return-types) Query methods must return one of the following: ```csharp ReadModel IEnumerable List ReadModel[] Task Task> IAsyncEnumerable ISubject ISubject> ``` ## Example [Section titled “Example”](#example) ### Violation [Section titled “Violation”](#violation) ```csharp [ReadModel] public class User { public Guid Id { get; set; } public string Name { get; set; } // ARC0001: Invalid return type public static string GetName(Guid id) { return "name"; } } ``` ### Fix [Section titled “Fix”](#fix) ```csharp [ReadModel] public class User { public Guid Id { get; set; } public string Name { get; set; } public static User GetById(Guid id) => new(); public static IEnumerable GetAll() => []; public static Task GetByIdAsync(Guid id) => Task.FromResult(new User()); } ``` ## Why This Rule Exists [Section titled “Why This Rule Exists”](#why-this-rule-exists) ReadModel query methods form the public query surface of a ReadModel. Standardized return types ensure: * The runtime can recognize and execute queries consistently. * Query results are predictable and can be composed with async patterns. * Streaming and reactive query patterns are supported where appropriate. ## Related Rules [Section titled “Related Rules”](#related-rules) * [ARC0002](/arc/backend/code-analysis/arc0002/): Missing \[Command] attribute on command-like type # ARC0002: Missing [Command] attribute on command-like type ## Rule [Section titled “Rule”](#rule) Types that look like commands (properties plus `Handle` methods) must be marked with `[Command]` to be recognized as commands. ## Severity [Section titled “Severity”](#severity) Warning ## Example [Section titled “Example”](#example) ### Violation [Section titled “Violation”](#violation) ```csharp public record CreateUser { public string Name { get; set; } // ARC0002: Missing [Command] attribute public void Handle() { // Command logic } } ``` ### Fix [Section titled “Fix”](#fix) ```csharp [Command] public record CreateUser { public string Name { get; set; } public string Email { get; set; } public void Handle() { // Command logic } } ``` ```csharp [Command] public record CreateUser { public string Name { get; set; } public UserCreatedResult Handle() { return new UserCreatedResult { UserId = Guid.NewGuid() }; } } ``` ## Why This Rule Exists [Section titled “Why This Rule Exists”](#why-this-rule-exists) Commands are identified by the `[Command]` attribute. Without it: * Handlers are not discovered by the runtime. * Validation and metadata generation are incomplete. * Command routing becomes inconsistent across the system. ## Related Rules [Section titled “Related Rules”](#related-rules) * [ARC0001](/arc/backend/code-analysis/arc0001/): Incorrect query method signature on ReadModel # ARC0003: Handle() must be on [Command] type ## Rule [Section titled “Rule”](#rule) Public `Handle()` methods that operate on a `[Command]` type must be declared on the command type itself. ## Severity [Section titled “Severity”](#severity) Error ## Example [Section titled “Example”](#example) ### Violation [Section titled “Violation”](#violation) ```csharp using Cratis.Arc.Commands.ModelBound; [Command] public record CreateOrder(string OrderId); public class CreateOrderHandler { // ARC0003: Handle() for CreateOrder is not on the command type itself public void Handle(CreateOrder command) { } } ``` ### Fix [Section titled “Fix”](#fix) ```csharp using Cratis.Arc.Commands.ModelBound; [Command] public record CreateOrder(string OrderId) { public void Handle() { } } ``` # ARC0004: [Command] type must have public Handle() method ## Rule [Section titled “Rule”](#rule) Any type marked with `[Command]` must declare a public instance `Handle()` method. ## Severity [Section titled “Severity”](#severity) Error ## Example [Section titled “Example”](#example) ### Violation [Section titled “Violation”](#violation) ```csharp using Cratis.Arc.Commands.ModelBound; [Command] public record CreateOrder { public string OrderId { get; set; } } ``` ```csharp using Cratis.Arc.Commands.ModelBound; [Command] public record CreateOrder { internal void Handle() { } } ``` ### Fix [Section titled “Fix”](#fix) ```csharp using Cratis.Arc.Commands.ModelBound; [Command] public record CreateOrder { public string OrderId { get; set; } public void Handle() { } } ``` # ARC0005: Value produced by Provide is not consumed by Handle ## Rule [Section titled “Rule”](#rule) When a command declares a `Provide()` method, every value it produces must be consumed by a parameter of the command’s `Handle()` method. A produced value that no `Handle` parameter can receive is almost always a mistake. Control values that short-circuit execution rather than feed `Handle` — `ValidationResult`, `AuthorizationResult`, and `CommandResult` — are exempt. ## Severity [Section titled “Severity”](#severity) Warning ## Example [Section titled “Example”](#example) ### Violation [Section titled “Violation”](#violation) ```csharp using Cratis.Arc.Commands.ModelBound; [Command] public record ApproveLoan(ApplicantId Applicant) { public CreditScore Provide(ICreditBureau bureau) => bureau.GetScore(Applicant); public void Handle() { } } ``` `Provide` returns a `CreditScore`, but `Handle` has no parameter that can receive it. ### Fix [Section titled “Fix”](#fix) ```csharp using Cratis.Arc.Commands.ModelBound; [Command] public record ApproveLoan(ApplicantId Applicant) { public CreditScore Provide(ICreditBureau bureau) => bureau.GetScore(Applicant); public LoanApproved Handle(CreditScore creditScore) => new(Applicant, creditScore); } ``` See [Provide data to a command handler](/arc/scenarios/provide-data-to-a-command/) for the full pattern. # ARC0006: Command-scoped read model can be missing ## Rule [Section titled “Rule”](#rule) Command-scoped read models can be missing. A `[Key]` or event source id tells Arc which read model instance to resolve, but it does not prove that the projection instance exists. A projection may never have been created, may have been removed, may be temporarily unavailable during projection rebuild, or may not be the source of truth for the invariant being checked. This rule is a warning to make that choice explicit. Declare the parameter nullable when the command handles missing projected state as part of normal behavior. Keep it non-nullable when missing state should fail as a required dependency. This rule reports non-nullable read model parameters in: * constructors of `CommandValidator` types * public instance `Provide()` methods on `[Command]` types * public instance `Handle()` methods on `[Command]` types The analyzer recognizes read models marked with `[ReadModel]` and read models discovered through Chronicle `IProjectionFor` projections. ## Severity [Section titled “Severity”](#severity) Warning ## Example [Section titled “Example”](#example) ### Reported Code [Section titled “Reported Code”](#reported-code) ```csharp using Cratis.Arc.Commands; using Cratis.Arc.Commands.ModelBound; using Cratis.Arc.Queries.ModelBound; using Cratis.Chronicle.Keys; [Command] public record RemoveContact([Key] Guid CustomerId, Guid ContactId); public class RemoveContactValidator : CommandValidator { public RemoveContactValidator(Customer customer) { } } [ReadModel] public class Customer { } ``` `Customer` is a command-scoped read model. If it does not exist for the command’s event source id, Arc cannot inject a value into the non-nullable validator parameter, so the command fails with a dependency-resolution error. ### Option 1: Handle Missing State [Section titled “Option 1: Handle Missing State”](#option-1-handle-missing-state) Use a nullable read model dependency when missing projected state is a valid command outcome. For example, a registration command can use the absence of a `Customer` read model to allow registration, and reject only when the customer already exists: ```csharp using Cratis.Arc.Commands; using Cratis.Arc.Commands.ModelBound; using Cratis.Chronicle.Keys; [Command] public record RegisterCustomer([Key] Guid CustomerId, string Name); public class RegisterCustomerValidator : CommandValidator { public RegisterCustomerValidator(Customer? customer) { RuleFor(_ => customer) .Null() .WithMessage("Customer is already registered"); } } ``` ### Option 2: Require Existing State [Section titled “Option 2: Require Existing State”](#option-2-require-existing-state) Keep the parameter non-nullable when the command really requires the read model to exist and absence should fail as a programming, configuration, projection, or consistency error. In that case, no code change is needed; the warning exists so the required-state choice is reviewed deliberately. For example, a command that submits an existing order may require the order projection before any state rules can be evaluated: ```csharp using Cratis.Arc.Commands; using Cratis.Arc.Commands.ModelBound; using Cratis.Chronicle.Keys; [Command] public record SubmitOrder([Key] Guid OrderId); public class SubmitOrderValidator : CommandValidator { public SubmitOrderValidator(OrderReadModel order) { RuleFor(_ => order.Status) .Equal(OrderStatus.ReadyForSubmission) .WithMessage("Only orders that are ready for submission can be submitted"); RuleFor(_ => order.Lines) .NotEmpty() .WithMessage("Order must have at least one line"); } } ``` The same rule applies if a command’s `Provide()` or `Handle()` method takes a command-scoped read model parameter. Make the parameter nullable when missing projected state is part of the command’s valid behavior. Keep it non-nullable when missing state should fail as a required dependency. ## Why This Rule Exists [Section titled “Why This Rule Exists”](#why-this-rule-exists) Chronicle read models represent current projected state. Missing projected state can be meaningful: an entity may not be registered yet, may already have been removed, or may not have reached a state represented by that projection. It can also be exceptional: the command may target an event source that should already have a projection. Before the command runs, Arc resolves command-scoped read models from the command context. Nullable read model parameters allow validators, `Provide()`, and `Handle()` to receive `null` and make an explicit decision. Non-nullable parameters require Arc to throw `CannotResolveCommandDependency` or `CannotResolveValidatorDependency` when the read model cannot be resolved or resolves to `null`. If correctness depends on source-of-truth state, prefer aggregate or event-stream state over a read-model validator. Read models are best used for projected-state checks where the projection is the right input to the decision. ## Related Documentation [Section titled “Related Documentation”](#related-documentation) * [Use current state in a command](/arc/scenarios/use-current-state-in-a-command/) * [Read models in commands](/arc/backend/chronicle/read-models/injecting-into-commands/) * [When read model resolution fails](/arc/backend/chronicle/read-models/failures/) * [Command validation](/arc/backend/commands/validation/) * [Provide data to a command handler](/arc/scenarios/provide-data-to-a-command/) # Commands A command is how something *asks your system to change* — open an account, check out a book, change an address. In Arc a command is a small, intent-revealing record that carries the data for that change and knows how to handle itself. There’s no separate handler class, no controller boilerplate to write: you declare the intent and what it does, and Arc wires up the HTTP endpoint, validation, and a typed TypeScript proxy for the frontend. ``` POST binds + validates returns typed proxy React UI Arc endpoint Command record + Handle() CommandResult ``` ## Your first command [Section titled “Your first command”](#your-first-command) Here’s the whole thing — the command, its data, and its behavior, in one record: ```csharp public interface IAccounts { Task Open(AccountId id, AccountHolder owner); } [Command] public record OpenAccount(AccountId Id, AccountHolder Owner) { public Task Handle(IAccounts accounts) => accounts.Open(Id, Owner); } ``` `Handle()` is defined **directly on the record** — that’s the convention. Arc discovers it, exposes the command as an HTTP `POST`, binds the incoming JSON to the record, runs any validation, then calls `Handle()`. Whatever you inject into `Handle()` is resolved from the container. In this example `IAccounts` is an application-owned service; it might write MongoDB, EF Core, another application service, or anything else your slice owns. With the [Chronicle integration](/arc/backend/chronicle/) installed, a command can return events instead and let Arc append them for you. Tip When a decision needs data you have to *fetch* — external data, application-service data, a score, or a lookup — move that fetch into a `Provide()` method next to `Handle()`, so `Handle` stays a pure, easily tested function of its arguments. See [Provide data to a command handler](/arc/scenarios/provide-data-to-a-command/). `Handle()` can return what suits the operation: * **nothing** (`void` / `Task`) — fire-and-forget changes * **a value** — becomes a typed `CommandResult` the frontend can read * **a tuple** — return an event *and* a value (e.g. a generated id) * **a `Result`** — model success and failure explicitly Whatever you return, the caller gets a `CommandResult` carrying success, validation, and authorization state — so the frontend always knows what happened. ## Two ways to define one [Section titled “Two ways to define one”](#two-ways-to-define-one) | Style | What it looks like | Reach for it when | | ----------------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------- | | [Model-bound](/arc/backend/commands/model-bound/) | A `[Command]` record with `Handle()`, as above | **The default.** Least boilerplate; the intent and behavior live together. | | [Controller-based](/arc/backend/commands/controller-based/) | A command type posted to a controller action | You need full control over the HTTP surface, or you’re integrating with existing controllers. | ## Then make it bulletproof [Section titled “Then make it bulletproof”](#then-make-it-bulletproof) Once the command exists, layer on the cross-cutting concerns Arc handles for you: | Concern | Page | | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | Validate input before it runs | [Validation](/arc/backend/commands/validation/) | | Check validity without executing (pre-flight) | [Command Validation](/arc/backend/commands/command-validation/) | | Run a command in code, not over HTTP | [Command Pipeline](/arc/backend/commands/command-pipeline/) | | Carry ambient values through the pipeline | [Command Context](/arc/backend/commands/command-context/) | | Commit a command’s events atomically — or not at all | [Transactional Commands](/arc/backend/commands/transactional-commands/) | | Apply cross-cutting logic to every command | [Command Filters](/arc/backend/commands/command-filters/) · [Command Execution Scopes](/arc/backend/commands/command-execution-scopes/) | | Authorize by role or policy | [Authorization](/arc/backend/core/authorization/) | | Shape the response the frontend receives | [Response Value Handlers](/arc/backend/commands/response-value-handlers/) · [Response Examples](/arc/backend/commands/response-examples/) | ## The payoff: it’s already on the frontend [Section titled “The payoff: it’s already on the frontend”](#the-payoff-its-already-on-the-frontend) You didn’t write a DTO or an API client. When you build the backend, Arc’s [proxy generator](/arc/backend/proxy-generation/) emits a typed TypeScript proxy for `OpenAccount`, ready to call from React with full type checking — see [Commands in React](/arc/frontend/react/commands/). Rename a property in the C# record, rebuild, and the frontend won’t compile until it’s fixed. That’s the whole point of building on Arc: one definition, typed end to end. Next, read about the [queries](/arc/backend/queries/) that read the data your commands change. # Command Context The `CommandContext` is a core component of the non-controller-based command pipeline in Cratis Arc. It provides contextual information and values that are available throughout the command execution lifecycle. ## Overview [Section titled “Overview”](#overview) The `CommandContext` is a record that encapsulates all the necessary information about a command being executed: ```csharp public record CommandContext( CorrelationId CorrelationId, Type Type, object Command, IEnumerable Dependencies, CommandContextValues Values, ValidationResultSeverity? AllowedSeverity = default, object? Response = default, IServiceProvider? ServiceProvider = default, CancellationToken CancellationToken = default); ``` ### Properties [Section titled “Properties”](#properties) * **CorrelationId**: A unique identifier for tracking the command execution across the system * **Type**: The type of the command being executed * **Command**: The actual command instance * **Dependencies**: The resolved dependencies required to handle the command * **Values**: A collection of key-value pairs providing additional context * **AllowedSeverity**: The highest validation severity the caller allows before the command short-circuits * **Response**: The response, **if any**, that is returned as part of the command result * **ServiceProvider**: The scoped service provider used for command execution * **CancellationToken**: The cancellation token for the command execution ## Command Context Values [Section titled “Command Context Values”](#command-context-values) The `Values` property is a `CommandContextValues` instance that acts as a case-insensitive dictionary of contextual information. These values are populated through implementations of `ICommandContextValuesProvider`. ### How Values Are Populated [Section titled “How Values Are Populated”](#how-values-are-populated) The command pipeline uses the `CommandContextValuesBuilder` to collect values from all registered `ICommandContextValuesProvider` implementations. Each provider receives the command instance being executed and contributes its values. If there are overlapping keys, the last provider’s value takes precedence. ## Extending with Custom Values [Section titled “Extending with Custom Values”](#extending-with-custom-values) To add your own values to the command context, implement the `ICommandContextValuesProvider` interface: ```csharp public interface ICommandContextValuesProvider { CommandContextValues Provide(object command); } ``` The `command` parameter provides access to the command instance being executed, allowing providers to customize their values based on the specific command type or content. ### Example Implementation [Section titled “Example Implementation”](#example-implementation) Here’s an example of a custom provider that adds audit tracking information: ```csharp public class AuditContextValuesProvider : ICommandContextValuesProvider { private readonly IDateTimeProvider _dateTimeProvider; private readonly IHttpContextAccessor _httpContextAccessor; public AuditContextValuesProvider(IDateTimeProvider dateTimeProvider, IHttpContextAccessor httpContextAccessor) { _dateTimeProvider = dateTimeProvider; _httpContextAccessor = httpContextAccessor; } public CommandContextValues Provide(object command) { var values = new CommandContextValues(); values["ExecutedAt"] = _dateTimeProvider.UtcNow; values["ExecutedBy"] = _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier) ?? "System"; values["TraceId"] = Activity.Current?.TraceId.ToString() ?? Guid.NewGuid().ToString(); // Example of using command information values["CommandType"] = command.GetType().Name; return values; } } ``` ### Registration [Section titled “Registration”](#registration) The provider will be automatically discovered and registered by the dependency injection system if it’s in the application’s assembly, or you can register it manually: ```csharp services.AddSingleton(); ``` ## Accessing Command Context [Section titled “Accessing Command Context”](#accessing-command-context) Within command handlers, filters, or other components in the command pipeline, you can access the current command context through the `ICommandContextAccessor`: ```csharp [Command] public record MyCommand(string SomeProperty) { public object Handle(ICommandContextAccessor contextAccessor) { // Access values from the current context var context = contextAccessor.Current; if (context.Values.TryGetValue("ExecutedBy", out var executedBy)) { // Use the execution user information for logging or business logic } return new { Success = true }; } } ``` ## Non-Controller-Based Pipeline [Section titled “Non-Controller-Based Pipeline”](#non-controller-based-pipeline) The `CommandContext` is specifically designed for the non-controller-based command pipeline. In this pipeline: 1. Commands are executed through the `ICommandPipeline` 2. The context is created automatically with resolved dependencies 3. Values are populated from all registered providers 4. The context is made available throughout the execution chain 5. Filters can inspect and modify the execution based on context values 6. Response value handlers can use context information for processing results This differs from the controller-based approach where ASP.NET Core’s built-in dependency injection and model binding handle much of the context management. ## Best Practices [Section titled “Best Practices”](#best-practices) * Keep value providers lightweight and fast * Use descriptive keys for your context values * Avoid storing large objects in context values * Consider the lifetime of your providers (typically singleton) * Handle cases where expected values might not be present * Use the context values for cross-cutting concerns like auditing, logging, and authorization # Command Execution Scopes Command execution scopes let you bracket the execution of a command in the **model-bound pipeline** with a lifetime concern that must see the command’s final outcome — begin something before the command runs, and complete it afterwards knowing whether the command succeeded. This is the extension point behind [transactional commands](/arc/backend/commands/transactional-commands/), and you can use it for your own concerns such as database transactions, metrics that need the final result, or outbox-style coordination. > **Note**: A [command filter](/arc/backend/commands/command-filters/) runs *before* the handler and can stop the command. An execution scope runs *around* the whole execution — it always completes, with the final `CommandResult`, whether the command succeeded, failed validation, or threw. ## How It Works [Section titled “How It Works”](#how-it-works) Implementations of `ICommandExecutionScope` are discovered automatically — no registration needed. For every command the pipeline: 1. Calls `Begin(context)` on every scope after the `CommandContext` is established, before filters and the handler run. 2. Executes the command — filters, handler, and response value handlers. 3. Calls `Complete(context, result)` on every scope with the final, mutable `CommandResult` — **exactly once**, on every outcome, including validation failures and exceptions. An exception thrown from `Complete` is folded into the command’s result as an exception outcome rather than propagating to the caller. `Begin` is deliberately synchronous so ambient state a scope establishes — such as an `AsyncLocal`-based unit of work — flows into the command’s execution. `Complete` is asynchronous and may mutate the `CommandResult` to reflect the outcome of completing the scope. Scopes nest: they complete in the reverse of the order they began, like `using` blocks. The relative order between different scope implementations is unspecified — design scopes to be independent of each other. `Complete` can also be invoked when your `Begin` never ran (for example when another scope’s `Begin` threw), so implementations must tolerate completing without having begun. ## Implementing a Custom Execution Scope [Section titled “Implementing a Custom Execution Scope”](#implementing-a-custom-execution-scope) ```csharp using Cratis.Arc.Commands; public class CommandTimingScope : ICommandExecutionScope { static readonly AsyncLocal _started = new(); public void Begin(CommandContext context) => _started.Value = TimeProvider.System.GetTimestamp(); public Task Complete(CommandContext context, CommandResult result) { var elapsed = TimeProvider.System.GetElapsedTime(_started.Value); Metrics.RecordCommandDuration(context.Type.Name, elapsed, result.IsSuccess); return Task.CompletedTask; } } ``` Because a scope instance is shared across concurrent commands, keep per-command state in an `AsyncLocal` (as above) or in the `CommandContext`, never in instance fields. ## Mutating the Result — Handle With Care [Section titled “Mutating the Result — Handle With Care”](#mutating-the-result--handle-with-care) `Complete` receives the final, **mutable** `CommandResult`, and mutating it is powerful enough to lie with. Scopes should *enrich* the result — add validation results, exception outcomes, context — and **never erase failures**: clearing `ValidationResults` or `ExceptionMessages` can flip a failed command into a reported success *after* other scopes already acted on the failure. The transactional scope, for example, rolls the command’s events back when the result is unsuccessful — a scope that then scrubs the failure makes the caller believe events were committed that never were. The same caution applies to mutating `CommandContext` values: downstream consumers act on them, so change them only when you own the consequence. The `CommandContext` gives you: * `CorrelationId` — the unique identifier for the command execution * `Type` and `Command` — the command type and instance * `ServiceProvider` — the command’s own scope, for resolving collaborators * `Values` — ambient values carried through the pipeline ## Built-in Scopes [Section titled “Built-in Scopes”](#built-in-scopes) | Scope | Package | Purpose | | --------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `TransactionalCommandScope` | `Cratis.Arc.Chronicle` | Makes every command a [transactional scope](/arc/backend/commands/transactional-commands/): begins a Chronicle unit of work, commits the command’s enrolled events atomically when the command succeeds — surfacing constraint violations on the `CommandResult` — and rolls them back when the command fails. Also observes immediate appends so a failed one fails the command instead of being silently swallowed. | ## Scope of the Extension Point [Section titled “Scope of the Extension Point”](#scope-of-the-extension-point) Execution scopes run wherever the model-bound command pipeline runs: commands executed over HTTP, directly through [`ICommandPipeline`](/arc/backend/commands/command-pipeline/), from reactors, and in the `CommandScenario` test harness. Controller-based commands do not go through the pipeline and are not covered. # Command Filters Command filters provide a way to intercept and filter commands before they are handled in the **non-controller-based pipeline**. This allows you to implement cross-cutting concerns such as validation, authorization, logging, or custom business rules that should be applied to commands before they reach their handlers. > **Note**: Command filters are specifically for the model-bound command pipeline. For controller-based commands, use standard ASP.NET Core filters instead. ## How It Works [Section titled “How It Works”](#how-it-works) Command filters are executed as part of the command pipeline, before the actual command handler is invoked. If a filter determines that a command should not proceed (e.g., validation fails), it can return an unsuccessful `CommandResult` to stop the pipeline execution. ## Implementing a Custom Command Filter [Section titled “Implementing a Custom Command Filter”](#implementing-a-custom-command-filter) To create a custom command filter, implement the `ICommandFilter` interface: ```csharp using Cratis.Arc.Commands; public class MyCustomFilter : ICommandFilter { public async Task OnExecution(CommandContext context) { // Your filtering logic here // Return success to allow the command to continue return CommandResult.Success(context.CorrelationId); // Or return an error result to stop execution // return new CommandResult // { // CorrelationId = context.CorrelationId, // IsAuthorized = false, // ValidationResults = [/* your validation errors */] // }; } } ``` The `CommandContext` provides access to: * `CorrelationId` - The unique identifier for the command execution * `Type` - The type of the command being executed * `Command` - The actual command instance * `Dependencies` - Any dependencies resolved for the command handler * `Values` - Additional context values that may have been set ## Registering Custom Filters [Section titled “Registering Custom Filters”](#registering-custom-filters) Command filters are automatically discovered and registered through the dependency injection container. Simply ensure your filter class implements `ICommandFilter` and it will be included in the command pipeline. ## Cross-Cutting Authorization by Namespace [Section titled “Cross-Cutting Authorization by Namespace”](#cross-cutting-authorization-by-namespace) You can implement a command filter that applies authorization rules to all commands in a namespace instead of adding `[Authorize]` or `[Roles]` attributes to each command type. ```csharp using Cratis.Arc.Commands; using Cratis.Arc.Http; namespace MyApp.Features.Security; public class NamespaceAuthorizationCommandFilter(IHttpRequestContextAccessor requestContextAccessor) : ICommandFilter { const string ProtectedNamespace = "MyApp.Features.Payments"; const string RequiredRole = "Payments"; public Task OnExecution(CommandContext context) { var isProtectedCommand = context.Type.Namespace?.StartsWith(ProtectedNamespace, StringComparison.Ordinal) ?? false; if (!isProtectedCommand) { return Task.FromResult(CommandResult.Success(context.CorrelationId)); } var hasRole = requestContextAccessor.Current?.User.IsInRole(RequiredRole) ?? false; return Task.FromResult( hasRole ? CommandResult.Success(context.CorrelationId) : CommandResult.Unauthorized(context.CorrelationId, $"Role '{RequiredRole}' is required.")); } } ``` This pattern is useful when you want one place to enforce authorization for a full slice or feature area. ## Built-in Filters [Section titled “Built-in Filters”](#built-in-filters) The following filters are provided out of the box in the `Cratis.Arc.Commands.Filters` namespace: | Filter | Description | | -------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `DataAnnotationValidationFilter` | Validates commands using data annotations (e.g., `[Required]`, `[Range]`, etc.) applied to command properties | | `FluentValidationFilter` | Validates commands using FluentValidation validators, supporting nested object validation | | `AuthorizationFilter` | Provides a foundation for command authorization (currently returns success by default) | ### DataAnnotationValidationFilter [Section titled “DataAnnotationValidationFilter”](#dataannotationvalidationfilter) This filter automatically validates commands that have properties decorated with data annotation attributes: ```csharp [Command] public record CreateUser( [Required] string Name, [EmailAddress] string Email, [Range(18, 120)] int Age); ``` If validation fails, the filter returns a `CommandResult` with validation errors, preventing the command from being handled. ### FluentValidationFilter [Section titled “FluentValidationFilter”](#fluentvalidationfilter) This filter works with FluentValidation validators that are discovered automatically. It supports recursive validation of nested objects within the command: ```csharp public class CreateUserValidator : CommandValidator { public CreateUserValidator() { RuleFor(x => x.Name).NotEmpty().MaximumLength(100); RuleFor(x => x.Email).NotEmpty().EmailAddress(); RuleFor(x => x.Age).InclusiveBetween(18, 120); } } ``` ### AuthorizationFilter [Section titled “AuthorizationFilter”](#authorizationfilter) This filter provides the foundation for implementing command-level authorization. With this you can leverage the `Authorize` attribute from ASP.NET Core. ```csharp [Command] [Authorize(Roles = "Administrator")] public record CreateUser( string Name, string Email, int Age); ``` Or the convenience wrapper provided by Cratis Arc for roles, allowing a more intuitive way of specifying multiple roles: ```csharp [Command] [Roles("System", "Admin")] public record CreateUser( string Name, string Email, int Age); ``` ## Best Practices [Section titled “Best Practices”](#best-practices) * Keep filters focused on a single concern (validation, authorization, etc.) * Return meaningful error messages in `ValidationResult` objects * Use the `CorrelationId` from the context for tracking and logging * Consider performance implications, especially for filters that run on every command * Test filters independently to ensure they work correctly in isolation # Command Pipeline The `ICommandPipeline` service provides a way to execute commands programmatically, bypassing the HTTP layer. This is useful for scenarios where you need to execute commands from within your application code rather than through HTTP requests. ## When to Use ICommandPipeline [Section titled “When to Use ICommandPipeline”](#when-to-use-icommandpipeline) The command pipeline is particularly useful for: * **Background services or scheduled tasks** - Execute commands as part of scheduled jobs * **Event handlers** - React to events by executing commands * **Internal service-to-service communication** - Execute commands between services without HTTP overhead * **Testing scenarios** - Execute commands directly in integration tests * **Saga or workflow orchestration** - Coordinate multiple commands as part of a larger workflow ## Basic Usage [Section titled “Basic Usage”](#basic-usage) `ICommandPipeline` provides two forms for every operation: a **scope-free** form that creates its own service scope automatically, and a **scope-explicit** form where you supply the `IServiceProvider` yourself. ### Without a service provider (recommended for most cases) [Section titled “Without a service provider (recommended for most cases)”](#without-a-service-provider-recommended-for-most-cases) Inject `ICommandPipeline` and call `Execute` directly. The pipeline creates and disposes a dedicated service scope for each call — no manual scope management needed: ```csharp public class OrderProcessingService { readonly ICommandPipeline _commandPipeline; public OrderProcessingService(ICommandPipeline commandPipeline) { _commandPipeline = commandPipeline; } public async Task ProcessOrder(Order order) { var result = await _commandPipeline.Execute(new ProcessOrderCommand(order.Id, order.Items)); if (result.IsSuccess) { // Command executed successfully } else { foreach (var error in result.ValidationResults) { // Process validation errors } } } } ``` This is the right choice for background services, scheduled tasks, and any code that does not live inside an existing DI scope. ### With a service provider (share an existing scope) [Section titled “With a service provider (share an existing scope)”](#with-a-service-provider-share-an-existing-scope) If you are already inside a scoped lifetime — for example a Reactor, an event handler, or an HTTP endpoint — pass the current `IServiceProvider` so handler dependencies share the same scope as the caller: ```csharp public class OrderCreatedReactor { readonly ICommandPipeline _commandPipeline; readonly IServiceProvider _serviceProvider; public OrderCreatedReactor(ICommandPipeline commandPipeline, IServiceProvider serviceProvider) { _commandPipeline = commandPipeline; _serviceProvider = serviceProvider; } public async Task Handle(OrderCreated @event) { var result = await _commandPipeline.Execute( new SendOrderConfirmation(@event.OrderId, @event.CustomerEmail), _serviceProvider); } } ``` ## Cancellation [Section titled “Cancellation”](#cancellation) HTTP command endpoints pass the request-aborted token into the command execution automatically. That token can be injected into `Provide()` and `Handle()` as a `CancellationToken`. When you execute commands directly, pass the token to the pipeline: ```csharp public class ImportWorker { readonly ICommandPipeline _commandPipeline; public ImportWorker(ICommandPipeline commandPipeline) { _commandPipeline = commandPipeline; } public Task Import(CatalogId catalogId, CancellationToken cancellationToken) => _commandPipeline.Execute(new ImportCatalog(catalogId), cancellationToken); } ``` Use the scope-explicit form when the command should share the caller’s scoped services: ```csharp var result = await _commandPipeline.Execute( new ImportCatalog(catalogId), _serviceProvider, cancellationToken); ``` If you also use validation severity filtering, pass both values: ```csharp var result = await _commandPipeline.Execute( command, _serviceProvider, allowedSeverity: ValidationResultSeverity.Warning, cancellationToken); ``` ## Command Results [Section titled “Command Results”](#command-results) The `ICommandPipeline.Execute()` method returns a `CommandResult` with comprehensive information about the execution: ```csharp var result = await _commandPipeline.Execute(command); if (!result.IsAuthorized) { // Handle unauthorized access — the command was not executed } if (result.IsSuccess) { // Command executed successfully } else { // Handle validation errors foreach (var validationResult in result.ValidationResults) { // Process each validation error } } ``` ### CommandResult Properties [Section titled “CommandResult Properties”](#commandresult-properties) | Property | Type | Description | | ------------------- | ------------------------------- | ------------------------------------------------------ | | `IsSuccess` | `bool` | Whether the command executed successfully | | `IsAuthorized` | `bool` | Whether the user was authorized to execute the command | | `IsValid` | `bool` | Whether the command passed validation | | `HasExceptions` | `bool` | Whether any exceptions occurred during execution | | `ValidationResults` | `IEnumerable` | Validation errors if the command failed validation | | `ExceptionMessages` | `IEnumerable` | Exception messages if exceptions occurred | | `CorrelationId` | `CorrelationId` | The correlation ID for tracking the command | When using the generic `Execute` overload, the returned `CommandResult` adds one more property: | Property | Type | Description | | ---------- | ---------- | -------------------------------------------------------------------------------------------------------------- | | `Response` | `TResult?` | The typed value returned by the command handler, or `null` if the command did not succeed or returned no value | ## Exception Handling [Section titled “Exception Handling”](#exception-handling) When using `ICommandPipeline` programmatically, exceptions in the command handler are caught and returned as part of the `CommandResult`: ```csharp var result = await _commandPipeline.Execute(command); if (result.HasExceptions) { // An exception was thrown during command execution foreach (var message in result.ExceptionMessages) { _logger.LogError("Command failed: {Message}", message); } } ``` ## Validation Without Execution [Section titled “Validation Without Execution”](#validation-without-execution) The `Validate` method runs authorization and validation filters without invoking the command handler. It follows the same two forms as `Execute`. **Without a service provider:** ```csharp var validationResult = await _commandPipeline.Validate(command); if (validationResult.IsValid) { var result = await _commandPipeline.Execute(command); } ``` **With a service provider (to share an existing scope):** ```csharp var validationResult = await _commandPipeline.Validate(command, _serviceProvider); if (validationResult.IsValid) { var result = await _commandPipeline.Execute(command, _serviceProvider); } ``` This is useful for pre-flight validation before committing to command execution. ## Context and Authentication [Section titled “Context and Authentication”](#context-and-authentication) When executing commands programmatically, the current execution context (including user identity and claims) is automatically used. The command pipeline respects: * **Correlation ID** - Automatically tracked for request tracing * **User context** - The current user’s identity and claims are used for authorization * **Tenant context** - Multi-tenancy context is preserved If you need to execute commands under a different context, you’ll need to manage the authentication context appropriately in your application. ## Background Service Example [Section titled “Background Service Example”](#background-service-example) Here’s an example of using `ICommandPipeline` in a background service: ```csharp public class OrderExpirationService : BackgroundService { readonly ICommandPipeline _commandPipeline; readonly IOrderRepository _orderRepository; readonly ILogger _logger; public OrderExpirationService( ICommandPipeline commandPipeline, IOrderRepository orderRepository, ILogger logger) { _commandPipeline = commandPipeline; _orderRepository = orderRepository; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var expiredOrders = await _orderRepository.GetExpiredOrders(); foreach (var order in expiredOrders) { // Each Execute call creates and disposes its own service scope var result = await _commandPipeline.Execute(new ExpireOrder(order.Id)); if (!result.IsSuccess) { _logger.LogWarning( "Failed to expire order {OrderId}: {Errors}", order.Id, string.Join(", ", result.ValidationResults.Select(v => v.Message))); } } await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); } } } ``` ## Event Handler Example [Section titled “Event Handler Example”](#event-handler-example) Using `ICommandPipeline` in an event handler: ```csharp public class OrderCreatedEventHandler { readonly ICommandPipeline _commandPipeline; public OrderCreatedEventHandler(ICommandPipeline commandPipeline) { _commandPipeline = commandPipeline; } public async Task Handle(OrderCreated @event) { // Send confirmation email when an order is created var command = new SendOrderConfirmation(@event.OrderId, @event.CustomerEmail); var result = await _commandPipeline.Execute(command); if (!result.IsSuccess) { // Handle failure - maybe queue for retry } } } ``` ## Typed Command Results [Section titled “Typed Command Results”](#typed-command-results) When a command handler returns a value, use the generic `Execute` overload to get back a `CommandResult` with a strongly-typed `Response` property instead of working with `object?`. Both scope forms are available: ```csharp [Command] public record CreateOrder(IEnumerable Items) { public OrderId Handle(IOrderService orderService) { return orderService.CreateOrder(Items); } } // Without a service provider — pipeline creates its own scope var result = await _commandPipeline.Execute(new CreateOrder(items)); // With a service provider — share the caller's scope var result = await _commandPipeline.Execute(new CreateOrder(items), _serviceProvider); if (result.IsSuccess) { // response is strongly typed — no cast required OrderId orderId = result.Response!; await NotifyCustomer(orderId); } ``` The generic overload covers all the same failure paths as the non-generic one. When the command is unauthorized, fails validation, has no handler, or throws an exception, the result is still a valid `CommandResult` — `Response` is just `default`: ```csharp var result = await _commandPipeline.Execute(new CreateOrder(items)); if (!result.IsAuthorized) { // result.Response is null — command was never executed } if (!result.IsValid) { // result.Response is null — validation failed before execution } if (result.HasExceptions) { // result.Response is null — an exception was thrown during execution } ``` If the handler returns a different type from what you requested, an `InvalidCastException` is thrown. This is a programmer error — the type you pass to `Execute` must match the type the command handler returns. If the handler returns no value at all (a `void`-equivalent handler), `Response` is `null`. The non-generic `Execute` overload is equally valid in this case: ```csharp // Fine when you don't need a typed response var result = await _commandPipeline.Execute(new CancelOrder(orderId)); ``` # Command Validation The Arc provides built-in support for validating commands without executing them. This enables pre-flight validation to provide early feedback to users before performing potentially expensive or state-changing operations. ## Overview [Section titled “Overview”](#overview) Command validation allows you to check authorization and validation rules without executing the command handler. This is useful for: * **Early User Feedback**: Show validation errors before the user submits a form * **UX Improvements**: Enable/disable submit buttons based on validation state * **Authorization Checks**: Verify user permissions without side effects * **Progressive Validation**: Validate fields as users interact with forms For frontend usage of command validation, see: * [Core Validation](/arc/frontend/core/commands/validation/) - TypeScript/JavaScript API * [React Command Validation](/arc/frontend/react/commands/validation/) - React-specific patterns and hooks ## Backend Support [Section titled “Backend Support”](#backend-support) ### ICommandPipeline.Validate [Section titled “ICommandPipeline.Validate”](#icommandpipelinevalidate) The `ICommandPipeline` interface provides a `Validate` method that runs only authorization and validation filters: ```csharp public interface ICommandPipeline { /// /// Validates the given command without executing it. /// Task Validate(object command); } ``` **Key Characteristics:** * Runs all command filters (authorization, validation) * Does **not** invoke the command handler * Returns a `CommandResult` with validation and authorization status * No side effects on the system ### Example Usage [Section titled “Example Usage”](#example-usage) ```csharp public class OrderService { private readonly ICommandPipeline _commandPipeline; public OrderService(ICommandPipeline commandPipeline) { _commandPipeline = commandPipeline; } public async Task CanCreateOrder(CreateOrder command) { var result = await _commandPipeline.Validate(command); return result.IsSuccess; } public async Task CreateOrder(CreateOrder command) { // Optionally validate first var validationResult = await _commandPipeline.Validate(command); if (!validationResult.IsSuccess) { // Handle validation errors return; } // Execute the command var result = await _commandPipeline.Execute(command); // Process result... } } ``` ### Model-Bound Commands [Section titled “Model-Bound Commands”](#model-bound-commands) For model-bound commands, validation endpoints are automatically created alongside execute endpoints: **Execute Endpoint**: `POST /api/orders/create-order` **Validate Endpoint**: `POST /api/orders/create-order/validate` The validation endpoint accepts the same payload as the execute endpoint but only runs filters. ### Controller-Based Commands [Section titled “Controller-Based Commands”](#controller-based-commands) For controller-based commands, validation endpoints are **automatically discovered and created** at application startup. The system scans all controller actions that: * Are POST methods * Have a single `[FromBody]` parameter (the command) For each matching controller action, a corresponding `/validate` endpoint is automatically registered. **Example Controller:** ```csharp [Route("api/carts")] public class Carts : ControllerBase { [HttpPost("add")] public Task AddItemToCart([FromBody] AddItemToCart command) { // Execute the command } } ``` **Automatically Created Endpoints:** * **Execute**: `POST /api/carts/add` * **Validate**: `POST /api/carts/add/validate` *(automatically created)* **Key Points:** * Validation endpoints are automatically created for all controller command actions * No attributes or special configuration required * The system detects commands by looking for POST actions with `[FromBody]` parameters * The route pattern for validation is: `{controller-action-route}/validate` * Only validation and authorization filters run; the action method is not executed **How It Works:** During application startup, the `CommandValidationRouteConvention` automatically: 1. Identifies command actions (POST methods that implement command patterns) 2. Creates corresponding `/validate` routes for each command action using ASP.NET Core’s application model conventions 3. Routes all requests through the standard ASP.NET Core pipeline, including: * Authorization filters * Model binding * Command validation filters 4. The `CommandActionFilter` detects validation requests (paths ending with `/validate`) and skips action execution 5. Returns validation results without invoking the actual command handler This approach ensures validation requests go through the exact same pipeline as execution requests, maintaining consistency in authorization, model binding, and validation behavior. ## Validation Filters [Section titled “Validation Filters”](#validation-filters) The validation pipeline runs all registered command filters: ### Built-in Filters [Section titled “Built-in Filters”](#built-in-filters) 1. **AuthorizationFilter**: Checks user permissions 2. **DataAnnotationValidationFilter**: Validates data annotations 3. **FluentValidationFilter**: Runs FluentValidation validators For more information, see [Command Filters](/arc/backend/commands/command-filters/). ## Best Practices [Section titled “Best Practices”](#best-practices) ### When to Implement Validation [Section titled “When to Implement Validation”](#when-to-implement-validation) ✅ **Good Use Cases:** * Commands that modify critical business data * Commands with complex authorization requirements * Commands with expensive validation logic that benefits from early feedback * Commands used in interactive forms ❌ **Less Beneficial:** * Simple CRUD operations with minimal validation * Commands only executed by background processes * Commands with very fast execution times ### Performance Considerations [Section titled “Performance Considerations”](#performance-considerations) * Validation runs all filters, which may include database queries * Optimize validator implementations for performance * Consider caching authorization checks where appropriate * Use appropriate indexes for validation queries ## Security Considerations [Section titled “Security Considerations”](#security-considerations) * Validation endpoints run the same authorization filters as execute endpoints * Unauthorized users receive 401/403 responses from validation endpoints * Validation does not expose sensitive data since handlers aren’t executed * Validation results may reveal authorization policies (by design) * Always implement proper authorization filters for commands ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Validation endpoint returns 404 [Section titled “Validation endpoint returns 404”](#validation-endpoint-returns-404) **Cause**: The validation endpoint may not be properly registered for controller-based commands. **Solution**: Ensure the controller action follows the pattern: * Is a POST method * Has a single `[FromBody]` parameter * The validation endpoint should be automatically created at `{route}/validate` ### Validation is slow [Section titled “Validation is slow”](#validation-is-slow) **Cause**: Complex validation logic or database queries in validators. **Solution**: * Optimize validator implementations * Add appropriate database indexes * Consider caching validation results where appropriate * Profile validator performance to identify bottlenecks ### Validation passes but execute fails [Section titled “Validation passes but execute fails”](#validation-passes-but-execute-fails) **Cause**: State may have changed between validate and execute calls, or the handler encountered an error. **Solution**: This is expected behavior in concurrent systems. Validation is a pre-flight check, not a guarantee of execution success. # Controller Based Commands You can represent commands as regular ASP.NET Core Controller actions. ```csharp public record AddItemToCart(string sku, int quantity); [Route("api/carts")] public class Carts : ControllerBase { [HttpPost("add")] public Task AddItemToCart([FromBody] AddItemToCart command) { // Logic for handling... } } ``` > **Note**: If you’re using Cratis Arc [proxy generator](/arc/backend/proxy-generation/), the method name will become the command name for the generated TypeScript file and class. ## Bypassing Command Result Wrappers [Section titled “Bypassing Command Result Wrappers”](#bypassing-command-result-wrappers) By default, controller-based commands return results wrapped in a `CommandResult` structure. If you need to return the raw result from your controller action without this wrapper, you can use the `[AspNetResult]` attribute. For more details, see [Without wrappers](/arc/backend/asp-net-core/without-wrappers/). ## Automatic Validation Endpoints [Section titled “Automatic Validation Endpoints”](#automatic-validation-endpoints) Controller-based commands automatically get validation endpoints created at application startup. For any POST action with a `[FromBody]` parameter, a corresponding `/validate` endpoint is automatically registered. **Example:** ```csharp [Route("api/carts")] public class Carts : ControllerBase { [HttpPost("add")] public Task AddItemToCart([FromBody] AddItemToCart command) { // Execute the command } } ``` **Automatically Created Endpoints:** * Execute: `POST /api/carts/add` * Validate: `POST /api/carts/add/validate` *(created automatically)* The validation endpoint accepts the same payload as the execute endpoint but only runs validation and authorization filters without executing the command logic. For comprehensive documentation on command validation, see [Command Validation](/arc/backend/commands/command-validation/). # Model Bound Commands For a more lightweight approach; commands can be their own handlers. This is achieved by adorning your command with the `[Command]` attribute and implementing a `Handle()` method. ```csharp [Command] // The command attribute is needed public record AddItemToCart(string Sku, int Quantity) { public void Handle() { // Handle the command } } ``` > **Note**: If you’re using the Cratis ApplicationModel [proxy generator](/arc/backend/proxy-generation/), the name of the type will become the name of the command for the generated TypeScript file and class. If your handler has side-effects expressed in the return value, the command pipeline has an [extensibility point for return values](/arc/backend/commands/response-value-handlers/). You can then return anything you know there is a handler for. ## Discriminated Union [Section titled “Discriminated Union”](#discriminated-union) Your return type can leverage a discriminated union with [`OneOf<>`](https://github.com/mcintyre321/OneOf) to return different types of values depending on the situation, like for instance an explicit validation error. As long as there are [response handlers](/arc/backend/commands/response-value-handlers/) for any of the types of the discriminated union, your value will be handled. ```csharp using Cratis.Arc.Validation; using OneOf; [Command] public record AddItemToCart(string Sku, int Quantity) { public Result Handle() { if( /* code that checks if product is carried */ ) { var cartLineIdentifier = Guid.NewGuid(); // Logic for handling the actual adding... return cartLineIdentifier; } return ValidationResult.Error("Product is not carried anymore"); } } ``` ### Result with Tuple Alternatives [Section titled “Result with Tuple Alternatives”](#result-with-tuple-alternatives) You can also combine `Result` with tuples, allowing different alternatives to return different structures. This is useful when some code paths need to return a response with side effects (events, notifications), while others just return a simple value or error. ```csharp using Cratis.Arc.Validation; using OneOf; [Command] public record CreateOrder(string CustomerId, List Items) { public Result<(OrderId, OrderCreated), ValidationResult> Handle() { if (!IsValidOrder()) { return ValidationResult.Error("Invalid order"); } var orderId = OrderId.New(); // Create the order... // Return tuple with response (OrderId) and event (OrderCreated) return (orderId, new OrderCreated(orderId, CustomerId, Items)); } } ``` In this example: * When validation fails, the `ValidationResult` is returned and processed by the validation handler * When successful, the tuple `(OrderId, OrderCreated)` is returned: * `OrderCreated` is processed by its response value handler (e.g., Chronicle event handler) * `OrderId` becomes the command response (available in `CommandResult`) This pattern is particularly powerful when you want to: * Return different types based on business logic outcomes * Combine response values with side effects in success scenarios * Keep error handling separate from success handling ## Tuple [Section titled “Tuple”](#tuple) Sometimes you want to return a value that is part of the `CommandResult` and returned to the caller that invoked the command. By returning a tuple, the command pipeline will intelligently process each value to determine which should be the response and which should be processed by [response value handlers](/arc/backend/commands/response-value-handlers/). ### How Tuple Processing Works [Section titled “How Tuple Processing Works”](#how-tuple-processing-works) The command pipeline processes tuples as follows: 1. **Checks each value** against available response value handlers using their `CanHandle` method 2. **Values with handlers** are processed by their respective response value handlers 3. **Values without handlers** are considered potential response values 4. **If exactly one value has no handler**, it becomes the response in the `CommandResult` 5. **If multiple values have no handlers**, a `MultipleUnhandledTupleValues` is thrown 6. **If all values have handlers**, no response value is set ### Simple Tuple (2 values) [Section titled “Simple Tuple (2 values)”](#simple-tuple-2-values) ```csharp using Cratis.Arc.Validation; [Command] public record AddItemToCart(string Sku, int Quantity) { public (Guid, ItemAddedToCart) Handle() { var cartLineIdentifier = Guid.NewGuid(); // Logic for handling the actual adding... // Return the identifier and the consequence, in this case an event handled by Chronicle. return (cartLineIdentifier, new ItemAddedToCart(...)); } } ``` In this example, if `ItemAddedToCart` has a response value handler but `Guid` doesn’t, then the `Guid` becomes the response. ### Multi-dimensional Tuples (3+ values) [Section titled “Multi-dimensional Tuples (3+ values)”](#multi-dimensional-tuples-3-values) The system supports tuples with any number of values: ```csharp [Command] public record ProcessOrder(string OrderId) { public (Guid, OrderProcessed, ValidationResult, NotificationSent) Handle() { var confirmationId = Guid.NewGuid(); // Processing logic... return ( confirmationId, // Response (if no handler exists for Guid) new OrderProcessed(...), // Event (handled by event handler) validationResult, // Validation (handled by validation handler) new NotificationSent(...) // Notification (handled by notification handler) ); } } ``` In this example: * `OrderProcessed`, `ValidationResult`, and `NotificationSent` would be processed by their respective handlers * `Guid` (confirmationId) would become the response value * If multiple values lack handlers, an exception would be thrown ### Error Scenarios [Section titled “Error Scenarios”](#error-scenarios) If your tuple contains multiple values that don’t have corresponding response value handlers, the system will throw a `MultipleUnhandledTupleValues` with details about which values couldn’t be handled: ```csharp // This would throw an exception if neither string nor int have handlers public (string, int, SomeEvent) Handle() => ("response1", 42, new SomeEvent()); ``` ### Tuples with Result Values [Section titled “Tuples with Result Values”](#tuples-with-result-values) Tuples can also contain `Result` values. The command pipeline will unwrap the `Result` and process the inner value: ```csharp using Cratis.Arc.Validation; using OneOf; [Command] public record ProcessPayment(string OrderId, decimal Amount) { public (OrderId, Result) Handle() { var orderId = new OrderId(OrderId); if (ProcessPaymentWithProvider()) { return (orderId, new PaymentSucceeded(orderId, Amount)); } return (orderId, new PaymentFailed(orderId, "Insufficient funds")); } } ``` In this example: * `OrderId` becomes the response (assuming no handler exists for it) * The `Result` value is unwrapped, and the inner value (`PaymentSucceeded` or `PaymentFailed`) is processed by its respective handler ## Dependencies [Section titled “Dependencies”](#dependencies) Your command handler method can also take dependencies to any services configured in the service collection. This is done by just specifying your dependencies on the methods signature: ```csharp using Cratis.Arc.Validation; [Command] public record AddItemToCart(string Sku, int Quantity) { public void Handle(ICartService carts) { carts.AddItemToCart(Sku, Quantity); } } ``` `CancellationToken` is a special dependency. Arc injects it from the command execution context instead of resolving it from the service collection. HTTP command endpoints use the request-aborted token automatically. Programmatic callers can pass a token through `ICommandPipeline`. ```csharp [Command] public record ImportCatalog(CatalogId CatalogId) { public Task Provide(ICatalogs catalogs, CancellationToken cancellationToken) => catalogs.GetSnapshot(CatalogId, cancellationToken); public Task Handle(CatalogSnapshot snapshot, ICatalogImporter importer, CancellationToken cancellationToken) => importer.Import(snapshot, cancellationToken); } ``` ## Frontend Integration [Section titled “Frontend Integration”](#frontend-integration) Model-bound commands work seamlessly with the [proxy generator](/arc/backend/proxy-generation/), which automatically creates TypeScript proxies for your commands. The generated proxies provide: * Strong typing for command properties * Automatic validation integration * React hooks for easy frontend integration * Consistent error handling and response processing * Authorization status handling in command results # Authorization Model-bound commands support authorization through standard ASP.NET Core authorization attributes as well as the convenient `[Roles]` attribute provided by the Arc. ## Using the Authorize Attribute [Section titled “Using the Authorize Attribute”](#using-the-authorize-attribute) You can secure commands using the standard `[Authorize]` attribute at the class level: ```csharp [Command] [Authorize] public record AddItemToCart(string Sku, int Quantity) { public void Handle(ICartService carts) { carts.AddItemToCart(Sku, Quantity); } } ``` With role requirements: ```csharp [Command] [Authorize(Roles = "Admin,Manager")] public record DeleteProduct(ProductId Id) { public void Handle(IProductService products) { products.Delete(Id); } } ``` ## Using the Roles Attribute [Section titled “Using the Roles Attribute”](#using-the-roles-attribute) The Arc provides a more convenient `[Roles]` attribute for cleaner syntax when specifying multiple roles: ```csharp [Command] [Roles("Admin", "Manager")] public record UpdateProductPrice(ProductId Id, decimal NewPrice) { public void Handle(IProductService products) { products.UpdatePrice(Id, NewPrice); } } ``` The user needs to have **at least one** of the specified roles to execute the command. ## Anonymous Access with AllowAnonymous [Section titled “Anonymous Access with AllowAnonymous”](#anonymous-access-with-allowanonymous) Use `[AllowAnonymous]` to allow public access to specific commands. This is particularly useful when you have a global authorization requirement but need certain commands to be accessible without authentication: ```csharp [Command] [AllowAnonymous] public record RegisterUser(string Email, string Password) { public void Handle(IUserService users) { users.Register(Email, Password); } } ``` ### Combining with Global Authorization [Section titled “Combining with Global Authorization”](#combining-with-global-authorization) When your application has global authorization requirements (e.g., via `[Authorize]` on controllers or through middleware), you can use `[AllowAnonymous]` to exempt specific commands: ```csharp // This command can be executed without authentication // even if global authorization is configured [Command] [AllowAnonymous] public record RequestPasswordReset(string Email) { public void Handle(IPasswordResetService service) { service.SendResetEmail(Email); } } // This command requires authentication [Command] [Authorize] public record ChangePassword(string CurrentPassword, string NewPassword) { public void Handle(IPasswordService service) { service.ChangePassword(CurrentPassword, NewPassword); } } ``` ### Common Use Cases for AllowAnonymous [Section titled “Common Use Cases for AllowAnonymous”](#common-use-cases-for-allowanonymous) * **User registration** - New users need to create accounts before they can authenticate * **Password reset requests** - Users who forgot their password can’t authenticate * **Public data submissions** - Contact forms, feedback submissions * **Health checks or status endpoints** - System monitoring that shouldn’t require authentication ## Policy-Based Authorization [Section titled “Policy-Based Authorization”](#policy-based-authorization) For more complex authorization scenarios, you can use policy-based authorization: ```csharp [Command] [Authorize(Policy = "RequireElevatedAccess")] public record PerformSensitiveOperation(string Data) { public void Handle(ISensitiveOperationService service) { service.Execute(Data); } } ``` ## Authorization Results [Section titled “Authorization Results”](#authorization-results) When authorization fails, the command pipeline returns an unauthorized result. The command handler will not be executed: ```csharp var result = await commandPipeline.Execute(command); if (!result.IsAuthorized) { // Handle unauthorized access // The command was not executed } ``` ## Executing Commands from Server-Side Code [Section titled “Executing Commands from Server-Side Code”](#executing-commands-from-server-side-code) Authorization reads the principal from the current HTTP request. Server-side callers — reactors, hosted services, background jobs, sagas, or one command orchestrating another — have no HTTP request, so a command carrying `[Authorize]` or `[Roles]` would be denied. To run such a command as a trusted system actor, establish a server-side execution scope with `ISystemExecution`: ```csharp public class NightlyReconciliation(ISystemExecution systemExecution, ICommandPipeline pipeline) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { using (systemExecution.AsSystem("Administrator")) { await pipeline.Execute(new ReconcileLedger()); } } } ``` `AsSystem(params string[] roles)` runs as an authenticated system actor carrying exactly the roles you name — the normal role check still applies, so a `[Roles("Administrator")]` command passes while a `[Roles("Auditor")]` command is still denied. With no roles the actor satisfies `[Authorize]` but no `[Roles]`. Use `As(ClaimsPrincipal principal)` when you need to run as a specific principal. The scope is ambient and restores the previous context when disposed, so it flows into every command executed inside the `using` block, including nested calls. > The server-side principal is consulted **only when there is no HTTP request context**. On any HTTP request the request principal is always authoritative — a server-side scope can never influence authorization of an HTTP-origin command, and request-supplied data can never enter the scope. ### Executing Commands from Reactors [Section titled “Executing Commands from Reactors”](#executing-commands-from-reactors) When a reactor returns a command as a side effect, mark the reactor with `[ExecuteCommandsAsSystem]` to run those commands under the declared roles automatically: ```csharp [Reactor] [ExecuteCommandsAsSystem("Administrator")] public class ConsultantProvisioner : IReactor { public InviteConsultant ConsultantRequested(ConsultantRequested @event, EventContext context) => new(@event.Email); } ``` A reactor that instead injects `ICommandPipeline` and calls `Execute` directly establishes the scope itself: ```csharp public class ConsultantProvisioner(ISystemExecution systemExecution, ICommandPipeline pipeline) : IReactor { [OnceOnly] public async Task ConsultantRequested(ConsultantRequested @event, EventContext context) { using (systemExecution.AsSystem("Administrator")) { await pipeline.Execute(new InviteConsultant(@event.Email)); } } } ``` ## Best Practices [Section titled “Best Practices”](#best-practices) 1. **Apply authorization at the command level** - Each command should declare its own authorization requirements 2. **Use the `[Roles]` attribute** - More convenient than the standard `[Authorize(Roles = "...")]` syntax 3. **Be explicit about public access** - Use `[AllowAnonymous]` to clearly indicate intentionally public commands 4. **Consider the principle of least privilege** - Only grant the minimum access required 5. **Test authorization** - Ensure unauthorized users cannot execute protected commands 6. **Use policies for complex logic** - Implement custom authorization policies for domain-specific rules 7. **Log authorization failures** - Monitor and log unauthorized access attempts > **Note**: Authorization is evaluated as part of the command filter pipeline before the command handler is called. If authorization fails, the command will not be executed and the `CommandResult.IsAuthorized` will be `false`. # Command Response Examples This document provides examples of how command responses work with the automatic response handling feature. ## Simple Value Response [Section titled “Simple Value Response”](#simple-value-response) When a command handler returns a simple value without a corresponding value handler: ```csharp [Command] public record CreateUser(string Name, string Email) { public UserId Handle(IUserRepository userRepository) { var userId = new UserId(Guid.NewGuid()); var user = new User(userId, Name, Email); userRepository.Save(user); // Since no value handler exists for UserId, // this automatically becomes CommandResult return userId; } } ``` **Result**: `CommandResult` with the UserId as the Response property. ## OneOf Response [Section titled “OneOf Response”](#oneof-response) When a command returns a OneOf with values that may or may not have handlers: ```csharp [Command] public record ValidateAndCreateUser(string Name, string Email) { public OneOf Handle(IUserRepository userRepository, IUserValidator validator) { var validationResult = validator.Validate(Name, Email); if (!validationResult.IsValid) { // ValidationResult has a built-in handler, so this affects command success return OneOf.FromT1(validationResult); } var userId = new UserId(Guid.NewGuid()); var user = new User(userId, Name, Email); userRepository.Save(user); // No handler for UserId, so this becomes CommandResult return OneOf.FromT0(userId); } } ``` **Result**: Either a failed `CommandResult` (for validation errors) or `CommandResult` (for success). ## Tuple Response with Mixed Handlers [Section titled “Tuple Response with Mixed Handlers”](#tuple-response-with-mixed-handlers) When a command returns a tuple with some values having handlers and others not: ```csharp [Command] public record ProcessOrder(string OrderId) { public (OrderConfirmation, OrderProcessed, ValidationResult, AuditLog) Handle( IOrderService orderService, IOrderValidator validator, IAuditService auditService) { var validation = validator.Validate(OrderId); var confirmation = orderService.ProcessOrder(OrderId); var orderEvent = new OrderProcessed(OrderId, DateTime.UtcNow); var auditLog = auditService.CreateLog("Order processed", OrderId); return (confirmation, orderEvent, validation, auditLog); } } ``` Assuming the following handlers exist: * `OrderProcessed` → handled by event handler * `ValidationResult` → handled by validation handler * `AuditLog` → handled by audit handler * `OrderConfirmation` → **no handler** **Result**: `CommandResult` with the confirmation as the Response property, plus any side effects from the other handlers. ## Tuple with Multiple Unhandled Values (Error) [Section titled “Tuple with Multiple Unhandled Values (Error)”](#tuple-with-multiple-unhandled-values-error) This scenario throws an exception: ```csharp [Command] public record BadCommand() { public (string, int, SomeEvent) Handle() { // If no handlers exist for string and int, but SomeEvent has a handler return ("response1", 42, new SomeEvent()); } } ``` **Result**: `MultipleUnhandledTupleValues` because both `string` and `int` lack handlers. ## Custom Value Handler Example [Section titled “Custom Value Handler Example”](#custom-value-handler-example) Creating a handler to process specific values instead of making them responses: ```csharp public class OrderConfirmationHandler : ICommandResponseValueHandler { private readonly IEmailService _emailService; public OrderConfirmationHandler(IEmailService emailService) { _emailService = emailService; } public bool CanHandle(CommandContext commandContext, object value) { return value is OrderConfirmation; } public async Task Handle(CommandContext commandContext, object value) { var confirmation = (OrderConfirmation)value; // Send confirmation email as a side effect await _emailService.SendConfirmationEmail(confirmation); // Don't affect the command result return CommandResult.Success(commandContext.CorrelationId); } } ``` With this handler in place, `OrderConfirmation` values would be processed (email sent) rather than becoming responses. ## Migration from Previous Versions [Section titled “Migration from Previous Versions”](#migration-from-previous-versions) ### Before (Required Value Handlers) [Section titled “Before (Required Value Handlers)”](#before-required-value-handlers) ```csharp // Previously, this would require a value handler for UserId public UserId Handle() => new UserId(Guid.NewGuid()); // You had to create a handler like this: public class UserIdHandler : ICommandResponseValueHandler { public bool CanHandle(CommandContext context, object value) => value is UserId; public Task Handle(CommandContext context, object value) { var userId = (UserId)value; // Set the response manually... return Task.FromResult(new CommandResult(userId)); } } ``` ### After (Automatic Responses) [Section titled “After (Automatic Responses)”](#after-automatic-responses) ```csharp // Now this automatically becomes CommandResult public UserId Handle() => new UserId(Guid.NewGuid()); // No handler needed unless you want side effects ``` This reduces boilerplate while maintaining the same functionality. # Response Value Handlers Command handlers can return values that need to be processed by the command pipeline. The Arc provides a flexible system for handling these return values through Response Value Handlers. ## Automatic Response Handling [Section titled “Automatic Response Handling”](#automatic-response-handling) When a command handler returns a value, the command pipeline follows this logic: 1. **Check for Value Handlers**: The pipeline first checks if any registered response value handlers can handle the returned value using their `CanHandle` method 2. **Process with Handler**: If a value handler can handle the value, it processes the value and returns a `CommandResult` 3. **Automatic Response Creation**: If **no value handler** can handle the value, the pipeline automatically creates a `CommandResult` with the returned value as the response This means that **command handlers can return any type of value**, and it will either be processed by a specific handler or automatically become the command response. > **Note**: When a value handler appends events — for example the Chronicle handlers that append returned events — those appends enroll in the command’s transaction and commit atomically with it. See [Transactional Commands](/arc/backend/commands/transactional-commands/). ## Built-in Value Handlers [Section titled “Built-in Value Handlers”](#built-in-value-handlers) Out-of-the-box Cratis Arc comes with the following value handlers: | Type | Description | | ------------------------------------ | ---------------------------------------------------------------------------- | | ValidationResultResponseValueHandler | Responds to Cratis ValidationResult object and adds it to the command result | ## Command Handler Return Patterns [Section titled “Command Handler Return Patterns”](#command-handler-return-patterns) ### Single Value Return [Section titled “Single Value Return”](#single-value-return) ```csharp [Command] public record CreateUser(string Name, string Email) { public Guid Handle() { var userId = Guid.NewGuid(); // This will automatically create CommandResult with userId as response return userId; } } ``` ### Result Return [Section titled “Result Return”](#result-return) ```csharp using Cratis.Arc.Validation; using OneOf; [Command] public record CreateUser(string Name, string Email) { public Result Handle() { if (!IsValidEmail(Email)) { return ValidationResult.Error("Invalid email address"); } var userId = new UserId(Guid.NewGuid()); // If no handler can process UserId, it becomes CommandResult // If ValidationResultResponseValueHandler processes ValidationResult, it affects the command result return userId; } } ``` ### Tuple Return [Section titled “Tuple Return”](#tuple-return) ```csharp [Command] public record CreateUser(string Name, string Email) { public (UserId, AuditInfo) Handle() { var userId = new UserId(Guid.NewGuid()); var auditInfo = new AuditInfo(DateTime.UtcNow, "system"); // Each value is checked against available handlers // If no handler processes userId, it becomes the response // If a handler processes auditInfo, it affects the command result return (userId, auditInfo); } } ``` ## Creating Custom Value Handlers [Section titled “Creating Custom Value Handlers”](#creating-custom-value-handlers) You can create custom response value handlers by implementing the runtime `ICommandResponseValueHandler` interface. When the handler consumes a statically known type, also implement `ICommandResponseValueHandler` so build-time tools know that the value is handled on the server and must not be generated as a client response model: ```csharp public class AuditInfoResponseValueHandler : ICommandResponseValueHandler, ICommandResponseValueHandler { public bool CanHandle(CommandContext commandContext, object value) { return value is AuditInfo; } public Task Handle(CommandContext commandContext, object value) { var auditInfo = (AuditInfo)value; // Perform audit logging LogAuditEvent(auditInfo); // Return success - this doesn't affect the command response return Task.FromResult(CommandResult.Success(commandContext.CorrelationId)); } } ``` The Arc will automatically discover and register custom value handlers in the command pipeline. The typed interface is a declaration for tooling; it does not replace the runtime interface. Implementing only `ICommandResponseValueHandler` neither registers a runtime handler nor suppresses the client response. A handler whose accepted types are determined dynamically at runtime should implement only the runtime interface, because declaring an overly broad type such as `object` would hide legitimate client response models. Declarations are discovered from the application and every package it references. Only assemblies referencing `Cratis.Arc.Core` are inspected, and both contracts are matched on assembly identity rather than on name, so a look-alike interface declared elsewhere cannot claim a value and suppress its client response. ## Response Object Availability [Section titled “Response Object Availability”](#response-object-availability) When implementing a command response value handler, the `CommandContext.Response` property contains the response object returned by the command handler, **if any**. This property can be `null` in the following scenarios: * The command handler didn’t return anything (void method) * The command handler returned `null` * The command handler returned a single value that has no corresponding value handler (in which case that value becomes the automatic response) ### Tuple Processing Behavior [Section titled “Tuple Processing Behavior”](#tuple-processing-behavior) When a command handler returns a tuple, the command pipeline intelligently processes each value: 1. **Each value is evaluated** against all available response value handlers using their `CanHandle` method 2. **Values with handlers** are processed by their respective response value handlers 3. **Values without handlers** are considered potential response values: * If exactly **one value** has no handler, it becomes the response (available in `CommandContext.Response`) * If **multiple values** have no handlers, a `MultipleUnhandledTupleValues` is thrown * If **all values** have handlers, no response is set (`CommandContext.Response` remains `null`) ### Result Processing Behavior [Section titled “Result Processing Behavior”](#result-processing-behavior) When a command handler returns a `Result` or `OneOf` value: 1. **The inner value** is extracted from the Result/OneOf wrapper 2. **Value handlers are checked** using the `CanHandle` method on the inner value 3. **If a handler can process it**, the handler processes the value 4. **If no handler can process it**, the inner value automatically becomes a `CommandResult` response #### Result with Tuple Alternatives [Section titled “Result with Tuple Alternatives”](#result-with-tuple-alternatives) The command pipeline also supports `Result` types where one or more alternatives are tuples. In this case: 1. **The inner value** is extracted from the Result wrapper 2. **If the inner value is a tuple**, it is processed using the standard tuple processing rules (see above) 3. **If the inner value is a simple type**, it follows the standard Result processing rules ```csharp using Cratis.Arc.Validation; using OneOf; [Command] public record CreateOrder(string CustomerId, List Items) { public Result<(OrderId, OrderCreated), ValidationResult> Handle() { if (!IsValidOrder()) { return ValidationResult.Error("Invalid order"); } var orderId = OrderId.New(); // Return a tuple with response and event return (orderId, new OrderCreated(orderId, CustomerId, Items)); } } ``` In this example: * When validation fails, the `ValidationResult` is returned and processed by the validation handler * When successful, the tuple is returned: * `OrderCreated` is processed by its response value handler (e.g., event handler) * `OrderId` becomes the response (assuming it has no handler) ## Key Benefits [Section titled “Key Benefits”](#key-benefits) * **Zero Configuration**: Values without handlers automatically become responses * **Flexible Processing**: Custom handlers can perform side effects (logging, notifications, etc.) * **Type Safety**: Automatic responses are properly typed as `CommandResult` * **Backward Compatibility**: Existing value handlers continue to work as before ### Example Implementation [Section titled “Example Implementation”](#example-implementation) ```csharp public class MyResponseValueHandler : ICommandResponseValueHandler, ICommandResponseValueHandler { public bool CanHandle(CommandContext commandContext, object value) { // The commandContext.Response can be null here return value is MyValueType; } public Task Handle(CommandContext commandContext, object value) { // Access the response if available var response = commandContext.Response; // This can be null if (response is not null) { // Handle cases where the command returned a response // This typically happens when the command returns a tuple with unhandled values } // Process the value that was returned by the command var myValue = (MyValueType)value; // Perform side effects (logging, notifications, etc.) ProcessValue(myValue); return Task.FromResult(CommandResult.Success(commandContext.CorrelationId)); } } ``` ## Migration Notes [Section titled “Migration Notes”](#migration-notes) ### From Previous Versions [Section titled “From Previous Versions”](#from-previous-versions) If you were previously relying on the requirement that **all returned values must have corresponding value handlers**, this is no longer necessary. Values without handlers will now automatically become command responses, which provides: * **Simplified Development**: No need to create handlers for simple response values * **Better Developer Experience**: Commands can return domain objects directly * **Reduced Boilerplate**: Less code needed for basic response scenarios ### Backward Compatibility [Section titled “Backward Compatibility”](#backward-compatibility) All existing value handlers continue to work exactly as before. The new automatic response creation only applies to values that have no corresponding handlers, ensuring full backward compatibility. # Transactional Commands Every command executed through the command pipeline runs in a transactional scope. The events a command declares transactional — the events it **returns from `Handle()`** and appends made through the explicit **`eventLog.Transactional`** style — are committed together, atomically, when the command succeeds. If the command fails for **any** reason — a validation error, a constraint violation, or an exception — none of them are appended. > **Note**: The transactional scope applies to the **model-bound command pipeline** — commands executed over HTTP, directly through `ICommandPipeline`, from reactors, and in the `CommandScenario` test harness. Controller-based commands do not participate. Appends through an injected `IEventLog` (or `IEventStore.EventLog`) are **immediate and final** — they behave identically everywhere, inside and outside commands, returning the real `AppendResult`. They are still never silently swallowed: a failed immediate append during a command **fails the command**, which also rolls back everything enrolled in the transaction. ## Choosing an Append Style [Section titled “Choosing an Append Style”](#choosing-an-append-style) | You write | Semantics | What you get back | | ---------------------------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------ | | Return events from `Handle()` | Transactional — atomic with the command | The `CommandResult` is the outcome | | Return `EventsWithConcurrencyScopes` | Transactional — ordered cross-source events with the exact revisions the decision used | The `CommandResult` is the outcome | | `eventLog.Transactional.Append(...)` | Transactional — atomic with the command | A plain `Task` — no per-append result exists until commit | | `eventLog.Append(...)` / `eventStore.EventLog.Append(...)` | Immediate and final — same as any event sequence | The real `AppendResult`; a failed one also fails the command | Returning events is the recommended style: ```csharp [Command] public record StartOnboarding(OnboardingId OnboardingId, InvitationId InvitationId, OrganizationNumber OrganizationNumber) { public IEnumerable Handle() => [ new(OnboardingId, new OnboardingStarted(OrganizationNumber)), new(InvitationId, new AdminInvited(OnboardingId)) ]; } ``` If the `OnboardingStarted` append is rejected by a unique constraint on the organization number, the `AdminInvited` event on the other stream is rolled back with it — the command fails cleanly and the `CommandResult` carries the violation, attributed to the offending member. For imperative transactional appends, use the explicit `Transactional` style — its shape tells the truth about the deferral: `Append` returns a plain `Task`, because no per-append result exists until the command commits: ```csharp [Command] public record RegisterReadings(SensorId SensorId, IEnumerable Readings) { public async Task Handle(IEventLog eventLog) { foreach (var reading in Readings) { await eventLog.Transactional.Append(SensorId, new ReadingRegistered(reading)); } } } ``` And when you deliberately want an append to be immediate — final the moment it succeeds, surviving even if the command later fails — use the plain `Append`, which behaves exactly like it does everywhere else: ```csharp [Command] public record ImportReadings(SensorId SensorId) { public async Task Handle(IEventLog eventLog) { // Appends immediately — this event stays even if the command fails afterwards. await eventLog.Append(SensorId, new ImportAttempted()); // ... work that may fail ... } } ``` ## How It Works [Section titled “How It Works”](#how-it-works) When a command executes, Arc begins a Chronicle unit of work bounded by the command. Events the command returns — and appends through `Transactional` — enroll in it instead of hitting the event store immediately. When the command completes: * **Success** — the unit of work commits all enrolled events as one atomic operation. If the commit is rejected — for example by a unique constraint — the violation surfaces as a validation error on the `CommandResult`, attributed to the offending member, and the command fails. * **Failure** — the unit of work rolls back and none of the enrolled events are appended. Immediate appends are observed throughout: if one fails during the command, its violations surface on the `CommandResult` and the command fails — rolling back the enrolled events with it. ``` ChronicleUnit of WorkHandle()Command PipelineClientChronicleUnit of WorkHandle()Command PipelineClientNo enrolled events appendedalt[Command succeeded][Command failed (incl. a failed immediate append)]Execute commandBeginInvokeImmediate appends (final, real results)Return events (enrolled in UoW)CommitAppend enrolled events atomicallyResult (or constraint violations)Violations surface on CommandResultRollbackCommandResult ``` The mechanism behind this is the [command execution scope](/arc/backend/commands/command-execution-scopes/) extension point — the transactional scope is its built-in implementation. ## Nested Commands and Aggregates [Section titled “Nested Commands and Aggregates”](#nested-commands-and-aggregates) A command executed from within another command — for example through `ICommandPipeline` from a handler — joins the outermost command’s transaction. Only the outermost command commits or rolls back, so the whole composition is atomic for its enrolled events. Aggregate roots already use the unit of work for their `Commit()`. Within a command they share the command’s unit of work, so aggregate mutations and enrolled events from the same command commit together. Note that calling an aggregate’s `Commit()` inside a handler commits the command’s transaction **at that point** — anything enrolled afterwards is outside the committed batch. Prefer letting the command complete the transaction: apply the aggregate’s events and let the pipeline commit when the command finishes. ## Things to Know [Section titled “Things to Know”](#things-to-know) * **Immediate appends are final.** A successful `eventLog.Append(...)` is in the log the moment it returns, and stays there even if the command fails afterwards. The safety net guarantees a *failed* append fails the command — it cannot un-append earlier successful ones. If you need all-or-nothing, return events or use `Transactional`. * **Reads don’t see the transaction’s enrolled events.** Reading through `IEventLog` inside a handler queries the event store, which doesn’t contain events that are enrolled but not yet committed. Immediate appends *are* visible right away. * **A nested command’s result reflects enrollment, not the final outcome.** Its enrolled events commit — and any violation surfaces — when the outermost command completes. * **Never use `Transactional` from background work.** An append through `eventLog.Transactional` from a continuation that outlives the command targets a completed unit of work and is **silently lost**. Plain immediate appends from background work go to the event store normally. If you need side effects after events are committed, use a reactor. * **`Transactional` requires an active transaction.** Outside a command — for example in a controller action with no ambient unit of work — it throws; use the plain `Append` there. * **Chronicle’s ASP.NET Core unit of work middleware coexists.** A command always begins its own transaction rather than joining a request-level unit of work, and controller-based code keeps the request-level behavior it had. For how the guarantee shows up in tests — asserting that a failed command committed nothing transactional, and that violations surface on the result — see [Testing with Chronicle](/arc/backend/testing/chronicle/). # Validation Commands can be validated by either using [FluentValidation](https://docs.fluentvalidation.net/en/latest/) or the attribute based validators found in the `System.ComponentModel.DataAnnotations` namespace. The validators are performed before the commands handler method is invoked. If any validators cause a validation error, it will not invoke the command handler and just return a `CommandResult` with the errors in it. > **💡 Client-Side Validation**: When using FluentValidation, validation rules are automatically extracted by the [ProxyGenerator](/arc/backend/proxy-generation/validation/) and run on the client before server calls. This provides immediate feedback to users and reduces unnecessary server requests. ## Data Annotations [Section titled “Data Annotations”](#data-annotations) Depending on how you like to do your validation, with data annotations you can adorn a value on a command directly. This can be helpful if you’re trying to keep things lightweight and very cohesive. ```csharp [Command] public record AddItemToCart( [Required] string Sku, int Quantity) { public void Handle() { // Handle the command } } ``` The code adds the `[Required]` attribute to the `sku` property. > Note: The required attribute can also take a specific error message. ## Fluent Validation [Section titled “Fluent Validation”](#fluent-validation) When using [FluentValidation](https://docs.fluentvalidation.net/en/latest/) you get more control of an flexibility the flow of validation. For instance, the validator can take dependencies and with it you can call other systems that has the required knowledge for the validation rules you want for your commands. Given the same sample as for Data Annotation, this would be like the following: ```csharp [Command] public record AddItemToCart( string Sku, int Quantity) { public void Handle() { // Handle the command } } public class AddItemToCartValidators : CommandValidator { public AddItemToCartValidators() { RuleFor(c => c.Sku).NotEmpty().WithMessage("You have to provide a Sku"); } } ``` The code shows the `AddItemToCartValidators` class implementing the `CommandValidator<>`, which is required for validating commands. It makes the validator discoverable by the system and you don’t have to register it anywhere. ### Validator Dependencies [Section titled “Validator Dependencies”](#validator-dependencies) Command validators can take dependencies through their constructors. Arc resolves those dependencies from the same command scope used by `Provide()` and `Handle()`, so validators can check current state before the command handler runs. ```csharp public class RemoveContactValidator : CommandValidator { public RemoveContactValidator(Customer? customer) { RuleFor(_ => customer) .NotNull() .WithMessage("Customer is not registered"); When(_ => customer is not null, () => { RuleFor(command => command.ContactId) .Must(contactId => customer!.Contacts.Contains(contactId)) .WithMessage("Contact is not assigned to this customer"); }); } } ``` Nullable dependency parameters are allowed. If the dependency cannot be resolved, or resolves to `null`, Arc injects `null` into a nullable parameter. Use this when missing state is part of the command’s valid behavior, including read-model existence checks that should become validation messages. Non-nullable dependency parameters are treated as required. If Arc cannot resolve the dependency, or the resolved value is `null`, it throws a clear dependency-resolution exception instead of constructing the validator with an invalid value. For Chronicle read models, this is a deliberate choice that the projection is required to exist for the command. ```csharp public class SubmitOrderValidator : CommandValidator { public SubmitOrderValidator(OrderReadModel order) { RuleFor(_ => order.Status) .Equal(OrderStatus.ReadyForSubmission) .WithMessage("Only orders that are ready for submission can be submitted"); RuleFor(_ => order.Lines) .NotEmpty() .WithMessage("Order must have at least one line"); } } ``` The same dependency behavior applies to dependencies on `Provide()` and `Handle()` parameters. For Chronicle read models, the analyzer rule [ARC0006](/arc/backend/code-analysis/arc0006/) warns when a command-scoped read model parameter is non-nullable in a validator, `Provide()`, or `Handle()` so the required-state choice is explicit. ### Validating against projected state [Section titled “Validating against projected state”](#validating-against-projected-state) The `Customer?` and `OrderReadModel` dependencies above are Chronicle **read models** — current state projected from events, which Arc resolves for the command’s own key and injects without a query. This is the most common reason a validator takes a dependency at all. * [Use current state in a command](/arc/scenarios/use-current-state-in-a-command/) — the recipe. * [Read models in commands](/arc/backend/chronicle/read-models/injecting-into-commands/) — the full reference for validators, `Provide()`, and `Handle()`. * [When read model resolution fails](/arc/backend/chronicle/read-models/failures/) — including why this does not work through MVC controllers. # Validation Severity Filtering Validation severity filtering allows commands to specify which validation result severity levels should block execution. This enables flexible validation workflows where warnings and informational messages can be shown to users without preventing command execution. ## Overview [Section titled “Overview”](#overview) Validation results have different severity levels that indicate the importance of the validation issue: ```csharp public enum ValidationResultSeverity { /// /// The validation result is unknown. /// Unknown = 0, /// /// The validation result is informational. /// Information = 1, /// /// The validation result is a warning. /// Warning = 2, /// /// The validation result is an error. /// Error = 3 } ``` By default, only **Error** severity results block command execution. Warnings and Information results are filtered out and don’t prevent execution. ## Purpose [Section titled “Purpose”](#purpose) Severity filtering enables: * **User-Friendly Workflows**: Show warnings to users without blocking operations * **Confirmable Warnings**: Allow users to review and acknowledge warnings before proceeding * **Flexible Validation**: Apply different validation strictness based on context * **Progressive Execution**: Validate strictly first, then allow controlled overrides ## How It Works [Section titled “How It Works”](#how-it-works) ### Request Flow [Section titled “Request Flow”](#request-flow) 1. Client sends command with optional `X-Allowed-Severity` HTTP header 2. `CommandEndpointMapper` reads the header and parses severity value 3. `CommandPipeline` executes with `allowedSeverity` parameter 4. Validation filters run and return validation results 5. `FilterValidationResults` filters based on allowed severity 6. Only validation results with severity > `allowedSeverity` block execution ### CommandContext [Section titled “CommandContext”](#commandcontext) The `CommandContext` includes the allowed severity: ```csharp public record CommandContext( CorrelationId CorrelationId, Type Type, object Command, IEnumerable Dependencies, CommandContextValues Values, ValidationResultSeverity? AllowedSeverity = default, object? Response = default); ``` ### ICommandPipeline [Section titled “ICommandPipeline”](#icommandpipeline) The `ICommandPipeline` interface accepts an optional `allowedSeverity` parameter: ```csharp public interface ICommandPipeline { /// /// Executes the given command. /// /// The command to execute. /// The service provider scoped to the current request. /// Optional maximum validation result severity level to allow. /// A CommandResult representing the result of executing the command. Task Execute(object command, IServiceProvider serviceProvider, ValidationResultSeverity? allowedSeverity = default); /// /// Validates the given command without executing it. /// /// The command to validate. /// The service provider scoped to the current request. /// Optional maximum validation result severity level to allow. /// A CommandResult representing the validation result. Task Validate(object command, IServiceProvider serviceProvider, ValidationResultSeverity? allowedSeverity = default); } ``` ## Implementation [Section titled “Implementation”](#implementation) ### CommandPipeline [Section titled “CommandPipeline”](#commandpipeline) The `CommandPipeline` filters validation results after filters run: ```csharp public async Task Execute(object command, IServiceProvider serviceProvider, ValidationResultSeverity? allowedSeverity = default) { var correlationId = GetCorrelationId(); var result = CommandResult.Success(correlationId); try { handlerProviders.TryGetHandlerFor(command, out var commandHandler); if (commandHandler is null) { return CommandResult.MissingHandler(correlationId, command.GetType()); } var dependencies = commandHandler.Dependencies.Select(serviceProvider.GetRequiredService); var commandContext = new CommandContext( correlationId, command.GetType(), command, dependencies, contextValuesBuilder.Build(command), allowedSeverity); // Pass allowed severity to context contextModifier.SetCurrent(commandContext); result = await commandFilters.OnExecution(commandContext); // Filter validation results based on allowed severity result = FilterValidationResults(result, allowedSeverity); if (!result.IsSuccess) { return result; } var response = await commandHandler.Handle(commandContext); // Process response... } catch (Exception ex) { result.MergeWith(CommandResult.Error(correlationId, ex)); } return result; } ``` ### FilterValidationResults [Section titled “FilterValidationResults”](#filtervalidationresults) The filtering logic: ```csharp /// /// Filters validation results based on the allowed severity level. /// /// The command result to filter. This method modifies the ValidationResults property. /// The maximum allowed severity level. Results with higher severity will be kept. /// The modified command result. /// /// When allowedSeverity is null, only errors block execution (warnings and information are filtered out). /// When allowedSeverity is specified, only validation results with severity > allowedSeverity block execution. /// CommandResult FilterValidationResults(CommandResult result, ValidationResultSeverity? allowedSeverity) { if (allowedSeverity is null) { // Default behavior: only errors block execution (warnings and information are filtered out) result.ValidationResults = result.ValidationResults.Where(v => v.Severity == ValidationResultSeverity.Error).ToArray(); } else { // Filter out validation results with severity <= allowedSeverity result.ValidationResults = result.ValidationResults.Where(v => v.Severity > allowedSeverity).ToArray(); } return result; } ``` ### CommandEndpointMapper [Section titled “CommandEndpointMapper”](#commandendpointmapper) The `CommandEndpointMapper` reads the `X-Allowed-Severity` header from requests: ```csharp ValidationResultSeverity? allowedSeverity = default; if (context.Headers.TryGetValue("X-Allowed-Severity", out var severityHeader) && int.TryParse(severityHeader, out var severityValue)) { allowedSeverity = (ValidationResultSeverity)severityValue; } commandResult = validateOnly ? await commandPipeline.Validate(command, context.RequestServices, allowedSeverity) : await commandPipeline.Execute(command, context.RequestServices, allowedSeverity); ``` ## Creating Warnings in Validators [Section titled “Creating Warnings in Validators”](#creating-warnings-in-validators) ### FluentValidation [Section titled “FluentValidation”](#fluentvalidation) Use the custom `WithSeverity` method or leverage FluentValidation’s built-in severity: ```csharp public class CreateOrderValidator : CommandValidator { public CreateOrderValidator() { // Critical validation - Error severity (default) RuleFor(c => c.OrderNumber) .NotEmpty() .WithMessage("Order number is required"); // Warning - soft validation RuleFor(c => c.Quantity) .GreaterThan(0) .WithMessage("Order quantity is very low") .WithSeverity(Severity.Warning); // Information - helpful message RuleFor(c => c.DeliveryDate) .GreaterThan(DateTime.UtcNow.AddDays(7)) .WithMessage("Orders placed more than 7 days in advance may be eligible for free shipping") .WithSeverity(Severity.Info); } } ``` **Note**: You’ll need to configure the mapping from FluentValidation’s `Severity` to Arc’s `ValidationResultSeverity`: ```csharp // In your FluentValidationFilter or custom implementation var severity = validationFailure.Severity switch { Severity.Error => ValidationResultSeverity.Error, Severity.Warning => ValidationResultSeverity.Warning, Severity.Info => ValidationResultSeverity.Information, _ => ValidationResultSeverity.Error }; ``` ### Custom Validation Results [Section titled “Custom Validation Results”](#custom-validation-results) Create validation results with specific severity directly: ```csharp [Command] public record CreateOrder(string OrderNumber, int Quantity) { public (ValidationResult[], Order?) Handle(IInventoryService inventoryService) { var validationResults = new List(); // Check inventory var stock = inventoryService.GetStock(OrderNumber); if (stock == 0) { // Critical error - cannot proceed validationResults.Add(new ValidationResult( ValidationResultSeverity.Error, "Product is out of stock", [nameof(OrderNumber)], null)); } else if (stock < Quantity) { // Warning - user can override validationResults.Add(new ValidationResult( ValidationResultSeverity.Warning, $"Only {stock} units available. Order will be partially fulfilled.", [nameof(Quantity)], new { AvailableStock = stock })); } else if (stock < 10) { // Information - just FYI validationResults.Add(new ValidationResult( ValidationResultSeverity.Information, "Stock is running low. Consider ordering soon.", [nameof(OrderNumber)], null)); } // If only warnings/info, return them along with the order if (validationResults.Any() && validationResults.All(v => v.Severity < ValidationResultSeverity.Error)) { var order = new Order { OrderNumber = OrderNumber, Quantity = Quantity }; return (validationResults.ToArray(), order); } // If errors, return only validation results if (validationResults.Any(v => v.Severity == ValidationResultSeverity.Error)) { return (validationResults.ToArray(), null); } // All good var successOrder = new Order { OrderNumber = OrderNumber, Quantity = Quantity }; return ([], successOrder); } } ``` ## Programmatic Usage [Section titled “Programmatic Usage”](#programmatic-usage) ### Direct Pipeline Usage [Section titled “Direct Pipeline Usage”](#direct-pipeline-usage) You can use the pipeline directly with severity filtering: ```csharp public class OrderService { private readonly ICommandPipeline _commandPipeline; public OrderService(ICommandPipeline commandPipeline) { _commandPipeline = commandPipeline; } public async Task CreateOrderStrictly(CreateOrder command) { // Default behavior - only errors block return await _commandPipeline.Execute(command, serviceProvider); } public async Task CreateOrderAllowingWarnings(CreateOrder command) { // Allow warnings to pass through return await _commandPipeline.Execute( command, serviceProvider, ValidationResultSeverity.Warning); } public async Task CreateOrderWithConfirmation(CreateOrder command, bool userConfirmedWarnings) { // First attempt - strict validation var result = await _commandPipeline.Execute(command, serviceProvider); if (!result.IsSuccess && !result.HasExceptions) { // Check if only warnings var hasOnlyWarnings = result.ValidationResults.All(v => v.Severity == ValidationResultSeverity.Warning); if (hasOnlyWarnings && userConfirmedWarnings) { // User confirmed - allow warnings result = await _commandPipeline.Execute( command, serviceProvider, ValidationResultSeverity.Warning); } } return result; } } ``` ### Integration Testing [Section titled “Integration Testing”](#integration-testing) Test severity filtering in integration tests: ```csharp [Fact] public async Task should_block_execution_with_error_severity() { var command = new CreateOrder("INVALID", 1); var result = await _commandPipeline.Execute(command, _serviceProvider); result.IsSuccess.ShouldBeFalse(); result.ValidationResults.ShouldNotBeEmpty(); result.ValidationResults.ShouldAllBe(v => v.Severity == ValidationResultSeverity.Error); } [Fact] public async Task should_block_execution_with_warning_when_not_allowed() { var command = new CreateOrder("LOW-STOCK", 1); // Don't allow warnings var result = await _commandPipeline.Execute(command, _serviceProvider); result.IsSuccess.ShouldBeFalse(); result.ValidationResults.ShouldContain(v => v.Severity == ValidationResultSeverity.Warning); } [Fact] public async Task should_allow_execution_with_warning_when_allowed() { var command = new CreateOrder("LOW-STOCK", 1); // Allow warnings var result = await _commandPipeline.Execute( command, _serviceProvider, ValidationResultSeverity.Warning); result.IsSuccess.ShouldBeTrue(); } ``` ## Best Practices [Section titled “Best Practices”](#best-practices) ### When to Use Different Severities [Section titled “When to Use Different Severities”](#when-to-use-different-severities) **Error Severity** - Use for: * Required field validations * Data format errors * Business rule violations * Authorization failures * Data integrity issues **Warning Severity** - Use for: * Soft business rules that can be overridden * Potential issues that don’t prevent operation * Non-critical recommendations * Edge cases requiring user acknowledgment **Information Severity** - Use for: * Helpful tips and suggestions * Status information * Performance recommendations * Optional improvements ### Security Considerations [Section titled “Security Considerations”](#security-considerations) * **Never** use Warning severity for security validations * **Always** use Error severity for: * Authorization checks * Authentication failures * Security policy violations * Critical business rules * Don’t rely solely on client-side severity filtering * Server always validates with the same severity logic ### Performance Tips [Section titled “Performance Tips”](#performance-tips) * Severity filtering adds minimal overhead * Same validators run regardless of allowed severity * Consider validator performance separately * Use appropriate indexes for validation queries ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Warnings Not Filtered [Section titled “Warnings Not Filtered”](#warnings-not-filtered) **Cause**: Validators might be using Error severity instead of Warning. **Solution**: Check validator implementations and ensure they use appropriate severity: ```csharp // Wrong - using default Error severity RuleFor(c => c.Quantity) .GreaterThan(0) .WithMessage("Quantity should be positive"); // Correct - using Warning severity RuleFor(c => c.Quantity) .GreaterThan(0) .WithMessage("Quantity should be positive") .WithSeverity(Severity.Warning); ``` ### Errors Allowed Through [Section titled “Errors Allowed Through”](#errors-allowed-through) **Cause**: Incorrectly configured severity or using Error severity in `allowedSeverity` parameter. **Solution**: * Never pass `ValidationResultSeverity.Error` as the allowed severity * Verify validators are using Error severity for critical issues * Check that custom validation code sets correct severity ### Client and Server Results Differ [Section titled “Client and Server Results Differ”](#client-and-server-results-differ) **Cause**: Client-side and server-side validators may have different implementations. **Solution**: * Server validation is authoritative * Ensure client validators match server rules * Use FluentValidation with proxy generation for consistency * Test both client and server validation ## Related Documentation [Section titled “Related Documentation”](#related-documentation) * [Command Validation](/arc/backend/commands/command-validation/) - Pre-flight validation * [Validation](/arc/backend/commands/validation/) - Validation configuration * [Command Filters](/arc/backend/commands/command-filters/) - Validation pipeline * [Frontend Validation Severity Filtering](/arc/frontend/core/validation/severity-filtering/) - Client usage # Configuring Arc Arc reads all of its settings from a single `ArcOptions` object. Wherever Arc runs — an ASP.NET Core web app, an Arc.Core console or worker, or the full Cratis stack — you configure it the same way: bind `ArcOptions` from configuration, then optionally override it in code. This page is the map: the hosting models, the three configuration mechanisms, and the full `ArcOptions` tree. ## The three hosting models [Section titled “The three hosting models”](#the-three-hosting-models) | Host | Bootstrap | Activate | Use it for | | ---------------- | ------------------------------------------------------------------ | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | **ASP.NET Core** | `WebApplication.CreateBuilder(args)` → `builder.AddCratisArc(...)` | `app.UseCratisArc()` → `app.Run()` | A web API or full-stack app. The listen URL comes from Kestrel / `launchSettings.json`. | | **Arc.Core** | `ArcApplication.CreateBuilder(args)` → `builder.AddCratisArc(...)` | `app.UseCratisArc()` → `await app.RunAsync()` | A console app or worker with no ASP.NET Core. The listen URL comes from `ArcOptions.Hosting.ApplicationUrl`. | | **Cratis stack** | `WebApplication.CreateBuilder(args)` → `builder.AddCratis(...)` | `app.UseCratis()` → `app.Run()` | Arc + Chronicle in one host — see the [Cratis package](/arc/backend/chronicle/cratis-package/). | `AddCratisArc` takes its arguments in this order: `configureOptions` (an `Action`), `configureBuilder` (an `Action` for adding Chronicle, MongoDB, or EF Core), and `configSectionPath`. Use the named `configureBuilder:` argument when you only want to add a builder feature: ```csharp builder.AddCratisArc(configureBuilder: arc => arc.WithChronicle()); ``` For raw `IServiceCollection` wiring (advanced), `AddCratisArcCore()` registers the same services without the builder. ## Three ways to configure [Section titled “Three ways to configure”](#three-ways-to-configure) Every setting can be supplied three ways, layered in this order — later wins: 1. **`appsettings.json`** under the `Cratis:Arc` section. 2. **Environment variables** with the `Cratis__Arc__` prefix (.NET maps the `__` separator onto nested keys), for example `Cratis__Arc__GeneratedApis__RoutePrefix`. 3. **Code**, via the `configureOptions` callback — it runs after binding, so it overrides the file and the environment. ```csharp builder.AddCratisArc(options => { options.GeneratedApis.RoutePrefix = "v1/api"; // overrides appsettings / env }); ``` ## The ArcOptions tree [Section titled “The ArcOptions tree”](#the-arcoptions-tree) | Option | Type | Default | What it controls | | ----------------------------------------- | ----------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `CorrelationId.HttpHeader` | `string` | `X-Correlation-ID` | The header carrying the correlation ID. | | `Tenancy.ResolverType` | `TenantResolverType` | `Header` | How the tenant is resolved: `Header`, `Query`, `Claim`, `Subdomain`, `Development`, or `Fixed`. | | `Tenancy.HttpHeader` | `string` | `x-cratis-tenant-id` | The header used when `ResolverType` is `Header`, and the fallback header when it is `Subdomain`. | | `Tenancy.QueryParameter` | `string` | `tenantId` | The query parameter used when `ResolverType` is `Query`. | | `Tenancy.ClaimType` | `string` | `tenant_id` | The claim used when `ResolverType` is `Claim`. | | `Tenancy.FixedTenantId` | `string` | `development` | The tenant every request resolves to when `ResolverType` is `Fixed` or `Development`. | | `Tenancy.DevelopmentTenantId` | `string` | `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.RoutePrefix` | `string` | `api` | Base prefix for generated command and query routes. | | `GeneratedApis.SegmentsToSkipForRoute` | `int` | `0` | Namespace segments to drop when building a route. | | `GeneratedApis.IncludeCommandNameInRoute` | `bool` | `true` | Append the command name as the last route segment. | | `GeneratedApis.IncludeQueryNameInRoute` | `bool` | `true` | Append the query name as the last route segment. | | `Query.KeepAliveInterval` | `TimeSpan` | `00:00:30` | Keep-alive cadence for observable (real-time) queries. | | `IdentityDetailsProvider` | `Type?` | `null` (auto-discovered) | The identity details provider type. | | `Hosting.ApplicationUrl` | `string` | `http://+:5001/` | The listen URL — **Arc.Core only** (ignored under ASP.NET Core). | | `JsonSerializerOptions` | `JsonSerializerOptions` | Arc defaults | The serializer used across controllers, manual serialization, and generated endpoints. Configure in code only. | Route generation (`GeneratedApis`) and JSON serialization have worked examples on the [ASP.NET Core configuration](/arc/backend/asp-net-core/configuration/) page; `Query.KeepAliveInterval` is covered with the [observable query demultiplexer](/arc/backend/queries/observable-query-demultiplexer/). ## Adding features with the builder [Section titled “Adding features with the builder”](#adding-features-with-the-builder) The `configureBuilder` callback exposes `IArcBuilder`, which is where Arc’s pluggable backends attach: * `arc.WithChronicle()` — event sourcing with Cratis Chronicle. * `arc.WithMongoDB()` — MongoDB read models. See [MongoDB](/arc/backend/mongodb/). * `arc.WithEntityFrameworkCore()` — relational read models. See [Entity Framework](/arc/backend/entity-framework/). ## Identity and authentication [Section titled “Identity and authentication”](#identity-and-authentication) Arc resolves an identity details provider automatically by type discovery. Set `ArcOptions.IdentityDetailsProvider` to pin a specific type, or register one explicitly: ```csharp builder.Services.AddIdentityProvider(); ``` For authenticating requests, see [Authentication](/arc/backend/core/authentication/). When you use the [Cratis package](/arc/backend/chronicle/cratis-package/), Microsoft Identity Platform authentication is wired for you. ## Service provider validation [Section titled “Service provider validation”](#service-provider-validation) A singleton that takes a scoped dependency in its constructor holds that one instance forever — the classic captive dependency. In a multi-tenant application it is the difference between “the right tenant’s data” and “whichever tenant happened to be first”, and nothing about it fails loudly. .NET has a detector for exactly this: `ServiceProviderOptions.ValidateScopes`, which the host turns on in Development so the capture throws the moment you resolve it. Arc keeps that detector on. Every host Arc supports settles two `ServiceProviderOptions` fields for you: | Option | Value Arc applies | Why | | ----------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ValidateScopes` | `builder.Environment.IsDevelopment()` | The host’s own default, restated so it survives. On in Development, off everywhere else. | | `ValidateOnBuild` | `false` | Arc supplies registrations contextually — `IHostApplicationBuilder`, the type a convention binding is for, values only an executing command or an in-flight request can hand over. Eager validation constructs every registration up front and can resolve none of them, so leaving it on fails `Build()` outright. | The reason both fields have to be stated together is that `UseDefaultServiceProvider` and `ConfigureContainer` each start from a brand new options object — setting one field discards every other value the host had already applied. Turning `ValidateOnBuild` off without restating `ValidateScopes` is what silently took the captive-dependency check with it. ### Overriding it [Section titled “Overriding it”](#overriding-it) You own your container. State your own choice and it wins — with one ordering rule that differs by host: ```csharp // ASP.NET Core and the generic host: call it AFTER AddCratisArc. builder.AddCratisArc(); builder.Host.UseDefaultServiceProvider(options => { options.ValidateScopes = true; // on in every environment, not just Development options.ValidateOnBuild = false; // keep this off — see the table above }); ``` `AddCratisArc` calls `UseDefaultServiceProvider` itself on these hosts, and the last call wins, so a call placed *before* `AddCratisArc` is discarded. Arc.Core has no such ordering rule. `ArcApplicationBuilder` applies its defaults while it is being constructed, so a `ConfigureContainer` call — your own factory, Autofac, Lamar — replaces them whether you make it before or after `AddCratisArc`: ```csharp var builder = ArcApplication.CreateBuilder(args); builder.ConfigureContainer(new MyServiceProviderFactory()); // before or after — either wins builder.AddCratisArc(); ``` Caution `ValidateOnBuild = true` fails `Build()` on any Arc application. The failure is an `AggregateException` naming the registrations Arc supplies contextually, and it is not a defect in your wiring — leave the flag off. ### When you get no validation at all [Section titled “When you get no validation at all”](#when-you-get-no-validation-at-all) `ValidateScopes` follows `IsDevelopment()`, which is an exact match on the environment name `Development`. A host running under a custom name — `Local`, `Dev`, `Staging` — is *not* Development by that rule, so it gets no scope validation, exactly as a bare .NET host would. If you want the check there, ask for it explicitly with the override above. ## A note on CORS [Section titled “A note on CORS”](#a-note-on-cors) CORS is **not** an Arc option — configure it with standard ASP.NET Core (`builder.Services.AddCors(...)` and `app.UseCors(...)`). Arc neither wraps nor replaces it. If you opt queries into the [HTTP QUERY method](/arc/backend/queries/using-the-http-query-method/), add `QUERY` to your allowed methods (`policy.WithMethods("GET", "POST", "QUERY")`) — it is not a simple method, so cross-origin calls preflight. The default `GET` transport needs no CORS change. ## Where to go next [Section titled “Where to go next”](#where-to-go-next) * [ASP.NET Core configuration](/arc/backend/asp-net-core/configuration/) — route-generation examples and JSON serialization in depth. * [Arc.Core getting started](/arc/backend/core/getting-started/) — the console and worker host end to end. * [Tenancy](/arc/backend/tenancy/configuration/) — configure how the tenant is resolved. * [Identity](/arc/backend/identity/) — the identity system and providers. * [Proxy Generation](/arc/backend/proxy-generation/configuration/) — the build-time `CratisProxies*` settings that must match `GeneratedApis`. # Arc.Core Arc.Core provides a lightweight application model for building .NET applications without requiring ASP.NET Core. This is ideal for console applications, background services, microservices, and scenarios where you want minimal dependencies and maximum performance. ## Why Arc.Core? [Section titled “Why Arc.Core?”](#why-arccore) Arc.Core is designed for developers who want the power of Arc’s developer experience—commands, queries, identity, multi-tenancy, and validation—without the overhead of the full ASP.NET Core stack. It’s perfect for: * Console applications * Background services and workers * Lightweight microservices * Custom HTTP listeners * gRPC services * Scenarios requiring fast startup and low memory footprint * Native AOT (Ahead-of-Time) compilation scenarios ## Key Features [Section titled “Key Features”](#key-features) * **Minimal Dependencies** - No web server dependencies (Kestrel, HTTP.sys), no MVC/Razor dependencies * **Native AOT Ready** - Designed for Native AOT compilation with smaller binaries and faster startup * **Full Arc Features** - Commands, queries, identity, multi-tenancy, validation, and more * **Static File Serving** - Serve static assets and host Single Page Applications * **Flexible** - Use with any .NET application type * **Performance** - Faster startup times and lower memory consumption ## Topics [Section titled “Topics”](#topics) * [Overview](/arc/backend/core/overview/) - Learn about the motivation and design philosophy * [Getting Started](/arc/backend/core/getting-started/) - Build your first Arc.Core application * [Endpoint Mapping](/arc/backend/core/endpoint-mapping/) - Map custom HTTP endpoints with MapGet and MapPost * [Static Files](/arc/backend/core/static-files/) - Serve static files and host SPAs * [Authentication](/arc/backend/core/authentication/) - Implement custom authentication handlers * [Authorization](/arc/backend/core/authorization/) - Protect your endpoints with authorization attributes * [OpenAPI Specifications](/arc/backend/core/openapi/) - Generate OpenAPI documentation for your API ## Shared Features [Section titled “Shared Features”](#shared-features) Many Arc features work across both Arc.Core and ASP.NET Core: * [Tenancy](/arc/backend/tenancy/) - Tenant isolation and context management * [Identity](/arc/backend/identity/) - Core identity system and user details * [Commands](/arc/backend/commands/) - Command handling patterns * [Queries](/arc/backend/queries/) - Query patterns and conventions * [Chronicle](/arc/backend/chronicle/) - Event sourcing and CQRS * [MongoDB](/arc/backend/mongodb/) - MongoDB integration * [Entity Framework](/arc/backend/entity-framework/) - Entity Framework Core integration ## When to Use Arc.Core vs ASP.NET Core [Section titled “When to Use Arc.Core vs ASP.NET Core”](#when-to-use-arccore-vs-aspnet-core) **Use Arc.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 **Use [ASP.NET Core Integration](/arc/backend/asp-net-core/) 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 ## Next Steps [Section titled “Next Steps”](#next-steps) Ready to get started? Head over to the [Getting Started](/arc/backend/core/getting-started/) guide to build your first Arc.Core application. # Authentication Arc.Core provides a flexible authentication system that allows you to implement custom authentication handlers for your application. This is particularly useful for scenarios where you need to authenticate requests based on custom headers, tokens, or other mechanisms without relying on ASP.NET Core’s authentication middleware. ## Overview [Section titled “Overview”](#overview) The authentication system in Arc.Core is built around the `IAuthenticationHandler` interface. Multiple authentication handlers can be registered, and they’re executed in sequence until one successfully authenticates the request or returns a failure. ## Authentication Flow [Section titled “Authentication Flow”](#authentication-flow) The authentication system processes handlers in sequence: 1. Each registered `IAuthenticationHandler` is called in order 2. If a handler returns an authenticated result, the process stops and that result is used 3. If a handler returns a failure, the process stops and the failure is returned 4. If a handler returns anonymous, the next handler is tried 5. If all handlers return anonymous, the request is considered anonymous ## Authentication Results [Section titled “Authentication Results”](#authentication-results) Authentication handlers return an `AuthenticationResult` with one of three possible outcomes: | Outcome | Description | Usage | | ------------- | ---------------------------------------- | --------------------------------------------------------------------------- | | **Succeeded** | Authentication was successful | Return `AuthenticationResult.Succeeded(principal)` with a `ClaimsPrincipal` | | **Failed** | Authentication failed with a reason | Return `AuthenticationResult.Failed(reason)` with a failure reason | | **Anonymous** | Handler cannot authenticate this request | Return `AuthenticationResult.Anonymous` to let other handlers try | ## Microsoft Identity Platform (Azure) [Section titled “Microsoft Identity Platform (Azure)”](#microsoft-identity-platform-azure) When deploying to Azure — Static Web Apps, Container Apps, App Service, or any platform that injects the [EasyAuth](https://learn.microsoft.com/en-us/azure/app-service/overview-authentication-authorization) headers — the framework provides a ready-made handler so you do not need to write one yourself. ### ASP.NET Core (Arc package) [Section titled “ASP.NET Core (Arc package)”](#aspnet-core-arc-package) Call `AddMicrosoftIdentityPlatformIdentityAuthentication()` during service registration: ```csharp builder.Services.AddMicrosoftIdentityPlatformIdentityAuthentication(); ``` This registers `MicrosoftIDentityPlatformAuthHandler`, which reads the standard EasyAuth headers: | Header | Purpose | | ---------------------------- | ------------------------------------------------- | | `x-ms-client-principal-id` | User ID | | `x-ms-client-principal-name` | Display name | | `x-ms-client-principal` | Base64-encoded JSON payload with roles and claims | The reconstructed principal also carries the identity provider the ingress authenticated the caller with, as the reserved `MicrosoftIdentityPlatformClaims.IdentityProvider` (`urn:cratis:arc:identity:provider`) claim. Arc strips any claim of that type out of the forwarded payload before writing its own value, so the claim always holds exactly one value taken from one place — the `identityProvider` field of the forwarded principal. That is a guarantee of single provenance, not of authenticity: `x-ms-client-principal` is base64 rather than signed, and Arc does not check who sent it, so trust the claim exactly as far as you trust that header — only insofar as your ingress is the only thing that can set it. Read it with `FindFirst`/`FindAll` and never normalize the claim type yourself. See [Microsoft Identity Platform](/arc/backend/asp-net-core/microsoft-identity/) for the full setup guide, including what the identity provider claim does and does not guarantee, and how to test locally with a generated principal. ### Arc.Core (non-ASP.NET Core) [Section titled “Arc.Core (non-ASP.NET Core)”](#arccore-non-aspnet-core) The `AddMicrosoftIdentityPlatformIdentityAuthentication()` extension is only available in the Arc ASP.NET Core package. If you are using `ArcApplication` (the non-ASP.NET Core host), implement `IAuthenticationHandler` directly to read the same EasyAuth headers: ```csharp using System.Security.Claims; using System.Text; using System.Text.Json; using Cratis.Arc.Authentication; using Cratis.Arc.Http; using Cratis.Arc.Identity; public class MicrosoftIdentityPlatformAuthenticationHandler : IAuthenticationHandler { public Task HandleAuthentication(IHttpRequestContext context) { if (!context.Headers.TryGetValue(MicrosoftIdentityPlatformHeaders.IdentityIdHeader, out var userId)) { return Task.FromResult(AuthenticationResult.Anonymous); } var claims = new List { new(ClaimTypes.NameIdentifier, userId) }; if (context.Headers.TryGetValue(MicrosoftIdentityPlatformHeaders.IdentityNameHeader, out var userName)) { claims.Add(new Claim(ClaimTypes.Name, userName)); } if (context.Headers.TryGetValue(MicrosoftIdentityPlatformHeaders.PrincipalHeader, out var encoded)) { var json = Encoding.UTF8.GetString(Convert.FromBase64String(encoded)); var principal = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (principal is not null) { foreach (var role in principal.UserRoles ?? []) claims.Add(new Claim(ClaimTypes.Role, role)); foreach (var claim in principal.Claims ?? []) claims.Add(new Claim(claim.typ, claim.val)); } } var identity = new ClaimsIdentity(claims, "MicrosoftIdentityPlatform"); return Task.FromResult(AuthenticationResult.Succeeded(new ClaimsPrincipal(identity))); } } ``` The handler is discovered automatically — no explicit registration required. ## Implementing an Authentication Handler [Section titled “Implementing an Authentication Handler”](#implementing-an-authentication-handler) Here’s a basic example of implementing a custom authentication handler: ```csharp using System.Security.Claims; using Cratis.Arc.Authentication; using Cratis.Arc.Http; public class ApiKeyAuthenticationHandler : IAuthenticationHandler { const string ApiKeyHeader = "X-API-Key"; public Task HandleAuthentication(IHttpRequestContext context) { // Check if the API key header is present if (!context.Headers.TryGetValue(ApiKeyHeader, out var apiKey)) { // No API key present, let other handlers try return Task.FromResult(AuthenticationResult.Anonymous); } // Validate the API key if (!IsValidApiKey(apiKey)) { // Invalid API key, fail authentication return Task.FromResult( AuthenticationResult.Failed( new AuthenticationFailureReason("Invalid API key"))); } // Create a claims principal for the authenticated user var claims = new[] { new Claim(ClaimTypes.Name, "API User"), new Claim(ClaimTypes.NameIdentifier, "api-user-123"), new Claim("api_key", apiKey) }; var identity = new ClaimsIdentity(claims, "ApiKey"); var principal = new ClaimsPrincipal(identity); return Task.FromResult(AuthenticationResult.Succeeded(principal)); } bool IsValidApiKey(string apiKey) { // Your API key validation logic return apiKey == "your-secret-api-key"; } } ``` ## Common Authentication Patterns [Section titled “Common Authentication Patterns”](#common-authentication-patterns) ### Bearer Token Authentication [Section titled “Bearer Token Authentication”](#bearer-token-authentication) ```csharp public class BearerTokenAuthenticationHandler : IAuthenticationHandler { const string AuthorizationHeader = "Authorization"; const string BearerPrefix = "Bearer "; public async Task HandleAuthentication(IHttpRequestContext context) { if (!context.Headers.TryGetValue(AuthorizationHeader, out var authHeader)) { return AuthenticationResult.Anonymous; } if (!authHeader.StartsWith(BearerPrefix, StringComparison.OrdinalIgnoreCase)) { return AuthenticationResult.Anonymous; } var token = authHeader[BearerPrefix.Length..].Trim(); try { var principal = await ValidateAndDecodeToken(token); return AuthenticationResult.Succeeded(principal); } catch (Exception ex) { return AuthenticationResult.Failed( new AuthenticationFailureReason($"Token validation failed: {ex.Message}")); } } async Task ValidateAndDecodeToken(string token) { // Your token validation logic (e.g., JWT validation) // This is a simplified example await Task.CompletedTask; var claims = new[] { new Claim(ClaimTypes.NameIdentifier, "user-id"), new Claim(ClaimTypes.Name, "User Name") }; return new ClaimsPrincipal(new ClaimsIdentity(claims, "Bearer")); } } ``` ### Basic Authentication [Section titled “Basic Authentication”](#basic-authentication) ```csharp public class BasicAuthenticationHandler : IAuthenticationHandler { const string AuthorizationHeader = "Authorization"; const string BasicPrefix = "Basic "; public Task HandleAuthentication(IHttpRequestContext context) { if (!context.Headers.TryGetValue(AuthorizationHeader, out var authHeader)) { return Task.FromResult(AuthenticationResult.Anonymous); } if (!authHeader.StartsWith(BasicPrefix, StringComparison.OrdinalIgnoreCase)) { return Task.FromResult(AuthenticationResult.Anonymous); } var encodedCredentials = authHeader[BasicPrefix.Length..].Trim(); var credentials = Encoding.UTF8.GetString( Convert.FromBase64String(encodedCredentials)); var parts = credentials.Split(':', 2); if (parts.Length != 2) { return Task.FromResult( AuthenticationResult.Failed( new AuthenticationFailureReason("Invalid credentials format"))); } var username = parts[0]; var password = parts[1]; if (!ValidateCredentials(username, password)) { return Task.FromResult( AuthenticationResult.Failed( new AuthenticationFailureReason("Invalid username or password"))); } var claims = new[] { new Claim(ClaimTypes.Name, username), new Claim(ClaimTypes.NameIdentifier, username) }; var identity = new ClaimsIdentity(claims, "Basic"); var principal = new ClaimsPrincipal(identity); return Task.FromResult(AuthenticationResult.Succeeded(principal)); } bool ValidateCredentials(string username, string password) { // Your credential validation logic return username == "admin" && password == "secret"; } } ``` ### Custom Header Authentication [Section titled “Custom Header Authentication”](#custom-header-authentication) ```csharp public class CustomHeaderAuthenticationHandler : IAuthenticationHandler { const string UserIdHeader = "X-User-ID"; const string UserRoleHeader = "X-User-Role"; public Task HandleAuthentication(IHttpRequestContext context) { if (!context.Headers.TryGetValue(UserIdHeader, out var userId)) { return Task.FromResult(AuthenticationResult.Anonymous); } var role = context.Headers.TryGetValue(UserRoleHeader, out var roleValue) ? roleValue : "User"; var claims = new[] { new Claim(ClaimTypes.NameIdentifier, userId), new Claim(ClaimTypes.Role, role) }; var identity = new ClaimsIdentity(claims, "CustomHeader"); var principal = new ClaimsPrincipal(identity); return Task.FromResult(AuthenticationResult.Succeeded(principal)); } } ``` ## Registering Authentication Handlers [Section titled “Registering Authentication Handlers”](#registering-authentication-handlers) Authentication handlers are automatically discovered and registered by Arc.Core. Simply ensure your handler implements `IAuthenticationHandler` and is in a discoverable location: ```csharp // The handler will be automatically registered public class MyAuthenticationHandler : IAuthenticationHandler { public Task HandleAuthentication(IHttpRequestContext context) { // Implementation } } ``` If you need manual registration: ```csharp builder.Services.AddSingleton(); ``` ## Multiple Authentication Handlers [Section titled “Multiple Authentication Handlers”](#multiple-authentication-handlers) You can register multiple authentication handlers, and they’ll be executed in sequence: ```csharp public class ApiKeyHandler : IAuthenticationHandler { /* ... */ } public class BearerTokenHandler : IAuthenticationHandler { /* ... */ } public class BasicAuthHandler : IAuthenticationHandler { /* ... */ } ``` The handlers are tried in order until one returns either: * A successful authentication result * A failed authentication result If all handlers return `Anonymous`, the request is considered anonymous. ## Handler Execution Order [Section titled “Handler Execution Order”](#handler-execution-order) Handlers are executed in the order they’re discovered or registered. To control order, you can use explicit registration: ```csharp // Register in specific order builder.Services.AddSingleton(); builder.Services.AddSingleton(); ``` ## Working with Request Context [Section titled “Working with Request Context”](#working-with-request-context) Authentication handlers receive an `IHttpRequestContext` that provides access to request information including headers, query parameters, URL, and HTTP method. ## Best Practices [Section titled “Best Practices”](#best-practices) ### Return Anonymous for Non-Applicable Requests [Section titled “Return Anonymous for Non-Applicable Requests”](#return-anonymous-for-non-applicable-requests) If your handler doesn’t apply to a request, return `Anonymous` to let other handlers try: ```csharp if (!context.Headers.ContainsKey("X-My-Auth-Header")) { return Task.FromResult(AuthenticationResult.Anonymous); } ``` ### Provide Clear Failure Reasons [Section titled “Provide Clear Failure Reasons”](#provide-clear-failure-reasons) When authentication fails, provide clear, actionable error messages: ```csharp return AuthenticationResult.Failed( new AuthenticationFailureReason("API key is expired. Please generate a new key.")); ``` ### Use Dependency Injection [Section titled “Use Dependency Injection”](#use-dependency-injection) Handlers can use dependency injection for services they need: ```csharp public class JwtAuthenticationHandler( ILogger logger, ITokenValidator tokenValidator) : IAuthenticationHandler { public async Task HandleAuthentication(IHttpRequestContext context) { logger.LogDebug("Validating JWT token"); // Use injected services } } ``` ### Handle Exceptions Gracefully [Section titled “Handle Exceptions Gracefully”](#handle-exceptions-gracefully) Catch and handle exceptions within your handler: ```csharp try { var principal = await ValidateToken(token); return AuthenticationResult.Succeeded(principal); } catch (SecurityTokenException ex) { return AuthenticationResult.Failed( new AuthenticationFailureReason($"Token validation failed: {ex.Message}")); } catch (Exception ex) { logger.LogError(ex, "Unexpected error during authentication"); return AuthenticationResult.Failed( new AuthenticationFailureReason("Authentication error occurred")); } ``` ## Integration with Authorization [Section titled “Integration with Authorization”](#integration-with-authorization) Once a request is authenticated, the `ClaimsPrincipal` is available for authorization checks. See the [Authorization](/arc/backend/core/authorization/) documentation for how to protect endpoints using the `[Authorize]` and `[Roles]` attributes. ## Testing Authentication Handlers [Section titled “Testing Authentication Handlers”](#testing-authentication-handlers) When testing authentication handlers, use the `IHttpRequestContext` interface: ```csharp public class ApiKeyAuthenticationHandlerTests { [Fact] public async Task should_authenticate_with_valid_api_key() { var handler = new ApiKeyAuthenticationHandler(); var context = new TestHttpRequestContext { Headers = new Dictionary { ["X-API-Key"] = "valid-key" } }; var result = await handler.HandleAuthentication(context); result.IsAuthenticated.ShouldBeTrue(); } } ``` ## Next Steps [Section titled “Next Steps”](#next-steps) * [Authorization](/arc/backend/core/authorization/) - Learn how to protect endpoints with authorization attributes * [Identity](/arc/backend/identity/) - Integrate with Arc’s identity system * [Commands](/arc/backend/commands/) - Protect commands with authentication and authorization * [Queries](/arc/backend/queries/) - Protect queries with authentication and authorization # Authorization Arc.Core provides authorization capabilities through attributes that protect your commands and queries. This allows you to control access based on authentication status and user roles. ## Overview [Section titled “Overview”](#overview) Authorization in Arc.Core is attribute-based and supports: * **Authentication Requirements** - Require users to be authenticated * **Role-Based Authorization** - Restrict access to specific roles * **Anonymous Access** - Explicitly allow unauthenticated access * **Flexible Application** - Apply at class or method level Note **Model-bound is the default.** Arc applies these attributes directly to your `[Command]` records and `[ReadModel]` query methods — the [vertical-slice](/arc/vertical-slices/) style used throughout these docs, and what the examples below lead with. Some deeper sections show claims-based and custom checks for illustration; the part to take away is *where* the `[Authorize]`/`[Roles]` attributes go. For how access control fits together end to end, see [Understanding identity and access](/arc/understanding-identity-and-access/). ## Authorization Attributes [Section titled “Authorization Attributes”](#authorization-attributes) Arc.Core provides authorization through attributes: * **`[Authorize]`** - Requires authentication and optionally specifies roles or policies * **`[Roles]`** - Convenience attribute for role-based authorization * **`[AllowAnonymous]`** - Explicitly allows unauthenticated access ## Basic Usage [Section titled “Basic Usage”](#basic-usage) ### Requiring Authentication [Section titled “Requiring Authentication”](#requiring-authentication) Require users to be authenticated without specifying roles — put the attribute on the `[Command]` record, and its `Handle()` only runs for an authenticated user: ```csharp using Cratis.Arc.Commands; using Cratis.Arc.Authorization; [Authorize] [Command] public record UpdateProfile(ProfileId Id, ProfileName Name) { public ProfileRenamed Handle() => new(Name); } ``` ### Role-Based Authorization [Section titled “Role-Based Authorization”](#role-based-authorization) Restrict access to specific roles: ```csharp // Using the Authorize attribute [Authorize(Roles = "Admin")] [Command] public record DeleteUser(UserId Id) { public UserDeleted Handle() => new(); } // Using the Roles attribute (more readable for multiple roles) [Roles("Admin", "Manager")] [Command] public record ApproveRequest(RequestId Id) { public RequestApproved Handle() => new(); } ``` ### Anonymous Access [Section titled “Anonymous Access”](#anonymous-access) Explicitly allow anonymous access (useful when you have a fallback policy requiring authentication) — on a model-bound query, the attribute goes on the static query method: ```csharp [ReadModel] public record PublicData(DataId Id, string Value) { [AllowAnonymous] public static IEnumerable All(IMongoCollection collection) => collection.Find(_ => true).ToList(); } ``` ## Applying Authorization [Section titled “Applying Authorization”](#applying-authorization) Authorization attributes can be applied at different levels: ### Class-Level Authorization [Section titled “Class-Level Authorization”](#class-level-authorization) Apply to all commands or queries in a type: ```csharp [Authorize] [Command] public record UpdateSettings(SettingKey Key, string Value) { public SettingChanged Handle() => new(Key, Value); } [Roles("Admin")] [Command] public record DeleteAccount(AccountId Id) { public AccountDeleted Handle() => new(); } ``` ### Method-Level Authorization [Section titled “Method-Level Authorization”](#method-level-authorization) Apply an attribute to a single operation rather than the whole type. On a model-bound command the attribute goes on the `[Command]` record itself, and its `Handle()` only runs once the attribute’s requirements are met: ```csharp [Authorize] [Command] public record SecureCommand(string Data) { public SecureOperationCompleted Handle() => new(Data); } ``` ## Role-Based Scenarios [Section titled “Role-Based Scenarios”](#role-based-scenarios) ### Single Role Requirement [Section titled “Single Role Requirement”](#single-role-requirement) ```csharp // User must have the "Admin" role [Roles("Admin")] [Command] public record CreateAdmin(string Username) { public AdminCreated Handle() => new(Username); } ``` ### Multiple Role Requirement (OR Logic) [Section titled “Multiple Role Requirement (OR Logic)”](#multiple-role-requirement-or-logic) Users need **at least one** of the specified roles: ```csharp // User must have either "Admin" OR "Manager" role [Roles("Admin", "Manager")] [ReadModel] public record AuditLogEntry(AuditLogId Id, string Action) { public static IEnumerable All(IMongoCollection collection) => collection.Find(_ => true).ToList(); } ``` ### Combining with Standard Authorize [Section titled “Combining with Standard Authorize”](#combining-with-standard-authorize) You can mix `[Authorize]` and `[Roles]` if needed: ```csharp // Requires authentication via specific scheme AND a role [Authorize(AuthenticationSchemes = "Bearer")] [Roles("Admin")] [Command] public record SecureAdminCommand(string Data) { public SecureOperationCompleted Handle() => new(Data); } ``` ## AllowAnonymous Attribute [Section titled “AllowAnonymous Attribute”](#allowanonymous-attribute) The `[AllowAnonymous]` attribute explicitly allows unauthenticated access. On a model-bound query the attribute goes on the static query method: ```csharp [ReadModel] public record CatalogItem(CatalogItemId Id, string Name) { // Anyone can access this, even without authentication [AllowAnonymous] public static IEnumerable All(IMongoCollection collection) => collection.Find(_ => true).ToList(); } ``` ## Authorization Inheritance Rules [Section titled “Authorization Inheritance Rules”](#authorization-inheritance-rules) Authorization attributes follow specific inheritance rules: | Scenario | Result | | -------------------------------------------------------- | ------------------------------------------------ | | `[Authorize]` on class | Requires authentication for all operations | | `[Roles]` on class | Requires specified roles for all operations | | `[AllowAnonymous]` on class | Allows anonymous access for all operations | | Method attribute overrides class | Method-level attribute takes precedence | | Both `[Authorize]` and `[AllowAnonymous]` on same target | **Error** - throws `AmbiguousAuthorizationLevel` | ### Examples [Section titled “Examples”](#examples) A read model inherits authorization from the type, and a query method can override it: ```csharp // Authentication required for every query method on the read model [Authorize] [ReadModel] public record Profile(ProfileId Id, ProfileName Name) { // Inherits [Authorize] from the record — requires authentication public static IEnumerable All(IMongoCollection collection) => collection.Find(_ => true).ToList(); // Overrides with a more specific role requirement [Roles("Admin")] public static IEnumerable AllForAdmins(IMongoCollection collection) => collection.Find(_ => true).ToList(); } ``` A command record can override an `[AllowAnonymous]` default by requiring authentication on a single command: ```csharp // Anonymous access by default [AllowAnonymous] [Command] public record TrackPageView(string Path) { public PageViewTracked Handle() => new(Path); } // Authentication required for this command [Authorize] [Command] public record SubmitFeedback(string Message) { public FeedbackSubmitted Handle() => new(Message); } ``` Applying both `[Authorize]` and `[AllowAnonymous]` to the same target is an error: ```csharp // ERROR: This will throw AmbiguousAuthorizationLevel at startup [AllowAnonymous] [Authorize] // Cannot have both! [Command] public record InvalidCommand(string Data) { public OperationCompleted Handle() => new(Data); } ``` ## Working with Claims [Section titled “Working with Claims”](#working-with-claims) When a request is authenticated, the `ClaimsPrincipal` is available from the current `HttpContext`. Inject `IHttpContextAccessor` into a model-bound query method (or a command’s `Handle()`) and read `HttpContext?.User`: ```csharp using System.Security.Claims; using Microsoft.AspNetCore.Http; [ReadModel] public record UserProfile(UserId Id, string DisplayName) { // Returns the profile for whichever user is currently authenticated public static UserProfile? Mine( IHttpContextAccessor httpContextAccessor, IMongoCollection collection) { var user = httpContextAccessor.HttpContext?.User; var userId = user?.FindFirstValue(ClaimTypes.NameIdentifier); if (string.IsNullOrEmpty(userId)) { return null; } return collection.Find(profile => profile.Id == userId).FirstOrDefault(); } } ``` The same `httpContextAccessor.HttpContext?.User` gives you a `ClaimsPrincipal` — use `FindFirstValue(ClaimTypes.NameIdentifier)` for the user id, `FindFirstValue(ClaimTypes.Name)` for the name, and `IsInRole("Admin")` for role checks. ## Authorization Results [Section titled “Authorization Results”](#authorization-results) When authorization fails, Arc.Core automatically returns appropriate HTTP status codes: | Scenario | HTTP Status Code | Description | | --------------------- | ---------------- | ----------------------------------------------------------- | | **Not Authenticated** | 401 Unauthorized | User is not authenticated | | **Not Authorized** | 403 Forbidden | User is authenticated but doesn’t have required permissions | ## Custom Authorization Logic [Section titled “Custom Authorization Logic”](#custom-authorization-logic) The `[Authorize]` and `[Roles]` attributes handle coarse-grained access — whether the user is authenticated and in the right role. For row-level checks (for example, “you can only update your own orders”) put the logic inside `Handle()`: inject `IHttpContextAccessor`, read the current user’s claims, and return a `ValidationResult.Error(...)` when the guard fails. Make the return type `Result` so the framework knows the command can fail validation: ```csharp using System.Security.Claims; using Cratis.Arc.Validation; using Microsoft.AspNetCore.Http; [Authorize] [Command] public record UpdateOrder(OrderId Id, string Data) { public Result Handle( IHttpContextAccessor httpContextAccessor, IOrderRepository orders) { var user = httpContextAccessor.HttpContext?.User; var userId = user?.FindFirstValue(ClaimTypes.NameIdentifier); var order = orders.GetById(Id); // Custom authorization: a user can only update their own orders if (order.OwnerId != userId) { return ValidationResult.Error("You can only update your own orders."); } return new OrderUpdated(Data); } } ``` A returned `ValidationResult.Error` surfaces to the caller as a failed `CommandResult` with validation errors — the command does not append its event. ## Policy-Based Authorization [Section titled “Policy-Based Authorization”](#policy-based-authorization) While Arc.Core focuses on attribute-based authorization, you can implement policy-based logic inside `Handle()` by inspecting the current user’s roles and returning a `ValidationResult.Error(...)` when the policy fails: ```csharp using System.Security.Claims; using Cratis.Arc.Validation; using Microsoft.AspNetCore.Http; [Authorize] [Command] public record ApproveExpense(ExpenseId Id) { public Result Handle( IHttpContextAccessor httpContextAccessor, IExpenseRepository expenses) { var user = httpContextAccessor.HttpContext?.User; var expense = expenses.GetById(Id); // Custom policy: managers can approve up to $1000, directors unlimited var isManager = user?.IsInRole("Manager") ?? false; var isDirector = user?.IsInRole("Director") ?? false; if (!isManager && !isDirector) { return ValidationResult.Error("Only managers and directors can approve expenses."); } if (expense.Amount > 1000 && !isDirector) { return ValidationResult.Error("Only directors can approve expenses over $1000."); } return new ExpenseApproved(user?.FindFirstValue(ClaimTypes.Name) ?? "Unknown"); } } ``` ## Best Practices [Section titled “Best Practices”](#best-practices) ### Secure by Default [Section titled “Secure by Default”](#secure-by-default) Apply authorization at the broadest scope possible and override only when necessary. Put `[Authorize]` on the read model so every query method requires authentication, then opt specific methods out with `[AllowAnonymous]`: ```csharp // Good: secure by default, explicit opt-out [Authorize] [ReadModel] public record Resource(ResourceId Id, string Name) { // Inherits [Authorize] — requires authentication public static IEnumerable Mine(IMongoCollection collection) => collection.Find(_ => true).ToList(); // Explicitly allow anonymous access for this method [AllowAnonymous] public static IEnumerable Public(IMongoCollection collection) => collection.Find(_ => true).ToList(); } ``` ### Use Roles Attribute for Clarity [Section titled “Use Roles Attribute for Clarity”](#use-roles-attribute-for-clarity) Use `[Roles]` for better readability when specifying multiple roles: ```csharp // More readable [Roles("Admin", "Manager", "Supervisor")] [Command] public record ReviewApplication(ApplicationId Id) { public ApplicationReviewed Handle() => new(); } // Less readable [Authorize(Roles = "Admin,Manager,Supervisor")] [Command] public record ReviewApplication(ApplicationId Id) { public ApplicationReviewed Handle() => new(); } ``` ### Avoid Ambiguous Authorization [Section titled “Avoid Ambiguous Authorization”](#avoid-ambiguous-authorization) Never apply both `[Authorize]` and `[AllowAnonymous]` to the same target: ```csharp // ERROR: Will throw AmbiguousAuthorizationLevel [Authorize] [AllowAnonymous] [Command] public record AmbiguousCommand(string Data) { public OperationCompleted Handle() => new(Data); } ``` ### Document Authorization Requirements [Section titled “Document Authorization Requirements”](#document-authorization-requirements) Add XML documentation to clarify authorization requirements: ```csharp /// /// Deletes a user account. Requires Admin role. /// [Roles("Admin")] [Command] public record DeleteUser(UserId Id) { public UserDeleted Handle() => new(); } ``` ### Validate Claims in Handlers [Section titled “Validate Claims in Handlers”](#validate-claims-in-handlers) For complex authorization logic, validate claims inside `Handle()` by injecting `IHttpContextAccessor` and returning a `ValidationResult.Error(...)` when a required claim is missing: ```csharp using System.Security.Claims; using Cratis.Arc.Validation; using Microsoft.AspNetCore.Http; [Authorize] [Command] public record SecureCommand(string Data) { public Result Handle( IHttpContextAccessor httpContextAccessor) { var user = httpContextAccessor.HttpContext?.User; var userId = user?.FindFirstValue(ClaimTypes.NameIdentifier); if (string.IsNullOrEmpty(userId)) { return ValidationResult.Error("A user identifier claim is required."); } // Continue with business logic return new SecureOperationCompleted(Data); } } ``` ## Integration with Authentication [Section titled “Integration with Authentication”](#integration-with-authentication) Authorization works hand-in-hand with authentication. See the [Authentication](/arc/backend/core/authentication/) documentation for how to implement custom authentication handlers that provide the claims used by authorization. ## Testing Authorization [Section titled “Testing Authorization”](#testing-authorization) Authorization runs as part of the real command pipeline, so you test it with [`CommandScenario`](/arc/backend/testing/command-scenario/) — the same class used for testing validation and handler behavior. Instantiate the scenario, run the command with `Execute`, and assert on the resulting `CommandResult`: * `ShouldNotBeAuthorized()` — the command was rejected by an `[Authorize]` or `[Roles]` attribute. * `ShouldBeAuthorized()` — the command passed authorization. * `ShouldHaveValidationErrors()` / `ShouldHaveValidationErrorFor("message")` — an in-`Handle` guard returned a `ValidationResult.Error`. For the full scenario API, the assertion reference, and a worked authorization example, see [Command Scenarios](/arc/backend/testing/command-scenario/) and the wider [Testing](/arc/backend/testing/) guide. ## Next Steps [Section titled “Next Steps”](#next-steps) * [Authentication](/arc/backend/core/authentication/) - Implement custom authentication handlers * [Getting Started](/arc/backend/core/getting-started/) - Learn more about Arc.Core basics * [Identity](/arc/backend/identity/) - Integrate with Arc’s identity system * [Commands](/arc/backend/commands/) - Learn about command patterns and authorization * [Queries](/arc/backend/queries/) - Discover query features with authorization # Endpoint Mapping Arc.Core provides extension methods for manually mapping HTTP endpoints to your application. This gives you full control over route patterns, handlers, and endpoint metadata. ## Overview [Section titled “Overview”](#overview) While Arc automatically maps commands and queries to endpoints, you may need to create custom endpoints for specific scenarios such as: * Health checks * Webhooks * Custom API endpoints * Static file serving * Proxy endpoints The `MapGet()` and `MapPost()` extension methods allow you to define these endpoints fluently. ## Basic Usage [Section titled “Basic Usage”](#basic-usage) ### MapGet [Section titled “MapGet”](#mapget) Map a GET endpoint to handle HTTP GET requests: ```csharp using Cratis.Arc; using Cratis.Arc.Http; var builder = ArcApplication.CreateBuilder(args); var app = builder.Build(); app.MapGet("/health", async context => { await context.Write("OK"); }); await app.RunAsync(); ``` ### MapPost [Section titled “MapPost”](#mappost) Map a POST endpoint to handle HTTP POST requests: ```csharp app.MapPost("/webhook", async context => { var data = await context.ReadBodyAsJson(typeof(object)); // Process webhook data context.SetStatusCode(200); }); ``` ## Method Signatures [Section titled “Method Signatures”](#method-signatures) Both methods follow the same pattern: ```csharp ArcApplication MapGet( string pattern, Func handler, EndpointMetadata? metadata = null) ArcApplication MapPost( string pattern, Func handler, EndpointMetadata? metadata = null) ``` ### Parameters [Section titled “Parameters”](#parameters) * **pattern** - The route pattern (e.g., `/api/users`, `/health`) * **handler** - An async function that processes the HTTP request * **metadata** - Optional endpoint metadata for documentation and configuration ## Working with IHttpRequestContext [Section titled “Working with IHttpRequestContext”](#working-with-ihttprequestcontext) The handler function receives an `IHttpRequestContext` that provides access to the request and response: ### Reading Request Data [Section titled “Reading Request Data”](#reading-request-data) ```csharp app.MapPost("/api/data", async context => { // Query parameters var id = context.Query["id"]; // Headers var authToken = context.Headers["Authorization"]; // Cookies var sessionId = context.Cookies["SessionId"]; // Request body as JSON var data = await context.ReadBodyAsJson(typeof(MyData)); // Path and method var path = context.Path; var method = context.Method; }); ``` ### Writing Response Data [Section titled “Writing Response Data”](#writing-response-data) ```csharp app.MapGet("/api/users", async context => { var users = new[] { new { Id = 1, Name = "Alice" } }; // Set status code context.SetStatusCode(200); // Set content type context.ContentType = "application/json"; // Write JSON response await context.WriteResponseAsJson(users, users.GetType()); // Or write plain text // await context.Write("Hello World"); }); ``` ### Setting Response Headers [Section titled “Setting Response Headers”](#setting-response-headers) ```csharp app.MapGet("/api/data", async context => { context.SetResponseHeader("Cache-Control", "no-cache"); context.SetResponseHeader("X-Custom-Header", "value"); await context.Write("Data"); }); ``` ## Endpoint Metadata [Section titled “Endpoint Metadata”](#endpoint-metadata) Add metadata to provide documentation and configure endpoint behavior: ```csharp using Cratis.Arc.Http; app.MapGet("/api/users", async context => { // Handler implementation }, new EndpointMetadata( Name: "GetAllUsers", Summary: "Retrieves a list of all users", Tags: ["Users"], AllowAnonymous: false)); ``` ### Metadata Properties [Section titled “Metadata Properties”](#metadata-properties) * **Name** - Unique identifier for the endpoint (used as operationId in OpenAPI) * **Summary** - Human-readable description of what the endpoint does * **Tags** - Categories for grouping related endpoints * **AllowAnonymous** - Whether authentication is required (`false` = authentication required) ## Fluent API and Method Chaining [Section titled “Fluent API and Method Chaining”](#fluent-api-and-method-chaining) The extension methods return the `ArcApplication` instance, enabling fluent chaining: ```csharp app.MapGet("/health", async context => { await context.Write("OK"); }, new EndpointMetadata(Name: "Health", AllowAnonymous: true)) .MapGet("/version", async context => { await context.Write("1.0.0"); }, new EndpointMetadata(Name: "Version", AllowAnonymous: true)) .MapPost("/api/events", async context => { var evt = await context.ReadBodyAsJson(typeof(object)); context.SetStatusCode(202); }, new EndpointMetadata(Name: "ReceiveEvent", AllowAnonymous: false)); await app.RunAsync(); ``` ## Complete Examples [Section titled “Complete Examples”](#complete-examples) ### Health Check Endpoint [Section titled “Health Check Endpoint”](#health-check-endpoint) ```csharp app.MapGet("/health", async context => { var health = new { Status = "Healthy", Timestamp = DateTime.UtcNow, Version = "1.0.0" }; context.ContentType = "application/json"; await context.WriteResponseAsJson(health, health.GetType()); }, new EndpointMetadata( Name: "HealthCheck", Summary: "Returns the health status of the application", Tags: ["System"], AllowAnonymous: true)); ``` ### Webhook Handler [Section titled “Webhook Handler”](#webhook-handler) ```csharp app.MapPost("/webhooks/github", async context => { // Verify webhook signature var signature = context.Headers["X-Hub-Signature-256"]; // Read webhook payload var payload = await context.ReadBodyAsJson(typeof(object)); // Process webhook // ... your logic here ... context.SetStatusCode(200); await context.Write("Webhook received"); }, new EndpointMetadata( Name: "GitHubWebhook", Summary: "Receives GitHub webhook notifications", Tags: ["Webhooks"], AllowAnonymous: true)); ``` ### RESTful API Endpoint [Section titled “RESTful API Endpoint”](#restful-api-endpoint) ```csharp public record Product(int Id, string Name, decimal Price); app.MapGet("/api/products", async context => { var products = new[] { new Product(1, "Product A", 29.99m), new Product(2, "Product B", 39.99m) }; await context.WriteResponseAsJson(products, products.GetType()); }, new EndpointMetadata( Name: "ListProducts", Summary: "Get all available products", Tags: ["Products"], AllowAnonymous: true)); app.MapPost("/api/products", async context => { var product = await context.ReadBodyAsJson(typeof(Product)) as Product; if (product == null) { context.SetStatusCode(400); await context.Write("Invalid product data"); return; } // Save product logic here context.SetStatusCode(201); context.SetResponseHeader("Location", $"/api/products/{product.Id}"); await context.WriteResponseAsJson(product, typeof(Product)); }, new EndpointMetadata( Name: "CreateProduct", Summary: "Create a new product", Tags: ["Products"], AllowAnonymous: false)); ``` ### Custom Error Handler [Section titled “Custom Error Handler”](#custom-error-handler) ```csharp app.MapGet("/api/data/{id}", async context => { var id = context.Query["id"]; if (string.IsNullOrEmpty(id)) { context.SetStatusCode(400); var error = new { Error = "ID parameter is required" }; await context.WriteResponseAsJson(error, error.GetType()); return; } // Fetch data logic var data = FetchData(id); if (data == null) { context.SetStatusCode(404); var error = new { Error = $"Data with ID {id} not found" }; await context.WriteResponseAsJson(error, error.GetType()); return; } await context.WriteResponseAsJson(data, data.GetType()); }); ``` ## Authentication and Authorization [Section titled “Authentication and Authorization”](#authentication-and-authorization) Control access to endpoints using the `AllowAnonymous` metadata property: ### Public Endpoint [Section titled “Public Endpoint”](#public-endpoint) ```csharp app.MapGet("/public/info", async context => { await context.Write("Public information"); }, new EndpointMetadata( Name: "PublicInfo", AllowAnonymous: true)); // No authentication required ``` ### Protected Endpoint [Section titled “Protected Endpoint”](#protected-endpoint) ```csharp app.MapGet("/private/data", async context => { // Only accessible to authenticated users var user = context.User; await context.Write($"Hello, {user.Identity?.Name}"); }, new EndpointMetadata( Name: "PrivateData", AllowAnonymous: false)); // Authentication required ``` For more details on authentication, see [Authentication](/arc/backend/core/authentication/). ## Integration with OpenAPI [Section titled “Integration with OpenAPI”](#integration-with-openapi) Endpoints mapped with `MapGet()` and `MapPost()` are automatically included in the OpenAPI specification when using the OpenAPI extensions: ```csharp using Cratis.Arc; using Cratis.Arc.OpenApi; var builder = ArcApplication.CreateBuilder(args); var app = builder.Build(); // Map custom endpoints app.MapGet("/api/status", async context => { await context.Write("Running"); }, new EndpointMetadata( Name: "GetStatus", Summary: "Get application status", Tags: ["System"])) .MapOpenApi(); // Generate OpenAPI document await app.RunAsync(); ``` The OpenAPI document will include your custom endpoints with the metadata you provided. See [OpenAPI Specifications](/arc/backend/core/openapi/) for more details. ## Best Practices [Section titled “Best Practices”](#best-practices) ### Use Descriptive Route Patterns [Section titled “Use Descriptive Route Patterns”](#use-descriptive-route-patterns) ```csharp // Good app.MapGet("/api/users/{id}"); app.MapPost("/api/orders"); // Avoid app.MapGet("/u/{i}"); app.MapPost("/data"); ``` ### Provide Endpoint Metadata [Section titled “Provide Endpoint Metadata”](#provide-endpoint-metadata) Always include metadata for documentation and tooling: ```csharp app.MapGet("/api/resource", handler, new EndpointMetadata( Name: "GetResource", Summary: "Clear description", Tags: ["ResourceCategory"], AllowAnonymous: false)); ``` ### Handle Errors Gracefully [Section titled “Handle Errors Gracefully”](#handle-errors-gracefully) ```csharp app.MapPost("/api/data", async context => { try { var data = await context.ReadBodyAsJson(typeof(MyData)); // Process data } catch (Exception ex) { context.SetStatusCode(500); var error = new { Error = "Internal server error" }; await context.WriteResponseAsJson(error, error.GetType()); } }); ``` ### Set Appropriate Status Codes [Section titled “Set Appropriate Status Codes”](#set-appropriate-status-codes) ```csharp context.SetStatusCode(200); // OK context.SetStatusCode(201); // Created context.SetStatusCode(400); // Bad Request context.SetStatusCode(401); // Unauthorized context.SetStatusCode(404); // Not Found context.SetStatusCode(500); // Internal Server Error ``` ### Use Dependency Injection [Section titled “Use Dependency Injection”](#use-dependency-injection) Access services through the request context: ```csharp app.MapGet("/api/users", async context => { var userService = context.RequestServices.GetRequiredService(); var users = await userService.GetAllAsync(); await context.WriteResponseAsJson(users, users.GetType()); }); ``` ## Limitations [Section titled “Limitations”](#limitations) The current endpoint mapping implementation: * Supports GET and POST methods only * Does not support route parameters in the pattern (e.g., `/users/{id}`) * Does not support PUT, DELETE, or PATCH methods For full HTTP method support and advanced routing, consider using ASP.NET Core with Arc. ## See Also [Section titled “See Also”](#see-also) * [Getting Started](/arc/backend/core/getting-started/) - Learn the basics of Arc.Core * [OpenAPI Specifications](/arc/backend/core/openapi/) - Generate API documentation * [Authentication](/arc/backend/core/authentication/) - Secure your endpoints * [Authorization](/arc/backend/core/authorization/) - Control access to resources # Getting Started This guide walks you through building your first Arc.Core application from scratch. You’ll learn how to set up the application, define commands and queries, and run your service. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * .NET SDK 10.0.301 or later * Basic understanding of C# and .NET concepts ## Installation [Section titled “Installation”](#installation) Add the Arc.Core package to your project: ```bash dotnet add package Cratis.Arc ``` ## Basic Setup [Section titled “Basic Setup”](#basic-setup) Create a new console application and configure Arc.Core: ```csharp using Cratis.Arc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; var builder = ArcApplication.CreateBuilder(args); // Add Arc services builder.AddCratisArc(options => options.Hosting.ApplicationUrl = "http://localhost:5000/"); // Configure logging builder.Services.AddLogging(logging => { logging.AddConsole(); logging.SetMinimumLevel(LogLevel.Information); }); // Build and run the application var app = builder.Build(); // Wire up the Arc middleware and endpoints app.UseCratisArc(); Console.WriteLine("Application started on http://localhost:5000/"); Console.WriteLine("Press Ctrl+C to stop..."); await app.RunAsync(); ``` ## ArcApplicationBuilder API [Section titled “ArcApplicationBuilder API”](#arcapplicationbuilder-api) The `ArcApplicationBuilder` provides a familiar builder pattern for configuring your application: ### Creating a Builder [Section titled “Creating a Builder”](#creating-a-builder) ```csharp // Create with command-line arguments var builder = ArcApplication.CreateBuilder(args); // Or without arguments var builder = ArcApplication.CreateBuilder(); ``` ### Available Properties [Section titled “Available Properties”](#available-properties) The builder exposes several properties for configuration: ```csharp // Configuration system IConfigurationManager Configuration = builder.Configuration; // Host environment information IHostEnvironment Environment = builder.Environment; // Logging configuration ILoggingBuilder Logging = builder.Logging; // Service collection for dependency injection IServiceCollection Services = builder.Services; // Metrics configuration IMetricsBuilder Metrics = builder.Metrics; ``` ### Adding Arc Services [Section titled “Adding Arc Services”](#adding-arc-services) ```csharp builder.AddCratisArc( configureOptions: options => { // Configure Arc-specific options (ArcOptions) }, configureBuilder: arcBuilder => { // Add extensions like Chronicle, MongoDB, etc. }); ``` ## ArcApplication API [Section titled “ArcApplication API”](#arcapplication-api) ### Starting the Application [Section titled “Starting the Application”](#starting-the-application) The `UseCratisArc` method wires up the Arc middleware and endpoints. It takes no arguments: ```csharp // UseCratisArc takes no arguments — it wires up the middleware and endpoints. app.UseCratisArc(); ``` The listen URL is **not** passed here — it comes from configuration via `ArcOptions.Hosting.ApplicationUrl` (default `http://+:5001/`). Set it through the options callback when adding Arc services, or in `appsettings.json` under `Cratis:Arc:Hosting:ApplicationUrl`. ### Running the Application [Section titled “Running the Application”](#running-the-application) ```csharp // Run and block until shutdown (Ctrl+C) await app.RunAsync(); // Or control start/stop manually await app.StartAsync(); // ... do work ... await app.StopAsync(); ``` ## Working with Commands [Section titled “Working with Commands”](#working-with-commands) Commands represent actions or operations in your application. They’re automatically exposed as HTTP POST endpoints. ### Defining a Command [Section titled “Defining a Command”](#defining-a-command) ```csharp using Cratis.Arc.Commands; [Command] public record CreateUser(string Name, string Email) { public Task Handle(ILogger logger) { logger.LogInformation("Creating user: {Name}", Name); // Your business logic here return Task.CompletedTask; } } ``` ### Accessing Command Endpoints [Section titled “Accessing Command Endpoints”](#accessing-command-endpoints) Commands are exposed as POST endpoints: ```bash curl -X POST http://localhost:5000/api/your-app/create-user \ -H "Content-Type: application/json" \ -d '{"name":"John Doe","email":"john@example.com"}' ``` ## Working with Queries [Section titled “Working with Queries”](#working-with-queries) Queries represent data retrieval operations. They’re automatically exposed as HTTP GET endpoints. ### Defining a Query [Section titled “Defining a Query”](#defining-a-query) ```csharp using Cratis.Arc.Queries; [ReadModel] public record User(Guid Id, string Name) { // Exposed as GET; the method's parameters become query arguments. public static User GetUser(Guid id) => new(id, "John Doe"); } ``` ### Accessing Query Endpoints [Section titled “Accessing Query Endpoints”](#accessing-query-endpoints) Queries are exposed as GET endpoints: ```bash curl http://localhost:5000/api/your-app/get-user?id=123e4567-e89b-12d3-a456-426614174000 ``` ## Configuration [Section titled “Configuration”](#configuration) ### Using appsettings.json [Section titled “Using appsettings.json”](#using-appsettingsjson) Arc.Core supports standard .NET configuration: ```json { "Cratis": { "Arc": { "GeneratedApis": { "RoutePrefix": "api" }, "CorrelationId": { "HttpHeader": "X-Correlation-ID" }, "Tenancy": { "HttpHeader": "X-Custom-Tenant" } } }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning" } } } ``` ### Environment-Specific Configuration [Section titled “Environment-Specific Configuration”](#environment-specific-configuration) Use environment-specific files following .NET conventions: * `appsettings.json` - Base configuration * `appsettings.Development.json` - Development overrides * `appsettings.Production.json` - Production overrides ```csharp var builder = ArcApplication.CreateBuilder(args); // Configuration is automatically loaded based on environment var environment = builder.Environment.EnvironmentName; Console.WriteLine($"Running in {environment} environment"); ``` ### Custom Configuration Section [Section titled “Custom Configuration Section”](#custom-configuration-section) Specify a custom configuration section path: ```csharp builder.AddCratisArc( configSectionPath: "MyApp:ArcSettings" ); ``` ## Adding Services [Section titled “Adding Services”](#adding-services) ### Logging [Section titled “Logging”](#logging) ```csharp builder.Services.AddLogging(logging => { logging.AddConsole(); logging.AddDebug(); logging.SetMinimumLevel(LogLevel.Information); }); ``` ### Custom Services [Section titled “Custom Services”](#custom-services) ```csharp // Singleton builder.Services.AddSingleton(); // Scoped (per request) builder.Services.AddScoped(); // Transient (per injection) builder.Services.AddTransient(); ``` ### Using Arc Conventions [Section titled “Using Arc Conventions”](#using-arc-conventions) Arc supports automatic service registration using attributes: ```csharp // Services with [Singleton], [Scoped], or [Transient] attributes // are automatically registered [Singleton] public class MyService : IMyService { // Implementation } ``` ## Complete Example [Section titled “Complete Example”](#complete-example) Here’s a complete working example that demonstrates commands, queries, and services: ```csharp using Cratis.Arc; using Cratis.Arc.Commands; using Cratis.Arc.Queries; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; var builder = ArcApplication.CreateBuilder(args); builder.AddCratisArc(options => options.Hosting.ApplicationUrl = "http://localhost:5000/"); builder.Services.AddLogging(logging => { logging.AddConsole(); logging.SetMinimumLevel(LogLevel.Information); }); var app = builder.Build(); app.UseCratisArc(); Console.WriteLine("Application started!"); Console.WriteLine("Available endpoints:"); Console.WriteLine(" POST http://localhost:5000/api/my-app/greet"); Console.WriteLine(" GET http://localhost:5000/api/my-app/get-greeting?name=World"); Console.WriteLine(" GET http://localhost:5000/.cratis/me"); Console.WriteLine(); Console.WriteLine("Press Ctrl+C to stop..."); await app.RunAsync(); // Commands [Command] public record Greet(string Name) { public Task Handle(ILogger logger) { logger.LogInformation("Greeting {Name}", Name); return Task.CompletedTask; } } // Queries [ReadModel] public record Greeting(string Text) { public static Greeting GetGreeting(string name) => new($"Hello, {name}!"); } ``` ## Advanced Scenarios [Section titled “Advanced Scenarios”](#advanced-scenarios) ### Background Services [Section titled “Background Services”](#background-services) Combine Arc.Core with `IHostedService` for background processing: ```csharp public class BackgroundWorker(ILogger logger) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { logger.LogInformation("Background worker started"); while (!stoppingToken.IsCancellationRequested) { // Do background work await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); } logger.LogInformation("Background worker stopped"); } } // Register it builder.Services.AddHostedService(); ``` ### Integrating with Chronicle [Section titled “Integrating with Chronicle”](#integrating-with-chronicle) Add event sourcing capabilities: ```csharp builder.AddCratisArc(configureBuilder: arcBuilder => { arcBuilder.WithChronicle(); }); ``` ### Integrating with MongoDB [Section titled “Integrating with MongoDB”](#integrating-with-mongodb) Add MongoDB support: ```csharp builder.AddCratisArc(configureBuilder: arcBuilder => { arcBuilder.WithMongoDB(); }); ``` ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Endpoints Not Found [Section titled “Endpoints Not Found”](#endpoints-not-found) Ensure you’ve called `app.UseCratisArc()` before `app.RunAsync()`: ```csharp var app = builder.Build(); app.UseCratisArc(); // Must be called! await app.RunAsync(); ``` ### HTTP Listener Errors [Section titled “HTTP Listener Errors”](#http-listener-errors) If you get HTTP listener errors, ensure: 1. The port is not already in use 2. You have permissions to bind to the port (on Windows, non-admin users can’t bind to port 80) 3. Set the listen URL via `ArcOptions.Hosting.ApplicationUrl` (in the options callback or `appsettings.json` under `Cratis:Arc:Hosting:ApplicationUrl`) — it is not passed to `UseCratisArc`. Use `http://+:5001/` instead of `http://localhost:5001/` to listen on all interfaces ### Configuration Not Loading [Section titled “Configuration Not Loading”](#configuration-not-loading) Ensure `appsettings.json` is copied to output: ```xml PreserveNewest ``` ## Next Steps [Section titled “Next Steps”](#next-steps) Now that you have a basic Arc.Core application running, explore these topics: * [Authentication](/arc/backend/core/authentication/) - Implement custom authentication handlers * [Authorization](/arc/backend/core/authorization/) - Protect your endpoints with authorization attributes * [Commands](/arc/backend/commands/) - Learn about advanced command patterns * [Queries](/arc/backend/queries/) - Discover query features like filtering and pagination * [Identity](/arc/backend/identity/) - Integrate the identity system * [Tenancy](/arc/backend/tenancy/) - Configure multi-tenant applications * [Validation](/arc/backend/commands/validation/) - Add validation to commands and queries # Invariant Culture Arc.Core provides a `.UseInvariantCulture()` extension method on `IHostBuilder` that configures the 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) Applications running in distributed environments—different machines, containers, or cloud regions—may be 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) For an `ArcApplication`-based host (a worker or console service), set the default thread cultures to invariant before building the application: ```csharp using System.Globalization; using Cratis.Arc; CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture; var builder = ArcApplication.CreateBuilder(args); builder.AddCratisArc(); var app = builder.Build(); app.UseCratisArc(); await app.RunAsync(); ``` For a generic host, the `UseInvariantCulture()` extension on `IHostBuilder` does the same thing in one call: ```csharp using Cratis.Arc; using Microsoft.Extensions.Hosting; var host = Host.CreateDefaultBuilder(args) .UseInvariantCulture() .Build(); ``` ## What Gets Configured [Section titled “What Gets Configured”](#what-gets-configured) Both approaches set: * `CultureInfo.DefaultThreadCurrentCulture` to `CultureInfo.InvariantCulture` * `CultureInfo.DefaultThreadCurrentUICulture` to `CultureInfo.InvariantCulture` This ensures that all threads in the application, including background threads and thread pool threads, use invariant culture by default. ## When to Use [Section titled “When to Use”](#when-to-use) Invariant culture is recommended for: * **Background services and workers** that process data without a web request context * **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 locale-specific formatting should be handled on the client side rather than in the service itself. ## ASP.NET Core Applications [Section titled “ASP.NET Core Applications”](#aspnet-core-applications) If you are building an ASP.NET Core application, use the `WebApplicationBuilder` extension instead, which additionally configures the ASP.NET Core request localization middleware. See [Invariant Culture for ASP.NET Core](/arc/backend/asp-net-core/invariant-culture/) for details. # OpenAPI Specifications Arc provides built-in support for generating OpenAPI 3.0 specification documents from your registered HTTP endpoints. This allows you to document your API and integrate with tools like Swagger UI, Postman, and other OpenAPI-compatible clients. ## Overview [Section titled “Overview”](#overview) The OpenAPI support in Arc automatically generates a specification document based on: * Registered routes (GET, POST, PUT, DELETE, PATCH) * Endpoint metadata (operation IDs, summaries, tags) * Authentication requirements * Response codes ## Getting Started [Section titled “Getting Started”](#getting-started) To add OpenAPI support to your Arc application, you need to: 1. Add a reference to the `Cratis.Arc.OpenApi` package 2. Call the `MapOpenApi()` extension method on your `ArcApplication` ### Installation [Section titled “Installation”](#installation) Add the package reference to your project: ```bash dotnet add package Cratis.Arc.OpenApi ``` ### Basic Configuration [Section titled “Basic Configuration”](#basic-configuration) ```csharp using Cratis.Arc; using Cratis.Arc.OpenApi; var builder = ArcApplication.CreateBuilder(args); var app = builder.Build(); // Map the OpenAPI endpoint app.MapOpenApi(); await app.RunAsync(); ``` This will expose the OpenAPI specification document at `/openapi.json` by default. ## Configuration Options [Section titled “Configuration Options”](#configuration-options) The `MapOpenApi()` method accepts several optional parameters to customize the generated document: ### Custom Endpoint Path [Section titled “Custom Endpoint Path”](#custom-endpoint-path) Change where the OpenAPI document is served: ```csharp app.MapOpenApi(pattern: "/api/swagger.json"); ``` ### API Title and Version [Section titled “API Title and Version”](#api-title-and-version) Customize the API information in the document: ```csharp app.MapOpenApi( title: "My API", version: "2.0.0"); ``` ### Complete Example [Section titled “Complete Example”](#complete-example) ```csharp app.MapOpenApi( pattern: "/api/openapi.json", title: "Customer Management API", version: "1.0.0"); ``` ## Generated Document Structure [Section titled “Generated Document Structure”](#generated-document-structure) The OpenAPI document includes: ### API Information [Section titled “API Information”](#api-information) * **Title**: The name of your API * **Version**: The API version ### Servers [Section titled “Servers”](#servers) * Default server URL (`/`) ### Paths [Section titled “Paths”](#paths) All registered routes with their: * HTTP methods (get, post, put, delete, patch) * Operation IDs (from endpoint metadata) * Summaries (from endpoint metadata) * Tags (from endpoint metadata) * Response codes: * `200` - Success * `401` - Unauthorized (for authenticated endpoints) * `500` - Internal Server Error ### Security [Section titled “Security”](#security) If any endpoints require authentication (have `AllowAnonymous = false`), the document includes: * Bearer token security scheme * JWT format specification * Security requirements per operation ## Endpoint Metadata [Section titled “Endpoint Metadata”](#endpoint-metadata) To provide rich OpenAPI documentation, use endpoint metadata when registering routes: ```csharp app.MapGet("/api/customers", async context => { // Handler implementation }, new EndpointMetadata( Name: "GetCustomers", Summary: "Retrieves all customers", Tags: ["Customers"], AllowAnonymous: false)); ``` ### Metadata Properties [Section titled “Metadata Properties”](#metadata-properties) * **Name**: Becomes the `operationId` in the OpenAPI document * **Summary**: Becomes the `summary` in the OpenAPI document * **Tags**: Used for grouping operations in API documentation tools * **AllowAnonymous**: When `false`, adds security requirements to the operation ## Integration with API Clients [Section titled “Integration with API Clients”](#integration-with-api-clients) ### Swagger UI [Section titled “Swagger UI”](#swagger-ui) You can use the generated OpenAPI document with Swagger UI: ```html API Documentation
``` ### Postman [Section titled “Postman”](#postman) Import the OpenAPI document into Postman: 1. Open Postman 2. Click **Import** 3. Choose **Link** and enter your OpenAPI URL (e.g., `http://localhost:5000/openapi.json`) 4. Click **Continue** to import the collection ### Code Generation [Section titled “Code Generation”](#code-generation) Use OpenAPI generators to create client libraries: ```bash # Using openapi-generator-cli openapi-generator-cli generate \ -i http://localhost:5000/openapi.json \ -g csharp \ -o ./generated-client ``` ## Example Application [Section titled “Example Application”](#example-application) Here’s a complete example of an Arc application with OpenAPI support: ```csharp using Cratis.Arc; using Cratis.Arc.Http; using Cratis.Arc.OpenApi; var builder = ArcApplication.CreateBuilder(args); var app = builder.Build(); // Configure OpenAPI endpoint app.MapOpenApi( pattern: "/openapi.json", title: "Product Catalog API", version: "1.0.0"); // Register API endpoints app.MapGet("/api/products", async context => { var products = new[] { new { Id = 1, Name = "Product 1", Price = 29.99 }, new { Id = 2, Name = "Product 2", Price = 39.99 } }; await context.WriteResponseAsJson(products, products.GetType()); }, new EndpointMetadata( Name: "ListProducts", Summary: "Get all products in the catalog", Tags: ["Products"], AllowAnonymous: true)); app.MapPost("/api/products", async context => { var product = await context.ReadBodyAsJson(typeof(object)); context.SetStatusCode(201); await context.WriteResponseAsJson(product, product?.GetType() ?? typeof(object)); }, new EndpointMetadata( Name: "CreateProduct", Summary: "Create a new product", Tags: ["Products"], AllowAnonymous: false)); await app.RunAsync(); ``` ## Best Practices [Section titled “Best Practices”](#best-practices) ### Use Descriptive Names [Section titled “Use Descriptive Names”](#use-descriptive-names) Provide clear operation IDs and summaries: ```csharp new EndpointMetadata( Name: "GetCustomerById", Summary: "Retrieves a customer by their unique identifier", Tags: ["Customers"]) ``` ### Organize with Tags [Section titled “Organize with Tags”](#organize-with-tags) Group related endpoints together: ```csharp // All customer-related endpoints use the "Customers" tag Tags: ["Customers"] // Order-related endpoints use the "Orders" tag Tags: ["Orders"] ``` ### Document Security Requirements [Section titled “Document Security Requirements”](#document-security-requirements) Be explicit about authentication: ```csharp // Public endpoint new EndpointMetadata( Name: "GetPublicData", AllowAnonymous: true) // Protected endpoint new EndpointMetadata( Name: "GetUserData", AllowAnonymous: false) ``` ### Keep Versions Updated [Section titled “Keep Versions Updated”](#keep-versions-updated) Update the version when making breaking changes: ```csharp app.MapOpenApi( title: "My API", version: "2.0.0"); // Incremented for breaking changes ``` ## Limitations [Section titled “Limitations”](#limitations) The current OpenAPI implementation: * Generates basic request/response schemas (no detailed type information) * Does not include request body schemas * Does not include response body schemas * Supports standard HTTP methods (GET, POST, PUT, DELETE, PATCH) For more advanced OpenAPI features with full schema generation, consider using `Cratis.Arc.Swagger` with ASP.NET Core. ## See Also [Section titled “See Also”](#see-also) * [Getting Started](/arc/backend/core/getting-started/) - Learn how to build Arc.Core applications * [Authentication](/arc/backend/core/authentication/) - Implement authentication for your API * [Authorization](/arc/backend/core/authorization/) - Secure endpoints with authorization # Overview Arc.Core is a lightweight application framework that brings the Arc developer experience to .NET applications without requiring ASP.NET Core. It’s designed for scenarios where you need the power of Arc’s conventions—commands, queries, identity, multi-tenancy, and validation—but don’t need the full web framework. ## Motivation [Section titled “Motivation”](#motivation) Modern .NET development often requires building various types of applications beyond traditional web applications: * **Console Applications** - CLI tools, utilities, and batch processors * **Background Services** - Long-running workers and scheduled tasks * **Microservices** - Lightweight services with minimal overhead * **Custom Servers** - gRPC services, custom protocols, or specialized HTTP endpoints * **Containerized Workloads** - Applications optimized for containers and serverless environments For these scenarios, the full ASP.NET Core stack can be overkill, bringing unnecessary dependencies, slower startup times, and increased memory consumption. Arc.Core addresses this by providing a minimal foundation that preserves the Arc experience while removing web framework overhead. ## Design Philosophy [Section titled “Design Philosophy”](#design-philosophy) ### Minimal Dependencies [Section titled “Minimal Dependencies”](#minimal-dependencies) Arc.Core intentionally excludes the full ASP.NET Core stack: * **No Kestrel or HTTP.sys** - Uses .NET’s built-in `HttpListener` for HTTP scenarios * **No MVC/Razor** - No view rendering or controller infrastructure * **No Middleware Pipeline** - Simplified request handling * **Smaller Deployment Footprint** - Fewer assemblies to deploy This results in: * Faster startup times * Lower memory consumption * Smaller binary sizes * Reduced attack surface ### Native AOT Ready [Section titled “Native AOT Ready”](#native-aot-ready) Arc.Core is designed with Native AOT (Ahead-of-Time) compilation in mind: * **Faster Startup** - No JIT compilation at runtime * **Smaller Binaries** - Single-file executables with tree-shaking * **Lower Memory Footprint** - Reduced working set * **Predictable Performance** - No JIT warmup time > **Note**: While Arc.Core is designed to support AOT, full AOT compatibility depends on the features and libraries you use in your application. Always test your specific scenario. ### Full Arc Features [Section titled “Full Arc Features”](#full-arc-features) Despite being lightweight, Arc.Core provides all core Arc capabilities: * **Commands** - Automatic endpoint generation and handling * **Queries** - Filtering, sorting, and pagination support * **Identity System** - User authentication and authorization * **Multi-Tenancy** - Tenant isolation and context management * **Correlation ID Tracking** - Request tracing across services * **Validation** - Declarative validation with automatic error handling * **Type Discovery** - Convention-based type discovery * **Dependency Injection** - Full DI container support ### Flexibility [Section titled “Flexibility”](#flexibility) Arc.Core can be used in various scenarios: * **Standalone HTTP Services** - Build HTTP APIs without ASP.NET Core * **Console Applications** - Add commands and queries to CLI tools * **Background Workers** - Combine with `IHostedService` for background processing * **gRPC Services** - Use Arc features alongside gRPC * **Custom Protocols** - Build any type of .NET application with Arc conventions ## What It’s For [Section titled “What It’s For”](#what-its-for) ### Primary Use Cases [Section titled “Primary Use Cases”](#primary-use-cases) Arc.Core is ideal for: 1. **Lightweight Microservices** * Services that don’t need the full web stack * Container-optimized deployments * Fast startup requirements * Low memory constraints 2. **Console Applications** * CLI tools that expose HTTP endpoints for management * Batch processing with API integration * Developer tools and utilities 3. **Background Services** * Long-running workers with HTTP endpoints for health checks * Scheduled tasks with monitoring APIs * Message processors with control endpoints 4. **Native AOT Scenarios** * Applications requiring fast cold starts * Single-file deployments * Environments with strict size constraints 5. **Learning and Prototyping** * Simpler setup for learning Arc concepts * Rapid prototyping without web framework complexity * Testing Arc patterns in isolation ### When Not to Use [Section titled “When Not to Use”](#when-not-to-use) Arc.Core is **not** suitable when you need: * **Static File Serving** - Use ASP.NET Core’s static file middleware * **Razor Views** - Use ASP.NET Core MVC * **Advanced Middleware** - Use ASP.NET Core’s full middleware pipeline * **Swagger UI** - Use Arc with ASP.NET Core and Swagger extension * **High-Traffic Scenarios** - Consider ASP.NET Core with Kestrel for maximum throughput ## Architecture [Section titled “Architecture”](#architecture) Arc.Core is built around the `ArcApplicationBuilder` and `ArcApplication` abstractions, which mirror .NET’s `HostBuilder` pattern: ```csharp // Builder Pattern var builder = ArcApplication.CreateBuilder(args); builder.AddCratisArc(); // Configure services, logging, metrics, etc. // Application Pattern var app = builder.Build(); app.UseCratisArc(); await app.RunAsync(); ``` This familiar pattern makes it easy to transition between Arc.Core and ASP.NET Core-based Arc applications. ## Comparison with ASP.NET Core [Section titled “Comparison with ASP.NET Core”](#comparison-with-aspnet-core) | Aspect | Arc.Core | Arc with ASP.NET Core | | ----------------- | ---------------- | ----------------------- | | **Dependencies** | Minimal | Full ASP.NET Core stack | | **Startup Time** | Faster | Standard | | **Memory Usage** | Lower | Higher | | **Binary Size** | Smaller | Larger | | **AOT Support** | Designed for AOT | Limited AOT support | | **HTTP Server** | HttpListener | Kestrel/HTTP.sys | | **Middleware** | Basic | Full pipeline | | **Static Files** | ❌ No | ✅ Yes | | **Razor Views** | ❌ No | ✅ Yes | | **Swagger UI** | ❌ No | ✅ Yes | | **Commands** | ✅ Yes | ✅ Yes | | **Queries** | ✅ Yes | ✅ Yes | | **Identity** | ✅ Yes | ✅ Yes | | **Multi-Tenancy** | ✅ Yes | ✅ Yes | | **Validation** | ✅ Yes | ✅ Yes | ## Next Steps [Section titled “Next Steps”](#next-steps) Ready to build your first Arc.Core application? Head over to the [Getting Started](/arc/backend/core/getting-started/) guide. To learn about specific features: * [Authentication](/arc/backend/core/authentication/) - Implement custom authentication handlers * [Authorization](/arc/backend/core/authorization/) - Protect your endpoints with authorization attributes * [Invariant Culture](/arc/backend/core/invariant-culture/) - Guarantee consistent culture-sensitive behavior across all environments # Static Files Arc.Core provides built-in support for serving static files, similar to the `UseStaticFiles()` middleware in ASP.NET Core. This is essential for hosting Single Page Applications (SPAs), serving assets like CSS, JavaScript, images, and other static content. ## Basic Usage [Section titled “Basic Usage”](#basic-usage) To serve static files from the default `wwwroot` directory: ```csharp var builder = ArcApplication.CreateBuilder(args); builder.AddCratisArc(); var app = builder.Build(); // Enable static file serving from wwwroot app.UseStaticFiles(); app.UseCratisArc(); await app.RunAsync(); ``` > **Important**: `UseStaticFiles()` must be called **before** `UseCratisArc()` to ensure the static file configuration is registered before the HTTP listener starts. ## Configuration Options [Section titled “Configuration Options”](#configuration-options) You can customize static file serving using `StaticFileOptions`: ```csharp app.UseStaticFiles(options => { // Serve files from a custom directory options.FileSystemPath = "public"; // Add a URL prefix for static files options.RequestPath = "/static"; // Enable/disable serving default files (index.html, etc.) options.ServeDefaultFiles = true; // Customize which files are considered "default" files options.DefaultFileNames = ["index.html", "default.html"]; // Add custom MIME type mappings options.ContentTypeMappings["myext"] = "application/x-custom"; }); ``` ### StaticFileOptions Properties [Section titled “StaticFileOptions Properties”](#staticfileoptions-properties) | Property | Type | Default | Description | | ------------------------- | ----------------------------- | ------------------------------------------------------------ | -------------------------------------------------------- | | `FileSystemPath` | `string` | `"wwwroot"` | The directory to serve files from | | `RequestPath` | `string` | `""` (root) | URL prefix for static file requests | | `ServeDefaultFiles` | `bool` | `true` | Whether to serve index.html for directory requests | | `DefaultFileNames` | `IList` | `["index.html", "index.htm", "default.html", "default.htm"]` | Files to look for when requesting a directory | | `EnableDirectoryBrowsing` | `bool` | `false` | Whether to allow directory listing (not yet implemented) | | `ContentTypeMappings` | `IDictionary` | Empty | Custom file extension to MIME type mappings | ## File System Path Resolution [Section titled “File System Path Resolution”](#file-system-path-resolution) Arc.Core resolves relative paths in the following order: 1. **Current Working Directory** - First checks if the path exists relative to `Directory.GetCurrentDirectory()`. This is typically your project directory when running with `dotnet run`. 2. **Application Base Directory** - Falls back to `AppContext.BaseDirectory` (the `bin/Debug/net10.0/` output folder) if not found in the current directory. This behavior matches how ASP.NET Core resolves content root paths during development and ensures your `wwwroot` folder is found whether you’re running from the project directory or from the compiled output. ### Example: Custom Static Directory [Section titled “Example: Custom Static Directory”](#example-custom-static-directory) ```csharp // Serve from 'public' folder in your project directory app.UseStaticFiles(options => { options.FileSystemPath = "public"; }); // Serve from an absolute path app.UseStaticFiles(options => { options.FileSystemPath = "/var/www/static"; }); ``` ## SPA Fallback with MapFallbackToFile [Section titled “SPA Fallback with MapFallbackToFile”](#spa-fallback-with-mapfallbacktofile) For Single Page Applications that use client-side routing, you need to serve your `index.html` for any route that doesn’t match a static file or API endpoint. Use `MapFallbackToFile()`: ```csharp var app = builder.Build(); // Serve static files app.UseStaticFiles(); // Fallback to index.html for SPA routing app.MapFallbackToFile(); // Defaults to "index.html" // Or specify a custom file app.MapFallbackToFile("app.html"); app.UseCratisArc(); await app.RunAsync(); ``` The fallback file is served when: 1. No registered API route matches the request 2. No static file matches the request path 3. The request method is GET 4. The fallback file exists in the static files directory ## Complete SPA Example [Section titled “Complete SPA Example”](#complete-spa-example) Here’s a complete example for hosting a React, Angular, or Vue SPA: ```csharp using Cratis.Arc; var builder = ArcApplication.CreateBuilder(args); builder.AddCratisArc(); var app = builder.Build(); // Serve static files from wwwroot app.UseStaticFiles(); // Enable SPA fallback routing app.MapFallbackToFile(); // Configure Arc (maps API endpoints) app.UseCratisArc(); await app.RunAsync(); ``` With this configuration: * `/` → Serves `wwwroot/index.html` * `/styles.css` → Serves `wwwroot/styles.css` * `/js/app.js` → Serves `wwwroot/js/app.js` * `/dashboard/users/123` → Serves `wwwroot/index.html` (SPA route) * `/api/users` → Handled by your query endpoints ## Multiple Static File Configurations [Section titled “Multiple Static File Configurations”](#multiple-static-file-configurations) You can call `UseStaticFiles()` multiple times to serve from different directories: ```csharp // Serve from wwwroot at root app.UseStaticFiles(); // Serve uploaded files from a different directory app.UseStaticFiles(options => { options.FileSystemPath = "uploads"; options.RequestPath = "/files"; }); ``` ## Middleware Order [Section titled “Middleware Order”](#middleware-order) The order of middleware configuration is important: ```csharp var app = builder.Build(); // 1. Static files (first, so static assets are served quickly) app.UseStaticFiles(); // 2. SPA fallback (after static files, before API routes) app.MapFallbackToFile(); // 3. Arc configuration (API routes, etc.) app.UseCratisArc(); await app.RunAsync(); ``` ## MIME Types [Section titled “MIME Types”](#mime-types) Arc.Core includes built-in MIME type mappings for common file extensions: | Category | Extensions | | ---------- | -------------------------------------------------------- | | Text | `.txt`, `.html`, `.htm`, `.css`, `.csv`, `.xml` | | JavaScript | `.js`, `.mjs`, `.jsx`, `.ts`, `.tsx` | | JSON | `.json`, `.map` | | Images | `.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`, `.webp`, `.ico` | | Fonts | `.woff`, `.woff2`, `.ttf`, `.otf`, `.eot` | | Documents | `.pdf`, `.doc`, `.docx`, `.xls`, `.xlsx` | | Audio | `.mp3`, `.wav`, `.ogg`, `.m4a` | | Video | `.mp4`, `.webm`, `.avi`, `.mov` | | Web | `.wasm`, `.webmanifest` | For unknown extensions, `application/octet-stream` is returned. ### Adding Custom MIME Types [Section titled “Adding Custom MIME Types”](#adding-custom-mime-types) ```csharp app.UseStaticFiles(options => { options.ContentTypeMappings[".custom"] = "application/x-custom"; options.ContentTypeMappings[".data"] = "application/octet-stream"; }); ``` ## Security [Section titled “Security”](#security) Arc.Core includes protection against directory traversal attacks. Requests attempting to access files outside the configured static file directory (e.g., `/../../../etc/passwd`) will return a 404 response. # Entity Framework Core The Arc provides enhanced support for Entity Framework Core, offering simplified configuration, automatic database setup, and common patterns for working with EF Core in your applications. ## Topics [Section titled “Topics”](#topics) | Topic | Description | | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | [Getting Started](/arc/backend/entity-framework/getting-started/) | How to configure Entity Framework Core with Arc, including auto-discovery and observation support. | | [Base DbContext](/arc/backend/entity-framework/base-db-context/) | How to use the base DbContext class provided by the Arc. | | [Entity Mapping](/arc/backend/entity-framework/entity-mapping/) | How to configure entities using IEntityTypeConfiguration\ for clean, organized entity configuration. | | [Read Only DbContexts](/arc/backend/entity-framework/read-only/) | How to implement read-only database contexts for query scenarios. | | [Automatic Database hookup](/arc/backend/entity-framework/automatic-database-hookup/) | How the Arc automatically configures and sets up your databases. | | [Observing DbSet](/arc/backend/entity-framework/observing/) | How to monitor entity changes in real-time using reactive extensions. | | [Common Column Types](/arc/backend/entity-framework/common-column-types/) | Common column type configurations and conventions. | | [Property Extensions](/arc/backend/entity-framework/property-extensions/) | Property configuration extensions for cross-database compatibility. | | [Json](/arc/backend/entity-framework/json/) | Working with JSON columns and serialization in Entity Framework Core. | ## Overview [Section titled “Overview”](#overview) The Entity Framework Core integration in the Arc streamlines database operations by providing sensible defaults, automatic configuration, and patterns that work well with CQRS architecture. Whether you’re working with read-write or read-only contexts, the framework handles the complexity of setup and configuration while giving you the flexibility to customize when needed. # Automatic Database Hookup Out of the box we support the following databases: * Sqlite * PostgreSQL * Microsoft SQL Server There are different extension methods for adding `DbContext` types and also for resolving the correct database provider based on connection string. You can use the standard EF Core method with the Arc database detection extension: ```csharp services.AddDbContext(opt => opt.UseDatabaseFromConnectionString(".. your connection string..")); ``` > Note: From the connection string it will do the correct `.UseSqlite()`, `.UseNpgsql()` or `.UseSqlServer()` call on the builder. However, **it is recommended** to use the Arc registration methods which use the pooled factory pattern for better performance and to support multiple database providers: ```csharp services.AddDbContextWithConnectionString(".. your connection string..", (serviceProvider, opt) => { /* do whatever configuration you want */ }); ``` This method automatically: * Uses `AddPooledDbContextFactory` for improved performance * Applies all `BaseDbContext` configurations (interceptors, service replacements) * Registers both the factory and a scoped DbContext instance * Supports multiple database providers in the same application ## Multiple Database Providers [Section titled “Multiple Database Providers”](#multiple-database-providers) The Arc Entity Framework integration uses the **pooled factory pattern** (`IDbContextFactory`) internally to support multiple database providers in the same application. This is important because Entity Framework Core does not allow multiple database providers to be registered in the same service provider. The pooled factory approach provides: * **Multiple database provider support** - Different contexts can use different databases (SQLite, SQL Server, PostgreSQL) * **Improved performance** - DbContext instances are pooled and reused * **Reduced memory overhead** - Internal service providers are shared across pooled instances All registration methods automatically register both: * `IDbContextFactory` - For creating DbContext instances * `TDbContext` - Scoped instance created from the factory ### Using DbContext in Your Code [Section titled “Using DbContext in Your Code”](#using-dbcontext-in-your-code) You can inject DbContexts directly as you normally would: ```csharp public class MyService { private readonly MyDbContext _context; public MyService(MyDbContext context) { _context = context; } } ``` Or you can use `IDbContextFactory` when you need more control over the DbContext lifetime: ```csharp public class MyService { private readonly IDbContextFactory _contextFactory; public MyService(IDbContextFactory contextFactory) { _contextFactory = contextFactory; } public async Task ProcessAsync() { await using var context = await _contextFactory.CreateDbContextAsync(); // Use context } } ``` > **Important**: When using multiple DbContexts with different database providers (e.g., SQLite for testing and SQL Server for production), the factory pattern ensures each DbContext gets its own isolated service provider, preventing conflicts between providers. ## Read Only DbContexts [Section titled “Read Only DbContexts”](#read-only-dbcontexts) For any **read-only** `DbContext` there is also an extension method: ```csharp services.AddReadOnlyDbContextWithConnectionString(".. your connection string..", (serviceProvider, opt) => { /* do whatever configuration you want */ }); ``` ## Automatic Registration from Assemblies [Section titled “Automatic Registration from Assemblies”](#automatic-registration-from-assemblies) The framework provides methods to automatically discover and register all `ReadOnlyDbContext` types from specified assemblies: ```csharp // Register all ReadOnlyDbContext types from assemblies with a common options action services.AddReadModelDbContextsFromAssemblies((serviceProvider, opt) => opt.UseDatabaseFromConnectionString(connectionString), assembly1, assembly2); // Register all ReadOnlyDbContext types from assemblies with a connection string services.AddReadModelDbContextsWithConnectionStringFromAssemblies(connectionString, (serviceProvider, opt) => { /* additional options */ }, assembly1, assembly2); ``` ### Registration Filtering Rules [Section titled “Registration Filtering Rules”](#registration-filtering-rules) When using automatic registration, the framework applies the following filtering rules: 1. **Public Classes Only**: Only `public` DbContext classes will be automatically registered. Internal, private, or protected classes are ignored. 2. **Assembly Membership**: Only DbContext classes that belong to the specified assemblies will be considered for registration. 3. **Attribute-Based Exclusion**: Classes marked with the `IgnoreAutoRegistrationAttribute` will be excluded from automatic registration. ### Excluding DbContexts from Automatic Registration [Section titled “Excluding DbContexts from Automatic Registration”](#excluding-dbcontexts-from-automatic-registration) If you have a DbContext that should not be automatically registered (for example, if it requires special configuration or should be registered manually), you can exclude it using the `IgnoreAutoRegistrationAttribute`: ```csharp using Cratis.Arc; [IgnoreAutoRegistration] public class SpecialDbContext : ReadOnlyDbContext { // This DbContext will be ignored during automatic registration // and must be registered manually if needed } ``` This is useful for scenarios where: * The DbContext requires special configuration * You want to register it with different lifetime scopes * It’s used only in specific conditions * You want to register it manually with custom options # Base Db Context The `BaseDbContext` provides a pre-configured Entity Framework Core context that automatically applies common conventions and converters. This eliminates the need for manual configuration while ensuring consistent behavior across your application. ## Converters [Section titled “Converters”](#converters) The `BaseDbContext` provides automatic converter application when used with the Arc registration methods (`AddDbContextWithConnectionString` or `AddReadOnlyDbContext`). These methods configure all necessary services and interceptors at registration time to support the pooled factory pattern. The `BaseDbContext` automatically applies converters to all `DbSet<>` types defined on the context and any types that are referenced by those entity types. ## How Converters Are Applied [Section titled “How Converters Are Applied”](#how-converters-are-applied) The `BaseDbContext` determines which entity types should have converters applied. An entity type is considered relevant if it: * Is an owned entity type * Is directly exposed as a `DbSet<>` on the context * Is referenced as a property or collection element by any type exposed as a `DbSet<>` on the context This ensures that converters are applied not only to top-level entities but also to any related entities that are part of your domain model hierarchy. ### JSON Conversion [Section titled “JSON Conversion”](#json-conversion) Automatically applies [JSON conversion](/arc/backend/entity-framework/json/) for properties marked with the `[Json]` attribute, allowing complex objects to be stored as JSON in the database with cross-provider compatibility. ### ConceptAs Conversion [Section titled “ConceptAs Conversion”](#conceptas-conversion) Automatically applies [ConceptAs conversion](/arc/backend/entity-framework/concept-as-conversion/) for all properties that implement `ConceptAs`, ensuring domain concepts are properly stored and retrieved while maintaining type safety. ### GUID Conversion [Section titled “GUID Conversion”](#guid-conversion) Automatically applies [GUID conversion](/arc/backend/entity-framework/guid-conversion/) for all `Guid` properties, optimizing storage format and performance for each database provider. ## Usage [Section titled “Usage”](#usage) All you need to do is inherit from `BaseDbContext` and register it as you’d do with any other `DbContext`. Take the following `DbContext`: ```csharp using Cratis.Arc.EntityFrameworkCore; public class StoreDbContext : BaseDbContext { public DbSet Customers { get; set; } } ``` Then you register it as you normally would: ```csharp services.AddDbContext(opt => ...); ``` Or leveraging the [automatic database hookup](/arc/backend/entity-framework/automatic-database-hookup/) extensions provided by Cratis Arc. ## Important: DbContext Pooling [Section titled “Important: DbContext Pooling”](#important-dbcontext-pooling) The `BaseDbContext` is designed to work with the pooled factory pattern used by the Arc registration methods. When using `AddDbContextWithConnectionString` or `AddReadOnlyDbContext`, all configuration (service replacements, interceptors, observation support) is applied at registration time. **Do not override `OnConfiguring`** in your derived DbContext classes when using pooled contexts, as EF Core does not allow modifying options in `OnConfiguring` when pooling is enabled. All configuration must be done during registration. If you need custom configuration, pass it through the optional `optionsAction` parameter: ```csharp services.AddDbContextWithConnectionString( connectionString, opt => opt.EnableSensitiveDataLogging() // Custom configuration here ); ``` # Common Column Types One of the key design goals of Cratis Arc support for Entity Framework is to make it easy to support different databases for an application. If you’re willing to hand-roll Entity Framework migrations, you can leverage the extension methods that will give you a single migration but support different database types resolved at runtime. ## Available Column Extension Methods [Section titled “Available Column Extension Methods”](#available-column-extension-methods) The following table shows all the column extension methods available in Cratis Arc: | Extension Method | Description | Supported Types | Parameters | | ------------------------ | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | -------------------------------------------------------------- | | `StringColumn()` | Creates a string column with appropriate database-specific type (VARCHAR/NVARCHAR/TEXT) | string | `maxLength` (int?, optional), `nullable` (bool, default: true) | | `NumberColumn()` | Creates a numeric column with appropriate database-specific type for any numeric type | char, byte, sbyte, short, ushort, int, uint, long, ulong, float, double, decimal | `nullable` (bool, default: true) | | `BoolColumn()` | Creates a boolean column with appropriate database-specific type (BOOLEAN/BIT/INTEGER) | bool | `nullable` (bool, default: true) | | `AutoIncrementColumn()` | Creates an auto-incrementing integer column with appropriate database-specific annotations | int | None (always non-nullable) | | `GuidColumn()` | Creates a GUID/UUID column with appropriate database-specific type (UUID/UNIQUEIDENTIFIER/TEXT) | Guid | `nullable` (bool, default: true) | | `DateTimeOffsetColumn()` | Creates a DateTimeOffset column with appropriate database-specific type (TIMESTAMPTZ/DATETIMEOFFSET/TEXT) | DateTimeOffset | `nullable` (bool, default: true) | | `CoordinateColumn()` | Creates a Coordinate column with appropriate database-specific type (jsonb/nvarchar(max)/text) for geospatial data | Coordinate | `nullable` (bool, default: true) | | `JsonColumn()` | Creates a JSON column with appropriate database-specific type (jsonb/nvarchar(max)/text) | Any type | None (always non-nullable) | ## Database-Specific Type Mappings [Section titled “Database-Specific Type Mappings”](#database-specific-type-mappings) The extension methods automatically select the appropriate SQL type based on the database provider: ### String Types [Section titled “String Types”](#string-types) * **PostgreSQL**: `VARCHAR(n)` for limited length, `TEXT` for unlimited * **SQL Server**: `NVARCHAR(n)` for limited length, `NVARCHAR(MAX)` for unlimited * **SQLite**: `TEXT` ### Numeric Types [Section titled “Numeric Types”](#numeric-types) The `NumberColumn()` method supports all .NET numeric types and maps them appropriately: * **PostgreSQL**: SMALLINT, INTEGER, BIGINT, NUMERIC, REAL, DOUBLE PRECISION, DECIMAL * **SQL Server**: TINYINT, SMALLINT, INT, BIGINT, DECIMAL, REAL, FLOAT * **SQLite**: INTEGER, REAL (simplified type system) ### Boolean Types [Section titled “Boolean Types”](#boolean-types) * **PostgreSQL**: `BOOLEAN` * **SQL Server**: `BIT` * **SQLite**: `INTEGER` ### GUID/UUID Types [Section titled “GUID/UUID Types”](#guiduuid-types) * **PostgreSQL**: `UUID` * **SQL Server**: `UNIQUEIDENTIFIER` * **SQLite**: `TEXT` ### DateTime Types [Section titled “DateTime Types”](#datetime-types) * **PostgreSQL**: `TIMESTAMPTZ` * **SQL Server**: `DATETIMEOFFSET` * **SQLite**: `TEXT` ### Point Types [Section titled “Point Types”](#point-types) * **PostgreSQL**: `jsonb` * **SQL Server**: `nvarchar(max)` * **SQLite**: `text` > Note: Read more about Point in [this article](/arc/backend/entity-framework/point-conversion/) ### JSON Types [Section titled “JSON Types”](#json-types) * **PostgreSQL**: `jsonb` * **SQL Server**: `nvarchar(max)` * **SQLite**: `text` > Note: Read more about JSON in [this article](/arc/backend/entity-framework/json/) ## Usage Examples [Section titled “Usage Examples”](#usage-examples) ### Auto Incremental Primary Key [Section titled “Auto Incremental Primary Key”](#auto-incremental-primary-key) Auto incremental primary keys are very common but implemented in different ways with different annotations for them to actual auto increment. The `.AutoIncrementColumn()` extension method for the `ColumnsBuilder` gives you a way to configure it once. ```csharp [DbContext(typeof(EventLogDbContext))] [Migration($"EventLog_{nameof(v1_0_0)}")] public class v1_0_0 : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "EventLog", columns: table => new { SequenceNumber = table.AutoIncrementColumn(migrationBuilder), // Creates an auto increment column /* Other columns... */ }, constraints: table => table.PrimaryKey("PK_EventLog", x => x.SequenceNumber)); } } ``` ### String Columns with Length Limits [Section titled “String Columns with Length Limits”](#string-columns-with-length-limits) ```csharp columns: table => new { Name = table.StringColumn(migrationBuilder, maxLength: 100), // VARCHAR(100)/NVARCHAR(100) Description = table.StringColumn(migrationBuilder), // TEXT/NVARCHAR(MAX) } ``` ### Numeric Columns [Section titled “Numeric Columns”](#numeric-columns) ```csharp columns: table => new { Count = table.NumberColumn(migrationBuilder), // INTEGER/INT Price = table.NumberColumn(migrationBuilder), // DECIMAL Score = table.NumberColumn(migrationBuilder), // DOUBLE PRECISION/FLOAT/REAL } ``` ### Other Column Types [Section titled “Other Column Types”](#other-column-types) ```csharp columns: table => new { IsActive = table.BoolColumn(migrationBuilder), // BOOLEAN/BIT/INTEGER Id = table.GuidColumn(migrationBuilder, nullable: false), // UUID/UNIQUEIDENTIFIER/TEXT CreatedAt = table.DateTimeOffsetColumn(migrationBuilder), // TIMESTAMPTZ/DATETIMEOFFSET/TEXT Metadata = table.JsonColumn>(migrationBuilder), // jsonb/nvarchar(max)/text } ``` # ConceptAs Conversion The ConceptAs conversion feature provides automatic type conversion support for Cratis [Concepts](/arc/general/) in Entity Framework Core. This feature ensures that domain concepts are properly stored and retrieved from the database while maintaining type safety and domain integrity. ## What it does [Section titled “What it does”](#what-it-does) The ConceptAs conversion automatically configures Entity Framework Core to handle properties that are of `ConceptAs` type. When a concept property is encountered: 1. **Storage**: The underlying primitive value of the concept is stored in the database 2. **Retrieval**: The primitive value is automatically converted back to the concept instance when loaded from the database 3. **Comparison**: Proper value comparison is configured for change tracking and querying This seamless conversion allows you to use domain concepts in your entities without any manual configuration or boilerplate code. ## Why it’s important [Section titled “Why it’s important”](#why-its-important) Using ConceptAs conversion provides several key benefits: * **Domain Integrity**: Maintains strong typing and domain semantics throughout your application stack * **Automatic Configuration**: No need for manual value converter setup for each concept property * **Database Efficiency**: Stores only the primitive value, keeping database schema clean and performant * **Type Safety**: Prevents mixing of different concept types that share the same underlying primitive type * **Consistency**: Ensures all concept types are handled uniformly across your application ## Model Usage [Section titled “Model Usage”](#model-usage) Your entity models can use concepts directly without any special configuration: ```csharp using Cratis.Concepts; public class Customer { public CustomerId Id { get; set; } public CustomerName Name { get; set; } public EmailAddress Email { get; set; } } public class CustomerId(Guid value) : ConceptAs(value); public class CustomerName(string value) : ConceptAs(value); public class EmailAddress(string value) : ConceptAs(value); ``` The conversion will automatically: * Store `CustomerId` as a `Guid` in the database * Store `CustomerName` and `EmailAddress` as `string` values in the database * Convert back to the appropriate concept types when loading entities ## Manual Configuration [Section titled “Manual Configuration”](#manual-configuration) If you’re not using the [`BaseDbContext`](/arc/backend/entity-framework/base-db-context/), you can manually apply ConceptAs conversion in your `DbContext`: ```csharp using Cratis.Arc.EntityFrameworkCore.Concepts; public class StoreDbContext(DbContextOptions options) : DbContext(options) { public DbSet Customers { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { var entityTypes = modelBuilder.Model.GetEntityTypes(); modelBuilder.ApplyConceptAsConversion(entityTypes, Database.GetDatabaseType()); base.OnModelCreating(modelBuilder); } } ``` > Note: This is automatically configured for you when using the [`BaseDbContext`](/arc/backend/entity-framework/base-db-context/). ## How it works [Section titled “How it works”](#how-it-works) The conversion system uses reflection to: 1. Identify all properties in your entities that implement `ConceptAs` 2. Create appropriate `ValueConverter` instances that convert between the concept and its underlying primitive type 3. Configure `ValueComparer` instances for proper change tracking and equality comparisons 4. Apply these converters to the Entity Framework model builder The conversion is handled by the `ConceptAsConversion.ApplyConceptAsConversion()` extension method, which automatically discovers and configures all concept properties in your model. # Entity Mapping The Arc provides a clean, organized way to configure your Entity Framework Core entities through the `IEntityTypeConfiguration` interface from Microsoft.EntityFrameworkCore. This approach separates entity configuration from your DbContext, making your code more maintainable. ## Overview [Section titled “Overview”](#overview) Entity mapping in the Arc allows you to define how your entities are configured for Entity Framework Core in dedicated classes. These entity configurations are automatically discovered and applied when your DbContext is created, eliminating the need to override `OnModelCreating` in every DbContext that inherits from the [`BaseDbContext`](/arc/backend/entity-framework/base-db-context/). ## Creating Entity Configurations [Section titled “Creating Entity Configurations”](#creating-entity-configurations) To create an entity configuration, implement the `IEntityTypeConfiguration` interface for your entity type: ```csharp using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; public class UserConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { // Configure the primary key builder.HasKey(u => u.Id); // Configure properties builder.Property(u => u.Name) .HasMaxLength(100) .IsRequired(); builder.Property(u => u.Email) .HasMaxLength(255) .IsRequired(); // Configure indexes builder.HasIndex(u => u.Email) .IsUnique(); // Configure relationships builder.HasMany(u => u.Orders) .WithOne(o => o.User) .HasForeignKey(o => o.UserId); } } ``` ## Automatic Discovery and Registration [Section titled “Automatic Discovery and Registration”](#automatic-discovery-and-registration) Entity configurations are automatically discovered and registered when your application starts. The Arc: 1. **Discovers all implementations** of `IEntityTypeConfiguration` in your application 2. **Registers them with dependency injection** so they can have dependencies injected 3. **Applies them automatically** during `OnModelCreating` in your DbContext This happens automatically when you inherit from `BaseDbContext`: ```csharp public class MyDbContext : BaseDbContext { public MyDbContext(DbContextOptions options) : base(options) { } public DbSet Users { get; set; } public DbSet Orders { get; set; } // No need to override OnModelCreating - entity maps are applied automatically! } ``` ## Dependency Injection Support [Section titled “Dependency Injection Support”](#dependency-injection-support) Entity configurations support dependency injection, allowing you to inject services that might be needed for configuration: ```csharp public class UserConfiguration : IEntityTypeConfiguration { private readonly IConfiguration _configuration; public UserConfiguration(IConfiguration configuration) { _configuration = configuration; } public void Configure(EntityTypeBuilder builder) { builder.HasKey(u => u.Id); // Use injected configuration var maxNameLength = _configuration.GetValue("User:MaxNameLength", 100); builder.Property(u => u.Name) .HasMaxLength(maxNameLength) .IsRequired(); } } ``` ## Advanced Configuration Examples [Section titled “Advanced Configuration Examples”](#advanced-configuration-examples) ### Complex Property Configuration [Section titled “Complex Property Configuration”](#complex-property-configuration) ```csharp public class OrderConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.HasKey(o => o.Id); // Configure value objects builder.OwnsOne(o => o.ShippingAddress, address => { address.Property(a => a.Street).HasMaxLength(200); address.Property(a => a.City).HasMaxLength(100); address.Property(a => a.PostalCode).HasMaxLength(20); }); // Configure JSON columns builder.Property(o => o.Metadata) .HasJsonConversion(); // Configure computed columns builder.Property(o => o.TotalAmount) .HasComputedColumnSql("[Quantity] * [UnitPrice]"); } } ``` ### Multiple Entity Configurations [Section titled “Multiple Entity Configurations”](#multiple-entity-configurations) ```csharp public class ProductConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.HasKey(p => p.Id); builder.Property(p => p.Name) .HasMaxLength(200) .IsRequired(); builder.Property(p => p.Price) .HasPrecision(18, 2); // Table configuration builder.ToTable("Products", "Catalog"); // Soft delete configuration builder.HasQueryFilter(p => !p.IsDeleted); } } public class CategoryConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.HasKey(c => c.Id); builder.Property(c => c.Name) .HasMaxLength(100) .IsRequired(); // Self-referencing relationship builder.HasMany(c => c.Children) .WithOne(c => c.Parent) .HasForeignKey(c => c.ParentId); } } ``` ## Best Practices [Section titled “Best Practices”](#best-practices) ### Organization [Section titled “Organization”](#organization) * **One entity configuration per entity**: Keep each entity configuration focused on a single entity type * **Descriptive naming**: Use clear names like `UserConfiguration`, `OrderConfiguration`, etc. * **Logical grouping**: Place entity configurations in a dedicated folder (e.g., `Configuration/` or `Mapping/`) ### Configuration Guidelines [Section titled “Configuration Guidelines”](#configuration-guidelines) * **Configure all aspects**: Include keys, properties, relationships, and constraints * **Be explicit**: Don’t rely on conventions for critical configurations * **Use meaningful names**: Configure table and column names explicitly when needed * **Document complex logic**: Add comments for complex mapping logic ### Performance Considerations [Section titled “Performance Considerations”](#performance-considerations) * **Index configuration**: Configure indexes for frequently queried properties * **Query filters**: Use global query filters for soft delete patterns * **Relationship loading**: Configure loading behavior for relationships ## Integration with BaseDbContext [Section titled “Integration with BaseDbContext”](#integration-with-basedbcontext) When you inherit from `BaseDbContext`, entity configurations are automatically applied during model creation. The base class: 1. Discovers all `DbSet` properties on your DbContext 2. Looks for corresponding `IEntityTypeConfiguration` implementations 3. Applies the configurations during `OnModelCreating` This means you don’t need to manually register or apply entity configurations - they work automatically once you implement the interface. ## Migration Support [Section titled “Migration Support”](#migration-support) Entity configurations work seamlessly with Entity Framework Core migrations. When you add or modify entity configurations: 1. Run `dotnet ef migrations add ` to create a migration 2. The migration will include all changes from your entity configurations 3. Run `dotnet ef database update` to apply the changes The automatic discovery and application of entity configurations ensures that all your configurations are included in migrations without additional setup. # Getting Started with Entity Framework Core Arc provides two approaches for integrating Entity Framework Core into your application: 1. **Builder Pattern** (`WithEntityFrameworkCore`) - Recommended when using Arc’s `IArcBuilder` with full auto-discovery and observation support 2. **Direct Registration** - Flexible approach that works independently of Arc ## Builder Pattern (Recommended) [Section titled “Builder Pattern (Recommended)”](#builder-pattern-recommended) When using Arc’s application framework, the `WithEntityFrameworkCore()` extension method provides the most streamlined setup with automatic DbContext discovery and observation support. ### Basic Setup [Section titled “Basic Setup”](#basic-setup) ```csharp builder.AddCratisArc(configureBuilder: arcBuilder => { arcBuilder.WithEntityFrameworkCore(options => { options.ConnectionString = "Server=localhost;Database=MyDb;Trusted_Connection=true"; }); }); ``` This single configuration: * **Automatically discovers** all DbContext types inheriting from `BaseDbContext` or `ReadOnlyDbContext` * **Registers them** with the connection string using appropriate patterns (read-only vs read-write) * **Enables observation** support for real-time change notifications * **Applies** all Arc conventions (ConceptAs support, entity mapping, etc.) ### Configuration Options [Section titled “Configuration Options”](#configuration-options) The `EntityFrameworkCoreOptions` class provides the following configuration: | Property | Type | Default | Description | | ------------------------ | ---------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `ConnectionString` | `string` | `""` | The database connection string. Required for auto-discovery. | | `AutoDiscoverDbContexts` | `bool` | `true` | Whether to automatically discover and register DbContext types. | | `JsonConverters` | `IList` | `[]` | Additional `System.Text.Json` converters merged into `JsonConversionOptions` at startup. Use this to handle interface-typed or abstract `[Json]` properties. See [JSON Conversion](/arc/backend/entity-framework/json/#registering-custom-converters). | ### Auto-Discovery [Section titled “Auto-Discovery”](#auto-discovery) When `AutoDiscoverDbContexts` is enabled (default), the framework automatically: 1. Scans for all types inheriting from `BaseDbContext` 2. Identifies which ones are `ReadOnlyDbContext` subtypes 3. Registers read-only contexts with `AddReadOnlyDbContextWithConnectionString` 4. Registers read-write contexts with `AddDbContextWithConnectionString` 5. Excludes any types marked with `[IgnoreAutoRegistration]` ```csharp // Your DbContext types - automatically discovered and registered public class OrdersDbContext : BaseDbContext { public OrdersDbContext(DbContextOptions options) : base(options) { } public DbSet Orders { get; set; } } public class ReportingDbContext : ReadOnlyDbContext { public ReportingDbContext(DbContextOptions options) : base(options) { } public DbSet OrderSummaries { get; set; } } ``` ### Disabling Auto-Discovery [Section titled “Disabling Auto-Discovery”](#disabling-auto-discovery) If you need manual control over DbContext registration, disable auto-discovery: ```csharp builder.AddCratisArc(configureBuilder: arcBuilder => { arcBuilder.WithEntityFrameworkCore( configureOptions: options => { options.ConnectionString = "Server=localhost;Database=MyDb;Trusted_Connection=true"; options.AutoDiscoverDbContexts = false; }, configureEfCore: efBuilder => { // Manual registration with custom options efBuilder.AddDbContext((sp, opts) => { opts.EnableSensitiveDataLogging(); }); }); }); ``` ### Using the Builder for Manual Registration [Section titled “Using the Builder for Manual Registration”](#using-the-builder-for-manual-registration) The `IEntityFrameworkCoreBuilder` provides methods for manual DbContext registration: ```csharp arcBuilder.WithEntityFrameworkCore( configureOptions: options => { options.ConnectionString = "Server=localhost;Database=MyDb;Trusted_Connection=true"; options.AutoDiscoverDbContexts = false; }, configureEfCore: efBuilder => { // Use connection string from options efBuilder.AddDbContext(); // Or specify a different connection string efBuilder.AddDbContext("Server=archive;Database=Archive;Trusted_Connection=true"); }); ``` ### Excluding Types from Auto-Discovery [Section titled “Excluding Types from Auto-Discovery”](#excluding-types-from-auto-discovery) Use the `[IgnoreAutoRegistration]` attribute to exclude specific DbContext types: ```csharp [IgnoreAutoRegistration] public class TestDbContext : BaseDbContext { // This context won't be auto-registered } ``` ## Direct Registration (Flexible Approach) [Section titled “Direct Registration (Flexible Approach)”](#direct-registration-flexible-approach) If you’re not using Arc’s builder pattern or need more flexibility, you can register Entity Framework Core support directly on `IServiceCollection`. This approach doesn’t require the full Arc framework. ### Basic Direct Registration [Section titled “Basic Direct Registration”](#basic-direct-registration) ```csharp // Add observation services (optional, but recommended) services.AddEntityFrameworkCoreObservation(); // Register your DbContext with connection string services.AddDbContextWithConnectionString( "Server=localhost;Database=MyDb;Trusted_Connection=true"); ``` ### Direct Registration with Options [Section titled “Direct Registration with Options”](#direct-registration-with-options) ```csharp services.AddDbContextWithConnectionString( "Server=localhost;Database=MyDb;Trusted_Connection=true", (serviceProvider, options) => { options.EnableSensitiveDataLogging(); options.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); }); ``` ### Read-Only DbContext Registration [Section titled “Read-Only DbContext Registration”](#read-only-dbcontext-registration) ```csharp services.AddReadOnlyDbContextWithConnectionString( "Server=localhost;Database=MyDb;Trusted_Connection=true"); ``` ### Assembly-Based Discovery [Section titled “Assembly-Based Discovery”](#assembly-based-discovery) For direct registration with assembly scanning: ```csharp // Discover and register all ReadOnlyDbContext types from assemblies services.AddReadModelDbContextsWithConnectionStringFromAssemblies( "Server=localhost;Database=MyDb;Trusted_Connection=true", optionsAction: null, typeof(Program).Assembly, typeof(OrdersDbContext).Assembly); ``` ## Choosing the Right Approach [Section titled “Choosing the Right Approach”](#choosing-the-right-approach) | Feature | Builder Pattern | Direct Registration | | ------------------- | --------------- | ------------------------ | | Auto-discovery | ✅ Built-in | ⚠️ Assembly-based only | | Observation support | ✅ Automatic | ⚠️ Manual setup required | | Arc integration | ✅ Full | ❌ Not required | | Flexibility | Good | Maximum | | Configuration | Centralized | Distributed | **Use Builder Pattern when:** * You’re using Arc’s `IArcBuilder` pattern * You want automatic DbContext discovery * You want observation support without extra configuration * You prefer centralized configuration **Use Direct Registration when:** * You’re not using the full Arc framework * You need maximum flexibility in registration * You’re integrating with an existing application * You want fine-grained control over each DbContext ## Important: Registration Order [Section titled “Important: Registration Order”](#important-registration-order) When using direct registration with observation support, ensure you register observation services **before** calling `AddCratisArc()`: ```csharp // Register observation services first services.AddEntityFrameworkCoreObservation(); // Then register your DbContexts services.AddDbContextWithConnectionString(connectionString); // Finally, add Arc builder.AddCratisArc(); ``` This ensures the singleton `IEntityChangeTracker` is properly shared across all interceptors. > **Note**: When using `WithEntityFrameworkCore()`, this ordering is handled automatically. ## Next Steps [Section titled “Next Steps”](#next-steps) * [Base DbContext](/arc/backend/entity-framework/base-db-context/) - Learn about the base DbContext class * [Read Only DbContexts](/arc/backend/entity-framework/read-only/) - Implement read-only contexts for queries * [Observing DbSet](/arc/backend/entity-framework/observing/) - Create reactive queries with real-time updates * [Entity Mapping](/arc/backend/entity-framework/entity-mapping/) - Configure entities using clean patterns # GUID Conversion The GUID conversion feature provides automatic handling of `Guid` properties in Entity Framework Core, ensuring consistent storage and optimal database compatibility across different database providers. ## What it does [Section titled “What it does”](#what-it-does) The GUID conversion automatically configures Entity Framework Core to handle properties of type `Guid` using the most appropriate database representation for each provider: 1. **PostgreSQL**: Stores as native `uuid` type for optimal performance and storage efficiency 2. **SQL Server**: Stores as `uniqueidentifier` type with proper formatting 3. **SQLite**: Stores as `CHAR(36)` with proper string formatting 4. **Other providers**: Uses provider-specific optimizations when available This automatic configuration ensures that GUIDs are stored in the most efficient format for each database while maintaining compatibility and performance. ## Why it’s important [Section titled “Why it’s important”](#why-its-important) Using GUID conversion provides several key benefits: * **Cross-Database Compatibility**: Consistent GUID handling across different database providers * **Performance Optimization**: Uses native GUID types when available for better query performance * **Storage Efficiency**: Optimizes storage format for each database provider * **Automatic Configuration**: No need for manual configuration of GUID properties * **Index Performance**: Ensures GUIDs are stored in formats that support efficient indexing ## Model Usage [Section titled “Model Usage”](#model-usage) Your entity models can use `Guid` properties directly without any special configuration: ```csharp public class Customer { public Guid Id { get; set; } public Guid TenantId { get; set; } public string Name { get; set; } public DateTime CreatedAt { get; set; } } public class Order { public Guid Id { get; set; } public Guid CustomerId { get; set; } public Guid ProductId { get; set; } public decimal Amount { get; set; } } ``` The conversion will automatically: * Configure all `Guid` properties to use the optimal storage format for your database provider * Ensure proper indexing capabilities for GUID-based primary and foreign keys * Handle conversion between .NET `Guid` instances and database-specific representations ## Manual Configuration [Section titled “Manual Configuration”](#manual-configuration) If you’re not using the [`BaseDbContext`](/arc/backend/entity-framework/base-db-context/), you can manually apply GUID conversion in your `DbContext`: ```csharp using Cratis.Arc.EntityFrameworkCore; public class StoreDbContext(DbContextOptions options) : DbContext(options) { public DbSet Customers { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { var entityTypes = modelBuilder.Model.GetEntityTypes(); modelBuilder.ApplyGuidConversion(entityTypes, Database.GetDatabaseType()); base.OnModelCreating(modelBuilder); } } ``` > Note: This is automatically configured for you when using the [`BaseDbContext`](/arc/backend/entity-framework/base-db-context/). ## How it works [Section titled “How it works”](#how-it-works) The conversion system uses reflection to: 1. Identify all properties in your entities that are of type `Guid` 2. Apply the `.AsGuid()` extension method to configure optimal storage for the current database provider 3. Ensure proper value conversion and comparison for change tracking The conversion is handled by the `GuidConversion.ApplyGuidConversion()` extension method, which automatically discovers and configures all GUID properties in your model. ## Database Provider Specifics [Section titled “Database Provider Specifics”](#database-provider-specifics) The GUID conversion adapts to different database providers: * **PostgreSQL (Npgsql)**: Uses native `uuid` type for optimal performance * **SQL Server**: Uses `uniqueidentifier` with proper collation settings * **SQLite**: Uses `CHAR(36)` with hyphenated string format * **MySQL/MariaDB**: Uses `CHAR(36)` with appropriate character set * **Oracle**: Uses `RAW(16)` for binary storage efficiency This provider-specific optimization ensures the best performance and storage characteristics for your chosen database. # JSON Conversion The JSON conversion feature provides automatic serialization and deserialization of complex objects to JSON format in the database. This cross-database approach allows you to store rich data structures while maintaining compatibility across different database providers. ## What it does [Section titled “What it does”](#what-it-does) The JSON conversion automatically configures Entity Framework Core to handle properties marked with the `[Json]` attribute: 1. **Serialization**: Complex objects are automatically serialized to JSON when saving to the database 2. **Deserialization**: JSON data is automatically converted back to objects when loading from the database 3. **Cross-Provider Compatibility**: Uses the most appropriate JSON storage method for each database provider 4. **Concept Support**: Includes automatic support for Cratis concepts through the integrated converter factory This automatic configuration ensures that complex data structures are properly stored and retrieved while maintaining type safety and database compatibility. ## Why it’s important [Section titled “Why it’s important”](#why-its-important) Using JSON conversion provides several key benefits: * **Rich Data Storage**: Store complex objects and collections without creating separate tables * **Schema Flexibility**: Easily evolve object structures without complex database migrations * **Cross-Database Compatibility**: Consistent JSON handling across different database providers * **Performance**: Reduces the need for complex joins when loading related data * **Type Safety**: Maintains strong typing while providing flexible storage ## Model Usage [Section titled “Model Usage”](#model-usage) Your entity models can use complex properties by marking them with the `[Json]` attribute: ```csharp using Cratis.Arc.EntityFrameworkCore.Json; public class Customer { public Guid Id { get; set; } public string Name { get; set; } [Json] public IEnumerable
Addresses { get; set; } [Json] public CustomerPreferences Preferences { get; set; } } public class Address { public string Street { get; set; } public string City { get; set; } public string Country { get; set; } } ``` The conversion will automatically: * Store collections and complex objects as JSON in the database * Handle serialization of nested objects and collections * Support Cratis concepts within the JSON data * Maintain type safety when loading the data back ## Default Converters [Section titled “Default Converters”](#default-converters) `JsonConversionOptions` is pre-populated with all Arc default JSON converters so that common types work out of the box without any extra configuration: | Converter | Handles | | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | `ConceptAsJsonConverterFactory` | Cratis `ConceptAs` value types | | `EnumerableConceptAsJsonConverterFactory` | `IEnumerable>` sequences | | `EnumConverterFactory` | Enum values (serialised as integers) | | `DateOnlyJsonConverter` | `System.DateOnly` | | `TimeOnlyJsonConverter` | `System.TimeOnly` | | `TypeJsonConverter` | `System.Type` | | `UriJsonConverter` | `System.Uri` | | `EnumerableModelWithIdToConceptOrPrimitiveEnumerableConverterFactory` | Enumerable model-with-id to concept/primitive | | `DerivedTypeJsonConverterFactory` | Polymorphic types registered via `IDerivedTypes` (interfaces, abstract base classes) | ## Registering Custom Converters [Section titled “Registering Custom Converters”](#registering-custom-converters) Some property types require a custom `JsonConverter` to round-trip correctly — for example, interface types, abstract base classes, or discriminated unions. Without a matching converter, `JsonSerializer` throws `NotSupportedException: Deserialization of interface or abstract types is not supported`. Register additional converters at startup through the `JsonConverters` list on `EntityFrameworkCoreOptions`. The converters are appended after the built-in Arc defaults: ```csharp builder.AddCratisArc(configureBuilder: arcBuilder => { arcBuilder.WithEntityFrameworkCore(options => { options.ConnectionString = "..."; options.JsonConverters.Add(new MyRequestConverter()); }); }); ``` With the converter registered, an entity with an interface-typed `[Json]` property works correctly: ```csharp public interface IMyRequest { int Count { get; } } public sealed record DoThing(int Count) : IMyRequest; public class MyEntity { [Key] public Guid Id { get; set; } [Json] public IMyRequest Request { get; set; } = default!; // round-trips correctly } ``` ### Writing a Custom Converter [Section titled “Writing a Custom Converter”](#writing-a-custom-converter) Implement `JsonConverter` where `T` is the interface or abstract type that needs to be handled: ```csharp using System.Text.Json; using System.Text.Json.Serialization; public class MyRequestConverter : JsonConverter { public override IMyRequest? Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { using var doc = JsonDocument.ParseValue(ref reader); // Use a discriminator or well-known property to pick the concrete type var count = doc.RootElement.GetProperty("count").GetInt32(); return new DoThing(count); } public override void Write( Utf8JsonWriter writer, IMyRequest value, JsonSerializerOptions options) { writer.WriteStartObject(); writer.WriteNumber("count", value.Count); writer.WriteEndObject(); } } ``` ## Manual Configuration [Section titled “Manual Configuration”](#manual-configuration) If you’re not using the [`BaseDbContext`](/arc/backend/entity-framework/base-db-context/), you can manually apply JSON conversion in your `DbContext`: ```csharp using Cratis.Arc.EntityFrameworkCore.Json; public class StoreDbContext(DbContextOptions options) : DbContext(options) { public DbSet Customers { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { var entityTypes = modelBuilder.Model.GetEntityTypes(); modelBuilder.ApplyJsonConversion(entityTypes, Database.GetDatabaseType()); base.OnModelCreating(modelBuilder); } } ``` To include custom converters in a manual setup, pass a `JsonConversionOptions` instance directly: ```csharp protected override void OnModelCreating(ModelBuilder modelBuilder) { var jsonOptions = new JsonConversionOptions(); jsonOptions.JsonSerializerOptions.Converters.Add(new MyRequestConverter()); var entityTypes = modelBuilder.Model.GetEntityTypes(); modelBuilder.ApplyJsonConversion(entityTypes, Database.GetDatabaseType(), jsonOptions); base.OnModelCreating(modelBuilder); } ``` > Note: When using [`BaseDbContext`](/arc/backend/entity-framework/base-db-context/) with `WithEntityFrameworkCore`, the `JsonConversionOptions` singleton is resolved automatically from DI — you only need to populate `EntityFrameworkCoreOptions.JsonConverters` at startup. ## How it works [Section titled “How it works”](#how-it-works) The conversion system uses reflection to: 1. Identify all properties in your entities that are marked with the `[Json]` attribute 2. Create appropriate value converters that handle serialization and deserialization 3. Configure value comparers for proper change tracking 4. Apply these converters to the Entity Framework model builder The conversion is handled by the `JsonConversion.ApplyJsonConversion()` extension method, which automatically discovers and configures all JSON properties in your model. # LineString Conversion The LineString conversion feature provides automatic handling of `LineString` geospatial properties from Cratis.Fundamentals in Entity Framework Core, enabling storage of routes, paths, and other linear geographic features. ## What it does [Section titled “What it does”](#what-it-does) The LineString conversion automatically configures Entity Framework Core to handle properties of type `LineString` using JSON serialization: 1. **PostgreSQL**: Stores as `jsonb` type for efficient JSON queries 2. **SQL Server**: Stores as `nvarchar(max)` with JSON serialization 3. **SQLite**: Stores as `text` with JSON serialization This ensures LineStrings (ordered sequences of geographic points) are stored consistently across all database providers. ## Why it’s important [Section titled “Why it’s important”](#why-its-important) Using LineString conversion provides several key benefits: * **Route and Path Support**: Store delivery routes, walking paths, or any linear geographic features * **Cross-Database Compatibility**: Consistent LineString handling across different database providers * **JSON Serialization**: Uses standard JSON format for storage, making data human-readable * **Type Safety**: Maintains strong typing with the `LineString` type from Cratis.Fundamentals * **Automatic Configuration**: No need for manual configuration of LineString properties ## The LineString Type [Section titled “The LineString Type”](#the-linestring-type) The `LineString` type represents an ordered sequence of connected geographic points: ```csharp using Cratis.Geospatial; var points = new Point[] { new Point(longitude: -122.4194, latitude: 37.7749), new Point(longitude: -122.4185, latitude: 37.7750), new Point(longitude: -122.4170, latitude: 37.7755) }; var route = new LineString(points); ``` The LineString is serialized to JSON as: ```json { "coordinates": [ [-122.4194, 37.7749], [-122.4185, 37.7750], [-122.4170, 37.7755] ] } ``` ## Model Usage [Section titled “Model Usage”](#model-usage) Your entity models can use `LineString` properties directly: ```csharp using Cratis.Geospatial; public class DeliveryRoute { public Guid Id { get; set; } public string RouteName { get; set; } public LineString Path { get; set; } public DateTime CreatedAt { get; set; } } public class UserTrack { public Guid Id { get; set; } public Guid UserId { get; set; } public LineString TrackingPath { get; set; } public LineString? HistoricalPath { get; set; } // Nullable LineString } ``` The conversion will automatically: * Configure all `LineString` properties to use JSON serialization * Store the ordered points as a JSON array in the database * Handle conversion between .NET `LineString` instances and JSON strings ## Manual Configuration [Section titled “Manual Configuration”](#manual-configuration) If you’re not using the [`BaseDbContext`](/arc/backend/entity-framework/base-db-context/), you can manually apply LineString conversion: ```csharp using Cratis.Arc.EntityFrameworkCore; using Cratis.Geospatial; public class RouteDbContext(DbContextOptions options) : DbContext(options) { public DbSet Routes { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => { entity.Property(e => e.Path) .AsLineString(); }); base.OnModelCreating(modelBuilder); } } ``` > Note: This is automatically configured for you when using the [`BaseDbContext`](/arc/backend/entity-framework/base-db-context/). ## Migration Usage [Section titled “Migration Usage”](#migration-usage) When creating migrations, use the `LineStringColumn()` extension method: ### Creating a Table with LineString Column [Section titled “Creating a Table with LineString Column”](#creating-a-table-with-linestring-column) ```csharp [DbContext(typeof(RouteDbContext))] [Migration($"Routes_{nameof(v1_0_0)}")] public class v1_0_0 : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "DeliveryRoutes", columns: table => new { Id = table.GuidColumn(migrationBuilder), RouteName = table.StringColumn(migrationBuilder, maxLength: 200, nullable: false), Path = table.LineStringColumn(migrationBuilder, nullable: false), CreatedAt = table.DateTimeOffsetColumn(migrationBuilder, nullable: false) }, constraints: table => table.PrimaryKey("PK_Routes", x => x.Id)); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable(name: "DeliveryRoutes"); } } ``` ### Adding a LineString Column to Existing Table [Section titled “Adding a LineString Column to Existing Table”](#adding-a-linestring-column-to-existing-table) ```csharp [DbContext(typeof(RouteDbContext))] [Migration($"Routes_{nameof(v1_1_0)}")] public class v1_1_0 : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddLineStringColumn( name: "OptimizedPath", table: "DeliveryRoutes", nullable: true); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropColumn( name: "OptimizedPath", table: "DeliveryRoutes"); } } ``` > See [Common Column Types](/arc/backend/entity-framework/common-column-types/) for more information about column type extensions. ## How it works [Section titled “How it works”](#how-it-works) The conversion system uses a `ValueConverter` that: 1. **Serializes**: Converts `LineString` instances to JSON strings using `System.Text.Json` 2. **Deserializes**: Parses JSON strings back to `LineString` instances when reading from database 3. **Null handling**: Properly handles nullable `LineString?` properties 4. **Validation**: Ensures LineStrings contain at least 2 points The conversion is handled by the `AsLineString()` extension method, which configures the property with the appropriate `ValueConverter`. ## Database Provider Specifics [Section titled “Database Provider Specifics”](#database-provider-specifics) The LineString conversion adapts to different database providers: * **PostgreSQL (Npgsql)**: Uses `jsonb` type for efficient JSON queries * **SQL Server**: Uses `nvarchar(max)` with JSON string storage * **SQLite**: Uses `text` with JSON string storage ## Querying Considerations [Section titled “Querying Considerations”](#querying-considerations) When querying LineString data: ### All Providers [Section titled “All Providers”](#all-providers) For general LineString comparison: ```csharp // Exact match var routes = await context.Routes .Where(r => r.Path == targetPath) .ToListAsync(); // Null checks var routesWithPath = await context.Routes .Where(r => r.Path != null) .ToListAsync(); ``` > Note: For advanced geospatial queries (distance calculations along path, intersection detection, etc.), consider using database-specific extensions or computing results in application code. ## Related Topics [Section titled “Related Topics”](#related-topics) * [Common Column Types](/arc/backend/entity-framework/common-column-types/) - Column type extensions * [Property Extensions](/arc/backend/entity-framework/property-extensions/) - AsLineString() and other configuration methods * [Polygon Conversion](/arc/backend/entity-framework/polygon-conversion/) - Working with geographic areas * [Point Conversion](/arc/backend/entity-framework/point-conversion/) - Working with individual points * [JSON Conversion](/arc/backend/entity-framework/json/) - General JSON serialization support # Adding Columns to Existing Tables in Migrations When you need to add columns to existing tables in Entity Framework Core migrations, Cratis Arc provides database-agnostic extension methods that work seamlessly across PostgreSQL, SQL Server, and SQLite. These extension methods are similar to the [Common Column Types](/arc/backend/entity-framework/common-column-types/) used when creating tables, but are specifically designed for `AddColumn` operations in migrations. ## Available AddColumn Extension Methods [Section titled “Available AddColumn Extension Methods”](#available-addcolumn-extension-methods) The following table shows all the add column extension methods available in Cratis Arc: | Extension Method | Description | Supported Types | Parameters | | --------------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `AddStringColumn()` | Adds a string column with appropriate database-specific type (VARCHAR/NVARCHAR/TEXT) | string | `name`, `table`, `maxLength` (int?, optional), `nullable` (bool, default: true), `defaultValue` (string?, optional), `schema` (string?, optional) | | `AddNumberColumn()` | Adds a numeric column with appropriate database-specific type for any numeric type | char, byte, sbyte, short, ushort, int, uint, long, ulong, float, double, decimal | `name`, `table`, `nullable` (bool, default: true), `defaultValue` (object?, optional), `schema` (string?, optional) | | `AddBoolColumn()` | Adds a boolean column with appropriate database-specific type (BOOLEAN/BIT/INTEGER) | bool | `name`, `table`, `nullable` (bool, default: true), `defaultValue` (bool, default: false), `schema` (string?, optional) | | `AddAutoIncrementColumn()` | Adds an auto-incrementing integer column with appropriate database-specific annotations | int | `name`, `table`, `schema` (string?, optional) | | `AddGuidColumn()` | Adds a GUID/UUID column with appropriate database-specific type (UUID/UNIQUEIDENTIFIER/BLOB) | Guid | `name`, `table`, `nullable` (bool, default: true), `schema` (string?, optional) | | `AddDateTimeOffsetColumn()` | Adds a DateTimeOffset column with appropriate database-specific type (TIMESTAMPTZ/DATETIMEOFFSET/TEXT) | DateTimeOffset | `name`, `table`, `nullable` (bool, default: true), `schema` (string?, optional) | ## Database-Specific Type Mappings [Section titled “Database-Specific Type Mappings”](#database-specific-type-mappings) The extension methods automatically select the appropriate SQL type based on the database provider. See [Common Column Types](/arc/backend/entity-framework/common-column-types/) for detailed type mappings for each database. ## Usage Examples [Section titled “Usage Examples”](#usage-examples) ### Adding String Columns [Section titled “Adding String Columns”](#adding-string-columns) ```csharp [DbContext(typeof(MyDbContext))] [Migration($"MyContext_{nameof(v1_1_0)}")] public class v1_1_0 : Migration { protected override void Up(MigrationBuilder migrationBuilder) { // Add string column with length limit migrationBuilder.AddStringColumn( name: "Email", table: "Users", maxLength: 255, nullable: false); // Add unlimited text column with default value migrationBuilder.AddStringColumn( name: "Description", table: "Products", defaultValue: "No description provided"); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropColumn(name: "Email", table: "Users"); migrationBuilder.DropColumn(name: "Description", table: "Products"); } } ``` ### Adding Numeric Columns [Section titled “Adding Numeric Columns”](#adding-numeric-columns) ```csharp protected override void Up(MigrationBuilder migrationBuilder) { // Add integer column migrationBuilder.AddNumberColumn( name: "ViewCount", table: "Articles", defaultValue: 0, nullable: false); // Add decimal column for currency migrationBuilder.AddNumberColumn( name: "Price", table: "Products", nullable: true); // Add double column for ratings migrationBuilder.AddNumberColumn( name: "Rating", table: "Reviews", defaultValue: 0.0, nullable: false); } ``` ### Adding Boolean Columns [Section titled “Adding Boolean Columns”](#adding-boolean-columns) ```csharp protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddBoolColumn( name: "IsActive", table: "Users", defaultValue: true, nullable: false); migrationBuilder.AddBoolColumn( name: "IsDeleted", table: "Orders", defaultValue: false); } ``` ### Adding GUID Columns [Section titled “Adding GUID Columns”](#adding-guid-columns) ```csharp protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddGuidColumn( name: "ExternalId", table: "Users", nullable: false); migrationBuilder.AddGuidColumn( name: "CorrelationId", table: "Events", nullable: true); } ``` ### Adding DateTime Columns [Section titled “Adding DateTime Columns”](#adding-datetime-columns) ```csharp protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddDateTimeOffsetColumn( name: "LastLoginAt", table: "Users", nullable: true); migrationBuilder.AddDateTimeOffsetColumn( name: "CreatedAt", table: "Orders", nullable: false); } ``` ### Adding Auto-Increment Columns [Section titled “Adding Auto-Increment Columns”](#adding-auto-increment-columns) ```csharp protected override void Up(MigrationBuilder migrationBuilder) { // Add auto-increment column (useful for adding sequence numbers to existing tables) migrationBuilder.AddAutoIncrementColumn( name: "SequenceNumber", table: "AuditLog"); } ``` ### Using Schemas [Section titled “Using Schemas”](#using-schemas) For databases that support schemas (PostgreSQL, SQL Server), you can specify the schema: ```csharp protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddStringColumn( name: "Department", table: "Employees", schema: "hr", maxLength: 100, nullable: false); } ``` ## Benefits [Section titled “Benefits”](#benefits) Using these extension methods provides several advantages: * **Database Portability**: Write migrations once and support multiple database providers * **Type Safety**: Strongly typed methods reduce errors * **Consistency**: Ensures consistent type mapping across your application * **Simplified Syntax**: Less boilerplate code compared to manually specifying types * **Maintainability**: Centralized type mapping logic makes updates easier ## See Also [Section titled “See Also”](#see-also) * [Common Column Types](/arc/backend/entity-framework/common-column-types/) - For creating columns when defining new tables * [Base DbContext](/arc/backend/entity-framework/base-db-context/) - Learn about the base DbContext implementation * [Automatic Database Hookup](/arc/backend/entity-framework/automatic-database-hookup/) - Automatic database configuration # Observing DbSet<> Entity Framework Core observation support allows you to monitor changes to your entities in real-time using reactive extensions. This feature enables you to create observable queries that automatically update when data changes, either through your application or external database modifications. ## Configuration [Section titled “Configuration”](#configuration) To enable observation support, you only need to register the observation services in your service collection. ### Register Observation Services [Section titled “Register Observation Services”](#register-observation-services) Add observation services to your service collection: ```csharp services.AddEntityFrameworkCoreObservation(); ``` This registers the necessary services for tracking entity changes and database-level notifications. If your DbContext inherits from `BaseDbContext` and is registered using the Arc extension methods (`AddDbContextWithConnectionString` or `AddReadOnlyDbContext`), observation support is automatically enabled when the services are registered. No additional configuration is needed. ### Manual Configuration (Advanced) [Section titled “Manual Configuration (Advanced)”](#manual-configuration-advanced) If you’re not using `BaseDbContext` or the Arc registration methods, you can manually add observation support at registration time: ```csharp services.AddPooledDbContextFactory((serviceProvider, options) => { options.UseSqlServer(connectionString) .AddObservation(serviceProvider); }); services.AddScoped(serviceProvider => { var factory = serviceProvider.GetRequiredService>(); return factory.CreateDbContext(); }); ``` > **Important**: When using pooled DbContext factories (`AddPooledDbContextFactory`), all configuration must be done at registration time. You cannot modify options in `OnConfiguring` when pooling is enabled. ## Usage [Section titled “Usage”](#usage) Once configured, you can create observable queries using extension methods on `DbSet`. ### Observe a Collection [Section titled “Observe a Collection”](#observe-a-collection) Monitor changes to a collection of entities: ```csharp var observable = dbContext.MyEntities.Observe(); observable.Subscribe(entities => { // Handle updated collection Console.WriteLine($"Collection updated: {entities.Count()} items"); }); ``` ### Observe with Filter [Section titled “Observe with Filter”](#observe-with-filter) Apply filters to observe specific entities: ```csharp var observable = dbContext.Orders.Observe(order => order.Status == OrderStatus.Pending); observable.Subscribe(pendingOrders => { // Handle updates to pending orders only }); ``` ### Observe a Single Entity [Section titled “Observe a Single Entity”](#observe-a-single-entity) Monitor changes to a specific entity: ```csharp var observable = dbContext.Products.ObserveSingle(p => p.Sku == "ABC123"); observable.Subscribe(product => { // Handle updates to the specific product }); ``` ### Observe by Id [Section titled “Observe by Id”](#observe-by-id) Monitor a single entity using its identifier: ```csharp var observable = dbContext.Customers.ObserveById(customerId); observable.Subscribe(customer => { // Handle updates to the customer }); ``` ## How It Works [Section titled “How It Works”](#how-it-works) The observation feature combines two notification mechanisms: 1. **In-Process Changes**: Changes made through your application’s `DbContext` are tracked via EF Core interceptors 2. **Database-Level Changes**: External changes are detected using database-specific notification mechanisms: * SQL Server: Uses `SqlDependency` or polling * PostgreSQL: Uses `LISTEN/NOTIFY` * SQLite: Uses polling When any change is detected, the observable query is re-executed and subscribers are notified with the updated results. ## Change Detection [Section titled “Change Detection”](#change-detection) The observation system detects changes when: * Entities are added, modified, or deleted through `SaveChanges()` or `SaveChangesAsync()` * External processes modify the database (via database-level notifications) * Changes match the filter criteria of your observable query ## Best Practices [Section titled “Best Practices”](#best-practices) * Use filters to limit the scope of observations and improve performance * Dispose of subscriptions when no longer needed to prevent memory leaks * Consider using `ObserveSingle` or `ObserveById` when monitoring individual entities * Be mindful of database notification limits and capabilities for your specific database provider ## Example: Real-Time Dashboard [Section titled “Example: Real-Time Dashboard”](#example-real-time-dashboard) ```csharp public class OrderDashboard { private readonly IDisposable _subscription; public OrderDashboard(MyDbContext dbContext) { _subscription = dbContext.Orders .Observe(o => o.Status == OrderStatus.Processing) .Subscribe(processingOrders => { UpdateDashboard(processingOrders); }); } public void Dispose() { _subscription?.Dispose(); } private void UpdateDashboard(IEnumerable orders) { // Update UI or metrics } } ``` # Point Conversion The Point conversion feature provides automatic handling of `Point` geospatial properties from Cratis.Fundamentals in Entity Framework Core, ensuring consistent storage and optimal database compatibility across different database providers. ## What it does [Section titled “What it does”](#what-it-does) The Point conversion automatically configures Entity Framework Core to handle properties of type `Point` using the most appropriate database representation for each provider: 1. **PostgreSQL**: Stores as `jsonb` type for efficient JSON queries and optimal storage 2. **SQL Server**: Stores as `nvarchar(max)` with JSON serialization 3. **SQLite**: Stores as `text` with JSON serialization This automatic configuration ensures that Points (longitude/latitude pairs) are stored in a consistent format across all database providers while maintaining compatibility and optimal performance for each database. ## Why it’s important [Section titled “Why it’s important”](#why-its-important) Using Point conversion provides several key benefits: * **Geospatial Support**: Built-in support for storing location data (longitude/latitude pairs) * **Cross-Database Compatibility**: Consistent Point handling across different database providers * **JSON Serialization**: Uses standard JSON format for storage, making data human-readable and queryable * **Type Safety**: Maintains strong typing with the `Point` type from Cratis.Fundamentals * **Automatic Configuration**: No need for manual configuration of Point properties ## The Point Type [Section titled “The Point Type”](#the-point-type) The `Point` type from Cratis.Fundamentals represents a geographic point with longitude and latitude: ```csharp using Cratis.Geospatial; var point = new Point(longitude: -122.4194, latitude: 37.7749); // San Francisco ``` The point is serialized to JSON as: ```json { "longitude": -122.4194, "latitude": 37.7749 } ``` ## Model Usage [Section titled “Model Usage”](#model-usage) Your entity models can use `Point` properties directly without any special configuration when using the [`BaseDbContext`](/arc/backend/entity-framework/base-db-context/): ```csharp using Cratis.Geospatial; public class Store { public Guid Id { get; set; } public string Name { get; set; } public Point Location { get; set; } public DateTime CreatedAt { get; set; } } public class DeliveryPoint { public Guid Id { get; set; } public Guid OrderId { get; set; } public Point Destination { get; set; } public Point? CurrentLocation { get; set; } // Nullable point } ``` The conversion will automatically: * Configure all `Coordinate` properties to use JSON serialization for storage * Store the longitude and latitude as a JSON object in the database * Handle conversion between .NET `Coordinate` instances and JSON strings ## Manual Configuration [Section titled “Manual Configuration”](#manual-configuration) If you’re not using the [`BaseDbContext`](/arc/backend/entity-framework/base-db-context/), you can manually apply Point conversion in your `DbContext`: ```csharp using Cratis.Arc.EntityFrameworkCore; using Cratis.Geospatial; public class StoreDbContext(DbContextOptions options) : DbContext(options) { public DbSet Stores { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => { entity.Property(e => e.Location) .AsPoint(Database.GetDatabaseType()); }); base.OnModelCreating(modelBuilder); } } ``` > Note: This is automatically configured for you when using the [`BaseDbContext`](/arc/backend/entity-framework/base-db-context/). ## Migration Usage [Section titled “Migration Usage”](#migration-usage) When creating migrations, use the `PointColumn()` extension method for creating Point columns: ### Creating a Table with Point Column [Section titled “Creating a Table with Point Column”](#creating-a-table-with-point-column) ```csharp [DbContext(typeof(StoreDbContext))] [Migration($"Stores_{nameof(v1_0_0)}")] public class v1_0_0 : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "Stores", columns: table => new { Id = table.GuidColumn(migrationBuilder), Name = table.StringColumn(migrationBuilder, maxLength: 200, nullable: false), Location = table.PointColumn(migrationBuilder, nullable: false), CreatedAt = table.DateTimeOffsetColumn(migrationBuilder, nullable: false) }, constraints: table => table.PrimaryKey("PK_Stores", x => x.Id)); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable(name: "Stores"); } } ``` ### Adding a Point Column to Existing Table [Section titled “Adding a Point Column to Existing Table”](#adding-a-point-column-to-existing-table) ```csharp [DbContext(typeof(StoreDbContext))] [Migration($"Stores_{nameof(v1_1_0)}")] public class v1_1_0 : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddPointColumn( name: "WarehouseLocation", table: "Stores", nullable: true); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropColumn( name: "WarehouseLocation", table: "Stores"); } } ``` > See [Common Column Types](/arc/backend/entity-framework/common-column-types/) for more information about column type extensions. ## How it works [Section titled “How it works”](#how-it-works) The conversion system uses a `ValueConverter` that: 1. **Serializes**: Converts `Point` instances to JSON strings using `System.Text.Json` 2. **Deserializes**: Parses JSON strings back to `Point` instances when reading from database 3. **Null handling**: Properly handles nullable `Point?` properties The conversion is handled by the `AsPoint()` extension method, which configures the property with the appropriate `ValueConverter`. ## Database Provider Specifics [Section titled “Database Provider Specifics”](#database-provider-specifics) The Point conversion adapts to different database providers: * **PostgreSQL (Npgsql)**: Uses `jsonb` type for efficient JSON queries and indexing capabilities * **SQL Server**: Uses `nvarchar(max)` with JSON string storage * **SQLite**: Uses `text` with JSON string storage This provider-specific optimization ensures the best storage characteristics for your chosen database, with PostgreSQL gaining the additional benefit of being able to query within the JSON structure. ## Querying Considerations [Section titled “Querying Considerations”](#querying-considerations) When querying Point data: ### PostgreSQL [Section titled “PostgreSQL”](#postgresql) PostgreSQL’s `jsonb` type allows for efficient queries within the JSON structure: ```csharp // You can still query for stores with specific points var stores = await context.Stores .Where(s => s.Location == targetPoint) .ToListAsync(); ``` ### All Providers [Section titled “All Providers”](#all-providers) For general point comparison across all providers: ```csharp // Exact match var store = await context.Stores .FirstOrDefaultAsync(s => s.Location == knownPoint); // Null checks work as expected var storesWithLocation = await context.Stores .Where(s => s.Location != null) .ToListAsync(); ``` > Note: For advanced geospatial queries (distance calculations, radius searches, etc.), consider using database-specific extensions or computing distances in application code after retrieving the data. ## Related Topics [Section titled “Related Topics”](#related-topics) * [Common Column Types](/arc/backend/entity-framework/common-column-types/) - Column type extensions including PointColumn() * [Property Extensions](/arc/backend/entity-framework/property-extensions/) - AsPoint() and other property configuration methods * [JSON Conversion](/arc/backend/entity-framework/json/) - General JSON serialization support in Entity Framework Core * [Adding Columns in Migrations](/arc/backend/entity-framework/migrations-add-columns/) - AddPointColumn() method # Polygon Conversion The Polygon conversion feature provides automatic handling of `Polygon` geospatial properties from Cratis.Fundamentals in Entity Framework Core, enabling storage of complex geographic areas with optional interior boundaries (holes). ## What it does [Section titled “What it does”](#what-it-does) The Polygon conversion automatically configures Entity Framework Core to handle properties of type `Polygon` using JSON serialization: 1. **PostgreSQL**: Stores as `jsonb` type for efficient JSON queries 2. **SQL Server**: Stores as `nvarchar(max)` with JSON serialization 3. **SQLite**: Stores as `text` with JSON serialization This ensures Polygons (closed geographic shapes with optional holes) are stored consistently across all database providers. ## Why it’s important [Section titled “Why it’s important”](#why-its-important) Using Polygon conversion provides several key benefits: * **Geographic Areas**: Store service areas, regions, or any bounded geographic territory * **Complex Boundaries**: Support for interior holes (e.g., a region with an excluded zone) * **Cross-Database Compatibility**: Consistent Polygon handling across different database providers * **JSON Serialization**: Uses standard JSON format for storage, making data human-readable * **Type Safety**: Maintains strong typing with the `Polygon` type from Cratis.Fundamentals * **Automatic Configuration**: No need for manual configuration of Polygon properties ## The Polygon Type [Section titled “The Polygon Type”](#the-polygon-type) The `Polygon` type represents a closed geographic shape with a required exterior boundary (shell) and optional interior boundaries (holes): ```csharp using Cratis.Geospatial; // Simple polygon (no holes) var shellPoints = new Point[] { new Point(longitude: -122.4194, latitude: 37.7749), new Point(longitude: -122.4170, latitude: 37.7755), new Point(longitude: -122.4160, latitude: 37.7740), new Point(longitude: -122.4194, latitude: 37.7749) // Must close the ring }; var simplePolygon = new Polygon( new LinearRing(shellPoints), holes: [] ); // Polygon with a hole var holePoints = new Point[] { new Point(longitude: -122.4180, latitude: 37.7750), new Point(longitude: -122.4175, latitude: 37.7752), new Point(longitude: -122.4170, latitude: 37.7748), new Point(longitude: -122.4180, latitude: 37.7750) // Must close the ring }; var polygonWithHole = new Polygon( new LinearRing(shellPoints), holes: [new LinearRing(holePoints)] ); ``` The Polygon is serialized to JSON as: ```json { "shell": { "coordinates": [ [-122.4194, 37.7749], [-122.4170, 37.7755], [-122.4160, 37.7740], [-122.4194, 37.7749] ] }, "holes": [] } ``` ## Model Usage [Section titled “Model Usage”](#model-usage) Your entity models can use `Polygon` properties directly: ```csharp using Cratis.Geospatial; public class ServiceArea { public Guid Id { get; set; } public string AreaName { get; set; } public Polygon Boundary { get; set; } public DateTime CreatedAt { get; set; } } public class ExclusionZone { public Guid Id { get; set; } public Guid ServiceAreaId { get; set; } public Polygon RestrictedArea { get; set; } public Polygon? AdditionalRestriction { get; set; } // Nullable polygon } ``` The conversion will automatically: * Configure all `Polygon` properties to use JSON serialization * Store the shell and holes as a JSON structure in the database * Handle conversion between .NET `Polygon` instances and JSON strings ## Manual Configuration [Section titled “Manual Configuration”](#manual-configuration) If you’re not using the [`BaseDbContext`](/arc/backend/entity-framework/base-db-context/), you can manually apply Polygon conversion: ```csharp using Cratis.Arc.EntityFrameworkCore; using Cratis.Geospatial; public class ServiceAreaDbContext(DbContextOptions options) : DbContext(options) { public DbSet Areas { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => { entity.Property(e => e.Boundary) .AsPolygon(); }); base.OnModelCreating(modelBuilder); } } ``` > Note: This is automatically configured for you when using the [`BaseDbContext`](/arc/backend/entity-framework/base-db-context/). ## Migration Usage [Section titled “Migration Usage”](#migration-usage) When creating migrations, use the `PolygonColumn()` extension method: ### Creating a Table with Polygon Column [Section titled “Creating a Table with Polygon Column”](#creating-a-table-with-polygon-column) ```csharp [DbContext(typeof(ServiceAreaDbContext))] [Migration($"ServiceAreas_{nameof(v1_0_0)}")] public class v1_0_0 : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "ServiceAreas", columns: table => new { Id = table.GuidColumn(migrationBuilder), AreaName = table.StringColumn(migrationBuilder, maxLength: 200, nullable: false), Boundary = table.PolygonColumn(migrationBuilder, nullable: false), CreatedAt = table.DateTimeOffsetColumn(migrationBuilder, nullable: false) }, constraints: table => table.PrimaryKey("PK_ServiceAreas", x => x.Id)); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable(name: "ServiceAreas"); } } ``` ### Adding a Polygon Column to Existing Table [Section titled “Adding a Polygon Column to Existing Table”](#adding-a-polygon-column-to-existing-table) ```csharp [DbContext(typeof(ServiceAreaDbContext))] [Migration($"ServiceAreas_{nameof(v1_1_0)}")] public class v1_1_0 : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddPolygonColumn( name: "ExpandedBoundary", table: "ServiceAreas", nullable: true); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropColumn( name: "ExpandedBoundary", table: "ServiceAreas"); } } ``` > See [Common Column Types](/arc/backend/entity-framework/common-column-types/) for more information about column type extensions. ## How it works [Section titled “How it works”](#how-it-works) The conversion system uses a `ValueConverter` that: 1. **Serializes**: Converts `Polygon` instances to JSON strings using `System.Text.Json` 2. **Deserializes**: Parses JSON strings back to `Polygon` instances when reading from database 3. **Null handling**: Properly handles nullable `Polygon?` properties 4. **Validation**: Ensures rings are properly closed (first and last points match) and contain at least 4 points The conversion is handled by the `AsPolygon()` extension method, which configures the property with the appropriate `ValueConverter`. ## Polygon Requirements [Section titled “Polygon Requirements”](#polygon-requirements) When creating Polygon instances, ensure: * **Closed rings**: Each LinearRing must have identical first and last points (the ring must close) * **Minimum points**: Each ring must have at least 4 points (3 unique points + 1 closing duplicate) * **Correct order**: Exterior ring (shell) vertices should follow right-hand rule for GeoJSON compatibility * **No self-intersection**: Rings should not cross themselves ## Database Provider Specifics [Section titled “Database Provider Specifics”](#database-provider-specifics) The Polygon conversion adapts to different database providers: * **PostgreSQL (Npgsql)**: Uses `jsonb` type for efficient JSON queries * **SQL Server**: Uses `nvarchar(max)` with JSON string storage * **SQLite**: Uses `text` with JSON string storage ## Querying Considerations [Section titled “Querying Considerations”](#querying-considerations) When querying Polygon data: ### All Providers [Section titled “All Providers”](#all-providers) For general Polygon comparison: ```csharp // Exact match var areas = await context.Areas .Where(a => a.Boundary == targetBoundary) .ToListAsync(); // Null checks var areasWithBoundary = await context.Areas .Where(a => a.Boundary != null) .ToListAsync(); ``` > Note: For advanced geospatial queries (point-in-polygon, intersection detection, etc.), consider using database-specific extensions or computing results in application code. ## Related Topics [Section titled “Related Topics”](#related-topics) * [Common Column Types](/arc/backend/entity-framework/common-column-types/) - Column type extensions * [Property Extensions](/arc/backend/entity-framework/property-extensions/) - AsPolygon() and other configuration methods * [LineString Conversion](/arc/backend/entity-framework/linestring-conversion/) - Working with routes and paths * [Point Conversion](/arc/backend/entity-framework/point-conversion/) - Working with individual points * [JSON Conversion](/arc/backend/entity-framework/json/) - General JSON serialization support # Property Extensions Cratis Arc provides extension methods for Entity Framework Core’s `PropertyBuilder` to help configure properties with cross-database compatibility in mind. ## AsGuid() [Section titled “AsGuid()”](#asguid) The `AsGuid()` extension method configures a GUID property for optimal storage based on the database provider, ensuring compatibility across different database providers while using native types when available. > **Note**: You should use this in conjunction with the migration-based column type configuration, see [Common Column Types](/arc/backend/entity-framework/common-column-types/) which provides the `GuidColumn()` method for creating GUID columns in migrations. ### Why AsGuid()? [Section titled “Why AsGuid()?”](#why-asguid) Different database providers handle GUIDs differently: * **PostgreSQL**: Native UUID type with optimal performance * **SQL Server**: UNIQUEIDENTIFIER type with optimal performance * **SQLite**: No native GUID support, requires string conversion The `AsGuid()` method uses the provided database information to apply the appropriate configuration: * For **SQLite**: Converts GUIDs to strings using the “D” format (e.g., `550e8400-e29b-41d4-a716-446655440000`) * For **PostgreSQL and SQL Server**: Uses native GUID types for optimal performance This provides the best of both worlds: optimal performance on databases with native GUID support and compatibility for SQLite. ### Usage [Section titled “Usage”](#usage) #### Recommended Usage [Section titled “Recommended Usage”](#recommended-usage) ```csharp public class MyDbContext : DbContext { public DbSet Users { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => { entity.Property(e => e.Id) .AsGuid(Database.GetDatabaseType()); // Pass the database type for proper provider detection }); } } ``` > **Important**: The `database` parameter is required to ensure optimal configuration based on the actual database provider being used. ### What it does [Section titled “What it does”](#what-it-does) The `AsGuid()` method: 1. **Uses the provided database parameter** to determine the database provider (PostgreSQL, SQL Server, or SQLite) 2. **For SQLite**: Converts `Guid` values to string using the `ToString("D")` format for storage and parses them back when reading 3. **For PostgreSQL and SQL Server**: Uses native GUID types without conversion for optimal performance 4. **Ensures optimal behavior** for each supported database provider ### When to use [Section titled “When to use”](#when-to-use) Use `AsGuid()` when: * You need consistent GUID behavior across multiple database providers * Your application might be deployed with different database backends * You want optimal performance on databases with native GUID support * You want automatic SQLite compatibility with proper provider detection ### Why the Database parameter is required [Section titled “Why the Database parameter is required”](#why-the-database-parameter-is-required) The `database` parameter is required because: * **Ensures optimal performance**: Native GUID types are used for PostgreSQL and SQL Server, string conversion only for SQLite * **Explicit provider detection**: Eliminates guesswork and ensures the correct configuration is applied * **Better debugging**: Makes it clear which database provider configuration is being used * **Prevents misconfiguration**: Avoids scenarios where incorrect assumptions about the database provider could lead to suboptimal storage ## AsConcept() [Section titled “AsConcept()”](#asconcept) The `AsConcept()` extension method configures a property to use value conversion for concept types based on `ConceptAs`. This method provides automatic conversion between domain concepts and their underlying primitive values for database storage. > **Note**: This method is typically used for manual property configuration. In most cases, you should use the automatic conversion provided by the [`BaseDbContext`](/arc/backend/entity-framework/base-db-context/) or the [`ApplyConceptAsConversion`](/arc/backend/entity-framework/concept-as-conversion/) extension method. ### Why AsConcept()? [Section titled “Why AsConcept()?”](#why-asconcept) When working with domain concepts (types that inherit from `ConceptAs`), you need proper value conversion to: * **Store primitive values**: Only the underlying primitive value is stored in the database, keeping the schema clean and performant * **Maintain type safety**: Ensures concepts are properly converted back to their domain types when loading from the database * **Handle different databases**: Provides special handling for GUID concepts on SQLite (using string conversion) while using native types on other databases * **Enable change tracking**: Configures proper value comparison for Entity Framework’s change tracking system ### AsConcept() Usage [Section titled “AsConcept() Usage”](#asconcept-usage) #### Manual Property Configuration [Section titled “Manual Property Configuration”](#manual-property-configuration) ```csharp public class StoreDbContext : DbContext { public DbSet Customers { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => { entity.Property(e => e.Id) .AsConcept(Database.GetDatabaseType()); // Configure concept property manually entity.Property(e => e.Name) .AsConcept(Database.GetDatabaseType()); // Works with any ConceptAs type entity.Property(e => e.Email) .AsConcept(Database.GetDatabaseType()); }); } } public class Customer { public CustomerId Id { get; set; } public CustomerName Name { get; set; } public EmailAddress Email { get; set; } } public record CustomerId(Guid Value) : ConceptAs(Value); public record CustomerName(string Value) : ConceptAs(Value); public record EmailAddress(string Value) : ConceptAs(Value); ``` > **Important**: The `database` parameter is required to ensure optimal configuration based on the actual database provider being used. ### How AsConcept() works [Section titled “How AsConcept() works”](#how-asconcept-works) The `AsConcept()` method: 1. **Detects concept types**: Automatically identifies if the property is a `ConceptAs` type 2. **Creates value converters**: Generates appropriate `ValueConverter` instances that convert between concepts and their primitive values 3. **Handles GUID concepts specially**: For GUID-based concepts on SQLite, uses string conversion; for other databases, uses native GUID storage 4. **Configures value comparison**: Sets up proper `ValueComparer` for change tracking and equality operations 5. **Returns early for non-concepts**: If the property is not a concept type, returns without modification ### When to use AsConcept() [Section titled “When to use AsConcept()”](#when-to-use-asconcept) Use `AsConcept()` when: * You need manual control over individual concept property configuration * You’re not using the [`BaseDbContext`](/arc/backend/entity-framework/base-db-context/) which provides automatic concept conversion * You want to selectively configure only specific concept properties * You’re working with a custom `DbContext` that doesn’t apply global concept conversion ### When NOT to use [Section titled “When NOT to use”](#when-not-to-use) Avoid `AsConcept()` when: * You’re using [`BaseDbContext`](/arc/backend/entity-framework/base-db-context/) - concepts are automatically configured * You’re using [`ApplyConceptAsConversion()`](/arc/backend/entity-framework/concept-as-conversion/) - all concepts are configured automatically * The property is not a concept type - the method will return without changes but it’s unnecessary overhead ### Special handling for GUID concepts [Section titled “Special handling for GUID concepts”](#special-handling-for-guid-concepts) For concept types based on `Guid` (e.g., `ConceptAs`): * **SQLite**: Converts to string using “D” format for storage compatibility * **PostgreSQL and SQL Server**: Uses native GUID storage for optimal performance This ensures consistent behavior across database providers while maintaining optimal performance where native GUID support is available. # Read Only DbContext Typically in a CQRS model, your read models are not meant to be used for creating or updating state in the database. They’re meant to be read-only. In fact, you don’t even want to take advantage of the EntityFramework change tracking. There are 2 ways of doing read-only DbContexts; inheritance or using the registration methods that will configure without having to use inheritance. ## Purpose-Built Read Models [Section titled “Purpose-Built Read Models”](#purpose-built-read-models) A core principle of effective read model design is that **each read model should be purpose-built for a specific scenario** rather than reused across multiple use cases. This means: * Each `DbContext` represents a specific view or feature’s data needs * The entities and their relationships are tailored to exactly what that scenario requires * You don’t share read models between different features that have different relationship requirements This approach offers several benefits: * **Clarity**: Each read model clearly expresses what data a specific feature needs * **Performance**: No unnecessary data is loaded, and no conditional logic is needed to decide which relationships to include * **Maintainability**: Changes to one feature’s data needs don’t affect other features * **Simplicity**: The code remains straightforward without complex `.Include()` chains or conditional loading logic By following this pattern, automatic eager loading (described below) becomes a natural fit—since each read model is purpose-built, all its relationships are needed every time, eliminating the need for selective inclusion. ## ReadOnlyDbContext base class [Section titled “ReadOnlyDbContext base class”](#readonlydbcontext-base-class) The `ReadOnlyDbContext` base class gives you a base class that also inherits from the [`BaseDbContext`](/arc/backend/entity-framework/base-db-context/) to give you the common tools. All you need to do for your `DbContext` is to inherit from it as shown below: ```csharp using Cratis.Arc.EntityFrameworkCore; public class StoreDbContext : ReadOnlyDbContext { public DbSet Customers { get; set; } } ``` Then you register it as you normally would: ```csharp services.AddDbContext(opt => ...); ``` ## Automatic Eager Loading [Section titled “Automatic Eager Loading”](#automatic-eager-loading) The `ReadOnlyDbContext` automatically enables eager loading for all navigation properties across all entities. This means that when you query an entity, all its related entities will be automatically included without having to explicitly call `.Include()`. This behavior aligns perfectly with the purpose-built read model approach—since each DbContext is designed for a specific scenario with well-defined data needs, all relationships are typically required and should be loaded together. ### Disabling Eager Loading Globally [Section titled “Disabling Eager Loading Globally”](#disabling-eager-loading-globally) If you want to disable automatic eager loading for a specific DbContext, you can override the `IsEagerLoadingEnabled` property: ```csharp using Cratis.Arc.EntityFrameworkCore; public class StoreDbContext : ReadOnlyDbContext { public DbSet Customers { get; set; } protected override bool IsEagerLoadingEnabled => false; } ``` ### Disabling Eager Loading Per Query [Section titled “Disabling Eager Loading Per Query”](#disabling-eager-loading-per-query) For individual queries where you don’t want eager loading, use the `.IgnoreAutoIncludes()` method: ```csharp // This query will NOT load related entities automatically var customers = await context.Customers .IgnoreAutoIncludes() .ToListAsync(); ``` ### Disabling Eager Loading Per Entity [Section titled “Disabling Eager Loading Per Entity”](#disabling-eager-loading-per-entity) To disable automatic eager loading for specific navigation properties in your entity configuration, you can use the `AutoInclude(false)` method in your entity configuration: ```csharp protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity() .Navigation(c => c.Orders) .AutoInclude(false); } ``` This will prevent the `Orders` navigation from being automatically included when querying `Customer` entities, even though eager loading is enabled globally. ## Register [Section titled “Register”](#register) The other option is to let you inherit from whatever base `DbContext` you want and then instead leverage the extension methods: ```csharp services.AddReadOnlyDbContext((serviceProvider, opt) => ...); ``` ### Discover and register all in assemblies [Section titled “Discover and register all in assemblies”](#discover-and-register-all-in-assemblies) For convenience you can get all types inheriting from `DbContext` automatically discovered and registered in one call. ```csharp services.AddReadModelDbContextsFromAssemblies((serviceProvider, opt) => { /* Configure any options */ }, [Assembly.GetExecutingAssembly()]); ``` Or if you want it to automatically configure it with the correct database: ```csharp services.AddReadModelDbContextsWithConnectionStringFromAssemblies( ".. your connection string..", (serviceProvider, opt) => { /* Configure any options */ }, [Assembly.GetExecutingAssembly()]); ``` # Getting started > Set up an Arc backend and build your first command, read model, and live query while learning the CQRS boundary. Get an Arc backend up and building features. These first pages use a plain database — MongoDB or EF Core — so you can see Arc’s CQRS model in isolation: commands with `Handle()`, read models with query methods, validation, authorization, and generated TypeScript proxies. In a full Cratis information system, the same boundary usually sits on Chronicle’s event-sourced write side. Once you have a project: * **[Your first command and query](/arc/backend/getting-started/your-first-command/)** — build a backend slice end to end: a command with `Handle()`, the read model it writes, and the live query that serves it. * **[MongoDB integration](/arc/backend/mongodb/)** — configure Arc over MongoDB collections and observable change streams. * **[Entity Framework integration](/arc/backend/entity-framework/getting-started/)** — configure Arc over DbContexts and observed DbSets. When the backend compiles, it generates the TypeScript proxies your frontend consumes — continue with the [frontend getting started](/arc/frontend/getting-started/) or build the full database-backed path in the [Arc tutorial](/arc/tutorial/). When you are ready to put the event-sourced backbone underneath that boundary, continue with the [Chronicle integration](/arc/backend/chronicle/). # Your first command and query > Build a backend slice in Arc — a command that writes, a read model, and a live query that serves it — while learning the CQRS shape. Let’s build the backend half of a real feature: registering an author in a library app. It’s small, but it exercises the whole Arc loop — something *happens* (a command), it writes state, and that state is served back out (a query). We use a plain database in this lesson to keep the CQRS boundary visible; in a full Cratis information system, the same boundary usually writes events through Chronicle. In a layered app this would be files scattered across `Commands/`, `Handlers/`, and `ReadModels/`, and you’d hop between them to follow one behavior. Arc doesn’t force a layout, but we strongly recommend organizing by **feature**: everything below lives in one `Authors/` folder you read top to bottom. Here’s the shape we’re building: ``` Handle, writes AllAuthors RegisterAuthor command database Author read model query over HTTP, live ``` ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * **.NET SDK 8 or newer.** * **An Arc backend project** — add the `Cratis.Arc` packages to an ASP.NET Core app, or scaffold the full stack with the [Cratis templates](/chronicle/get-started/). * **A database Arc can write to** — MongoDB, or EF Core over SQLite/Postgres. Chronicle is not required for this lesson. 1. **A strongly-typed id.** Never pass raw `Guid`s around the domain — wrap them so the compiler keeps them straight and your signatures document themselves: ```csharp public record AuthorId(Guid Value) : ConceptAs(Value) { public static AuthorId New() => new(Guid.NewGuid()); } ``` 2. **The command — with `Handle()` on the record.** A command is a `record` marked `[Command]`. The behavior lives in a `Handle()` method **on the record itself** — there’s no separate handler class to find. Here it writes the new author to the database: ```csharp [Command] public record RegisterAuthor(AuthorId Id, AuthorName Name) { public Task Handle(IMongoCollection authors) => authors.InsertOneAsync(new Author(Id, Name)); } ``` `Handle()` returns `Task` — the write is the outcome. Need a different store or a collaborator? Add it as a parameter and Arc injects it: an `IMongoCollection`, your EF Core `DbContext`, a service, anything in the container. 3. **The read model and its query.** Declare the shape you want to query and mark it `[ReadModel]`. A static method exposes the query — return an observable so consumers get live updates: ```csharp [ReadModel] public record Author([property: Key] AuthorId Id, AuthorName Name) { public static ISubject> AllAuthors(IMongoCollection collection) => collection.Observe(); } ``` Notice what you didn't write That static `AllAuthors` method *is* your query — Arc exposes it over HTTP automatically. No controller, no routing, no DTO. And it’s **live**: `.Observe()` watches the change stream, so the moment the command writes, every subscriber re-renders. 4. **Build.** ```bash dotnet build ``` Building **generates the TypeScript proxies** for `RegisterAuthor` and `AllAuthors` so your frontend can call them type-safely — that’s the part that makes the next step (the UI) effortless. ## What you built [Section titled “What you built”](#what-you-built) In one folder, read top to bottom: * A `[Command]` with `Handle()` — intent and implementation in one place, no handler class. * A `[ReadModel]` with a live query method served over HTTP. That’s a complete vertical slice, over a plain database. The next feature is another folder just like it. Storing where you like This slice writes straight to MongoDB. Arc is persistence-agnostic — the same shape works over [EF Core](/arc/backend/entity-framework/getting-started/) or over Chronicle. When this belongs in the event-sourced model, the [Chronicle integration](/arc/backend/chronicle/add-event-sourcing/) moves the write side to events without touching your query or your frontend. ## Where to go next [Section titled “Where to go next”](#where-to-go-next) * **[Wire a UI to it](/arc/frontend/getting-started/)** — read this query and run this command from React, fully typed. * **[Build the library, full-stack](/arc/tutorial/)** — the threaded tutorial builds this into a real app, chapter by chapter. * Go deeper on [Commands](/arc/backend/commands/) and [Queries](/arc/backend/queries/). # Identity Arc identity support lets you enrich provider tokens with domain-specific user details, perform application-level authorization at ingress, and propagate a consistent identity payload to frontend clients. ## Overview [Section titled “Overview”](#overview) Identity tokens from providers typically contain limited information. Arc lets you compose additional identity details in your backend, authorize users before app entry, and publish one consistent identity payload for downstream services and frontend clients. Key capabilities: * Enrich provider identity with domain-specific details * Perform application-level authorization during ingress * Return a single consolidated identity payload * Reuse identity details across services and frontend clients ``` No Yes Identity Provider Token Arc Identity Provider Endpoint Authorized? HTTP 403 Identity Details JSON .cratis-identity Cookie Frontend Identity Consumption ``` ## Topics [Section titled “Topics”](#topics) | Topic | Description | | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | | [Provider Flow](/arc/backend/identity/provider-flow/) | Endpoint mapping, provider implementation, request flow, and frontend cookie integration. | | [Identity Contracts](/arc/backend/identity/contracts/) | `IdentityProviderContext` and `IdentityDetails` structures used by providers. | | [IdentityProvider Service](/arc/backend/identity/identity-provider-service/) | Advanced runtime identity retrieval and mutation with `IIdentityProvider`. | | [Development and Topologies](/arc/backend/identity/development-and-topologies/) | Development endpoints plus single-service and multi-service composition patterns. | # Identity Contracts Identity providers work with two key contracts: `IdentityProviderContext` as input and `IdentityDetails` as output. ## IdentityProviderContext [Section titled “IdentityProviderContext”](#identityprovidercontext) `IdentityProviderContext` contains the incoming identity data. | Property | Description | | -------- | ------------------------------------------------------------------ | | Id | The identity identifier from the identity provider | | Name | The display name of the identity | | Claims | Collection of `KeyValuePair` claims from the token | ## IdentityDetails [Section titled “IdentityDetails”](#identitydetails) `IdentityDetails` represents the provider result. | Property | Description | | ---------------- | ------------------------------------------------------- | | IsUserAuthorized | Whether the user is authorized to enter the application | | Details | Domain-specific details as an object payload | When `IsUserAuthorized` is `false`, Arc returns HTTP 403. When authorized, Arc returns HTTP 200. > Note: Providers can use constructor dependencies via dependency inversion. # Development and Topologies ## Development Users and Tenants [Section titled “Development Users and Tenants”](#development-users-and-tenants) For development scenarios, Arc automatically exposes HTTP endpoints that development tools use to discover available users and tenants. This eliminates the need to hard-code user lists in your frontend or development environment — just implement the providers and Arc surfaces them. ### Available Endpoints [Section titled “Available Endpoints”](#available-endpoints) When you implement the development providers, Arc automatically exposes: * `/.cratis/users` — Returns all available development users * `/.cratis/tenants` — Returns all available development tenants These endpoints are discovered and mapped automatically. No additional configuration needed. ### Implementing a Users Provider [Section titled “Implementing a Users Provider”](#implementing-a-users-provider) Create a class implementing `ICanProvideUsers`. Arc will discover it automatically. ```csharp using Cratis.Arc.Identity; public class DevelopmentUsersProvider : ICanProvideUsers { public Task> Provide() { var users = new List { new User( new ClientPrincipal { UserId = "alice@contoso.com", UserDetails = "Alice Developer", IdentityProvider = "aad", UserRoles = new[] { "admin", "developer" } }, Details: new { Department = "Engineering" } ), new User( new ClientPrincipal { UserId = "bob@contoso.com", UserDetails = "Bob Tester", IdentityProvider = "aad", UserRoles = new[] { "tester" } }, Details: new { Department = "QA" } ) }; return Task.FromResult>(users); } } ``` ### Implementing a Tenants Provider [Section titled “Implementing a Tenants Provider”](#implementing-a-tenants-provider) Similarly, create a class implementing `ICanProvideTenants`: ```csharp using Cratis.Arc.Tenancy; public class DevelopmentTenantsProvider : ICanProvideTenants { public Task> Provide() { var tenants = new List { new Tenant( Id: new TenantId("acme-corp"), Name: new TenantName("ACME Corporation") ), new Tenant( Id: new TenantId("widget-inc"), Name: new TenantName("Widget Inc") ) }; return Task.FromResult>(tenants); } } ``` ### How Development Tooling Uses These Endpoints [Section titled “How Development Tooling Uses These Endpoints”](#how-development-tooling-uses-these-endpoints) Development tools (like the Cratis Portal or custom dev dashboards) use these endpoints to populate dropdown menus and user selectors. Instead of hard-coding a list of test users or maintaining them in configuration, your code is the source of truth: * Frontend fetches `/cratis/users` to populate user-selection dropdowns * Frontend fetches `/cratis/tenants` to populate tenant-selection dropdowns * Developers can switch context without rebuilding You can have **multiple providers** — all registered providers are discovered and their results merged. This is useful when users or tenants come from different sources (database, configuration, external service). ### Multiple Providers [Section titled “Multiple Providers”](#multiple-providers) You can implement multiple providers of the same interface. All will be discovered and their results combined. This is useful when users or tenants come from different sources (configuration, database, external service) — each source gets its own provider, and Arc merges them automatically. ## Service Topologies [Section titled “Service Topologies”](#service-topologies) In a microservices architecture, you have several implementation options: 1. **Single service** — Implement `IProvideIdentityDetails` in the main service 2. **Multiple services** — Let ingress or reverse proxy call multiple services and merge the results 3. **Dedicated identity service** — Aggregate identity data in a specialized service Choose the topology that best fits your architecture and operational model. # IdentityProvider Service `IIdentityProvider` gives you advanced control over identity processing during HTTP requests. It is useful for stateless applications that need to associate request-specific selections or preferences with the current user. ## Purpose [Section titled “Purpose”](#purpose) `IIdentityProvider` lets you: * Get identity results from the identity cookie or current HTTP context * Write identity information to the response as cookies and JSON * Modify identity details during a request ## Key Methods [Section titled “Key Methods”](#key-methods) | Method | Description | | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `Get()` | Gets an `IdentityProviderResult` from the identity cookie when available, otherwise from the current HTTP context | | `Get()` | Gets an `IdentityProviderResult` with strongly-typed details | | `SetCookieForHttpResponse(IdentityProviderResult)` | Writes the identity result to the response as both a cookie and JSON | | `ModifyDetails(Func)` | Modifies identity details stored in the identity cookie | ## Use Case: Modifying User Preferences in Stateless Applications [Section titled “Use Case: Modifying User Preferences in Stateless Applications”](#use-case-modifying-user-preferences-in-stateless-applications) ```csharp public class UserPreferencesController : ControllerBase { private readonly IIdentityProvider _identityHandler; public UserPreferencesController(IIdentityProvider identityHandler) { _identityHandler = identityHandler; } [HttpPost("set-department")] public async Task SetDepartment([FromBody] string department) { await _identityHandler.ModifyDetails(details => details with { SelectedDepartment = department }); return Ok(); } } ``` # Provider Flow Arc identity enrichment is implemented as part of ingress so authorization and detail composition happen in one request. ## Endpoint Mapping [Section titled “Endpoint Mapping”](#endpoint-mapping) Map the identity provider endpoint in your application: ```csharp app.MapIdentityProvider(); ``` The well-known route is: * `/.cratis/me` ## Identity Details Provider [Section titled “Identity Details Provider”](#identity-details-provider) Implement `IProvideIdentityDetails` from `Cratis.Arc.Identity`. Arc discovers implementations automatically and invokes them when the identity endpoint is called. Your provider can: 1. Check whether the user is authorized to access the application 2. Compose domain-specific identity details 3. Return a consolidated identity response If the user is authorized, Arc writes the result as a base64-encoded JSON payload to the `.cratis-identity` cookie. ## Implementation Example [Section titled “Implementation Example”](#implementation-example) ```csharp public class IdentityDetailsProvider : IProvideIdentityDetails { public Task Provide(IdentityProviderContext context) { var result = new IdentityDetails(true, new { Hello = "World" }); return Task.FromResult(result); } } ``` ## Frontend Integration [Section titled “Frontend Integration”](#frontend-integration) Frontend identity support can consume the `.cratis-identity` cookie directly, avoiding a follow-up request for user details. For frontend details, see [React identity integration](/arc/frontend/react/identity/). # Introspection Arc exposes introspection endpoints that let you inspect the command and query surface of your application at runtime. ## Where to find the endpoints [Section titled “Where to find the endpoints”](#where-to-find-the-endpoints) When you call `UseCratisArc()`, Arc maps introspection endpoints automatically. * `/.cratis/commands` * `/.cratis/queries` * `/.cratis/identity-details/schema` (when an identity details provider is registered) These endpoints are mapped in both Arc.Core and ASP.NET Core hosting scenarios through `MapIntrospectionEndpoints()`. Automatically called when just using the out-of-the-box general builders for setting up an Arc or a Cratis application. ## What introspection does [Section titled “What introspection does”](#what-introspection-does) Introspection returns metadata, not business data. It helps you: * Discover available command and query endpoints. * Understand route paths and operation names. * Read operation summaries populated from type metadata. * Build tooling, diagnostics, and client-side discovery workflows. ## Topics [Section titled “Topics”](#topics) | Topic | Description | | ------------------------------------------------------------------------------ | --------------------------------------------------------- | | [Commands](/arc/backend/introspection/commands/) | Metadata shape and behavior for `/.cratis/commands`. | | [Queries](/arc/backend/introspection/queries/) | Metadata shape and behavior for `/.cratis/queries`. | | [Identity details schema](/arc/backend/introspection/identity-details-schema/) | JSON Schema shape for `/.cratis/identity-details/schema`. | # Introspection Commands Endpoint The commands introspection endpoint returns metadata for every mapped command. ## Endpoint [Section titled “Endpoint”](#endpoint) `GET /.cratis/commands` ## What it returns [Section titled “What it returns”](#what-it-returns) The endpoint returns a JSON array where each item describes one command endpoint. Each item includes: * `name`: Command type name. * `namespace`: Namespace derived from the mapped location. * `route`: The resolved command route. * `type`: Fully qualified command type name. * `documentationSummary`: Summary text from type metadata when available. * `payloadSchema`: JSON Schema describing the command payload contract (fields/properties and types). ## Typical uses [Section titled “Typical uses”](#typical-uses) * Generate dynamic command catalogs in internal tooling. * Verify command route conventions in development environments. * Support diagnostics for endpoint mapping and registration. # Introspection Identity Details Schema Endpoint The identity details schema introspection endpoint returns JSON Schema for the identity details contract exposed by your configured identity details provider. ## Endpoint [Section titled “Endpoint”](#endpoint) `GET /.cratis/identity-details/schema` ## When Arc maps this endpoint [Section titled “When Arc maps this endpoint”](#when-arc-maps-this-endpoint) Arc always maps this endpoint. ## What it returns [Section titled “What it returns”](#what-it-returns) The endpoint returns a JSON Schema document generated from the runtime identity details type: * If your provider implements `IProvideIdentityDetails`, Arc generates schema for `TDetails`. * If your provider only implements non-generic `IProvideIdentityDetails`, Arc generates schema for `object`. * If no identity details provider is configured, Arc returns an empty schema object (`{}`). ## Typical uses [Section titled “Typical uses”](#typical-uses) * Discover the identity details shape at runtime without hardcoding provider types. * Drive client-side tooling that validates or renders identity details data. * Keep integration tests and diagnostics aligned with the active identity provider contract. # Introspection Queries Endpoint The queries introspection endpoint returns metadata for every mapped query. ## Endpoint [Section titled “Endpoint”](#endpoint) `GET /.cratis/queries` ## What it returns [Section titled “What it returns”](#what-it-returns) The endpoint returns a JSON array where each item describes one query endpoint. Each item includes: * `name`: Query name. * `namespace`: Namespace derived from the mapped location. * `route`: The resolved query route, built with the same route helper used by the runtime query mapper. It honors the configured route prefix and `SegmentsToSkipForRoute` value. * `type`: Fully qualified query type name. * `documentationSummary`: Summary text from type metadata when available. * `argumentsSchema`: JSON Schema for query arguments, including argument names, types, and required fields. ## Typical uses [Section titled “Typical uses”](#typical-uses) * Build runtime query explorers for developers. * Validate query route generation across features. * Feed endpoint metadata into local API diagnostics tools. # MongoDB This documentation has been moved to a more comprehensive structure. Please visit the [MongoDB documentation section](/arc/backend/mongodb/) for detailed information about MongoDB integration in Cratis Applications. ## Quick Links [Section titled “Quick Links”](#quick-links) * [Getting Started](/arc/backend/mongodb/getting-started/) - Basic setup and configuration * [Serializers](/arc/backend/mongodb/serializers/) - Built-in serializers for common types * [Concepts](/arc/backend/mongodb/concepts/) - Working with Cratis Concepts in MongoDB * [Naming Policies](/arc/backend/mongodb/naming-policies/) - Configuring naming conventions * [Class Mapping](/arc/backend/mongodb/class-mapping/) - Custom BSON class mapping with automatic discovery * [Convention Packs](/arc/backend/mongodb/convention-packs/) - Advanced convention system and filtering # Watching Multiple Collections with a Shared Change Stream The `IMongoDBWatcher` provides a single, process-wide MongoDB change stream connection per database. Instead of opening one change stream per collection, you share one connection and funnel events from multiple collections through it. This reduces the number of open cursors and network connections, which matters when you observe many collections simultaneously. ## How it works [Section titled “How it works”](#how-it-works) ``` one change stream Join + Select Join + Select MongoDB Database IMongoDBWatcher ObserveBuilder ObserveBuilder ISubject ISubject ``` `IMongoDBWatcher` is registered as a singleton automatically when you call `UseCratisMongoDB()`. The underlying change stream opens the first time you call `Observe()` and is reused by every subsequent call. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * MongoDB 3.6 or later with a replica set (required for change streams) * `UseCratisMongoDB()` called during application startup ## Observing a single collection [Section titled “Observing a single collection”](#observing-a-single-collection) Inject `IMongoDBWatcher` and call `Observe()` to start watching a collection. Chain `.Join()` to include additional collections, then call `.Select()` to combine them into a result subject. Even when you only care about one collection, you must call `.Join().Select()` to materialize the subject — a single `Observe()` alone returns a builder, not a subject. ### Example — watch one collection alongside a lookup [Section titled “Example — watch one collection alongside a lookup”](#example--watch-one-collection-alongside-a-lookup) ```csharp public class BookCatalogService(IMongoDBWatcher watcher) { public ISubject GetLiveCatalog() { return watcher .Observe() .Join() .Select((books, authors) => new BookCatalog(books, authors)); } } public record BookCatalog(IEnumerable Books, IEnumerable Authors); ``` `Select()` is called once with the full current contents of each collection. It is called again every time any watched collection receives an insert, update, replace, or delete. ## Filtering observed documents [Section titled “Filtering observed documents”](#filtering-observed-documents) Pass a filter expression to `Observe()` or `Join()` to restrict which documents are loaded when the subject re-fetches after a change. ```csharp public ISubject GetLivePublishedCatalog() { return watcher .Observe(book => book.IsPublished) .Join(author => author.IsActive) .Select((books, authors) => new BookCatalog(books, authors)); } ``` > Filters apply to the re-fetch query, not to the change stream trigger. Any change to a collection causes a re-fetch regardless of whether the changed document matches the filter. ## Joining three collections [Section titled “Joining three collections”](#joining-three-collections) Chain a second `.Join()` to watch three collections at once. ```csharp public ISubject GetLiveLibrarySummary() { return watcher .Observe() .Join() .Join() .Select((books, authors, publishers) => new LibrarySummary(books, authors, publishers)); } public record LibrarySummary( IEnumerable Books, IEnumerable Authors, IEnumerable Publishers); ``` ## Using the subject in a query [Section titled “Using the subject in a query”](#using-the-subject-in-a-query) `ISubject` implements both `IObservable` and `IObserver`. You can pass it directly to any observable query: ```csharp [Route("api/catalog")] public class CatalogController(IMongoDBWatcher watcher) : Controller { [HttpGet("live")] public ISubject LiveCatalog() { return watcher .Observe(book => book.IsPublished) .Join() .Select((books, authors) => new BookCatalog(books, authors)); } } ``` ## Lifetime and disposal [Section titled “Lifetime and disposal”](#lifetime-and-disposal) `IMongoDBWatcher` is a singleton. The underlying change stream lives for the lifetime of the application. You do not need to manage its lifecycle. Individual subjects returned by `Select()` run a background loop that is tied to the subject itself — the loop stops when the subject completes or errors. Unsubscribing all observers from a subject causes it to complete and cleans up its internal resources automatically. ## See also [Section titled “See also”](#see-also) * [Observing Collections](/arc/backend/mongodb/observing-collections/) — Per-collection `IMongoCollection.Observe()` extension methods for single-collection scenarios. * [Getting Started](/arc/backend/mongodb/getting-started/) — How to register the MongoDB integration, including `IMongoDBWatcher`. # Class Mapping Class mapping in Cratis Applications provides a powerful way to customize how your .NET types are serialized to and from MongoDB BSON documents. The framework includes automatic discovery and registration of custom mappings. ## Overview [Section titled “Overview”](#overview) Class mapping allows you to: * **Customize field names**: Override property names in the database * **Configure serialization**: Specify custom serializers for properties * **Set up inheritance**: Configure polymorphic type hierarchies * **Control indexes**: Define which properties should be indexed * **Ignore properties**: Exclude certain properties from serialization ## IBsonClassMapFor Interface [Section titled “IBsonClassMapFor Interface”](#ibsonclassmapfor-interface) To create a custom class map, implement the `IBsonClassMapFor` interface: ```csharp public interface IBsonClassMapFor { void Configure(BsonClassMap classMap); } ``` ## Basic Class Map Example [Section titled “Basic Class Map Example”](#basic-class-map-example) ```csharp public class User { public UserId Id { get; set; } public string UserName { get; set; } public string Email { get; set; } public DateTime CreatedAt { get; set; } public DateTime? LastLoginAt { get; set; } public string PasswordHash { get; set; } // We don't want to serialize this } public class UserClassMap : IBsonClassMapFor { public void Configure(BsonClassMap classMap) { classMap.AutoMap(); // Set the ID field classMap.SetIdMember(classMap.GetMemberMap(u => u.Id)); // Custom field name classMap.GetMemberMap(u => u.UserName) .SetElementName("username"); // Ignore sensitive data classMap.UnmapMember(u => u.PasswordHash); // Custom serializer for dates classMap.GetMemberMap(u => u.CreatedAt) .SetSerializer(new DateTimeOffsetSupportingBsonDateTimeSerializer()); } } ``` ## Automatic Discovery [Section titled “Automatic Discovery”](#automatic-discovery) The framework automatically discovers and registers all implementations of `IBsonClassMapFor`: ```csharp // This happens automatically during UseCratisMongoDB() setup var types = Types.Instance; var classMaps = types.FindMultiple(typeof(IBsonClassMapFor<>)); foreach (var classMapType in classMaps) { // Automatic registration RegisterClassMap(classMapType); } ``` ## Registration Process [Section titled “Registration Process”](#registration-process) Class maps are registered during MongoDB initialization: 1. **Discovery**: All `IBsonClassMapFor` implementations are found 2. **Instantiation**: Each class map provider is created 3. **Configuration**: The `Configure` method is called with a `BsonClassMap` 4. **Convention Application**: Configured conventions are applied to the class map 5. **Registration**: The class map is registered with MongoDB’s `BsonClassMap.RegisterClassMap()` ## Advanced Mapping Examples [Section titled “Advanced Mapping Examples”](#advanced-mapping-examples) ### Inheritance Mapping [Section titled “Inheritance Mapping”](#inheritance-mapping) ```csharp public abstract class Document { public DocumentId Id { get; set; } public string Title { get; set; } public DateTime CreatedAt { get; set; } } public class TextDocument : Document { public string Content { get; set; } } public class ImageDocument : Document { public byte[] ImageData { get; set; } public string MimeType { get; set; } } public class DocumentClassMap : IBsonClassMapFor { public void Configure(BsonClassMap classMap) { classMap.AutoMap(); classMap.SetIdMember(classMap.GetMemberMap(d => d.Id)); // Configure inheritance classMap.SetIsRootClass(true); classMap.AddKnownType(typeof(TextDocument)); classMap.AddKnownType(typeof(ImageDocument)); } } public class TextDocumentClassMap : IBsonClassMapFor { public void Configure(BsonClassMap classMap) { classMap.AutoMap(); classMap.SetDiscriminator("text"); } } public class ImageDocumentClassMap : IBsonClassMapFor { public void Configure(BsonClassMap classMap) { classMap.AutoMap(); classMap.SetDiscriminator("image"); // Custom handling for binary data classMap.GetMemberMap(i => i.ImageData) .SetSerializer(new ByteArraySerializer()); } } ``` ### Complex Property Mapping [Section titled “Complex Property Mapping”](#complex-property-mapping) ```csharp public class OrderItem { public ProductId ProductId { get; set; } public int Quantity { get; set; } public decimal UnitPrice { get; set; } public decimal Total => Quantity * UnitPrice; // Computed property } public class Order { public OrderId Id { get; set; } public CustomerId CustomerId { get; set; } public List Items { get; set; } = []; public Address ShippingAddress { get; set; } public decimal Total => Items.Sum(i => i.Total); // Computed property } public class OrderClassMap : IBsonClassMapFor { public void Configure(BsonClassMap classMap) { classMap.AutoMap(); classMap.SetIdMember(classMap.GetMemberMap(o => o.Id)); // Don't serialize computed properties classMap.UnmapMember(o => o.Total); // Custom collection handling classMap.GetMemberMap(o => o.Items) .SetElementName("orderItems") .SetIgnoreIfNull(true); } } public class OrderItemClassMap : IBsonClassMapFor { public void Configure(BsonClassMap classMap) { classMap.AutoMap(); // Don't serialize computed properties classMap.UnmapMember(i => i.Total); // Custom decimal handling classMap.GetMemberMap(i => i.UnitPrice) .SetSerializer(new DecimalSerializer(BsonType.Decimal128)); } } ``` ### Custom Serializers in Class Maps [Section titled “Custom Serializers in Class Maps”](#custom-serializers-in-class-maps) ```csharp public class EventRecord { public EventId Id { get; set; } public EventType Type { get; set; } public object Data { get; set; } // Polymorphic data public Dictionary Metadata { get; set; } public DateTimeOffset Timestamp { get; set; } } public class EventRecordClassMap : IBsonClassMapFor { public void Configure(BsonClassMap classMap) { classMap.AutoMap(); classMap.SetIdMember(classMap.GetMemberMap(e => e.Id)); // Custom polymorphic serialization classMap.GetMemberMap(e => e.Data) .SetSerializer(new ObjectSerializer( type => type == typeof(object), ObjectSerializationOptions.Default)); // Custom dictionary serialization classMap.GetMemberMap(e => e.Metadata) .SetSerializer(new DictionaryInterfaceImplementerSerializer>()); // Use custom DateTime serializer classMap.GetMemberMap(e => e.Timestamp) .SetSerializer(new DateTimeOffsetSupportingBsonDateTimeSerializer()); } } ``` ## Convention Integration [Section titled “Convention Integration”](#convention-integration) Class maps work seamlessly with the convention system: ### Applying Conventions [Section titled “Applying Conventions”](#applying-conventions) After configuration, conventions are automatically applied: ```csharp public void Configure(BsonClassMap classMap) { classMap.AutoMap(); // Your custom configuration... } // Conventions are applied automatically after Configure() returns // This includes naming policies, ignore conventions, etc. ``` ### Extension Method [Section titled “Extension Method”](#extension-method) The framework provides an extension method to manually apply conventions: ```csharp public void Configure(BsonClassMap classMap) { classMap.AutoMap(); // Manual configuration classMap.GetMemberMap(u => u.UserName).SetElementName("custom_name"); // Apply conventions manually if needed classMap.ApplyConventions(); } ``` ## Error Handling [Section titled “Error Handling”](#error-handling) ### Duplicate Registration Prevention [Section titled “Duplicate Registration Prevention”](#duplicate-registration-prevention) Class maps are automatically protected against duplicate registration: ```csharp // This check happens automatically if (BsonClassMap.IsClassMapRegistered(typeof(T))) { return; // Skip if already registered } ``` ### Validation [Section titled “Validation”](#validation) The framework validates class map configurations: ```csharp public class InvalidClassMap : IBsonClassMapFor { public void Configure(BsonClassMap classMap) { // This would throw an exception during registration classMap.SetIdMember(null); } } ``` ## Best Practices [Section titled “Best Practices”](#best-practices) ### Use AutoMap First [Section titled “Use AutoMap First”](#use-automap-first) Always call `AutoMap()` first, then customize: ```csharp public void Configure(BsonClassMap classMap) { classMap.AutoMap(); // Set up defaults first // Then customize as needed classMap.SetIdMember(classMap.GetMemberMap(u => u.Id)); classMap.UnmapMember(u => u.PasswordHash); } ``` ### Organize by Domain [Section titled “Organize by Domain”](#organize-by-domain) Group related class maps together: ```csharp namespace MyApp.MongoDB.UserMappings { public class UserClassMap : IBsonClassMapFor { } public class UserProfileClassMap : IBsonClassMapFor { } public class UserSettingsClassMap : IBsonClassMapFor { } } ``` ### Handle Computed Properties [Section titled “Handle Computed Properties”](#handle-computed-properties) Don’t serialize computed properties: ```csharp public void Configure(BsonClassMap classMap) { classMap.AutoMap(); // These are computed at runtime classMap.UnmapMember(o => o.Total); classMap.UnmapMember(o => o.ItemCount); classMap.UnmapMember(o => o.IsComplete); } ``` ### Use Appropriate Serializers [Section titled “Use Appropriate Serializers”](#use-appropriate-serializers) Choose serializers that match your data requirements: ```csharp // For high-precision decimals classMap.GetMemberMap(p => p.Price) .SetSerializer(new DecimalSerializer(BsonType.Decimal128)); // For large integers classMap.GetMemberMap(p => p.LargeNumber) .SetSerializer(new Int64Serializer(BsonType.Int64)); // For enum values classMap.GetMemberMap(p => p.Status) .SetSerializer(new EnumSerializer(BsonType.String)); ``` ## Testing Class Maps [Section titled “Testing Class Maps”](#testing-class-maps) You can test your class maps to ensure they work correctly: ```csharp [Test] public void should_serialize_user_correctly() { // Arrange var user = new User { Id = UserId.New(), UserName = "testuser", Email = "test@example.com" }; // Act var document = user.ToBsonDocument(); // Assert document.Contains("_id").ShouldBeTrue(); document.Contains("username").ShouldBeTrue(); // Custom element name document.Contains("PasswordHash").ShouldBeFalse(); // Should be ignored } ``` ## Next Steps [Section titled “Next Steps”](#next-steps) * Learn about [Convention Packs](/arc/backend/mongodb/convention-packs/) for system-wide customizations * Explore [Concepts](/arc/backend/mongodb/concepts/) for domain-driven design patterns * Understand [Naming Policies](/arc/backend/mongodb/naming-policies/) for consistent field naming # Concepts Cratis Applications provides seamless integration between [Cratis Concepts](/arc/general/) and MongoDB through automatic serialization support. Concepts are domain-driven design primitives that wrap primitive types with business meaning. ## What are Concepts? [Section titled “What are Concepts?”](#what-are-concepts) Concepts are types that inherit from `ConceptAs` and provide type-safe wrappers around primitive values: ```csharp public record UserId(Guid Value) : ConceptAs(Value) { public static readonly UserId NotSet = new(Guid.Empty); public static implicit operator UserId(Guid value) => new(value); public static UserId New() => new(Guid.NewGuid()); } public record ProductName(string Value) : ConceptAs(Value) { public static readonly ProductName NotSet = new(string.Empty); public static implicit operator ProductName(string value) => new(value); } ``` ## Automatic Serialization [Section titled “Automatic Serialization”](#automatic-serialization) When you call `UseCratisMongoDB()`, all types implementing `ConceptAs` are automatically configured for MongoDB serialization through the `ConceptSerializationProvider`. ### How It Works [Section titled “How It Works”](#how-it-works) The `ConceptSerializer` handles the serialization by: 1. **Detection**: Automatically detects types that implement `ConceptAs` 2. **Unwrapping**: Serializes only the underlying value, not the wrapper 3. **Type Safety**: Ensures type validation during serialization/deserialization 4. **Performance**: Optimized to avoid unnecessary object creation ### Example Usage [Section titled “Example Usage”](#example-usage) ```csharp public class User { public UserId Id { get; set; } public ProductName Name { get; set; } public EmailAddress Email { get; set; } public DateTimeOffset CreatedAt { get; set; } } // Usage var user = new User { Id = UserId.New(), Name = "John Doe", // Implicit conversion Email = "john@example.com", CreatedAt = DateTimeOffset.Now }; // In MongoDB, this will be stored as: // { // "_id": "550e8400-e29b-41d4-a716-446655440000", // "name": "John Doe", // "email": "john@example.com", // "createdAt": ISODate("2024-01-15T10:30:00Z") // } ``` ## Supported Underlying Types [Section titled “Supported Underlying Types”](#supported-underlying-types) The `ConceptSerializer` supports all primitive types that MongoDB can natively handle: ### Numeric Types [Section titled “Numeric Types”](#numeric-types) * `int`, `uint`, `long`, `ulong` * `float`, `double`, `decimal` * `byte`, `sbyte`, `short`, `ushort` ### String and Character Types [Section titled “String and Character Types”](#string-and-character-types) * `string` * `char` ### Date and Time Types [Section titled “Date and Time Types”](#date-and-time-types) * `DateTime` * `DateTimeOffset` (uses the custom serializer) * `DateOnly` (uses the custom serializer) * `TimeOnly` (uses the custom serializer) ### Other Types [Section titled “Other Types”](#other-types) * `bool` * `Guid` * Any type that has a registered MongoDB serializer ## Error Handling [Section titled “Error Handling”](#error-handling) The concept serializer includes robust error handling: ### Type Validation [Section titled “Type Validation”](#type-validation) ```csharp // This will throw TypeIsNotAConcept exception var serializer = new ConceptSerializer(); // Invalid - string is not a concept ``` ### Null Safety [Section titled “Null Safety”](#null-safety) ```csharp public record OptionalId(Guid? Value) : ConceptAs(Value) { public static readonly OptionalId NotSet = new(null); } // Properly handles null values during serialization ``` ## Performance Benefits [Section titled “Performance Benefits”](#performance-benefits) ### Storage Efficiency [Section titled “Storage Efficiency”](#storage-efficiency) Concepts are serialized as their underlying values, meaning: * **No wrapper overhead**: Only the business value is stored * **Native MongoDB types**: Uses optimal BSON types for each primitive * **Index compatibility**: Underlying values can be indexed normally ### Memory Efficiency [Section titled “Memory Efficiency”](#memory-efficiency) * **Lazy initialization**: Concept instances are created only when needed * **Value semantics**: Record-based concepts minimize allocation overhead * **Implicit conversions**: Reduce explicit casting requirements ## Best Practices [Section titled “Best Practices”](#best-practices) ### Use Static Members for Common Values [Section titled “Use Static Members for Common Values”](#use-static-members-for-common-values) ```csharp public record Status(string Value) : ConceptAs(Value) { public static readonly Status Active = new("Active"); public static readonly Status Inactive = new("Inactive"); public static readonly Status Pending = new("Pending"); public static implicit operator Status(string value) => new(value); } ``` ### Implement Validation [Section titled “Implement Validation”](#implement-validation) ```csharp public record EmailAddress(string Value) : ConceptAs(Value) { public EmailAddress(string value) : this(Validate(value)) { } static string Validate(string email) { if (string.IsNullOrWhiteSpace(email)) throw new ArgumentException("Email cannot be empty"); if (!email.Contains('@')) throw new ArgumentException("Invalid email format"); return email; } public static implicit operator EmailAddress(string value) => new(value); } ``` ### Event Source Integration [Section titled “Event Source Integration”](#event-source-integration) For concepts used as Event Source IDs: ```csharp public record CustomerId(Guid Value) : ConceptAs(Value) { public static readonly CustomerId NotSet = new(Guid.Empty); public static implicit operator CustomerId(Guid value) => new(value); public static implicit operator EventSourceId(CustomerId id) => new(id.Value.ToString()); public static CustomerId New() => new(Guid.NewGuid()); } ``` ## Collection Examples [Section titled “Collection Examples”](#collection-examples) Concepts work seamlessly in collections: ```csharp public class Order { public OrderId Id { get; set; } public CustomerId CustomerId { get; set; } public IEnumerable ProductIds { get; set; } public Dictionary Products { get; set; } } // All concept types in collections are automatically handled ``` ## Query Support [Section titled “Query Support”](#query-support) Concepts can be used directly in MongoDB queries: ```csharp var customerId = CustomerId.New(); var orders = await collection .Find(o => o.CustomerId == customerId) .ToListAsync(); // The concept is automatically converted to its underlying value for the query ``` ## Next Steps [Section titled “Next Steps”](#next-steps) * Learn about [Class Mapping](/arc/backend/mongodb/class-mapping/) for complex type configurations * Explore [Convention Packs](/arc/backend/mongodb/convention-packs/) for customizing serialization behavior * Read about [Naming Policies](/arc/backend/mongodb/naming-policies/) for consistent field naming # Convention Packs Convention packs provide a powerful way to apply consistent configuration across all your MongoDB class mappings. Cratis Applications includes a comprehensive system for creating, providing, and filtering convention packs. ## What are Convention Packs? [Section titled “What are Convention Packs?”](#what-are-convention-packs) Convention packs are collections of conventions that MongoDB applies automatically to class maps during registration. They allow you to: * **Apply consistent rules**: Ensure all classes follow the same patterns * **Reduce boilerplate**: Avoid repeating configuration in every class map * **Conditional application**: Apply different rules to different types * **System-wide changes**: Modify behavior across your entire application ## Built-in Convention Packs [Section titled “Built-in Convention Packs”](#built-in-convention-packs) Cratis Applications automatically registers several convention packs: ### Naming Policy Convention [Section titled “Naming Policy Convention”](#naming-policy-convention) Applies your configured naming policy to all property names: ```csharp // Registered automatically with name: "Naming policy convention" RegisterConventionAsPack( conventionPackFilters, NamingPolicyNameConvention.ConventionName, new NamingPolicyNameConvention() ); ``` ### Ignore Extra Elements [Section titled “Ignore Extra Elements”](#ignore-extra-elements) Ignores unknown properties during deserialization: ```csharp // Registered automatically with name: "Ignore extra elements convention" RegisterConventionAsPack( conventionPackFilters, ConventionPacks.IgnoreExtraElements, new IgnoreExtraElementsConvention(true) ); ``` ### Read Model Collections Never Null [Section titled “Read Model Collections Never Null”](#read-model-collections-never-null) Materializes a `[ReadModel]`’s declared **non-nullable** collection members as empty collections instead of `null`: ```csharp // Registered automatically with name: "Read model collections never null convention", // scoped to types marked with [ReadModel] RegisterConventionAsPack( conventionPackFilters, ConventionPacks.ReadModelCollectionsNeverNull, new ReadModelCollectionsNeverNullConvention(), type => type.IsReadModel() ); ``` #### Why it exists — the driver is a separate boundary from Chronicle [Section titled “Why it exists — the driver is a separate boundary from Chronicle”](#why-it-exists--the-driver-is-a-separate-boundary-from-chronicle) A read model that declares a child collection as a non-nullable `IEnumerable` promises the type system that the value is there. The store is free to disagree. Chronicle’s read model sink writes **no field at all** for a child collection that has never had an element, and can write an **explicit `null`** for one whose last element went away. Nullable reference analysis has already concluded the member can never be `null`, so nothing warns, and an unguarded `.Any()` / `.All()` / `.Select()` throws at runtime. Chronicle closes this for **its own reader** with a `JsonTypeInfo` modifier on its client’s `JsonSerializerOptions`. That fix lives at Chronicle’s serialization boundary and **does not reach the MongoDB driver**. Reading a read model through `IMongoCollection` — the sanctioned way to query by anything other than the key, and what Arc’s own server-side paging runs on — goes through the driver instead. This convention is the same guarantee, restated where the driver can honor it. #### What it covers [Section titled “What it covers”](#what-it-covers) Both shapes a store can leave behind, through two different mechanisms: | Stored shape | Mechanism | Result for a non-nullable member | | ----------------------------------------- | -------------------------------------- | -------------------------------- | | The field is **absent** from the document | a default value on the member map | an empty collection | | The field is present and holds **`null`** | a serializer wrapping the member’s own | an empty collection | Both halves are needed: the driver never invokes a member’s serializer for an element that is not in the document, and a default value is never consulted for one that is. #### What it deliberately leaves alone [Section titled “What it deliberately leaves alone”](#what-it-deliberately-leaves-alone) * **Nullable collections.** A member declared `IEnumerable?` keeps the distinction between “no collection” and “an empty collection”, because that model asked for it. Absent stays `null`; a stored `null` stays `null`. * **Dictionaries.** `IDictionary`, `IReadOnlyDictionary`, and `Dictionary` are untouched. * **`string`.** It is an `IEnumerable`, and the classic trap here would be handing it an empty list. * **Types without `[ReadModel]`.** The scoping is what keeps this from redefining what `null` means for every BSON type in the process. * **Writing.** Serialization is unchanged — a collection that is `null` in memory is still written as `null`. #### Supported collection shapes [Section titled “Supported collection shapes”](#supported-collection-shapes) `IEnumerable`, `ICollection`, `IList`, `IReadOnlyCollection`, `IReadOnlyList`, `List`, `ISet`, `IReadOnlySet`, `HashSet`, and single-dimension arrays `T[]`. Interface-typed members are filled with a `List` (or a `HashSet` for the set interfaces). Any other collection type is left as the driver would leave it — the empty value is assigned straight into the member, so a shape that cannot be constructed safely is not guessed at. #### Opting out [Section titled “Opting out”](#opting-out) Per type, through the standard mechanism: ```csharp [IgnoreConventions(ConventionPacks.ReadModelCollectionsNeverNull)] [ReadModel] public record RawProjection(string Id, IEnumerable Children); ``` Per member, by stating the default the member wants — the convention skips any member carrying a `[BsonDefaultValue]`, for an absent element and a stored `null` alike: ```csharp [ReadModel] public record Parent(string Id, [property: BsonDefaultValue(null)] IEnumerable Children); ``` #### Things to know [Section titled “Things to know”](#things-to-know) * **The default replaces whatever a property initializer set.** A member written as `public IEnumerable Children { get; set; } = [];` still comes back empty, but as a `List` rather than the `Child[]` the collection expression produced. A member whose initializer sets a **meaningful non-empty** default would be silently emptied — use one of the opt-outs above for that member. * **Registration order matters.** A class map freezes its conventions the first time it is built, so this is registered during `AddCratisMongoDB`, before anything can deserialize. ## Creating Convention Pack Providers [Section titled “Creating Convention Pack Providers”](#creating-convention-pack-providers) To provide your own convention packs, implement `ICanProvideMongoDBConventionPacks`: ```csharp public interface ICanProvideMongoDBConventionPacks { IEnumerable Provide(); } ``` ### Example Provider [Section titled “Example Provider”](#example-provider) ```csharp public class CustomConventionPackProvider : ICanProvideMongoDBConventionPacks { public IEnumerable Provide() { // Read-only conventions yield return new MongoDBConventionPackDefinition( "ReadOnly Properties", new ConventionPack { new ReadOnlyPropertiesConvention() } ); // Enum string serialization yield return new MongoDBConventionPackDefinition( "Enum Conventions", new ConventionPack { new EnumRepresentationConvention(BsonType.String) } ); // Custom discriminator handling yield return new MongoDBConventionPackDefinition( "Custom Discriminator", new ConventionPack { new CustomDiscriminatorConvention( CustomObjectDiscriminatorConvention.Instance, GetTypesWithExistingDiscriminators() ) } ); } private static IEnumerable GetTypesWithExistingDiscriminators() { // Return types that already have discriminator configuration yield return typeof(BaseDocument); yield return typeof(AuditableEntity); } } ``` ### Advanced Convention Examples [Section titled “Advanced Convention Examples”](#advanced-convention-examples) ```csharp public class DomainConventionPackProvider : ICanProvideMongoDBConventionPacks { public IEnumerable Provide() { // ID field conventions yield return new MongoDBConventionPackDefinition( "ID Field Conventions", new ConventionPack { new NamedIdMemberConvention("Id", "id", "_id"), new StringObjectIdIdGeneratorConvention() } ); // Ignore null values yield return new MongoDBConventionPackDefinition( "Ignore Null Values", new ConventionPack { new IgnoreIfNullConvention(true) } ); // Custom date handling yield return new MongoDBConventionPackDefinition( "Date Conventions", new ConventionPack { new DateTimeSerializationOptionsConvention( DateTimeKind.Utc, BsonType.DateTime ) } ); } } ``` ## Filtering Conventions [Section titled “Filtering Conventions”](#filtering-conventions) You can control which types convention packs apply to using filters. ### ICanFilterMongoDBConventionPacksForType [Section titled “ICanFilterMongoDBConventionPacksForType”](#icanfiltermongodbconventionpacksfortype) Implement this interface to create custom filters: ```csharp public interface ICanFilterMongoDBConventionPacksForType { bool ShouldInclude(string conventionPackName, IConventionPack conventionPack, Type type); } ``` ### Example Filters [Section titled “Example Filters”](#example-filters) ```csharp public class DomainModelFilter : ICanFilterMongoDBConventionPacksForType { public bool ShouldInclude(string conventionPackName, IConventionPack conventionPack, Type type) { // Only apply naming conventions to domain models if (conventionPackName == NamingPolicyNameConvention.ConventionName) { return type.Namespace?.Contains("Domain.Models") == true; } return true; } } public class NoConventionsForDTOs : ICanFilterMongoDBConventionPacksForType { public bool ShouldInclude(string conventionPackName, IConventionPack conventionPack, Type type) { // Don't apply any conventions to DTOs if (type.Name.EndsWith("DTO") || type.Name.EndsWith("Dto")) { return false; } return true; } } public class LegacySystemFilter : ICanFilterMongoDBConventionPacksForType { public bool ShouldInclude(string conventionPackName, IConventionPack conventionPack, Type type) { // Don't apply naming conventions to legacy types if (conventionPackName == NamingPolicyNameConvention.ConventionName && type.Namespace?.Contains("Legacy") == true) { return false; } return true; } } ``` ## IgnoreConventions Attribute [Section titled “IgnoreConventions Attribute”](#ignoreconventions-attribute) For fine-grained control, use the `IgnoreConventions` attribute on specific types: ### Ignore All Conventions [Section titled “Ignore All Conventions”](#ignore-all-conventions) ```csharp [IgnoreConventions] public class RawDocument { public string _id { get; set; } // Keep exact field names public string user_name { get; set; } // No naming policy applied public object extra_data { get; set; } // No serialization conventions } ``` ### Ignore Specific Conventions [Section titled “Ignore Specific Conventions”](#ignore-specific-conventions) ```csharp [IgnoreConventions(NamingPolicyNameConvention.ConventionName)] public class ExactFieldNames { public string UserName { get; set; } // Stored as "UserName" public string EmailAddr { get; set; } // Stored as "EmailAddr" } [IgnoreConventions(ConventionPacks.IgnoreExtraElements)] public class StrictDocument { public string Name { get; set; } // Will throw exception if extra fields are present during deserialization } ``` ### Multiple Ignore Attributes [Section titled “Multiple Ignore Attributes”](#multiple-ignore-attributes) ```csharp [IgnoreConventions(NamingPolicyNameConvention.ConventionName)] [IgnoreConventions("Custom Enum Convention")] public class SpecialDocument { public string PropertyName { get; set; } // No naming policy public MyEnum Status { get; set; } // No enum convention } ``` ## Built-in Convention Pack Names [Section titled “Built-in Convention Pack Names”](#built-in-convention-pack-names) The framework defines constants for well-known convention pack names: ```csharp public static class ConventionPacks { public const string IgnoreExtraElements = "Ignore extra elements convention"; public const string ReadModelCollectionsNeverNull = "Read model collections never null convention"; } public class NamingPolicyNameConvention { public const string ConventionName = "Naming policy convention"; } ``` ## Advanced Convention Pack Examples [Section titled “Advanced Convention Pack Examples”](#advanced-convention-pack-examples) ### Audit Field Conventions [Section titled “Audit Field Conventions”](#audit-field-conventions) ```csharp public class AuditConventionPackProvider : ICanProvideMongoDBConventionPacks { public IEnumerable Provide() { yield return new MongoDBConventionPackDefinition( "Audit Fields", new ConventionPack { new AuditFieldConvention() } ); } } public class AuditFieldConvention : ConventionBase, IMemberMapConvention { public void Apply(BsonMemberMap memberMap) { var memberName = memberMap.MemberName; // Auto-configure audit fields if (memberName == "CreatedAt" || memberName == "UpdatedAt") { memberMap.SetSerializer(new DateTimeOffsetSupportingBsonDateTimeSerializer()); if (memberName == "CreatedAt") { memberMap.SetIgnoreIfDefault(true); } } // Configure user audit fields if (memberName == "CreatedBy" || memberName == "UpdatedBy") { memberMap.SetIgnoreIfNull(true); } } } ``` ### Validation Conventions [Section titled “Validation Conventions”](#validation-conventions) ```csharp public class ValidationConventionPackProvider : ICanProvideMongoDBConventionPacks { public IEnumerable Provide() { yield return new MongoDBConventionPackDefinition( "Required Fields", new ConventionPack { new RequiredFieldConvention() } ); } } public class RequiredFieldConvention : ConventionBase, IMemberMapConvention { public void Apply(BsonMemberMap memberMap) { var memberInfo = memberMap.MemberInfo; // Check for Required attribute if (memberInfo.GetCustomAttribute() != null) { memberMap.SetIgnoreIfDefault(false); memberMap.SetIgnoreIfNull(false); } } } ``` ## Registration and Lifecycle [Section titled “Registration and Lifecycle”](#registration-and-lifecycle) ### Automatic Discovery [Section titled “Automatic Discovery”](#automatic-discovery) Convention pack providers are automatically discovered during setup: ```csharp // This happens during UseCratisMongoDB() var types = Types.Instance; var providers = types.FindMultiple(); var filters = types.FindMultiple(); // Providers and filters are registered automatically ``` ### Registration Order [Section titled “Registration Order”](#registration-order) Convention packs are registered in the order they’re provided. If multiple conventions affect the same aspect, later conventions may override earlier ones. ### Filter Application [Section titled “Filter Application”](#filter-application) For each convention pack, all filters are consulted: ```csharp static bool ShouldInclude( IEnumerable filters, string conventionPackName, IConventionPack conventionPack, Type type) { // All filters must return true for the convention pack to be applied return filters.All(filter => filter.ShouldInclude(conventionPackName, conventionPack, type)); } ``` ## Performance Considerations [Section titled “Performance Considerations”](#performance-considerations) ### Filter Efficiency [Section titled “Filter Efficiency”](#filter-efficiency) Convention pack filters are called for every type, so keep them efficient: ```csharp // Good: Simple, fast checks public bool ShouldInclude(string conventionPackName, IConventionPack conventionPack, Type type) { return !type.Name.EndsWith("DTO"); } // Avoid: Expensive operations public bool ShouldInclude(string conventionPackName, IConventionPack conventionPack, Type type) { return !type.GetCustomAttributes().Any(attr => attr is DTOAttribute); } ``` ### Caching Results [Section titled “Caching Results”](#caching-results) Consider caching filter results for frequently-checked types: ```csharp public class CachedFilter : ICanFilterMongoDBConventionPacksForType { private static readonly ConcurrentDictionary<(string, Type), bool> _cache = new(); public bool ShouldInclude(string conventionPackName, IConventionPack conventionPack, Type type) { return _cache.GetOrAdd((conventionPackName, type), key => ComputeShouldInclude(key.Item1, conventionPack, key.Item2)); } private bool ComputeShouldInclude(string conventionPackName, IConventionPack conventionPack, Type type) { // Expensive computation here return ExpensiveCheck(type); } } ``` ## Testing Convention Packs [Section titled “Testing Convention Packs”](#testing-convention-packs) You can test your convention packs to ensure they work correctly: ```csharp [Test] public void should_apply_naming_convention_to_domain_models() { // Arrange var classMap = new BsonClassMap(); classMap.AutoMap(); // Act classMap.ApplyConventions(); // Assert var memberMap = classMap.GetMemberMap(m => m.PropertyName); memberMap.ElementName.ShouldEqual("propertyName"); // camelCase applied } [Test] public void should_ignore_conventions_when_attribute_present() { // Arrange var classMap = new BsonClassMap(); classMap.AutoMap(); // Act classMap.ApplyConventions(); // Assert var memberMap = classMap.GetMemberMap(m => m.PropertyName); memberMap.ElementName.ShouldEqual("PropertyName"); // No naming convention applied } ``` ## Best Practices [Section titled “Best Practices”](#best-practices) ### Keep Conventions Simple [Section titled “Keep Conventions Simple”](#keep-conventions-simple) Each convention should have a single responsibility: ```csharp // Good: Single purpose public class DateTimeUtcConvention : IMemberMapConvention { public void Apply(BsonMemberMap memberMap) { if (memberMap.MemberType == typeof(DateTime)) { memberMap.SetSerializer(new DateTimeSerializer(DateTimeKind.Utc)); } } } // Avoid: Multiple concerns public class MegaConvention : IMemberMapConvention { public void Apply(BsonMemberMap memberMap) { // Handles dates, strings, numbers, etc. - too complex } } ``` ### Use Descriptive Names [Section titled “Use Descriptive Names”](#use-descriptive-names) Convention pack names should clearly indicate their purpose: ```csharp // Good: Clear naming "Audit Field Conventions" "Required Field Validation" "Legacy System Compatibility" // Avoid: Vague naming "Custom Convention" "Special Rules" "Fixes" ``` ### Document Filter Logic [Section titled “Document Filter Logic”](#document-filter-logic) Make filter logic clear and well-documented: ```csharp public class ApiModelFilter : ICanFilterMongoDBConventionPacksForType { /// /// Applies naming conventions only to API models (types ending with "ApiModel") /// and excludes internal types from convention processing. /// public bool ShouldInclude(string conventionPackName, IConventionPack conventionPack, Type type) { if (conventionPackName == NamingPolicyNameConvention.ConventionName) { return type.Name.EndsWith("ApiModel") && !type.IsNotPublic; } return true; } } ``` ## Next Steps [Section titled “Next Steps”](#next-steps) * Learn about [Class Mapping](/arc/backend/mongodb/class-mapping/) for type-specific configurations * Explore [Concepts](/arc/backend/mongodb/concepts/) for domain-driven design patterns * Understand [Naming Policies](/arc/backend/mongodb/naming-policies/) for consistent property naming * Review [Serializers](/arc/backend/mongodb/serializers/) for custom type handling # Geospatial Types > Store and serialize geographic data with Point, LineString, and Polygon types Cratis provides first-class support for geospatial data in MongoDB using types from `Cratis.Geospatial`. The serializers follow the GeoJSON standard, making them compatible with MongoDB’s geospatial query operators and external mapping services. ## Supported Types [Section titled “Supported Types”](#supported-types) * **[Point](/arc/backend/mongodb/geospatial/point/)** — Single geographic coordinates (longitude, latitude) * **[LineString](/arc/backend/mongodb/geospatial/linestring/)** — Routes and paths (ordered sequences of points) * **[Polygon](/arc/backend/mongodb/geospatial/polygon/)** — Geographic areas with optional interior boundaries (holes) ## GeoJSON Compatibility [Section titled “GeoJSON Compatibility”](#geojson-compatibility) All geospatial types are serialized in GeoJSON format, enabling: * **MongoDB Spatial Queries** — Use `$near`, `$geoWithin`, and other spatial operators * **Interoperability** — Work seamlessly with mapping services and GIS tools * **Standards Compliance** — Follow industry-standard GeoJSON specification ## Getting Started [Section titled “Getting Started”](#getting-started) Choose the type that matches your use case: * Use **Point** for storing single locations (stores, users, events) * Use **LineString** for routes, paths, or trajectories * Use **Polygon** for service areas, regions, or geographic boundaries See the individual type documentation for implementation examples and best practices. # LineString Serializer > Store routes and paths (ordered point sequences) in MongoDB The LineString serializer stores ordered sequences of connected points as GeoJSON LineString objects, ideal for representing routes, paths, and trajectories. ## Usage [Section titled “Usage”](#usage) ```csharp using Cratis.Geospatial; public class DeliveryRoute { public ObjectId Id { get; set; } public string RouteName { get; set; } public LineString Path { get; set; } public DateTime CreatedAt { get; set; } } // Create a line string var points = new[] { new Point(-122.4194, 37.7749), new Point(-122.4185, 37.7750), new Point(-122.4170, 37.7755) }; var route = new LineString(points); ``` ## Storage Format [Section titled “Storage Format”](#storage-format) LineStrings are serialized as GeoJSON LineString documents: ```json { "_id": ObjectId("..."), "routeName": "Downtown Loop", "path": { "type": "LineString", "coordinates": [ [-122.4194, 37.7749], [-122.4185, 37.7750], [-122.4170, 37.7755] ] }, "createdAt": ISODate("2024-06-09T...") } ``` ## Querying [Section titled “Querying”](#querying) ### Finding Routes That Pass Through a Point [Section titled “Finding Routes That Pass Through a Point”](#finding-routes-that-pass-through-a-point) ```csharp // Create a geospatial index collection.Indexes.CreateOne( new CreateIndexModel( Builders.IndexKeys.Geo2DSphere(r => r.Path) ) ); // Find routes near a specific point var routesNearby = await collection.Find( Builders.Filter.Near( r => r.Path, new Point(-122.418, 37.775), maxDistance: 1000 ) ).ToListAsync(); ``` ### Finding Routes Intersecting an Area [Section titled “Finding Routes Intersecting an Area”](#finding-routes-intersecting-an-area) ```csharp // Find routes within a polygon boundary var routesInArea = await collection.Find( Builders.Filter.GeoWithin( r => r.Path, polygon ) ).ToListAsync(); ``` ## Common Patterns [Section titled “Common Patterns”](#common-patterns) ### Tracking User Movement [Section titled “Tracking User Movement”](#tracking-user-movement) ```csharp public class UserTrack { public ObjectId Id { get; set; } public ObjectId UserId { get; set; } public LineString Path { get; set; } public DateTime StartTime { get; set; } public DateTime EndTime { get; set; } } // Record a user's movement over time var trackPoints = new[] { new Point(-122.419, 37.774), new Point(-122.418, 37.775), new Point(-122.417, 37.776) }; var track = new UserTrack { UserId = userId, Path = new LineString(trackPoints), StartTime = DateTime.UtcNow.AddHours(-1), EndTime = DateTime.UtcNow }; ``` ## Best Practices [Section titled “Best Practices”](#best-practices) * **Minimum Points**: Ensure at least 2 points in the sequence * **Coordinate Order**: Use `[longitude, latitude]` for each point * **Create Indexes**: Add `2dsphere` indexes for spatial query performance * **Ordered Sequence**: Points should represent movement in chronological or logical order * **Nullable Support**: Use `LineString?` for optional paths ## Limitations [Section titled “Limitations”](#limitations) * MongoDB’s spatial queries treat LineStrings as paths but don’t enforce properties like “no self-intersection” * Distance calculations along the path require application-level computation * Query results with `$near` return distance to the nearest point on the line, not along it ## Related [Section titled “Related”](#related) * [Point](/arc/backend/mongodb/geospatial/point/) — Individual coordinates * [Polygon](/arc/backend/mongodb/geospatial/polygon/) — Geographic areas # Point Serializer > Store single geographic coordinates (longitude, latitude) in MongoDB The Point serializer stores geographic coordinates as GeoJSON Point objects, compatible with MongoDB’s geospatial queries. ## Usage [Section titled “Usage”](#usage) ```csharp using Cratis.Geospatial; public class Store { public ObjectId Id { get; set; } public string Name { get; set; } public Point Location { get; set; } } // Create a point var point = new Point(longitude: -122.4194, latitude: 37.7749); ``` ## Storage Format [Section titled “Storage Format”](#storage-format) Points are serialized as GeoJSON Point documents: ```json { "_id": ObjectId("..."), "name": "Downtown Store", "location": { "type": "Point", "coordinates": [-122.4194, 37.7749] } } ``` The `coordinates` array follows GeoJSON format: `[longitude, latitude]`. ## Querying [Section titled “Querying”](#querying) ### Finding Nearby Points [Section titled “Finding Nearby Points”](#finding-nearby-points) Use MongoDB’s `$near` operator with geospatial indexes: ```csharp // Create a geospatial index first collection.Indexes.CreateOne( new CreateIndexModel( Builders.IndexKeys.Geo2DSphere(s => s.Location) ) ); // Query for stores near a point var nearbyStores = await collection.Find( Builders.Filter.Near( s => s.Location, new Point(-122.4, 37.78), maxDistance: 5000 // 5km in meters ) ).ToListAsync(); ``` ### Checking if Point is Within Area [Section titled “Checking if Point is Within Area”](#checking-if-point-is-within-area) ```csharp // Find points within a polygon boundary var storesInArea = await collection.Find( Builders.Filter.GeoWithin( s => s.Location, polygon ) ).ToListAsync(); ``` ## Best Practices [Section titled “Best Practices”](#best-practices) * **Coordinate Order**: Always use `[longitude, latitude]` (not latitude, longitude) * **Create Indexes**: Create `2dsphere` indexes on Point properties for efficient spatial queries * **Null Handling**: Points can be nullable (`Point?`) for optional locations * **Validation**: Ensure longitude is between -180 and 180, latitude between -90 and 90 ## Related [Section titled “Related”](#related) * [LineString](/arc/backend/mongodb/geospatial/linestring/) — Routes and paths * [Polygon](/arc/backend/mongodb/geospatial/polygon/) — Geographic areas # Polygon Serializer > Store geographic areas with optional interior boundaries (holes) in MongoDB The Polygon serializer stores geographic boundaries as GeoJSON Polygon objects, supporting complex shapes with interior holes for representing service areas, regions, and geographic boundaries. ## Usage [Section titled “Usage”](#usage) ```csharp using Cratis.Geospatial; public class ServiceArea { public ObjectId Id { get; set; } public string AreaName { get; set; } public Polygon Boundary { get; set; } } // Create a simple polygon var shellPoints = new[] { new Point(-122.4194, 37.7749), new Point(-122.4170, 37.7755), new Point(-122.4160, 37.7740), new Point(-122.4194, 37.7749) // Must close the ring }; var simpleArea = new ServiceArea { AreaName = "Downtown", Boundary = new Polygon(new LinearRing(shellPoints), holes: []) }; // Create a polygon with a hole (excluded zone) var holePoints = new[] { new Point(-122.4180, 37.7750), new Point(-122.4175, 37.7752), new Point(-122.4170, 37.7748), new Point(-122.4180, 37.7750) // Must close the ring }; var areaWithHole = new ServiceArea { AreaName = "Downtown (with exclusion)", Boundary = new Polygon( new LinearRing(shellPoints), holes: [new LinearRing(holePoints)] ) }; ``` ## Storage Format [Section titled “Storage Format”](#storage-format) Polygons are serialized as GeoJSON Polygon documents: ```json { "_id": ObjectId("..."), "areaName": "Downtown", "boundary": { "type": "Polygon", "coordinates": [ [ [-122.4194, 37.7749], [-122.4170, 37.7755], [-122.4160, 37.7740], [-122.4194, 37.7749] ] ] } } ``` With a hole: ```json { "boundary": { "type": "Polygon", "coordinates": [ [ [-122.4194, 37.7749], [-122.4170, 37.7755], [-122.4160, 37.7740], [-122.4194, 37.7749] ], [ [-122.4180, 37.7750], [-122.4175, 37.7752], [-122.4170, 37.7748], [-122.4180, 37.7750] ] ] } } ``` The first coordinate array is the exterior shell; subsequent arrays are interior holes. ## Querying [Section titled “Querying”](#querying) ### Finding Points Within Area [Section titled “Finding Points Within Area”](#finding-points-within-area) ```csharp // Create a geospatial index collection.Indexes.CreateOne( new CreateIndexModel( Builders.IndexKeys.Geo2DSphere(a => a.Boundary) ) ); // Find areas containing a point var areasContainingPoint = await collection.Find( Builders.Filter.GeoWithin( a => a.Boundary, point ) ).ToListAsync(); ``` ### Finding Overlapping Polygons [Section titled “Finding Overlapping Polygons”](#finding-overlapping-polygons) ```csharp // Find areas that intersect with a given polygon var overlappingAreas = await collection.Find( Builders.Filter.Intersects( a => a.Boundary, queryPolygon ) ).ToListAsync(); ``` ## Common Patterns [Section titled “Common Patterns”](#common-patterns) ### Service Territory Definition [Section titled “Service Territory Definition”](#service-territory-definition) ```csharp public class ServiceTerritory { public ObjectId Id { get; set; } public ObjectId CompanyId { get; set; } public string TerritoryName { get; set; } public Polygon ServingArea { get; set; } public List ExcludedZones { get; set; } = []; } // Single territory with excluded zones represented in the polygon var territory = new ServiceTerritory { CompanyId = companyId, TerritoryName = "Northern District", ServingArea = new Polygon(shellRing, holes: excludedZones) }; ``` ### Multi-Part Regions (Using Multiple Documents) [Section titled “Multi-Part Regions (Using Multiple Documents)”](#multi-part-regions-using-multiple-documents) ```csharp // For disconnected regions, store as separate documents // or use MultiPolygon pattern (multiple Polygons in a collection) var islands = new[] { new ServiceArea { AreaName = "Island A", Boundary = polygonA }, new ServiceArea { AreaName = "Island B", Boundary = polygonB } }; ``` ## Polygon Requirements [Section titled “Polygon Requirements”](#polygon-requirements) When creating Polygon instances, ensure: * **Closed Rings**: First and last points of each ring must be identical * **Minimum Points**: Each ring must have at least 4 points (3 unique + 1 closing duplicate) * **Winding Order**: Exterior ring vertices should follow right-hand rule (counter-clockwise when viewed from above) * **No Self-Intersection**: Rings should not cross themselves * **Hole Ordering**: Hole rings should follow clockwise winding (interior winding) ## Best Practices [Section titled “Best Practices”](#best-practices) * **Create Indexes**: Add `2dsphere` indexes on Polygon properties for spatial query performance * **Use Holes for Exclusions**: Instead of multiple polygons, use holes for interior exclusions * **Validate Geometry**: Ensure polygons are valid GeoJSON before storing * **Nullable Support**: Use `Polygon?` for optional boundaries * **Query Strategically**: Use `$geoWithin` to find points/areas within a region ## Performance Considerations [Section titled “Performance Considerations”](#performance-considerations) * **Geospatial Indexes**: Essential for good query performance on large datasets * **Polygon Complexity**: Very complex polygons (many points) may slow queries * **Batch Operations**: Group spatial queries when checking multiple points ## Related [Section titled “Related”](#related) * [Point](/arc/backend/mongodb/geospatial/point/) — Individual coordinates * [LineString](/arc/backend/mongodb/geospatial/linestring/) — Routes and paths # Getting Started MongoDB integration in Cratis Applications is designed to be simple to set up while providing powerful defaults that work out of the box. ## Basic Setup [Section titled “Basic Setup”](#basic-setup) The simplest way to add MongoDB support to your application is through the configuration extension methods provided for `WebApplicationBuilder` or `HostBuilder`. ### With WebApplicationBuilder [Section titled “With WebApplicationBuilder”](#with-webapplicationbuilder) ```csharp var builder = WebApplication.CreateBuilder(args); builder.AddCratisArc(); builder.UseCratisMongoDB(); var app = builder.Build(); app.UseCratisArc(); ``` ### With HostBuilder [Section titled “With HostBuilder”](#with-hostbuilder) ```csharp var host = Host.CreateDefaultBuilder(args) .AddCratisArc() .UseCratisMongoDB() .Build(); ``` ## What Gets Configured [Section titled “What Gets Configured”](#what-gets-configured) When you call `UseCratisMongoDB()`, the following components are automatically configured: ### Default Serializers [Section titled “Default Serializers”](#default-serializers) * **DateTimeOffset**: Proper handling of timezone information * **DateOnly**: .NET 6+ date-only types * **TimeOnly**: .NET 6+ time-only types * **System.Type**: Serialization of .NET type information * **Guid**: Configured to use the standard .NET representation instead of MongoDB’s legacy format * **Cratis Concepts**: Automatic serialization for all types implementing `ConceptAs` ### Convention Packs [Section titled “Convention Packs”](#convention-packs) * **Naming Policy Convention**: Applies your configured naming policy to all properties * **Ignore Extra Elements**: Ignores unknown properties during deserialization ### Automatic Discovery [Section titled “Automatic Discovery”](#automatic-discovery) * **Class Maps**: All implementations of `IBsonClassMapFor` are automatically discovered and registered * **Convention Pack Providers**: All implementations of `ICanProvideMongoDBConventionPacks` are discovered * **Convention Pack Filters**: All implementations of `ICanFilterMongoDBConventionPacksForType` are discovered ## Configuration Options [Section titled “Configuration Options”](#configuration-options) You can customize the MongoDB setup by providing configuration options: ```csharp builder.UseCratisMongoDB(configureMongoDB: mongoBuilder => { mongoBuilder .WithCamelCaseNamingPolicy() .WithServerResolver() .WithDatabaseResolver(); }); ``` ## Connection Configuration [Section titled “Connection Configuration”](#connection-configuration) The framework uses resolver patterns for determining connection details: ### Server Connection [Section titled “Server Connection”](#server-connection) Implement `IMongoServerResolver` to provide connection string logic: ```csharp public class MyServerResolver : IMongoServerResolver { public MongoUrl Resolve() { return new MongoUrl("mongodb://localhost:27017"); } } ``` ### Database Name [Section titled “Database Name”](#database-name) Implement `IMongoDatabaseNameResolver` to provide database naming logic: ```csharp public class MyDatabaseNameResolver : IMongoDatabaseNameResolver { public string Resolve() { return "MyApplicationDatabase"; } } ``` ## Sane Defaults [Section titled “Sane Defaults”](#sane-defaults) ### Guid Representation [Section titled “Guid Representation”](#guid-representation) One of the most common pain points when working with MongoDB and .NET is Guid serialization. By default, MongoDB stores Guids using a legacy binary format that can cause issues. Cratis Applications configures Guids to use the standard .NET representation, making them more predictable and interoperable. ### Error Handling [Section titled “Error Handling”](#error-handling) The framework includes sensible error handling for common configuration issues: * Missing server resolver configuration * Missing database name resolver configuration * Missing naming policy configuration These will throw descriptive exceptions with guidance on how to fix the configuration. ## Next Steps [Section titled “Next Steps”](#next-steps) * Learn about [Serializers](/arc/backend/mongodb/serializers/) for custom type handling * Explore [Concepts](/arc/backend/mongodb/concepts/) for domain-driven design patterns * Configure [Naming Policies](/arc/backend/mongodb/naming-policies/) for consistent property naming * Set up [Class Mapping](/arc/backend/mongodb/class-mapping/) for complex type mappings * Implement [Convention Packs](/arc/backend/mongodb/convention-packs/) for advanced customization # Naming Policies Naming policies in Cratis Applications control how collection names and property names are transformed when serializing to MongoDB. This ensures consistent naming conventions across your database. ## Overview [Section titled “Overview”](#overview) Collection names and their members are named based on a naming policy (`INamingPolicy`). The framework provides flexible configuration options and built-in policies for common scenarios. ## Default Behavior [Section titled “Default Behavior”](#default-behavior) The default naming policy does not alter the input, giving you the names exactly as defined in your types: ```csharp public class UserAccount { public string UserName { get; set; } // Stored as: "UserName" public string EmailAddress { get; set; } // Stored as: "EmailAddress" public DateTime CreatedDate { get; set; } // Stored as: "CreatedDate" } ``` ## Built-in Naming Policies [Section titled “Built-in Naming Policies”](#built-in-naming-policies) ### Camel Case Policy [Section titled “Camel Case Policy”](#camel-case-policy) The most common naming policy is camel case, which converts PascalCase property names to camelCase: ```csharp builder.UseCratisMongoDB(configureMongoDB: builder => builder.WithCamelCaseNamingPolicy()); ``` With camel case policy: ```csharp public class UserAccount { public string UserName { get; set; } // Stored as: "userName" public string EmailAddress { get; set; } // Stored as: "emailAddress" public DateTime CreatedDate { get; set; } // Stored as: "createdDate" } ``` ## Custom Naming Policies [Section titled “Custom Naming Policies”](#custom-naming-policies) You can create your own naming policy by implementing `INamingPolicy`. The interface has two members: `GetPropertyName` transforms a member name, and `GetReadModelName` transforms a type into a collection name: ```csharp public class SnakeCaseNamingPolicy : INamingPolicy { public string GetPropertyName(string name) => ToSnakeCase(name); public string GetReadModelName(Type readModelType) => ToSnakeCase(readModelType.Name); static string ToSnakeCase(string name) { if (string.IsNullOrEmpty(name)) return name; var result = new StringBuilder(); for (int i = 0; i < name.Length; i++) { if (i > 0 && char.IsUpper(name[i])) result.Append('_'); result.Append(char.ToLower(name[i])); } return result.ToString(); } } ``` ### Registering Custom Policies [Section titled “Registering Custom Policies”](#registering-custom-policies) Register your custom naming policy by passing an instance to `WithNamingPolicy`: ```csharp builder.UseCratisMongoDB(configureMongoDB: builder => builder.WithNamingPolicy(new SnakeCaseNamingPolicy())); ``` This also works when your naming policy requires constructor parameters — just construct the instance with the values you need: ```csharp var namingPolicy = new CustomNamingPolicy(prefix: "app_", suffix: "_v1"); builder.UseCratisMongoDB(configureMongoDB: builder => builder.WithNamingPolicy(namingPolicy)); ``` ## How Naming Policies Work [Section titled “How Naming Policies Work”](#how-naming-policies-work) ### Convention Integration [Section titled “Convention Integration”](#convention-integration) Naming policies are applied through the `NamingPolicyNameConvention`, which is automatically registered as a convention pack. This convention: 1. **Applies to all members**: Processes every property and field in your classes 2. **Uses configured policy**: Applies the naming policy you’ve configured 3. **Integrates with filtering**: Respects convention pack filters and ignore attributes ### Property Name Transformation [Section titled “Property Name Transformation”](#property-name-transformation) The convention applies to: * **Public properties**: All public get/set properties * **Public fields**: Public field members (if configured) * **Nested objects**: Properties within embedded documents * **Collection elements**: Properties of objects within arrays ## Configuration Examples [Section titled “Configuration Examples”](#configuration-examples) ### Multiple Policies [Section titled “Multiple Policies”](#multiple-policies) You can create policies that combine multiple transformations: ```csharp public class CompoundNamingPolicy : INamingPolicy { private readonly INamingPolicy[] _policies; public CompoundNamingPolicy(params INamingPolicy[] policies) { _policies = policies; } public string GetPropertyName(string name) => _policies.Aggregate(name, (current, policy) => policy.GetPropertyName(current)); public string GetReadModelName(Type readModelType) => _policies.Aggregate(readModelType.Name, (current, policy) => policy.GetPropertyName(current)); } // Usage var policy = new CompoundNamingPolicy( new CamelCaseNamingPolicy(), new PrefixNamingPolicy("data_") ); builder.UseCratisMongoDB(configureMongoDB: builder => builder.WithNamingPolicy(policy)); ``` ### Conditional Policies [Section titled “Conditional Policies”](#conditional-policies) Create policies that apply different rules based on the property name: ```csharp public class ConditionalNamingPolicy : INamingPolicy { public string GetPropertyName(string name) { // Don't transform ID fields if (name.EndsWith("Id", StringComparison.OrdinalIgnoreCase)) return name.ToLower(); // Use camel case for everything else return char.ToLower(name[0]) + name[1..]; } public string GetReadModelName(Type readModelType) => char.ToLower(readModelType.Name[0]) + readModelType.Name[1..]; } ``` ## Convention Pack Integration [Section titled “Convention Pack Integration”](#convention-pack-integration) The naming policy is implemented as a MongoDB convention pack, which means: ### Automatic Application [Section titled “Automatic Application”](#automatic-application) ```csharp // This is registered automatically during setup ConventionRegistry.Register( NamingPolicyNameConvention.ConventionName, new ConventionPack { new NamingPolicyNameConvention() }, type => /* filter logic */ ); ``` ### Filtering Support [Section titled “Filtering Support”](#filtering-support) You can control which types the naming policy applies to using [convention pack filters](/arc/backend/mongodb/convention-packs/#filtering-conventions): ```csharp public class NoNamingPolicyForDTOs : ICanFilterMongoDBConventionPacksForType { public bool ShouldInclude(string conventionPackName, IConventionPack conventionPack, Type type) { if (conventionPackName == NamingPolicyNameConvention.ConventionName) { return !type.Name.EndsWith("DTO"); } return true; } } ``` ## Ignore Naming Conventions [Section titled “Ignore Naming Conventions”](#ignore-naming-conventions) For specific types that shouldn’t use naming policies, use the `IgnoreConventions` attribute: ```csharp [IgnoreConventions(NamingPolicyNameConvention.ConventionName)] public class LegacyDocument { public string UserName { get; set; } // Stored as: "UserName" (unchanged) public string EmailAddr { get; set; } // Stored as: "EmailAddr" (unchanged) } ``` ## Impact on Queries [Section titled “Impact on Queries”](#impact-on-queries) Remember that naming policies affect how you write queries: ### With Camel Case Policy [Section titled “With Camel Case Policy”](#with-camel-case-policy) ```csharp // Property defined as: public string UserName { get; set; } // Stored in MongoDB as: "userName" // Query using the MongoDB field name var filter = Builders.Filter.Eq("userName", "john.doe"); // Or use expression trees (automatically converted) var users = await collection .Find(u => u.UserName == "john.doe") // Automatically converted to "userName" .ToListAsync(); ``` ## Error Handling [Section titled “Error Handling”](#error-handling) ### Missing Policy Configuration [Section titled “Missing Policy Configuration”](#missing-policy-configuration) If no naming policy is configured, the framework will throw a descriptive error: ```shell NamingPolicyNotConfigured: A naming policy for MongoDB has not been configured. Please configure it using the WithNamingPolicy method. ``` ### Null or Empty Names [Section titled “Null or Empty Names”](#null-or-empty-names) Naming policies should handle edge cases: ```csharp public string GetPropertyName(string name) { if (string.IsNullOrWhiteSpace(name)) return name; // Return unchanged for invalid input // Your transformation logic return TransformName(name); } ``` ## Best Practices [Section titled “Best Practices”](#best-practices) ### Consistency [Section titled “Consistency”](#consistency) Choose one naming convention and apply it consistently across your application: ```json // Good: Consistent camelCase { "userId": "123", "userName": "john", "createdAt": "2024-01-15T10:30:00Z" } // Avoid: Mixed conventions { "userId": "123", "UserName": "john", "created_at": "2024-01-15T10:30:00Z" } ``` ### Consider External Systems [Section titled “Consider External Systems”](#consider-external-systems) If you’re integrating with external systems that expect specific naming conventions, align your policy accordingly: ```csharp // For systems expecting snake_case builder.WithNamingPolicy(new SnakeCaseNamingPolicy()); // For JavaScript/JSON APIs expecting camelCase builder.WithCamelCaseNamingPolicy(); ``` ### Performance Considerations [Section titled “Performance Considerations”](#performance-considerations) Naming policies are called for every property during serialization setup, so keep them efficient: ```csharp // Good: Simple, efficient transformation public string GetPropertyName(string name) => char.ToLower(name[0]) + name[1..]; // Avoid: Complex regex or multiple string operations public string GetPropertyName(string name) => Regex.Replace(name, "([A-Z])", "_$1").ToLower().Trim('_'); ``` ## Next Steps [Section titled “Next Steps”](#next-steps) * Learn about [Class Mapping](/arc/backend/mongodb/class-mapping/) for custom type configurations * Explore [Convention Packs](/arc/backend/mongodb/convention-packs/) for advanced customization * Understand [Concepts](/arc/backend/mongodb/concepts/) and how they work with naming policies # Observing Collections The MongoDB extensions in Cratis Applications provide powerful reactive programming capabilities through collection observation. This feature allows you to create observables that automatically notify subscribers when documents in a MongoDB collection change, providing real-time updates to your application. ## Overview [Section titled “Overview”](#overview) Collection observation leverages MongoDB’s Change Streams feature combined with Reactive Extensions (Rx.NET) to provide a seamless way to watch for changes in your data. The system automatically handles initial data loading, change detection, and notification of observers. ## Key Features [Section titled “Key Features”](#key-features) * **Real-time Updates**: Automatically receive notifications when documents change * **Filtering Support**: Observe only documents matching specific criteria * **Multiple Observation Types**: Observe collections, single documents, or documents by ID * **Query Context Awareness**: Integrates with Cratis query context for paging and sorting * **Automatic Cleanup**: Proper resource management and cleanup when observations are disposed ## Basic Collection Observation [Section titled “Basic Collection Observation”](#basic-collection-observation) ### Observing All Documents [Section titled “Observing All Documents”](#observing-all-documents) ```csharp public class AuthorService { private readonly IMongoCollection _collection; public AuthorService(IMongoCollection collection) { _collection = collection; } public IObservable> ObserveAllAuthors() { return _collection.Observe(); } } ``` ### Observing with Filter (Expression) [Section titled “Observing with Filter (Expression)”](#observing-with-filter-expression) ```csharp public IObservable> ObserveActiveAuthors() { return _collection.Observe(author => author.IsActive); } ``` ### Observing with Filter Definition [Section titled “Observing with Filter Definition”](#observing-with-filter-definition) ```csharp public IObservable> ObserveAuthorsByCategory(string category) { var filter = Builders.Filter.Eq(a => a.Category, category); return _collection.Observe(filter); } ``` ## Single Document Observation [Section titled “Single Document Observation”](#single-document-observation) ### Observing Single Document with Filter [Section titled “Observing Single Document with Filter”](#observing-single-document-with-filter) ```csharp public IObservable ObserveFeaturedAuthor() { return _collection.ObserveSingle(author => author.IsFeatured); } ``` ### Observing Document by ID [Section titled “Observing Document by ID”](#observing-document-by-id) ```csharp public IObservable ObserveAuthorById(AuthorId authorId) { return _collection.ObserveById(authorId); } ``` ## Advanced Usage [Section titled “Advanced Usage”](#advanced-usage) ### With Find Options [Section titled “With Find Options”](#with-find-options) ```csharp public IObservable> ObserveRecentAuthors() { var options = new FindOptions { Sort = Builders.Sort.Descending(a => a.CreatedAt), Limit = 10 }; return _collection.Observe( author => author.CreatedAt > DateTime.UtcNow.AddDays(-30), options); } ``` ### Subscribing to Changes [Section titled “Subscribing to Changes”](#subscribing-to-changes) ```csharp public class AuthorNotificationService { private readonly IDisposable _subscription; public AuthorNotificationService(IMongoCollection collection) { _subscription = collection .Observe(author => author.IsActive) .Subscribe( authors => HandleAuthorsChanged(authors), error => HandleError(error), () => HandleCompleted()); } private void HandleAuthorsChanged(IEnumerable authors) { // React to changes in active authors Console.WriteLine($"Active authors updated: {authors.Count()} authors"); } private void HandleError(Exception error) { // Handle observation errors Console.WriteLine($"Error observing authors: {error.Message}"); } private void HandleCompleted() { // Handle observation completion Console.WriteLine("Author observation completed"); } public void Dispose() { _subscription?.Dispose(); } } ``` ## Integration with Queries [Section titled “Integration with Queries”](#integration-with-queries) The observation system integrates seamlessly with the Cratis query context, supporting paging and sorting: ```csharp [Route("api/authors")] public class AuthorsController : Controller { private readonly IMongoCollection _collection; public AuthorsController(IMongoCollection collection) { _collection = collection; } [HttpGet("observe")] public IObservable> ObserveAuthors( [FromQuery] int page = 1, [FromQuery] int pageSize = 10, [FromQuery] string? sortBy = null, [FromQuery] string sortDirection = "asc") { // Query context will be automatically applied to the observation return _collection.Observe(author => author.IsPublished); } } ``` ## Change Types Supported [Section titled “Change Types Supported”](#change-types-supported) The observation system monitors the following MongoDB change stream operations: * **Insert**: New documents added to the collection * **Update**: Existing documents modified * **Replace**: Documents replaced entirely * **Delete**: Documents removed from the collection ## Performance Considerations [Section titled “Performance Considerations”](#performance-considerations) ### Filtering Early [Section titled “Filtering Early”](#filtering-early) Always apply filters to reduce the amount of data being observed: ```csharp // Good - filtered observation var activeAuthors = collection.Observe(author => author.IsActive); // Avoid - observing all then filtering in memory var allAuthors = collection.Observe() .Select(authors => authors.Where(a => a.IsActive)); ``` ### Resource Management [Section titled “Resource Management”](#resource-management) Properly dispose of subscriptions to avoid memory leaks: ```csharp public class AuthorService : IDisposable { private readonly CompositeDisposable _subscriptions = new(); public void StartObserving() { var subscription = _collection .Observe(author => author.IsActive) .Subscribe(HandleAuthorsChanged); _subscriptions.Add(subscription); } public void Dispose() { _subscriptions?.Dispose(); } } ``` ### Batch Updates [Section titled “Batch Updates”](#batch-updates) The system automatically batches rapid successive changes to reduce notification frequency and improve performance. ## Error Handling [Section titled “Error Handling”](#error-handling) Robust error handling is essential when working with observables: ```csharp public void ObserveWithErrorHandling() { _collection .Observe(author => author.IsActive) .Retry(3) // Retry up to 3 times on error .Catch(Observable.Empty>()) // Continue with empty on final failure .Subscribe( authors => HandleAuthors(authors), error => _logger.LogError(error, "Failed to observe authors")); } ``` ## Best Practices [Section titled “Best Practices”](#best-practices) ### Use Specific Filters [Section titled “Use Specific Filters”](#use-specific-filters) Apply filters to observe only the data you need: ```csharp // Good - specific filter collection.Observe(doc => doc.Status == "Active" && doc.Type == "Premium"); // Avoid - broad observation with post-filtering collection.Observe().Where(docs => docs.All(d => d.Status == "Active")); ``` ### Dispose Properly [Section titled “Dispose Properly”](#dispose-properly) Always dispose subscriptions when they’re no longer needed: ```csharp public class ComponentWithObservation : IDisposable { private IDisposable? _subscription; public void StartObserving() { _subscription = collection.Observe().Subscribe(HandleData); } public void Dispose() { _subscription?.Dispose(); } } ``` ### Handle Connection Issues [Section titled “Handle Connection Issues”](#handle-connection-issues) Implement retry logic for connection interruptions: ```csharp collection .Observe(filter) .RetryWhen(errors => errors .SelectMany(error => Observable.Timer(TimeSpan.FromSeconds(5))) .Take(5)) // Retry 5 times with 5-second intervals .Subscribe(HandleData); ``` ## Integration with Dependency Injection [Section titled “Integration with Dependency Injection”](#integration-with-dependency-injection) Register observation services in your DI container: ```csharp public void ConfigureServices(IServiceCollection services) { services.AddSingleton(); } public interface IAuthorObservationService { IObservable> ObserveActiveAuthors(); IObservable ObserveAuthorById(AuthorId id); } public class AuthorObservationService : IAuthorObservationService { private readonly IMongoCollection _collection; public AuthorObservationService(IMongoCollection collection) { _collection = collection; } public IObservable> ObserveActiveAuthors() { return _collection.Observe(author => author.IsActive); } public IObservable ObserveAuthorById(AuthorId id) { return _collection.ObserveById(id); } } ``` ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Common Issues [Section titled “Common Issues”](#common-issues) #### Change Stream Not Starting [Section titled “Change Stream Not Starting”](#change-stream-not-starting) * Ensure MongoDB version supports change streams (3.6+) * Verify replica set configuration * Check user permissions for change stream operations #### Memory Leaks [Section titled “Memory Leaks”](#memory-leaks) * Always dispose subscriptions when no longer needed * Use `CompositeDisposable` for managing multiple subscriptions * Implement `IDisposable` in classes that create observations #### Performance Issues [Section titled “Performance Issues”](#performance-issues) * Apply filters at the database level, not in memory * Limit the scope of observations to necessary data * Monitor change stream performance in MongoDB logs ### Debugging [Section titled “Debugging”](#debugging) Enable logging to troubleshoot observation issues: ```csharp services.Configure(options => { options.AddFilter("MongoDB.Driver.MongoCollection", LogLevel.Debug); }); ``` This will provide detailed information about change stream operations and any issues encountered during observation. # MongoDB Welcome to the MongoDB documentation for Cratis Applications. This section covers all aspects of MongoDB integration, from basic setup to advanced configuration and mapping features. MongoDB support in Cratis Applications provides a comprehensive set of features designed to make working with MongoDB in .NET applications seamless and productive. The framework sets up sensible defaults while allowing for extensive customization when needed. ## Key Features [Section titled “Key Features”](#key-features) * **Easy Setup**: Simple configuration with sane defaults * **Custom Serializers**: Built-in serializers for common .NET types * **Concept Support**: Automatic serialization support for Cratis Concepts * **Naming Policies**: Flexible naming conventions for collections and properties * **Class Mapping**: Automatic discovery and registration of custom mappings * **Convention Packs**: Extensible convention system with filtering capabilities ## Topics [Section titled “Topics”](#topics) * [**Getting Started**](/arc/backend/mongodb/getting-started/) - Basic setup and configuration * [**Serializers**](/arc/backend/mongodb/serializers/) - Built-in serializers for common types * [**Concepts**](/arc/backend/mongodb/concepts/) - Working with Cratis Concepts in MongoDB * [**Naming Policies**](/arc/backend/mongodb/naming-policies/) - Configuring naming conventions * [**Class Mapping**](/arc/backend/mongodb/class-mapping/) - Custom BSON class mapping with automatic discovery * [**Convention Packs**](/arc/backend/mongodb/convention-packs/) - Advanced convention system and filtering * [**Tenancy**](/arc/backend/mongodb/tenancy/) - MongoDB tenant database naming strategies ## Quick Start [Section titled “Quick Start”](#quick-start) To get started with MongoDB in your application: ```csharp var builder = WebApplication.CreateBuilder(args); builder.AddCratisArc(); builder.UseCratisMongoDB(); var app = builder.Build(); app.UseCratisArc(); ``` This configures your application with MongoDB support, including all default serializers, conventions, and mappings. # Serializers Cratis Applications provides a comprehensive set of custom serializers for MongoDB to handle common .NET types that don’t have built-in MongoDB support or need special handling. ## Built-in Serializers [Section titled “Built-in Serializers”](#built-in-serializers) The following serializers are automatically registered when you call `UseCratisMongoDB()`: ### DateTimeOffset Support [Section titled “DateTimeOffset Support”](#datetimeoffset-support) **Class**: `DateTimeOffsetSupportingBsonDateTimeSerializer` Provides proper serialization of `DateTimeOffset` values, preserving timezone information that would otherwise be lost with MongoDB’s default DateTime handling. ```csharp public class MyDocument { public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.Now; public DateTimeOffset UpdatedAt { get; set; } } ``` The serializer supports different BSON representations: * `BsonType.DateTime` (default) * `BsonType.String` * `BsonType.Int64` ### DateOnly Serializer [Section titled “DateOnly Serializer”](#dateonly-serializer) **Class**: `DateOnlySerializer` Handles .NET 6+ `DateOnly` types, storing them efficiently in MongoDB: ```csharp public class EventRecord { public DateOnly EventDate { get; set; } public string Description { get; set; } } ``` ### TimeOnly Serializer [Section titled “TimeOnly Serializer”](#timeonly-serializer) **Class**: `TimeOnlySerializer` Handles .NET 6+ `TimeOnly` types for time-of-day values: ```csharp public class Schedule { public TimeOnly StartTime { get; set; } public TimeOnly EndTime { get; set; } } ``` ### TimeSpan Serializer [Section titled “TimeSpan Serializer”](#timespan-serializer) **Class**: `TimeSpanSerializer` Handles serialization of `TimeSpan` values: ```csharp public class Task { public TimeSpan Duration { get; set; } public TimeSpan EstimatedTime { get; set; } } ``` ### Geospatial Serializers [Section titled “Geospatial Serializers”](#geospatial-serializers) Cratis provides specialized serializers for geospatial types from `Cratis.Geospatial`. These types follow the GeoJSON specification and are fully integrated with MongoDB’s geospatial query operators. For comprehensive documentation on storing and querying geographic data, see the [Geospatial Types](/arc/backend/mongodb/geospatial/) section, which covers: * **[Point](/arc/backend/mongodb/geospatial/point/)** — Single coordinates for locations and landmarks * **[LineString](/arc/backend/mongodb/geospatial/linestring/)** — Routes, paths, and trajectories * **[Polygon](/arc/backend/mongodb/geospatial/polygon/)** — Geographic areas and boundaries with optional exclusion zones ### Type Serializer [Section titled “Type Serializer”](#type-serializer) **Class**: `TypeSerializer` Serializes `System.Type` instances, useful for polymorphic scenarios or when storing type information: ```csharp public class TypedDocument { public Type DocumentType { get; set; } public object Data { get; set; } } ``` ## Guid Configuration [Section titled “Guid Configuration”](#guid-configuration) One of the most important default configurations is for `System.Guid`. MongoDB historically used a legacy GUID representation that could cause issues. Cratis Applications configures Guids to use the standard representation: ```csharp // This is done automatically during setup BsonDefaults.GuidRepresentation = GuidRepresentation.Standard; BsonDefaults.GuidRepresentationMode = GuidRepresentationMode.V3; ``` This ensures that: * Guids are stored in a predictable format * They work correctly with .NET applications * There are no surprises when viewing data in MongoDB tools ## Custom Serializers [Section titled “Custom Serializers”](#custom-serializers) You can register additional serializers if needed: ### Using MongoDB’s Registration [Section titled “Using MongoDB’s Registration”](#using-mongodbs-registration) ```csharp BsonSerializer.RegisterSerializer(new MyCustomSerializer()); ``` ### Using Serialization Providers [Section titled “Using Serialization Providers”](#using-serialization-providers) For more complex scenarios, implement `IBsonSerializationProvider`: ```csharp public class MySerializationProvider : IBsonSerializationProvider { public IBsonSerializer GetSerializer(Type type) { if (type == typeof(MyCustomType)) { return new MyCustomTypeSerializer(); } return null; } } // Register the provider BsonSerializer.RegisterSerializationProvider(new MySerializationProvider()); ``` ## Serializer Configuration [Section titled “Serializer Configuration”](#serializer-configuration) Some serializers support configuration through interfaces: ### Representation Configurable [Section titled “Representation Configurable”](#representation-configurable) Serializers implementing `IRepresentationConfigurable` can be configured for different BSON representations: ```csharp // Configure DateTimeOffset to serialize as string var serializer = new DateTimeOffsetSupportingBsonDateTimeSerializer() .WithRepresentation(BsonType.String); BsonSerializer.RegisterSerializer(serializer); ``` ## Polymorphic Serialization [Section titled “Polymorphic Serialization”](#polymorphic-serialization) For complex inheritance hierarchies, the framework includes custom discriminator handling: ### Custom Object Discriminator Convention [Section titled “Custom Object Discriminator Convention”](#custom-object-discriminator-convention) The `CustomObjectDiscriminatorConvention` provides better handling of polymorphic types by using more readable type strings instead of .NET’s default assembly-qualified names. ```csharp public abstract class BaseDocument { public string Id { get; set; } } public class TextDocument : BaseDocument { public string Content { get; set; } } public class ImageDocument : BaseDocument { public byte[] ImageData { get; set; } } ``` The discriminator will use simplified type names making the stored documents more readable and portable. ## Performance Considerations [Section titled “Performance Considerations”](#performance-considerations) ### Serializer Caching [Section titled “Serializer Caching”](#serializer-caching) MongoDB serializers are cached by type, so there’s no performance penalty for using custom serializers once they’re registered. ### Concept Serializers [Section titled “Concept Serializers”](#concept-serializers) The [Concept serializers](/arc/backend/mongodb/concepts/) are optimized to serialize only the underlying value, not the wrapper object, providing efficient storage and retrieval. ## Error Handling [Section titled “Error Handling”](#error-handling) Serializers include comprehensive error handling: * **Type validation**: Ensures only appropriate types are serialized * **Null handling**: Proper null value handling across all serializers * **Format validation**: Validates input data before serialization ## Next Steps [Section titled “Next Steps”](#next-steps) * Learn about [Concept serialization](/arc/backend/mongodb/concepts/) for domain-driven design * Explore [Class Mapping](/arc/backend/mongodb/class-mapping/) for custom type mapping * Configure [Naming Policies](/arc/backend/mongodb/naming-policies/) for consistent field naming # Tenancy For tenancy concepts and tenant resolution, see the [tenancy overview](/arc/backend/tenancy/). This page focuses on MongoDB-specific tenant database naming through a custom database name resolver. ## Custom Database Resolvers [Section titled “Custom Database Resolvers”](#custom-database-resolvers) You can create a custom database naming strategy by implementing a database name resolver and registering it with the MongoDB integration. ## Custom Resolver Example [Section titled “Custom Resolver Example”](#custom-resolver-example) ```csharp using Cratis.Arc.MongoDB; using Cratis.Arc.Tenancy; using Microsoft.Extensions.Options; public class CustomMongoDatabaseNameResolver( ITenantIdAccessor tenantIdAccessor, IOptions options) : IMongoDatabaseNameResolver { public string Resolve() { var baseName = options.Value.Database; var tenantId = tenantIdAccessor.Current; return tenantId == TenantId.NotSet ? baseName : $"{tenantId.Value}_{baseName}"; } } ``` ## Registering the Resolver [Section titled “Registering the Resolver”](#registering-the-resolver) ```csharp builder.UseCratisMongoDB(configureMongoDB: mongodb => { mongodb.WithDatabaseResolver(); }); ``` ## When to Use a Custom Resolver [Section titled “When to Use a Custom Resolver”](#when-to-use-a-custom-resolver) * Match existing naming conventions. * Add environment or region prefixes. * Integrate with legacy database layouts. * Implement specialized isolation or sharding rules. # Microsoft.AspNetCore.OpenApi The `Cratis.Arc.OpenApi` package provides deep integration with `Microsoft.AspNetCore.OpenApi` (.NET 10+), automatically generating accurate API documentation for all Arc-specific features and conventions. ## Overview [Section titled “Overview”](#overview) | Topic | Description | | ------------------------------------------------------------ | ------------------------------------------------------------------------------------ | | [Concepts](/arc/backend/open-api/concepts/) | How concept types are mapped to their underlying primitive types in the API schema. | | [Commands](/arc/backend/open-api/commands/) | How command responses are wrapped with `CommandResult` in the API documentation. | | [Queries](/arc/backend/open-api/queries/) | How query responses are wrapped with `QueryResult`, including pagination parameters. | | [Enums](/arc/backend/open-api/enums/) | How enum values are represented as string names rather than integers. | | [FromRequest Attribute](/arc/backend/open-api/from-request/) | How complex model binding with `[FromRequest]` is reflected in the API schema. | | [Model-Bound Operations](/arc/backend/open-api/model-bound/) | How minimal API command and query endpoints appear in the API documentation. | ## Setup [Section titled “Setup”](#setup) Add the `Cratis.Arc.OpenApi` NuGet package to your project and call `AddConcepts()` inside your `AddOpenApi` configuration: ```csharp builder.Services.AddOpenApi(options => options.AddConcepts()); app.MapOpenApi(); ``` The `AddConcepts()` method registers all schema and operation transformers automatically. ## Requirements [Section titled “Requirements”](#requirements) The `Cratis.Arc.OpenApi` package targets `.NET 10` and later. The transformer API (`IOpenApiSchemaTransformer`, `IOpenApiOperationTransformer`) and the underlying `Microsoft.OpenApi` 2.x schema types are only available from .NET 10 onwards. ## Relationship to Swagger [Section titled “Relationship to Swagger”](#relationship-to-swagger) Arc also ships a separate `Cratis.Arc.Swagger` package for Swashbuckle-based Swagger UI. Both packages cover the same set of Arc features, but use different APIs: | | `Cratis.Arc.Swagger` | `Cratis.Arc.OpenApi` | | ------------ | ------------------------------------------------- | -------------------------------------------------------------------------------------------- | | Framework | Swashbuckle (`ISchemaFilter`, `IOperationFilter`) | `Microsoft.AspNetCore.OpenApi` (`IOpenApiSchemaTransformer`, `IOpenApiOperationTransformer`) | | .NET version | net8.0+ | net10.0+ | | Registration | `services.AddSwaggerGen(o => o.AddConcepts())` | `services.AddOpenApi(o => o.AddConcepts())` | Both are fully independent; choose the one that matches your toolchain. # Commands Arc commands follow a request/response pattern where every HTTP handler that performs a mutation returns a `CommandResult` or `CommandResult` envelope. The `CommandResultOperationTransformer` automatically updates the generated operation documentation to reflect this. ## What the transformer does [Section titled “What the transformer does”](#what-the-transformer-does) For every endpoint whose controller action or method is identified as a command (not marked with `[AspNetResult]`), the transformer: 1. Replaces the 200 response schema with `CommandResult` (for `void`/`Task` returns) or `CommandResult` (for typed returns). 2. Adds standard error response schemas for 400, 403, and 500 status codes — all using the same `CommandResult`/`CommandResult` schema so clients only need to handle one type. ## Response status codes [Section titled “Response status codes”](#response-status-codes) | Status code | Meaning | | ----------- | ------------------------------------- | | 200 | Command executed successfully | | 400 | Validation error or malformed payload | | 403 | Forbidden — insufficient permissions | | 500 | Unexpected server error | ## Concept return types [Section titled “Concept return types”](#concept-return-types) If the command returns a concept (a type inheriting from `ConceptAs`), the transformer unwraps the concept to its underlying primitive type before generating the `CommandResult` schema. ```csharp public record InvoiceId(Guid Value) : ConceptAs(Value); // Controller action — return type is InvoiceId [HttpPost] public Task CreateInvoice(CreateInvoice command) { ... } ``` The documented 200 response will use `CommandResult` (uuid format) rather than `CommandResult`. ## Opting out [Section titled “Opting out”](#opting-out) Decorate the action with `[AspNetResult]` to bypass the transformer and expose the raw return type directly: ```csharp [HttpPost] [AspNetResult] public Task CreateInvoice(CreateInvoice command) { ... } ``` # Concepts Arc uses [concept types](/arc/backend/core/overview/) as strongly-typed wrappers around primitives such as `Guid`, `string`, or `int`. Without special handling, the generated API schema would expose these as complex objects with a single `Value` property—which is rarely what API consumers expect. The `ConceptSchemaTransformer` detects any schema type that inherits from `ConceptAs` and replaces the schema with the equivalent JSON primitive type of the underlying value. ## Example [Section titled “Example”](#example) ```csharp public record CustomerId(Guid Value) : ConceptAs(Value); ``` Without the transformer the schema for `CustomerId` would be: ```json { "type": "object", "properties": { "value": { "type": "string", "format": "uuid" } } } ``` With the transformer the schema becomes: ```json { "type": "string", "format": "uuid" } ``` This applies wherever the concept type appears — as a request parameter, in a request body, or in a response schema. ## Supported Underlying Types [Section titled “Supported Underlying Types”](#supported-underlying-types) The transformer maps any primitive or common .NET type that `ConceptAs` supports: | .NET type | JSON Schema type | Format | | ----------------------------- | ---------------- | ------------------ | | `Guid` | `string` | `uuid` | | `string` | `string` | — | | `int` / `long` | `integer` | `int32` / `int64` | | `float` / `double` | `number` | `float` / `double` | | `bool` | `boolean` | — | | `DateTimeOffset` / `DateTime` | `string` | `date-time` | # Enums By default, `System.Text.Json` serializes enums as integers. This makes API documentation less readable because consumers see numeric values rather than meaningful names. The `EnumSchemaTransformer` replaces integer enum values in the schema with their string names, making the documentation self-explanatory without requiring additional annotations. ## Example [Section titled “Example”](#example) ```csharp public enum InvoiceStatus { Draft, Sent, Paid, Overdue } ``` Without the transformer the schema for `InvoiceStatus` would be: ```json { "type": "integer", "enum": [0, 1, 2, 3] } ``` With the transformer the schema becomes: ```json { "type": "string", "enum": ["Draft", "Sent", "Paid", "Overdue"] } ``` ## Behaviour [Section titled “Behaviour”](#behaviour) The transformer applies to every enum type that appears in the API schema — request bodies, response bodies, and query/route parameters alike. No additional attributes or configuration are required. Note The transformer only modifies the OpenAPI schema. To ensure your runtime serialisation matches (i.e. that JSON actually serialises enum values as strings), add `JsonStringEnumConverter` to your serialisation options: ```csharp builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.Converters.Add(new JsonStringEnumConverter())); ``` # FromRequest Attribute The `[FromRequest]` attribute lets you combine data from multiple parts of an HTTP request — body, route, and query string — into a single model object. See [FromRequest Attribute](/arc/backend/asp-net-core/from-request/) for full details on how model binding works. ## OpenAPI documentation [Section titled “OpenAPI documentation”](#openapi-documentation) Without special handling, parameters decorated with `[FromRequest]` would appear as individual query/route parameters in the API documentation even though they are bound as a single body object at runtime. The `FromRequestOperationTransformer` and `FromRequestSchemaTransformer` correct this. ### What the transformers do [Section titled “What the transformers do”](#what-the-transformers-do) 1. **`FromRequestOperationTransformer`** — For each parameter marked `[FromRequest]`: * Removes the parameter from the `parameters` list of the operation. * Creates a `requestBody` entry using the parameter’s type schema. 2. **`FromRequestSchemaTransformer`** — Ensures the schema for the request body accurately reflects the model’s properties, excluding any properties that come from route or query binding. ## Example [Section titled “Example”](#example) ```csharp public record UpdateCustomerRequest( [property: FromRoute] CustomerId CustomerId, string Name, string Email); [HttpPut("{customerId}")] public Task UpdateCustomer([FromRequest] UpdateCustomerRequest request) { ... } ``` The generated OpenAPI operation will show: * **Path parameter**: `customerId` (from the route) * **Request body**: a JSON schema with `name` and `email` properties Rather than listing all three as individual query/route parameters. # Model-Bound Operations Arc supports minimal API-style endpoints for commands and queries, called *model-bound operations*. These endpoints are registered automatically by the Arc infrastructure and follow a convention-based naming scheme (`Execute`). Because model-bound endpoints do not use traditional controller actions, the standard `CommandResultOperationTransformer` and `QueryResultOperationTransformer` transformers do not apply to them. The `ModelBound.CommandOperationTransformer` and `ModelBound.QueryOperationTransformer` fill this gap. ## Registration [Section titled “Registration”](#registration) Model-bound transformers are included automatically when you call `AddConcepts()`: ```csharp builder.Services.AddOpenApi(options => options.AddConcepts()); ``` They can also be registered independently: ```csharp builder.Services.AddOpenApi(options => options.AddModelBoundOperationTransformers()); ``` ## Command operations [Section titled “Command operations”](#command-operations) The `ModelBound.CommandOperationTransformer` matches operations whose `operationId` starts with `Execute` and resolves the command type from the registered `ICommandHandlerProviders`. For matched operations it: 1. Sets the `requestBody` to a schema of the command type. 2. Sets the 200 response schema to `CommandResult` or `CommandResult` (depending on the handler return type). 3. Adds 400, 403, and 500 error response schemas. ```csharp // The command is automatically discovered and the endpoint is documented [Command] public record CreateInvoice(CustomerId Customer, decimal Amount) { public Task Handle() { ... } } ``` ## Query operations [Section titled “Query operations”](#query-operations) The `ModelBound.QueryOperationTransformer` matches operations whose `operationId` starts with `Execute` and resolves the query performer from the registered `IQueryPerformerProviders`. For matched operations it: 1. Adds each query parameter from the performer’s parameter list as a query string parameter. 2. Adds paging and sorting parameters when `IQueryPerformer.SupportsPaging` is `true`. 3. Sets the 200 response schema to `QueryResult`. 4. Adds 400, 403, and 500 error response schemas. ## Pagination and sorting parameters [Section titled “Pagination and sorting parameters”](#pagination-and-sorting-parameters) For query performers that support paging, the following query parameters are added: | Parameter | Type | Description | | --------------- | -------------------------- | ------------------------ | | `sortby` | `string` | Field name to sort by | | `sortDirection` | `string` (`asc` \| `desc`) | Sort direction | | `pageSize` | `integer` | Number of items per page | | `page` | `integer` | Page number (0-based) | # Queries Arc queries return results wrapped in a `QueryResult` envelope that carries the data along with paging metadata and error information. The `QueryResultOperationTransformer` ensures this is accurately reflected in the generated API documentation. ## What the transformer does [Section titled “What the transformer does”](#what-the-transformer-does) For every endpoint whose method is identified as a query (not marked with `[AspNetResult]`), the transformer: 1. Replaces the 200 response schema with `QueryResult`. 2. Adds standard error response schemas for 400, 403, and 500 status codes. 3. Appends paging and sorting query parameters when the return type is an enumerable (`IEnumerable`, `IQueryable`, etc.). ## Pagination and sorting parameters [Section titled “Pagination and sorting parameters”](#pagination-and-sorting-parameters) When a query endpoint returns an enumerable result, the following query parameters are added automatically: | Parameter | Type | Description | | --------------- | -------------------------- | ------------------------ | | `sortby` | `string` | Field name to sort by | | `sortDirection` | `string` (`asc` \| `desc`) | Sort direction | | `pageSize` | `integer` | Number of items per page | | `page` | `integer` | Page number (0-based) | ## Response status codes [Section titled “Response status codes”](#response-status-codes) | Status code | Meaning | | ----------- | ------------------------------------ | | 200 | Query executed successfully | | 400 | Invalid request or parameters | | 403 | Forbidden — insufficient permissions | | 500 | Unexpected server error | ## Opting out [Section titled “Opting out”](#opting-out) Decorate the action with `[AspNetResult]` to bypass the transformer and expose the raw return type directly: ```csharp [HttpGet] [AspNetResult] public Task> GetInvoices() { ... } ``` # Backend Overview Arc is a layered application framework. The Arc.Core package provides the base application model without ASP.NET Core, while the Arc ASP.NET Core package builds on top of Arc.Core to add web-specific integration. ## What Is Arc.Core [Section titled “What Is Arc.Core”](#what-is-arccore) Arc.Core gives you the core application model for commands, queries, multi-tenancy, identity, and extensibility without pulling in ASP.NET Core. It is the right choice when you: * Need a minimal hosting surface * Run on platforms where ASP.NET Core is not available or not allowed * Want to avoid ASP.NET Core dependencies or AOP constraints * Build device or desktop workloads such as MAUI ## What Arc Adds [Section titled “What Arc Adds”](#what-arc-adds) The Arc ASP.NET Core package builds on Arc.Core and adds web framework capabilities like controllers, model binding, middleware integration, and OpenAPI/Swagger features. It is the right choice when you: * Build HTTP APIs and web backends * Need ASP.NET Core routing and filters * Want automatic endpoint generation and API documentation ## Layering Model [Section titled “Layering Model”](#layering-model) The relationship is a strict layering model: Arc.Core is the foundation, and Arc with ASP.NET Core builds on top of it. ``` Arc.Core Core application model Arc ASP.NET Core Web integration ``` ## Deployment Scenarios [Section titled “Deployment Scenarios”](#deployment-scenarios) Arc.Core is the safe default when you cannot use ASP.NET Core, for example on devices that run MAUI or similar technologies where AOP-based ASP.NET Core builds are not allowed. Arc with ASP.NET Core is the best fit for web services and APIs that benefit from the full ASP.NET Core stack. ## Related Topics [Section titled “Related Topics”](#related-topics) * [Arc.Core Overview](/arc/backend/core/overview/) * [ASP.NET Core Integration](/arc/backend/asp-net-core/) * [Getting Started](/arc/backend/getting-started/) # Proxy Generation The TypeScript proxy generator automatically creates strongly-typed client code for your commands, queries, and types — ensuring compile-time type safety between your backend and frontend without any manual synchronization. ## Overview [Section titled “Overview”](#overview) The proxy generator analyzes your backend API at build time and generates: * TypeScript interfaces for all request and response types * Strongly-typed proxy classes for invoking commands and queries * TypeScript enums for all C# enum types referenced by your API * Full IntelliSense support in your IDE ## Enum Generation [Section titled “Enum Generation”](#enum-generation) C# enums referenced by any command, query, or type are automatically discovered and generated as TypeScript enums. Member names are converted to camelCase. **C# source:** ```csharp public enum ReadModelStatus { Unknown = 0, Active = 1, Inactive = 2, Archived = 3 } ``` **Generated TypeScript:** ```typescript export enum ReadModelStatus { unknown = 0, active = 1, inactive = 2, archived = 3, } ``` ### Flags Enums [Section titled “Flags Enums”](#flags-enums) Enums decorated with `[Flags]` receive special treatment: the generator uses a dedicated template that, in addition to the enum declaration, emits an `allXxx` constant combining every non-zero member with the bitwise OR operator. This constant is useful when you need to represent “all flags set” or build a full bitmask without repeating every member name by hand. **C# source:** ```csharp [Flags] public enum AnchorEdges { None = 0, Top = 1 << 0, Right = 1 << 1, Bottom = 1 << 2, Left = 1 << 3, } ``` **Generated TypeScript:** ```typescript export enum AnchorEdges { none = 0, top = 1, right = 2, bottom = 4, left = 8, } export const allAnchorEdges = AnchorEdges.top | AnchorEdges.right | AnchorEdges.bottom | AnchorEdges.left; ``` The `allAnchorEdges` constant excludes `none` (value `0`) because a zero-valued member contributes nothing to a bitwise OR expression. The name follows the convention `all` and is exported alongside the enum. ## Configuration [Section titled “Configuration”](#configuration) Configuration details for the proxy generator will be documented here. ## Usage [Section titled “Usage”](#usage) Usage examples and best practices will be provided in this section. ## See Also [Section titled “See Also”](#see-also) * [Commands](/arc/backend/commands/) - Learn about command patterns * [Queries](/arc/backend/queries/) - Learn about query patterns # Command Proxy Generation The proxy generator creates TypeScript command classes that provide type-safe command execution with React hook integration. ## Supported Approaches [Section titled “Supported Approaches”](#supported-approaches) Commands can be implemented using two approaches, both of which are supported by the proxy generator: * **Controller-based**: Commands in ASP.NET Core controllers using `[HttpPost]` attributes * **Model-bound**: Simplified approach where a type represents the command directly For detailed information on implementing commands, see the [Commands documentation](/arc/backend/commands/). ## How Commands are Discovered [Section titled “How Commands are Discovered”](#how-commands-are-discovered) ### Controller-based Commands [Section titled “Controller-based Commands”](#controller-based-commands) The generator discovers controller-based commands by looking for: * Methods marked with `[HttpPost]` * Parameters marked with `[FromBody]`, `[FromRoute]`, or `[FromQuery]` See [Controller-based Commands](/arc/backend/commands/controller-based/) for implementation details. ### Model-bound Commands [Section titled “Model-bound Commands”](#model-bound-commands) The generator discovers model-bound commands by finding types that: * Are decorated with the `[Command]` attribute * Have a `Handle()` method (the command handler) The type name becomes the command name, and all properties of the type become the command properties in the generated TypeScript. See [Model-bound Commands](/arc/backend/commands/model-bound/) for implementation details. ## Generated Command Structure [Section titled “Generated Command Structure”](#generated-command-structure) Generated command classes: * Extend the `Command` base class from `@cratis/arc/commands` * Include all properties from route parameters, query parameters, and body content * Provide a static `use()` method for React hook integration * Include the proper route based on the configuration ## Generated Artifacts [Section titled “Generated Artifacts”](#generated-artifacts) For each command, the generator creates: 1. **Interface**: An `ICommandName` interface with all command properties 2. **Class**: A `CommandName` class extending `Command` 3. **Route**: The HTTP route derived from the controller route or model-bound configuration ## Excluding Commands from Generation [Section titled “Excluding Commands from Generation”](#excluding-commands-from-generation) To exclude specific controller-based commands from proxy generation, mark them with the `[AspNetResult]` attribute. This is useful when you want to handle the response manually or when the command returns a non-standard result. ## Route Configuration [Section titled “Route Configuration”](#route-configuration) The generated route is affected by the `CratisProxiesSkipCommandNameInRoute` configuration option: * When `false` (default): The command type name is included in the route * When `true`: The command type name is excluded from the route **Automatic Conflict Detection**: When `CratisProxiesSkipCommandNameInRoute` is `true`, the proxy generator automatically detects if multiple commands exist in the same namespace (after skipping segments). If a conflict is detected, the command name is automatically included in the route to prevent route collisions. This behavior is consistent with the runtime endpoint mapping. For example: * Single command in namespace: Route is clean without type name (e.g., `/api/orders`) * Multiple commands in same namespace: Type names are added automatically (e.g., `/api/orders/create-order`, `/api/orders/update-order`) See [Configuration](/arc/backend/proxy-generation/configuration/) for more details on route configuration options. ## Frontend Usage [Section titled “Frontend Usage”](#frontend-usage) The generated command proxies integrate with React through the `use()` static method, which returns: * The command instance with all properties * A setter function for updating command values The command can then be executed using the `execute()` method, which returns a `CommandResult` with success/failure information and any validation errors. For frontend usage patterns, see the [@cratis/arc documentation](https://www.npmjs.com/package/@cratis/arc). # Configuration Configuration documentation has moved. See the [Configuration section](/arc/backend/proxy-generation/configuration/) for the full reference, organized by topic: * [Basic Options](/arc/backend/proxy-generation/configuration/basic/) * [Library Mode](/arc/backend/proxy-generation/configuration/library-mode/) * [Type Exclusions](/arc/backend/proxy-generation/configuration/type-exclusions/) * [Namespace Roots](/arc/backend/proxy-generation/configuration/namespace-roots/) * [Assembly-to-Package Mappings](/arc/backend/proxy-generation/configuration/assembly-package-mappings/) * [Routing](/arc/backend/proxy-generation/configuration/routing/) * [Output Behavior](/arc/backend/proxy-generation/configuration/output-behavior/) # Assembly-to-Package Mappings When your project references types from an external assembly that already has a corresponding TypeScript npm package, you can tell the proxy generator to import those types from the package instead of regenerating them locally. ## Use Case [Section titled “Use Case”](#use-case) Consider this solution structure: ```text MyCompany.Shared/ ← Shared class library + npm package (@mycompany/shared) MyCompany.Inventory/ ← Application referencing Shared MyCompany.Purchasing/ ← Application referencing Shared ``` Without a mapping, the proxy generator would regenerate `Money.ts` and `ProductId.ts` inside each application’s frontend — duplicating types that already exist in `@mycompany/shared`. With a mapping, those types are imported from the package: ```typescript import { Money, ProductId } from '@mycompany/shared'; ``` ## Configuration [Section titled “Configuration”](#configuration) ```xml ``` Multiple shared libraries: ```xml ``` | Attribute | Description | | ---------- | ------------------------------------------- | | `Assembly` | C# assembly name (without `.dll` extension) | | `Package` | npm package name to import from | ## Behavior [Section titled “Behavior”](#behavior) * Types from the mapped assembly are **not** generated as local TypeScript files. * Any command, query, or type that references a mapped type imports it from the configured package. * The mapping applies to all types in the assembly — classes, records, enums, and interfaces. ## CLI [Section titled “CLI”](#cli) ```bash proxygenerator MyCompany.Inventory.dll output-path \ --assembly-to-package=MyCompany.Shared=@mycompany/shared \ --assembly-to-package=MyCompany.UiModels=@mycompany/ui-models ``` # Basic Options ## Required [Section titled “Required”](#required) ```xml $(MSBuildThisFileDirectory)../Web ``` `CratisProxiesOutputPath` is the only required property. It specifies the directory where generated TypeScript files are written — typically the root of your frontend project. ## Namespace Segment Skipping [Section titled “Namespace Segment Skipping”](#namespace-segment-skipping) ```xml 1 ``` `CratisProxiesSegmentsToSkip` controls how many leading namespace segments are stripped when mapping C# namespaces to output folders. **Example:** With namespaces `Api.MyFeature`, `Domain.MyFeature`, and `Read.MyFeature` and `SegmentsToSkip=1`: Without skipping: ```text Api/MyFeature/ Domain/MyFeature/ Read/MyFeature/ ``` With skipping: ```text MyFeature/ ``` For more control over namespace-to-folder mapping, see [Namespace Roots](/arc/backend/proxy-generation/configuration/namespace-roots/). ## Source File as Output File [Section titled “Source File as Output File”](#source-file-as-output-file) ```xml true ``` By default, one TypeScript file is generated per C# type. When `CratisProxiesUseSourceFileAsOutputFile` is `true`, all types defined in the same `.cs` source file are combined into a single `.ts` file named after the source file. **Example:** `AccountCommands.cs` containing `CreateAccount`, `UpdateAccount`, `DeleteAccount` generates: Default: ```text AccountCommands/ ├── CreateAccount.ts ├── UpdateAccount.ts └── DeleteAccount.ts ``` With `CratisProxiesUseSourceFileAsOutputFile=true`: ```text AccountCommands/ └── AccountCommands.ts ``` > **Note:** This feature requires PDB debug symbols alongside the compiled assembly. Without PDB information the generator falls back to one file per type. Generate proxies during development with a Debug build (`dotnet build -c Debug`), then commit the generated TypeScript. Release and publish builds can consume those committed proxies without regenerating them. This is a recommended workflow rather than a Release restriction: the generator still runs in Release whenever `CratisProxiesOutputPath` is configured. ### CLI [Section titled “CLI”](#cli) ```bash proxygenerator assembly.dll output-path --use-source-file-as-output-file ``` ## Decorator Metadata [Section titled “Decorator Metadata”](#decorator-metadata) Generated types use `@field(...)` property decorators and `@derivedType(...)` class decorators. The decorators keep the runtime serialization metadata beside the type and property they describe, with no proxy-generator configuration required. TypeScript 5.2 and newer support these decorators through the standard decorator transform. Leave `experimentalDecorators` unset or set it to `false`; `@cratis/fundamentals` consumes the standard decorator metadata when the generated class is defined. Existing applications can continue using TypeScript’s legacy decorator transform with `experimentalDecorators` set to `true`. The generated proxy source is the same in both modes, so you can change compiler modes without regenerating a different proxy shape. If Babel transforms the generated proxies, configure its decorators plugin for the `2023-11` protocol. Hermes executes the JavaScript that Babel produces; Hermes does not transform decorator syntax itself, so the Babel step must run before the bundle reaches Hermes. # Library Mode By default, the proxy generator only produces TypeScript for types that are **directly used** by commands and queries — types referenced as properties or return values flow in transitively. Any other public types in your assembly are ignored. **Library mode** generates TypeScript for **every public type** in the assembly, regardless of whether it appears in any command or query. This is useful for shared libraries or packages where the TypeScript consumer needs the full type surface. ## Enabling Library Mode [Section titled “Enabling Library Mode”](#enabling-library-mode) ```xml true ``` ### CLI [Section titled “CLI”](#cli) ```bash proxygenerator assembly.dll output-path --library-mode ``` ## Behavior [Section titled “Behavior”](#behavior) When library mode is on: * All public, non-abstract classes and records in every project assembly are collected and generated as TypeScript interfaces. * All public interfaces are included. * All public enums are included. * Abstract classes are skipped (they cannot be instantiated). * Types excluded via [`ExcludeType` or `ExcludeNamespace`](/arc/backend/proxy-generation/configuration/type-exclusions/) are still skipped. * Types from assemblies mapped via [`AssemblyToPackageMapping`](/arc/backend/proxy-generation/configuration/assembly-package-mappings/) are still imported from their package rather than regenerated. ## Combining with Other Options [Section titled “Combining with Other Options”](#combining-with-other-options) Library mode pairs naturally with type exclusions and namespace roots: ```xml true ``` # Namespace Roots A namespace root pins a namespace as the base of the output folder hierarchy and places its output under a named base folder. When a type’s namespace begins with a configured root, the root prefix is stripped and the remainder is placed under the specified folder. This is an alternative to [`CratisProxiesSegmentsToSkip`](/arc/backend/proxy-generation/configuration/basic/#namespace-segment-skipping) that works by name rather than by a fixed segment count, making it more resilient when namespace depths vary across a project. ## Configuration [Section titled “Configuration”](#configuration) ```xml ``` | Attribute | Description | | ----------- | --------------------------------------------------- | | `Namespace` | The C# namespace prefix to match | | `Folder` | The output sub-folder to place matching types under | **Example:** With namespace root `MyApp.Features` → `features`: | C# namespace | Output folder | | ---------------------------------- | -------------------------------------------------------------------- | | `MyApp.Features.Auth.Registration` | `features/Auth/Registration/` | | `MyApp.Features.Billing.Invoices` | `features/Billing/Invoices/` | | `MyApp.Features.Auth` | `features/Auth/` | | `MyApp.Features` | `features/` | | `MyApp.Other` | *(falls back to segment-skip logic — namespace root does not apply)* | ## Multiple Roots [Section titled “Multiple Roots”](#multiple-roots) You can declare multiple roots. The longest matching namespace wins: ```xml ``` ## Priority [Section titled “Priority”](#priority) Namespace roots **only take effect for types whose namespace matches the root**. Types that do not match any configured root fall back to `CratisProxiesSegmentsToSkip` as normal. ## CLI [Section titled “CLI”](#cli) Pass one or more `--namespace-root` flags using `=` to separate the namespace from the folder: ```bash proxygenerator assembly.dll output-path \ --namespace-root=MyApp.Features=features \ --namespace-root=MyApp.SharedTypes=shared ``` # Output Behavior ## Incremental Generation (Default) [Section titled “Incremental Generation (Default)”](#incremental-generation-default) By default (`CratisProxiesSkipOutputDeletion=true`), the proxy generator uses incremental generation: * Only files whose content has changed are written to disk — unchanged file timestamps are preserved. * [File index tracking](/arc/backend/proxy-generation/file-index-tracking/) automatically removes orphaned files from renamed or deleted commands and queries. * Build times are faster because only changed files are regenerated. * Proxy files committed to the repository keep their original timestamps when another developer builds. This default suits virtually all projects. ## Full Regeneration [Section titled “Full Regeneration”](#full-regeneration) To delete the entire output directory on every build instead of updating incrementally: ```xml false ``` When full regeneration is enabled: * All proxies are recreated on every build. * Any manual files in the output directory are deleted. * Build times may be longer. * Committed proxy files will always appear modified after a build. **Recommendation:** Keep the default (`true`) for incremental generation. Set to `false` only if you specifically need a guaranteed clean-state output. ## Index File Generation [Section titled “Index File Generation”](#index-file-generation) An `index.ts` barrel file is generated for each output folder by default. To disable: ```xml true ``` ## File Index Tracking [Section titled “File Index Tracking”](#file-index-tracking) File index tracking records which files the generator owns so that orphaned files (from renamed or deleted commands/queries) are cleaned up automatically. To disable: ```xml true ``` See [File Index Tracking](/arc/backend/proxy-generation/file-index-tracking/) for details. ## CLI [Section titled “CLI”](#cli) ```bash proxygenerator assembly.dll output-path \ --skip-output-deletion \ --skip-index-generation \ --skip-file-index-tracking ``` # Configuration The proxy generator is configured through MSBuild properties and item groups in your `.csproj` file. Configuration is split into the following topics: * [Basic Options](/arc/backend/proxy-generation/configuration/basic/) — output path, segments to skip, source file output, decorator metadata * [Library Mode](/arc/backend/proxy-generation/configuration/library-mode/) — generate TypeScript for every public type in the assembly * [Type Exclusions](/arc/backend/proxy-generation/configuration/type-exclusions/) — exclude specific types or namespaces from generation * [Namespace Roots](/arc/backend/proxy-generation/configuration/namespace-roots/) — pin a namespace as the folder root * [Assembly-to-Package Mappings](/arc/backend/proxy-generation/configuration/assembly-package-mappings/) — import from external npm packages instead of regenerating * [Routing](/arc/backend/proxy-generation/configuration/routing/) — control how API routes are built * [Output Behavior](/arc/backend/proxy-generation/configuration/output-behavior/) — incremental vs. full regeneration ## Quick Reference [Section titled “Quick Reference”](#quick-reference) | Property / Item | Default | Topic | | ---------------------------------------- | ------------ | --------------------------------------------------------------------------------------------- | | `CratisProxiesOutputPath` | *(required)* | [Basic](/arc/backend/proxy-generation/configuration/basic/) | | `CratisProxiesSegmentsToSkip` | `0` | [Basic](/arc/backend/proxy-generation/configuration/basic/) | | `CratisProxiesUseSourceFileAsOutputFile` | `false` | [Basic](/arc/backend/proxy-generation/configuration/basic/) | | `CratisProxiesLibraryMode` | `false` | [Library Mode](/arc/backend/proxy-generation/configuration/library-mode/) | | `` | — | [Type Exclusions](/arc/backend/proxy-generation/configuration/type-exclusions/) | | `` | — | [Type Exclusions](/arc/backend/proxy-generation/configuration/type-exclusions/) | | `` | — | [Namespace Roots](/arc/backend/proxy-generation/configuration/namespace-roots/) | | `` | — | [Assembly-to-Package](/arc/backend/proxy-generation/configuration/assembly-package-mappings/) | | `CratisProxiesSkipCommandNameInRoute` | `false` | [Routing](/arc/backend/proxy-generation/configuration/routing/) | | `CratisProxiesSkipQueryNameInRoute` | `false` | [Routing](/arc/backend/proxy-generation/configuration/routing/) | | `CratisProxiesApiPrefix` | `api` | [Routing](/arc/backend/proxy-generation/configuration/routing/) | | `CratisProxiesSkipIndexGeneration` | `false` | [Output Behavior](/arc/backend/proxy-generation/configuration/output-behavior/) | | `CratisProxiesSkipOutputDeletion` | `true` | [Output Behavior](/arc/backend/proxy-generation/configuration/output-behavior/) | | `CratisProxiesSkipFileIndexTracking` | `false` | [Output Behavior](/arc/backend/proxy-generation/configuration/output-behavior/) | # Routing The proxy generator builds API route strings for commands and queries from the C# namespace hierarchy. These options let you adjust the resulting routes. ## API Prefix [Section titled “API Prefix”](#api-prefix) ```xml api ``` The `CratisProxiesApiPrefix` value is prepended to every generated route. Default is `api`. **Example:** Namespace `MyApp.Orders.Registration` with prefix `api` → `/api/orders/registration`. ## Excluding Type Names from Routes [Section titled “Excluding Type Names from Routes”](#excluding-type-names-from-routes) By default, the command or query type name is appended to the route: * Command `RegisterAuthor` → `/api/authors/registration/register-author` * Query `GetActiveAuthors` → `/api/authors/listing/get-active-authors` Setting either flag to `true` removes the type name from the route: ```xml true true ``` ### Automatic Conflict Detection [Section titled “Automatic Conflict Detection”](#automatic-conflict-detection) When the type name is skipped, the generator detects route conflicts automatically. If multiple commands or queries share the same namespace, their type names are re-added to prevent collisions. This matches the runtime endpoint mapping behavior exactly. **Example with a single command in `MyApp.Orders.Commands`:** ```text CreateOrderCommand → /api/orders/commands ``` **Example with multiple commands in `MyApp.Orders.Commands`:** ```text CreateOrderCommand → /api/orders/commands/create-order-command UpdateOrderCommand → /api/orders/commands/update-order-command DeleteOrderCommand → /api/orders/commands/delete-order-command ``` ## CLI [Section titled “CLI”](#cli) ```bash proxygenerator assembly.dll output-path \ --api-prefix=v1 \ --skip-command-name-in-route \ --skip-query-name-in-route ``` # Type Exclusions You can prevent specific types or entire namespaces from appearing in the generated TypeScript output by declaring `ExcludeType` or `ExcludeNamespace` item groups. Exclusions apply to: * Commands and queries (excluded types are not generated as proxy classes) * Types collected transitively from command/query properties * All types collected in [library mode](/arc/backend/proxy-generation/configuration/library-mode/) ## Excluding a Specific Type [Section titled “Excluding a Specific Type”](#excluding-a-specific-type) Use `ExcludeType` with the fully qualified C# type name (namespace + class name): ```xml ``` ## Excluding an Entire Namespace [Section titled “Excluding an Entire Namespace”](#excluding-an-entire-namespace) Use `ExcludeNamespace` with a glob pattern. The `*` wildcard matches any sequence of characters: ```xml ``` ### Glob Pattern Rules [Section titled “Glob Pattern Rules”](#glob-pattern-rules) | Pattern | Matches | | ------------------ | ------------------------------------------------------------------------- | | `MyApp.Internal*` | Any namespace that begins with `MyApp.Internal` | | `MyApp.*.Internal` | Any namespace matching that structure exactly (e.g. `MyApp.Foo.Internal`) | | `MyApp.Internal` | The exact namespace `MyApp.Internal` only | The `*` wildcard matches any sequence of characters including `.`, so `MyApp*` matches `MyApp`, `MyApp.Features`, `MyApp.Features.Auth`, etc. ## Combining Both [Section titled “Combining Both”](#combining-both) You can freely mix `ExcludeType` and `ExcludeNamespace` in the same item group: ```xml ``` ## CLI [Section titled “CLI”](#cli) Pass one or more `--exclude-type` and/or `--exclude-namespace` flags: ```bash proxygenerator assembly.dll output-path \ --exclude-type=MyApp.Internal.SecretPayload \ --exclude-namespace=MyApp.Tests* ``` # File Index Tracking The proxy generator uses inline metadata comments in generated files to enable intelligent cleanup of stale files when commands or queries are removed from your codebase. ## Overview [Section titled “Overview”](#overview) When you rename or delete a command or query from your backend, the corresponding TypeScript proxy file would normally be left behind. File tracking solves this by embedding metadata in each generated file and automatically removing orphaned files that are no longer needed. ## How it Works [Section titled “How it Works”](#how-it-works) 1. **Metadata Comments**: Each generated file includes a first-line comment with metadata: ```typescript // @generated by Cratis. Source: MyApp.Commands.CreateOrder. Time: 2024-12-08T12:00:00.0000000Z. Hash: 1E9523083D8C6457F194B1CB045877E19BE204FB42F9F6E5574B77FC79B3ABDA ``` 2. **Orphan Detection**: When running proxy generation, the generator scans all `.ts` files in the output directory: * Files with the `@generated by Cratis` marker that weren’t just generated are identified as orphaned * These orphaned files represent renamed or deleted types and are automatically removed 3. **Index.ts Management**: The generator intelligently maintains `index.ts` files: * Only includes exports for files that were actually generated * Preserves manual edits (comments, custom imports) in existing `index.ts` files * Automatically removes exports for deleted files * Deletes the `index.ts` file if the directory becomes empty 4. **Legacy Support**: The `.cratis/GeneratedFileIndex.json` file is still maintained for backwards compatibility but is superseded by the metadata-based approach. ## Configuration [Section titled “Configuration”](#configuration) ### Disabling File Tracking [Section titled “Disabling File Tracking”](#disabling-file-tracking) File tracking is enabled by default. To disable the metadata-based orphan detection, add the following to your `.csproj`: ```xml true ``` **Note**: This also disables the legacy `.cratis/GeneratedFileIndex.json` tracking. ### Disabling Index Generation [Section titled “Disabling Index Generation”](#disabling-index-generation) If you prefer to manage `index.ts` files manually, you can disable automatic index generation: ```xml true ``` When index generation is disabled: * No `index.ts` files are created or updated * Existing `index.ts` files are left untouched * You have full control over module exports This is useful when: * You have custom export logic in your `index.ts` files * You want to selectively export certain files * You’re integrating with existing manual code organization ### CLI Usage [Section titled “CLI Usage”](#cli-usage) When using the proxy generator CLI directly, you can use these flags: ```bash # Disable metadata-based file tracking proxygenerator assembly.dll output-path --skip-file-index-tracking # Disable index.ts generation proxygenerator assembly.dll output-path --skip-index-generation # Combine both options proxygenerator assembly.dll output-path --skip-file-index-tracking --skip-index-generation ``` You can also specify a custom project directory for the legacy `.cratis` folder: ```bash proxygenerator assembly.dll output-path --project-directory=/path/to/project ``` ## Version Control [Section titled “Version Control”](#version-control) ### Generated Files [Section titled “Generated Files”](#generated-files) The generated TypeScript files should be committed to version control: commands/ ```gitignore # Do NOT ignore generated proxies - they're part of your frontend codebase # queries/ ``` Each file is self-documenting with its metadata comment, making it easy to understand: * What generated the file * When it was last generated * The source C# type ### Legacy Index File [Section titled “Legacy Index File”](#legacy-index-file) The `.cratis` folder contains the legacy `GeneratedFileIndex.json` file. You can safely add it to `.gitignore`: ```gitignore .cratis/ ``` This is recommended because: * The metadata-based tracking approach is now the primary mechanism * The index is regenerated on each build * Different developers may have different build outputs * CI/CD pipelines will generate fresh indexes ## Example Scenario [Section titled “Example Scenario”](#example-scenario) Consider the following scenario: 1. **Initial State**: You have `CreateOrder.ts` and `UpdateOrder.ts` in your commands folder, both with metadata comments. 2. **Change**: You rename `UpdateOrder` to `ModifyOrder` in your C# code. 3. **Build**: The proxy generator runs and: * Generates `CreateOrder.ts` with updated timestamp (unchanged logic) * Generates `ModifyOrder.ts` (new file with metadata) * Detects `UpdateOrder.ts` has the `@generated` marker but wasn’t just generated * Identifies it as orphaned and deletes it * Updates `index.ts` to export only `CreateOrder` and `ModifyOrder` 4. **Result**: Your frontend has clean, up-to-date proxies without stale files. ## Index.ts File Management [Section titled “Index.ts File Management”](#indexts-file-management) The generator intelligently manages `index.ts` files to balance automation with flexibility: ### Generated Files Only [Section titled “Generated Files Only”](#generated-files-only) The `index.ts` file only includes exports for files that were generated by the proxy generator: ```typescript // Manual comment or import you added import { customHelper } from './helpers'; export * from './CreateOrder'; export * from './UpdateOrder'; ``` If you manually create a file like `CustomOrder.ts` in the same directory, it won’t be automatically added to `index.ts`. This prevents the generator from managing files it didn’t create. ### Preserving Manual Edits [Section titled “Preserving Manual Edits”](#preserving-manual-edits) Any non-export lines (comments, imports, etc.) in your `index.ts` are preserved: ```typescript // This comment will be kept across regenerations import { something } from 'somewhere'; // Exports are managed automatically export * from './CreateOrder'; export * from './UpdateOrder'; ``` ### Disabling Index Generation [Section titled “Disabling Index Generation”](#disabling-index-generation-1) If you want complete control over `index.ts` files, disable automatic generation: ```xml true ``` ## Behavior When Disabled [Section titled “Behavior When Disabled”](#behavior-when-disabled) When file tracking is disabled (`CratisProxiesSkipFileIndexTracking=true`): * No metadata-based orphan detection occurs * Stale files are not automatically cleaned up * The legacy `.cratis` folder and index file are not created * The `CratisProxiesSkipOutputDeletion` option becomes more important for managing old files * You may need to manually delete old proxy files when renaming or removing backend code When index generation is disabled (`CratisProxiesSkipIndexGeneration=true`): * No `index.ts` files are created or modified * Existing `index.ts` files are left completely untouched * You have full manual control over module exports ### Relationship with Output Deletion [Section titled “Relationship with Output Deletion”](#relationship-with-output-deletion) **Important**: By default, the proxy generator deletes the entire output directory on every build (`CratisProxiesSkipOutputDeletion=false`), which means: * File index tracking is not strictly necessary when using default settings * All proxies are regenerated from scratch on every build * No orphaned files can exist because the directory is wiped clean File index tracking becomes **essential** when you enable incremental generation: ```xml true false ``` With incremental generation enabled, file tracking automatically removes orphaned files when you rename or delete commands/queries, keeping your output directory clean without requiring a full deletion on every build. **Recommendation**: Use `CratisProxiesSkipOutputDeletion=true` with file tracking enabled (default) when proxies are intertwined with your feature code. Use the default full deletion when proxies are in a dedicated folder. See [Configuration - Output Behavior](/arc/backend/proxy-generation/configuration/output-behavior/) for more details. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Files Not Being Cleaned Up [Section titled “Files Not Being Cleaned Up”](#files-not-being-cleaned-up) 1. **Check if tracking is enabled**: Ensure `CratisProxiesSkipFileIndexTracking` is not set to `true`. 2. **Verify metadata comments**: Check that generated files have the `// @generated by Cratis` comment on the first line. 3. **Clean and rebuild**: Delete the output folder and rebuild to regenerate all files with metadata. ### Index.ts Not Updating [Section titled “Index.ts Not Updating”](#indexts-not-updating) 1. **Check if index generation is enabled**: Ensure `CratisProxiesSkipIndexGeneration` is not set to `true`. 2. **Verify generated files**: Only files generated by the proxy generator are included in `index.ts`. 3. **Check for manual files**: Files you created manually won’t be added to `index.ts`. ### Legacy Index File Issues [Section titled “Legacy Index File Issues”](#legacy-index-file-issues) If the legacy `.cratis/GeneratedFileIndex.json` file causes issues: 1. Delete the `.cratis` folder 2. Rebuild your project 3. The metadata-based tracking will handle cleanup automatically # Getting Started This guide covers the installation and basic setup of the Cratis Arc proxy generator. ## Package Dependency [Section titled “Package Dependency”](#package-dependency) To enable proxy generation, add a reference to the [Cratis.Arc.ProxyGenerator.Build](https://www.nuget.org/packages/Cratis.Arc.ProxyGenerator.Build) NuGet package to your project: ```xml ``` > **Important**: All projects that contain controllers, commands, or queries should reference this package, as the proxy generation runs as part of the compilation process. ## Required Configuration [Section titled “Required Configuration”](#required-configuration) Configure the proxy generator by adding MSBuild properties to your `.csproj` file: ```xml $(MSBuildThisFileDirectory)../Web ``` * `CratisProxiesOutputPath`: Specifies where the generated TypeScript files will be written. This should typically point to your frontend project directory. > **Note**: The proxy generator uses incremental generation by default — only files whose content has changed are written to disk, and timestamps of unchanged files are preserved. This means committed proxies will not be modified by a build unless their source types actually change. See [Configuration - Output Behavior](/arc/backend/proxy-generation/configuration/output-behavior/) for details. ## Frontend Prerequisites [Section titled “Frontend Prerequisites”](#frontend-prerequisites) The generated proxies depend on the [@cratis/arc](https://www.npmjs.com/package/@cratis/arc) NPM package. Install it in your frontend project: ```bash npm install @cratis/arc ``` ## Build Integration [Section titled “Build Integration”](#build-integration) The proxy generation runs automatically during the build process. Simply build your project: ```bash dotnet build ``` The generator will: 1. Load your compiled assembly 2. Discover controllers, commands, and queries 3. Analyze parameter types and return values 4. Generate TypeScript proxies with proper typing 5. Create index files for easy importing 6. Maintain the folder structure based on namespaces ## What Gets Generated [Section titled “What Gets Generated”](#what-gets-generated) The proxy generator creates TypeScript proxies for: * **Commands**: Both controller-based and model-bound approaches. See [Commands documentation](/arc/backend/commands/) for implementation details. * **Queries**: Single model, enumerable, and observable queries. See [Queries documentation](/arc/backend/queries/) for implementation details. * **Types**: Complex types used as parameters or return values * **Enums**: Enumerations referenced by commands or queries * **Identity Details**: Types from `IProvideIdentityDetails` implementations ## Next Steps [Section titled “Next Steps”](#next-steps) * Learn about all [Configuration Options](/arc/backend/proxy-generation/configuration/) * Understand [Command Proxy Generation](/arc/backend/proxy-generation/commands/) * Understand [Query Proxy Generation](/arc/backend/proxy-generation/queries/) * Learn about [Identity Details Type Generation](/arc/backend/proxy-generation/identity-details/) # Identity Details Type Generation The proxy generator can automatically discover and generate TypeScript types for your custom identity details. This enables type-safe access to identity information in your frontend application. ## Overview [Section titled “Overview”](#overview) When you implement a strongly-typed identity details provider using `IProvideIdentityDetails`, the proxy generator will: 1. Discover all implementations of `IProvideIdentityDetails` in your assemblies 2. Extract the `TDetails` type parameter 3. Generate TypeScript representations of these types 4. Include them in the standard proxy output alongside commands and queries ## The Generic Interface [Section titled “The Generic Interface”](#the-generic-interface) Arc provides a generic version of the identity details provider interface: ```csharp public interface IProvideIdentityDetails : IProvideIdentityDetails where TDetails : class; ``` This interface extends the base `IProvideIdentityDetails` interface but adds type information that the proxy generator can discover and use. For detailed information on implementing identity providers, see the [Identity documentation](/arc/backend/identity/). ## How Discovery Works [Section titled “How Discovery Works”](#how-discovery-works) The proxy generator scans all loaded assemblies for classes that: 1. Are concrete (non-abstract) classes 2. Implement `IProvideIdentityDetails` 3. Have a valid type argument for `TDetails` The `TDetails` type is then processed like any other complex type, generating a corresponding TypeScript interface. ## Generated Artifacts [Section titled “Generated Artifacts”](#generated-artifacts) For each identity details type discovered, the generator creates: 1. **TypeScript Interface**: An interface matching the C# type structure 2. **Nested Types**: Any complex types referenced by the details type 3. **Export Entry**: Added to the appropriate `index.ts` file ## Benefits of Using the Generic Interface [Section titled “Benefits of Using the Generic Interface”](#benefits-of-using-the-generic-interface) | Feature | `IProvideIdentityDetails` | `IProvideIdentityDetails` | | -------------------- | ------------------------- | ----------------------------------- | | Runtime behavior | Identical | Identical | | Type information | Lost at runtime | Captured for generation | | Proxy generation | No type generated | TypeScript type generated | | Frontend type safety | Manual typing required | Automatic type safety | ## Best Practices [Section titled “Best Practices”](#best-practices) 1. **Use the Generic Interface**: Always prefer `IProvideIdentityDetails` over the base interface to enable proxy generation. 2. **Keep Types Simple**: Identity details types should be simple DTOs without complex logic or dependencies. 3. **Avoid Sensitive Data**: Never include sensitive information like tokens or passwords in identity details. 4. **Use Records**: Consider using C# records for immutable identity detail types. ## Frontend Integration [Section titled “Frontend Integration”](#frontend-integration) The generated TypeScript types can be used with the identity system in `@cratis/arc/identity`. The types ensure that your frontend code has compile-time type safety when accessing identity details. For frontend usage patterns, see the [@cratis/arc documentation](https://www.npmjs.com/package/@cratis/arc). # Proxy Generation Cratis Arc includes a powerful proxy generation tool that automatically creates TypeScript proxies for your commands and queries during the build process. This eliminates the need to manually write frontend integration code or consult Swagger documentation, as the proxies provide compile-time type safety and intellisense support. ## Overview [Section titled “Overview”](#overview) The proxy generator runs as part of your build process using the C# Roslyn compiler’s code generator extensibility. It analyzes your compiled assemblies and generates TypeScript representations for: * **Commands**: HTTP POST operations (both controller-based and model-bound) * **Queries**: HTTP GET operations (single, enumerable, and observable) * **Types**: Complex types used in command/query parameters or return values * **Enums**: Enumerations used in your domain models * **Identity Details**: Custom identity types from `IProvideIdentityDetails` implementations ## Supported Approaches [Section titled “Supported Approaches”](#supported-approaches) The proxy generator supports both command and query implementation patterns: * **Controller-based**: Traditional ASP.NET Core controllers with `[HttpPost]` and `[HttpGet]` attributes * **Model-bound**: Simplified approach where types represent the command or query directly Both approaches generate equivalent TypeScript proxies, allowing you to choose the implementation style that best fits your needs. ## How It Works [Section titled “How It Works”](#how-it-works) The proxy generation runs automatically during the build process through the `CratisProxyGenerator` MSBuild target, which executes after the `AfterBuild` target. The generator: 1. Loads your compiled assembly 2. Discovers controllers, commands, and queries 3. Analyzes parameter types and return values 4. Discovers identity details types from `IProvideIdentityDetails` implementations 5. Generates TypeScript proxies with proper typing 6. Creates index files for easy importing 7. Maintains the folder structure based on namespaces 8. Tracks generated files for intelligent cleanup ## Generated Output Structure [Section titled “Generated Output Structure”](#generated-output-structure) The proxy generator maintains the folder structure based on your C# namespaces (after applying segment skipping). For each directory containing generated files, an `index.ts` file is automatically created that exports all the generated artifacts. ## Topics [Section titled “Topics”](#topics) * [Getting Started](/arc/backend/proxy-generation/getting-started/) - Installation and setup * [Configuration](/arc/backend/proxy-generation/configuration/) - All configuration options * [Commands](/arc/backend/proxy-generation/commands/) - Command proxy generation * [Queries](/arc/backend/proxy-generation/queries/) - Query proxy generation * [Type Mapping](/arc/backend/proxy-generation/type-mapping/) - How .NET types translate to TypeScript * [Identity Details](/arc/backend/proxy-generation/identity-details/) - Identity details type generation * [File Index Tracking](/arc/backend/proxy-generation/file-index-tracking/) - Incremental cleanup of generated files # Query Proxy Generation The proxy generator creates TypeScript query classes that provide type-safe query execution with React hook integration. ## Supported Approaches [Section titled “Supported Approaches”](#supported-approaches) Queries can be implemented using two approaches, both of which are supported by the proxy generator: * **Controller-based**: Queries in ASP.NET Core controllers using `[HttpGet]` attributes * **Model-bound**: Simplified approach where a type represents the query directly For detailed information on implementing queries, see the [Queries documentation](/arc/backend/queries/). ## Query Types [Section titled “Query Types”](#query-types) The generator supports three types of queries: | Type | Description | Use Case | | ---------------- | -------------------------------- | -------------------------------- | | **Single model** | Returns a single object | Fetching a specific entity by ID | | **Enumerable** | Returns an array of objects | Listing or searching entities | | **Observable** | Real-time updates via WebSockets | Live data feeds, dashboards | ## How Queries are Discovered [Section titled “How Queries are Discovered”](#how-queries-are-discovered) ### Controller-based Queries [Section titled “Controller-based Queries”](#controller-based-queries) The generator discovers controller-based queries by looking for: * Methods marked with `[HttpGet]` * Return types that indicate the query type: * Single object → Single model query * `IEnumerable`, `List`, etc. → Enumerable query * `IObservable` → Observable query See [Controller-based Queries](/arc/backend/queries/controller-based/) for implementation details. ### Model-bound Queries [Section titled “Model-bound Queries”](#model-bound-queries) The generator discovers model-bound queries by finding types that: * Are decorated with the `[ReadModel]` attribute * Have static methods that constitute query operations Each static method on the read model becomes a separate query. The method name becomes the query name, and method parameters (excluding injected dependencies) become the query parameters in the generated TypeScript. See [Model-bound Queries](/arc/backend/queries/model-bound/) for implementation details. ## Generated Query Structure [Section titled “Generated Query Structure”](#generated-query-structure) Generated query classes provide: * Type-safe parameter handling through an interface * React hooks for integration (`useQuery` or `useObservableQuery`) * The proper route based on the configuration ## Generated Artifacts [Section titled “Generated Artifacts”](#generated-artifacts) For each query, the generator creates: 1. **Parameters Interface**: An `IQueryNameParameters` interface (if the query has parameters) 2. **Query Class**: Extends `QueryFor` or `ObservableQueryFor` 3. **Route**: The HTTP route derived from the controller route or model-bound configuration ## Query Base Classes [Section titled “Query Base Classes”](#query-base-classes) Depending on the query type, the generated class extends: | Query Type | Base Class | | ------------ | ---------------------------- | | Single model | `QueryFor` | | Enumerable | `QueryFor` | | Observable | `ObservableQueryFor` | ## Excluding Queries from Generation [Section titled “Excluding Queries from Generation”](#excluding-queries-from-generation) To exclude specific controller-based queries from proxy generation, mark them with the `[AspNetResult]` attribute. This is useful when you want to handle the response manually or when the query returns a non-standard result. ## Route Configuration [Section titled “Route Configuration”](#route-configuration) The generated route is affected by the `CratisProxiesSkipQueryNameInRoute` configuration option: * When `false` (default): The query type name is included in the route * When `true`: The query type name is excluded from the route **Automatic Conflict Detection**: When `CratisProxiesSkipQueryNameInRoute` is `true`, the proxy generator automatically detects if multiple query methods exist in the same namespace (after skipping segments). If a conflict is detected, the query name is automatically included in the route to prevent route collisions. This behavior is consistent with the runtime endpoint mapping. For example: * Single query in namespace: Route is clean without type name (e.g., `/api/products`) * Multiple queries in same namespace: Type names are added automatically (e.g., `/api/products/get-all`, `/api/products/get-by-category`) See [Configuration](/arc/backend/proxy-generation/configuration/) for more details on route configuration options. ## Frontend Usage [Section titled “Frontend Usage”](#frontend-usage) The generated query proxies integrate with React through the `use()` static method, which returns a query result object containing: * `data`: The query result (typed according to the return type) * `isLoading`: Loading state indicator * `error`: Any error that occurred * Additional state depending on query type For observable queries, the result also includes connection state information. For frontend usage patterns, see the [@cratis/arc documentation](https://www.npmjs.com/package/@cratis/arc). # Type Mapping The proxy generator translates the .NET types on your commands, queries and read models into TypeScript types on the generated proxies. This page records that translation, so you can tell from the C# what the browser will actually receive. ## Primitive and common types [Section titled “Primitive and common types”](#primitive-and-common-types) | .NET type | TypeScript type | Metadata constructor | Imported from | | ------------------------------------------------------------------------------------------------ | ------------------------- | -------------------- | ---------------------- | | `bool` | `boolean` | `Boolean` | — | | `string`, `char`, `Uri` | `string` | `String` | — | | `byte`, `sbyte`, `short`, `int`, `long`, `ushort`, `uint`, `ulong`, `float`, `double`, `decimal` | `number` | `Number` | — | | `DateTime`, `DateTimeOffset` | `Date` | `Date` | — | | `DateOnly` | `DateOnly` | `DateOnly` | `@cratis/fundamentals` | | `TimeOnly` | `TimeOnly` | `TimeOnly` | `@cratis/fundamentals` | | `Guid` | `Guid` | `Guid` | `@cratis/fundamentals` | | `TimeSpan` | `TimeSpan` | `TimeSpan` | `@cratis/fundamentals` | | `Cratis.Geospatial.Point`, `LineString`, `Polygon` | same name | same name | `@cratis/fundamentals` | | `object`, `JsonNode`, `JsonObject`, `JsonArray`, `JsonDocument` | `Record` | `Object` | — | The metadata constructor is passed to the generated `@field(...)` decorator, which records the runtime type used during deserialization. An enum becomes a TypeScript `enum` and travels as its underlying number. A `ConceptAs` is unwrapped to `T` and mapped by this same table. A `Nullable` is unwrapped to `T` and the generated property is declared optional. Collections become arrays. A dictionary becomes `Record` when its key maps to `string`, and `ValueMap` otherwise. ## Dates, times and instants [Section titled “Dates, times and instants”](#dates-times-and-instants) `DateTime` and `DateTimeOffset` denote instants, so they map to the JavaScript `Date` that also denotes one. `DateOnly` and `TimeOnly` do not. A calendar date has no time and no zone; a time of day has no date. Both cross the wire as their ISO-8601 string — `"2026-05-12"` and `"14:30:45"` — and each has a type of its own in `@cratis/fundamentals` that holds exactly that, with no instant invented for it. This matters because a `Date` cannot hold either value without inventing one that was never sent: ```typescript new Date('2026-05-12') // 2026-05-12T00:00:00.000Z — UTC midnight, an instant nobody sent new Date('14:30:45') // Invalid Date — a time of day is not a date at all ``` The first is the more dangerous of the two, because it looks like it worked. UTC midnight read back through any browser-local getter reports the *previous* day everywhere west of UTC, while remaining correct at or east of it — so the bug is invisible to a developer in Europe and constant for a user in the Americas: ```typescript const asAnInstant = new Date('2026-05-12'); asAnInstant.toLocaleDateString('en-CA', { timeZone: 'Europe/Oslo' }); // '2026-05-12' asAnInstant.toLocaleDateString('en-CA', { timeZone: 'America/New_York' }); // '2026-05-11' ← wrong ``` `DateOnly` holds the three parts the server sent, so there is no instant to convert and nothing to shift: ```typescript readModel.dueDate.toString(); // '2026-05-12', in every time zone readModel.dueDate.year; // 2026 readModel.dueDate.day; // 12 ``` Where you genuinely need a `Date` — to feed a date picker, or to do calendar arithmetic — `toDate()` constructs one at midnight in the local zone. It is a method rather than what the value is, precisely because calling it invents a time, and that choice belongs at the call site making it: ```typescript const localMidnight = readModel.dueDate.toDate(); ``` `TimeOnly` works the same way, with `hour`, `minute`, `second` and `millisecond`. ## Declaring how your own types cross the wire [Section titled “Declaring how your own types cross the wire”](#declaring-how-your-own-types-cross-the-wire) The table above is the default. A `TypeToTsType` item overrides it, and is also how you declare a type the generator has never seen: ```xml ``` Every `DateOnly` then generates as `LocalDate`, imported from `@acme/time`. Omit `Package` to generate a bare TypeScript type with no import. Mappings are consulted **ahead of** the built-in table, so this corrects an existing type as readily as it declares a new one. A build that configures none generates exactly what it generated before. Note The defaults are chosen to be right without configuration — reach for a mapping when your application wants a *different* type, not to work around a default that is wrong. Whatever you map to has to be able to deserialize from what the server actually sends. `DateOnly` arrives as `"2026-05-12"` and `TimeOnly` as `"14:30:45"`, so register a converter for the type you map to with `JsonSerializer.registerConverter`, or the value arrives as the raw string wearing the declared type’s name. ## Unmapped types [Section titled “Unmapped types”](#unmapped-types) A type not in the table above, and not declared through `TypeToTsType`, is generated as its own TypeScript class, in a file mirroring its namespace, and imported into whatever references it. Types from assemblies configured as package-mapped are imported from that package instead of being generated. # Validation The ProxyGenerator automatically extracts validation rules from your backend validators and generates corresponding TypeScript validation code for the frontend. This ensures that validation rules are defined once on the backend and automatically enforced on both client and server. The system supports two validation approaches: * **FluentValidation**: Class-based validators using the FluentValidation library * **DataAnnotations**: Attribute-based validation using `System.ComponentModel.DataAnnotations` ## Overview [Section titled “Overview”](#overview) The validation extraction provides: * **Automatic Rule Extraction**: Discovers and extracts both FluentValidation rules and DataAnnotations attributes using reflection * **Type-Safe Generation**: Generates type-safe TypeScript validators for commands and queries * **Custom Message Preservation**: Extracts and carries over custom error messages declared as literals; messages declared as a factory are left to the server to resolve * **Multiple Validation Styles**: Support for both FluentValidation class-based and DataAnnotations attribute-based validation * **Version Independence**: Uses reflection-based type checking without hard dependencies on FluentValidation ## How It Works [Section titled “How It Works”](#how-it-works) The ProxyGenerator uses reflection to: 1. **Discover Validators**: Find all `AbstractValidator` implementations and properties with DataAnnotations attributes for command and query types 2. **Extract Rules**: Analyze validation rules from both FluentValidation and DataAnnotations without requiring package references 3. **Generate TypeScript**: Create validators with the same rules as the backend, and with the messages it can know at build time 4. **Integrate Automatically**: Generated validators run before server calls ## FluentValidation Support [Section titled “FluentValidation Support”](#fluentvalidation-support) ### Supported Validation Rules [Section titled “Supported Validation Rules”](#supported-validation-rules) The following FluentValidation rules are automatically converted to TypeScript: | FluentValidation Rule | TypeScript Rule | Generated Code Example | | ------------------------- | ----------------------- | ------------------------------------------------- | | `NotEmpty()` | `notEmpty()` | `this.ruleFor(c => c.email).notEmpty()` | | `NotNull()` | `notNull()` | `this.ruleFor(c => c.value).notNull()` | | `EmailAddress()` | `emailAddress()` | `this.ruleFor(c => c.email).emailAddress()` | | `MinimumLength(n)` | `minLength(n)` | `this.ruleFor(c => c.name).minLength(2)` | | `MaximumLength(n)` | `maxLength(n)` | `this.ruleFor(c => c.name).maxLength(50)` | | `Length(min, max)` | `length(min, max)` | `this.ruleFor(c => c.code).length(3, 10)` | | `Matches(pattern)` | `matches(pattern)` | `this.ruleFor(c => c.phone).matches(/^\d+$/)` | | `GreaterThan(n)` | `greaterThan(n)` | `this.ruleFor(c => c.quantity).greaterThan(0)` | | `GreaterThanOrEqualTo(n)` | `greaterThanOrEqual(n)` | `this.ruleFor(c => c.age).greaterThanOrEqual(18)` | | `LessThan(n)` | `lessThan(n)` | `this.ruleFor(c => c.discount).lessThan(100)` | | `LessThanOrEqualTo(n)` | `lessThanOrEqual(n)` | `this.ruleFor(c => c.rating).lessThanOrEqual(5)` | ### Backend Validator Example [Section titled “Backend Validator Example”](#backend-validator-example) Define a validator on the backend using FluentValidation: ```csharp public class CreateUserCommand { public string Email { get; set; } = string.Empty; public int Age { get; set; } public string Name { get; set; } = string.Empty; } public class CreateUserCommandValidator : BaseValidator { public CreateUserCommandValidator() { RuleFor(x => x.Email) .NotEmpty() .WithMessage("Email address is required") .EmailAddress(); RuleFor(x => x.Age) .GreaterThanOrEqualTo(18); RuleFor(x => x.Name) .NotEmpty() .MinimumLength(2) .MaximumLength(50); } } ``` ## Generated TypeScript Validator [Section titled “Generated TypeScript Validator”](#generated-typescript-validator) The ProxyGenerator automatically generates the corresponding TypeScript validator: ```typescript export class CreateUserCommandValidator extends CommandValidator { constructor() { super(); this.ruleFor(c => c.email) .notEmpty() .withMessage('Email address is required') .emailAddress(); this.ruleFor(c => c.age) .greaterThanOrEqual(18); this.ruleFor(c => c.name) .notEmpty() .minLength(2) .maxLength(50); } } ``` ## DataAnnotations Support [Section titled “DataAnnotations Support”](#dataannotations-support) As an alternative to FluentValidation, you can use `System.ComponentModel.DataAnnotations` attributes directly on your command and query properties. The ProxyGenerator will automatically extract these attributes and generate equivalent TypeScript validators. ### Supported DataAnnotations Attributes [Section titled “Supported DataAnnotations Attributes”](#supported-dataannotations-attributes) The following DataAnnotations attributes are automatically converted to TypeScript: | DataAnnotations Attribute | TypeScript Rule | Example | | ---------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------- | | `[Required]` | `notEmpty()` | `[Required] public string Name { get; set; }` | | `[EmailAddress]` | `emailAddress()` | `[EmailAddress] public string Email { get; set; }` | | `[MinLength(n)]` | `minLength(n)` | `[MinLength(2)] public string Code { get; set; }` | | `[MaxLength(n)]` | `maxLength(n)` | `[MaxLength(50)] public string Title { get; set; }` | | `[StringLength(max)]` | `maxLength(max)` | `[StringLength(100)] public string Description { get; set; }` | | `[StringLength(max, MinimumLength=min)]` | `length(min, max)` | `[StringLength(50, MinimumLength=3)]` | | `[Range(min, max)]` | `greaterThanOrEqual(min).lessThanOrEqual(max)` | `[Range(0, 150)] public int Age { get; set; }` | | `[RegularExpression(pattern)]` | `matches(pattern)` | `[RegularExpression(@"^\d+$")]` | | `[Url]` | `matches(urlPattern)` | `[Url] public string Website { get; set; }` | | `[Phone]` | `matches(phonePattern)` | `[Phone] public string PhoneNumber { get; set; }` | ### DataAnnotations Example [Section titled “DataAnnotations Example”](#dataannotations-example) Define validation using attributes on your command or query: ```csharp public class RegisterUserCommand { [Required(ErrorMessage = "Name is required")] [StringLength(50, MinimumLength = 2, ErrorMessage = "Name must be between 2 and 50 characters")] public string Name { get; set; } = string.Empty; [Required] [EmailAddress(ErrorMessage = "Valid email address is required")] public string Email { get; set; } = string.Empty; [Range(18, 150, ErrorMessage = "Age must be between 18 and 150")] public int Age { get; set; } [Url] public string Website { get; set; } = string.Empty; [Phone] public string PhoneNumber { get; set; } = string.Empty; } ``` ### Generated TypeScript from DataAnnotations [Section titled “Generated TypeScript from DataAnnotations”](#generated-typescript-from-dataannotations) The ProxyGenerator generates a TypeScript validator from the DataAnnotations attributes: ```typescript export class RegisterUserCommandValidator extends CommandValidator { constructor() { super(); this.ruleFor(c => c.name) .notEmpty() .withMessage('Name is required') .length(2, 50) .withMessage('Name must be between 2 and 50 characters'); this.ruleFor(c => c.email) .notEmpty() .emailAddress() .withMessage('Valid email address is required'); this.ruleFor(c => c.age) .greaterThanOrEqual(18) .lessThanOrEqual(150) .withMessage('Age must be between 18 and 150'); this.ruleFor(c => c.website) .matches(/^https?:\/\/.+/); this.ruleFor(c => c.phoneNumber) .matches(/^\+?[1-9]\d{1,14}$/); } } ``` ### Choosing Between FluentValidation and DataAnnotations [Section titled “Choosing Between FluentValidation and DataAnnotations”](#choosing-between-fluentvalidation-and-dataannotations) Both approaches are fully supported, and you can choose based on your preferences: **FluentValidation**: * ✅ More expressive and readable for complex validation logic * ✅ Better separation of concerns (validation in separate class) * ✅ More flexible and powerful rule composition * ✅ Easier to unit test validation logic independently **DataAnnotations**: * ✅ More concise for simple validation rules * ✅ Validation rules are co-located with properties * ✅ No additional dependencies required (built into .NET) * ✅ Familiar to developers from ASP.NET MVC/Web API You can also mix both approaches in the same application - the ProxyGenerator will extract rules from both sources. ## Default Error Messages [Section titled “Default Error Messages”](#default-error-messages) All validation rules have sensible default error messages that are automatically used when no custom message is specified: ```typescript // C# without custom message RuleFor(x => x.Age).GreaterThanOrEqualTo(18); // Generated TypeScript (with default message) this.ruleFor(c => c.age).greaterThanOrEqual(18); // Default message: "'age' must be greater than or equal to 18." ``` ## Custom Error Messages [Section titled “Custom Error Messages”](#custom-error-messages) Custom error messages defined using `.WithMessage()` are automatically extracted and included in the generated validators: ```csharp // C# with custom message RuleFor(x => x.Email) .NotEmpty() .WithMessage("Email address is required"); // Generated TypeScript this.ruleFor(c => c.email) .notEmpty() .withMessage('Email address is required'); ``` ### Deferred messages are not projected [Section titled “Deferred messages are not projected”](#deferred-messages-are-not-projected) FluentValidation also accepts a message as a factory — `.WithMessage(_ => Messages.EmailRequired)` — which is the form a message read from a resource, a tenant setting or any other ambient state takes. **The generator does not project those.** The rule still crosses to the client; only the message stays behind: ```csharp // C# with a deferred message RuleFor(x => x.Email) .NotEmpty() .WithMessage(_ => Messages.EmailRequired); // Generated TypeScript — the rule mirrors, the message does not this.ruleFor(c => c.email) .notEmpty(); // The client rule falls back to its own default: "'email' must not be empty." ``` A factory is deferred because its value is not known yet. The generator runs on a build machine, in a different process from the browser that will show the message, at a different time and under different ambient state — so any value it obtained by calling the factory would be an answer for the wrong conditions, frozen into an artifact nobody reviews. A delegate is opaque, so the generator cannot tell a factory that returns a constant from one that reads the culture, and it does not guess. Note This matters more than it looks, because a generated client rule that fails **suppresses the request**. Had the message been projected, the build machine’s answer would be the one the user sees and the server that would have resolved it correctly would never be asked. Where a rule’s exact wording must reach the user, either declare it as a literal — which genuinely is context-free — or express the rule in a shape the generator does not mirror (`Must`, `MustAsync`, or a check in `Handle()`), so that it is the server that rejects and messages it. ## Query Validation [Section titled “Query Validation”](#query-validation) Query parameters can also be validated using the same approach: ```csharp public class SearchUsersQuery { public string SearchTerm { get; set; } = string.Empty; public int MinAge { get; set; } } public class SearchUsersQueryValidator : BaseValidator { public SearchUsersQueryValidator() { RuleFor(x => x.SearchTerm).MinimumLength(3); RuleFor(x => x.MinAge).GreaterThanOrEqualTo(0).LessThanOrEqualTo(150); } } ``` Generated TypeScript: ```typescript export class SearchUsersQueryValidator extends QueryValidator { constructor() { super(); this.ruleFor(c => c.searchTerm).minLength(3); this.ruleFor(c => c.minAge).greaterThanOrEqual(0).lessThanOrEqual(150); } } ``` Observable queries get the same validator as one-shot queries. It runs when you call `perform()`, and when you `subscribe()` — a subscription rejected by validation delivers an invalid `QueryResult` to your callback rather than opening a connection, so a subscriber can tell “these arguments are wrong” apart from “no data yet”. ## Concept Validation [Section titled “Concept Validation”](#concept-validation) A `ConceptValidator` is extracted for every command property and query parameter of that concept’s type, so a rule written once reaches the client everywhere the concept is used: ```csharp public record EmailAddress(string Value) : ConceptAs(Value); public class EmailAddressValidator : ConceptValidator { public EmailAddressValidator() => RuleFor(x => x.Value).EmailAddress().WithMessage("Must be a valid email address"); } public class RegisterUser { public EmailAddress Email { get; set; } = new(string.Empty); } ``` A concept is represented in TypeScript by its underlying primitive, so the rule is attached to the property that carries it: ```typescript export class RegisterUserValidator extends CommandValidator { constructor() { super(); this.ruleFor(c => c.email) .emailAddress() .withMessage('Must be a valid email address'); } } ``` Concept rules add to a model’s own rules rather than replacing them. If `RegisterUser` also has a `CommandValidator` with a rule on `Email`, both apply — which is what the server does, so the client agrees with it. Only a concept sitting directly on a property or parameter is extracted. The client rule builder resolves a single property name, so it cannot express a rule against a concept nested deeper in the graph. Those rules still run server-side, where the whole object graph is walked. ## Limitations [Section titled “Limitations”](#limitations) The ProxyGenerator can only extract validation rules that can be executed client-side. The following are **not supported**: * **Custom validators using `.Must()`**: Business logic that requires server-side execution * **Async validators**: Rules that make database or service calls * **Complex predicates**: Conditions that depend on server-side data * **Cross-property validation**: Rules that compare multiple properties (partially supported) ### Validators with Constructor Dependencies [Section titled “Validators with Constructor Dependencies”](#validators-with-constructor-dependencies) Validators can have constructor dependencies that are used for server-side validation. The ProxyGenerator handles this automatically: ```csharp public class AssignPersonnelValidator : CommandValidator { public AssignPersonnelValidator(PersonnelAlreadyAssigned personnelAlreadyAssigned) { RuleFor(x => x.Name).Length(1, 100).NotEmpty(); RuleFor(x => x.Age).NotEmpty(); RuleFor(x => x.RoleId).NotNull(); RuleFor(x => x.PersonId).NotNull(); // This rule requires server-side execution and won't be extracted RuleFor(x => x) .MustAsync(async (command, ct) => !await personnelAlreadyAssigned(command.MissionId, command.PersonId)) .WithMessage("Personnel is already assigned to this mission."); } } ``` The ProxyGenerator will: 1. Create the validator instance with `null` values for dependencies 2. Extract all simple validation rules (`.Length()`, `.NotEmpty()`, `.NotNull()`) 3. Skip rules that require dependencies (`.MustAsync()` with dependency usage) 4. Generate TypeScript with only the client-side compatible rules This allows you to keep all validation logic in one place while only the client-compatible rules are extracted. ### Recommended Approach [Section titled “Recommended Approach”](#recommended-approach) * Use **out-of-the-box FluentValidation rules** for client-side validation * Use **`.Must()` and custom validators** for server-side business rules * Keep **simple validation rules** on the client for better UX * Always enforce **all validation on the server** for security ## Client-Side Validation Flow [Section titled “Client-Side Validation Flow”](#client-side-validation-flow) 1. User fills out a form and submits a command or query 2. Generated validator runs automatically before the HTTP call 3. If validation fails, errors are returned immediately without server call 4. If validation passes, the request proceeds to the server 5. Server runs the same validation plus any server-only rules ## Best Practices [Section titled “Best Practices”](#best-practices) 1. **Define Rules Once**: Always define validation on the backend and let ProxyGenerator extract them 2. **Use Simple Rules**: Keep client-side rules simple and use server-side for complex logic 3. **Custom Messages**: Provide user-friendly messages using `.WithMessage()` 4. **Don’t Duplicate**: Never manually write frontend validators - let the generator handle it 5. **Server Authority**: Always validate on the server regardless of client validation ## Related Topics [Section titled “Related Topics”](#related-topics) * [Backend Command Validation](/arc/backend/commands/validation/) - Defining validation rules on the backend * [Frontend Validation](/arc/frontend/core/validation/) - How client-side validation works * [ProxyGenerator Configuration](/arc/backend/proxy-generation/configuration/) - Configuring the ProxyGenerator # Queries If [commands](/arc/backend/commands/) are how your system *changes*, queries are how it’s *read*. A query pulls data out and shapes it for a caller — a list, a detail view, a dashboard. Arc takes the same approach it does for commands: you express the read, and it handles parameter binding, validation, authorization, and a typed TypeScript proxy for the frontend. No hand-written API client, no untyped JSON. And queries have a superpower commands don’t: they can be **observable**. An observable query holds a live connection and pushes fresh results to the client whenever the underlying data changes — so a React list re-renders the moment a command updates the read model behind it. ``` GET / subscribe binds + validates reads result / live stream React UI Arc endpoint Query Read model ``` ## Two ways to define one [Section titled “Two ways to define one”](#two-ways-to-define-one) | Style | What it looks like | Reach for it when | | ---------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | [Model-bound](/arc/backend/queries/model-bound/) | A read method declared on the model, discovered by convention | **The default.** Least boilerplate; the query lives with the data it reads. | | [Controller-based](/arc/backend/queries/controller-based/) | A query method on a controller | You need full control over the HTTP route, or you’re integrating with existing controllers. | A controller-based read is as plain as it looks: ```csharp [HttpGet("starting-with")] public IEnumerable StartingWith([FromQuery] string? filter) => _collection.Find(Builders.Filter.Regex( "name", $"^{filter ?? string.Empty}.*")).ToList(); ``` Parameters you declare (`[FromQuery]`, route values) become **typed arguments** on the generated proxy — so the frontend calls `StartingWith.use({ filter: '' })` with the compiler checking the shape. ## Request/response or live? [Section titled “Request/response or live?”](#requestresponse-or-live) This is the choice that defines a query: | | Request / response | [Observable](/arc/frontend/react/queries/observable-queries/) | | -------------- | ------------------------------- | ------------------------------------------------------------- | | **Behavior** | Fetch once, return a result | Subscribe; results push on every change | | **Transport** | A normal HTTP `GET` | SSE or WebSocket, hub-routed by default | | **Use it for** | One-off reads, reports, exports | Anything a user watches — lists, dashboards, status | Favor observable queries for screens that should stay current. The live loop — a command changes state, the read model updates, every subscribed browser re-renders — is the experience Arc is built to make effortless. In a Chronicle-backed slice that state change is an appended event and a projection; in a current-state slice it can be a MongoDB document or EF entity changing directly. ## The pipeline around it [Section titled “The pipeline around it”](#the-pipeline-around-it) Every query runs through a pipeline where Arc applies the cross-cutting concerns for you: | Concern | Page | | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | | How a query is processed end to end | [Query Pipeline](/arc/backend/queries/query-pipeline/) | | Validate query parameters | [Validation](/arc/backend/queries/validation/) | | Authorize by role or policy | [Authorization](/arc/backend/core/authorization/) | | Transform read models before they’re served (mask, decrypt, enrich) | [Read Model Interception](/arc/backend/queries/read-model-interception/) | | Stream many live queries over one connection | [Observable Query Hub](/arc/backend/queries/observable-query-demultiplexer/) | | Re-check authorization while a live stream is running | [Observable Query Emission Guards](/arc/backend/queries/observable-query-emission-guards/) | | Debug a live query from the terminal | [Use Observable Queries with cURL](/arc/backend/queries/using-observable-queries-with-curl/) | ## The payoff: typed reads in React [Section titled “The payoff: typed reads in React”](#the-payoff-typed-reads-in-react) As with commands, building the backend generates a typed proxy for every query. From React you call its `.use()` hook and get back a strongly-typed result with its loading and validation state — see [Queries in React](/arc/frontend/react/queries/). One definition, read safely from C# all the way to the component. # Change Stream Observable collection queries in Arc deliver full snapshots on every update by default. As collections grow, shipping the entire collection on every MongoDB change stream event becomes expensive. The **change stream** feature reduces this overhead by computing a delta — which items were added, replaced, or removed — and attaching it to each `QueryResult` as a `ChangeSet`. ## How It Works [Section titled “How It Works”](#how-it-works) When the `ObservableQueryDemultiplexer` receives a new snapshot from a `ISubject>`, it compares the snapshot with the previous one using the `ChangeSetComputor` and populates `QueryResult.ChangeSet` with the delta. The full snapshot is still available in `QueryResult.Data` — the `ChangeSet` is additive, not a replacement. ### Identity-Based Delta (Recommended) [Section titled “Identity-Based Delta (Recommended)”](#identity-based-delta-recommended) When the item type exposes a property conventionally named `Id` (case-insensitive), the computor uses it to build a precise three-way diff: | Operation | Condition | | ---------- | ------------------------------------------------------------------------------------------ | | `Added` | An item with a new `Id` value appears in the current snapshot. | | `Replaced` | An item with the same `Id` exists in both snapshots but its JSON representation differs. | | `Removed` | An item with an `Id` present in the previous snapshot is absent from the current snapshot. | ### JSON-Hash Fallback [Section titled “JSON-Hash Fallback”](#json-hash-fallback) When no `Id` property is found, the computor serializes each item to JSON and uses the full JSON as a hash key. This surfaces `Added` and `Removed` items but **cannot detect `Replaced`** (because item identity is unknown). ## Wire Format [Section titled “Wire Format”](#wire-format) The `ChangeSet` is serialized as part of `QueryResult` and sent over the WebSocket or SSE connection alongside the full `Data` field: ```json { "data": [ /* full current snapshot */ ], "changeSet": { "added": [ /* new items */ ], "replaced": [ /* updated items */ ], "removed": [ /* deleted items */ ] } } ``` When no `ChangeSet` is present on a `QueryResult`, the client must treat `Data` as the full current snapshot. ## `ChangeSet` Type [Section titled “ChangeSet Type”](#changeset-type) ```csharp public class ChangeSet { public IEnumerable Added { get; set; } = []; public IEnumerable Replaced { get; set; } = []; public IEnumerable Removed { get; set; } = []; } ``` ## `ChangeSetComputor` [Section titled “ChangeSetComputor”](#changesetcomputor) The `ChangeSetComputor` class is responsible for delta computation and can be used independently: ```csharp var computor = new ChangeSetComputor(serializerOptions); // First call — all items are Added ChangeSet initial = computor.Compute(null, currentItems); // Subsequent calls — computes the delta ChangeSet delta = computor.Compute(previousItems, currentItems); ``` ### Identity Property Discovery [Section titled “Identity Property Discovery”](#identity-property-discovery) `ChangeSetComputor.FindIdentityProperty(type)` locates the identity property by looking for a property named `Id` (case-insensitive). This static helper can be used in tests or custom infrastructure: ```csharp PropertyInfo? idProp = ChangeSetComputor.FindIdentityProperty(typeof(MyReadModel)); ``` ## See Also [Section titled “See Also”](#see-also) * [Change Stream — Frontend](/arc/frontend/react/queries/change-stream/) — React `useChangeStream()` hook, transfer mode configuration, and usage examples. * [Observable Query Demultiplexer](/arc/backend/queries/observable-query-demultiplexer/) — The backend component that manages WebSocket and SSE connections and invokes the `ChangeSetComputor`. * [Observable Query Hub](/arc/backend/queries/observable-query-demultiplexer/) — Wire protocol reference for observable query connections. # Controller Based Queries You can represent queries as regular ASP.NET Core Controller actions with HTTP GET methods. ```csharp public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance); [Route("api/accounts")] public class Accounts : Controller { readonly IMongoCollection _collection; public Accounts(IMongoCollection collection) => _collection = collection; [HttpGet] public IEnumerable AllAccounts() => _collection.Find(_ => true).ToList(); } ``` > Note: This particular model represents its values as concepts - a value type encapsulation that makes us not use primitives - thus creating clearer APIs and models. If you’re using the Cratis Arc [proxy generator](/arc/backend/proxy-generation/), the method name will become the query name for the generated TypeScript file and class. ## Key Features [Section titled “Key Features”](#key-features) Controller-based queries provide several powerful features: * **Standard ASP.NET Core routing** and HTTP verb support * **Flexible return types** including collections, single objects, and custom response wrappers * **Dependency injection** for services and repositories * **Query arguments** via route parameters, query strings, and request bodies * **Async support** for asynchronous operations * **Observable queries** for real-time data streaming * **Custom route templates** for RESTful API design ## When to Use Controller-Based Queries [Section titled “When to Use Controller-Based Queries”](#when-to-use-controller-based-queries) Controller-based queries are ideal when you: * Want explicit control over HTTP routing and URL structure * Need to leverage existing ASP.NET Core features like filters, middleware, or custom attributes * Are building RESTful APIs with standard HTTP conventions * Want to separate query logic from your read models * Need complex routing scenarios with multiple parameters ## Bypassing Query Result Wrappers [Section titled “Bypassing Query Result Wrappers”](#bypassing-query-result-wrappers) By default, controller-based queries return results wrapped in a `QueryResult` structure. If you need to return the raw result from your controller action without this wrapper, you can use the `[AspNetResult]` attribute. For more details, see [Without wrappers](/arc/backend/asp-net-core/without-wrappers/). ## Related Topics [Section titled “Related Topics”](#related-topics) * [Route Templates](/arc/backend/queries/controller-based/route-templates/) - Learn about URL routing and parameter binding * [Query Arguments](/arc/backend/queries/controller-based/query-arguments/) - How to handle different types of query parameters * [Return Types](/arc/backend/queries/controller-based/return-types/) - Understanding different response formats * [Observable Queries](/arc/backend/queries/controller-based/observable-queries/) - Real-time data streaming with WebSockets * [Dependency Injection](/arc/backend/queries/controller-based/dependency-injection/) - Working with services and repositories ## Basic Example with Async Support [Section titled “Basic Example with Async Support”](#basic-example-with-async-support) For asynchronous operations, you can return `Task`: ```csharp [HttpGet] public async Task> AllAccountsAsync() { var result = await _collection.FindAsync(_ => true); return result.ToList(); } ``` > **Note**: The [proxy generator](/arc/backend/proxy-generation/) automatically creates TypeScript types for your controller methods, making them strongly typed on the frontend as well. # Dependency Injection Controller-based queries support full dependency injection through their constructors, allowing you to inject services, repositories, loggers, and other dependencies from the service collection. ## Constructor Injection [Section titled “Constructor Injection”](#constructor-injection) The most common pattern is to inject dependencies through the controller’s constructor: ```csharp [Route("api/accounts")] public class Accounts : Controller { readonly IAccountService _accountService; readonly ILogger _logger; readonly IMongoCollection _collection; public Accounts( IAccountService accountService, ILogger logger, IMongoCollection collection) { _accountService = accountService; _logger = logger; _collection = collection; } [HttpGet] public IEnumerable AllAccounts() { _logger.LogInformation("Retrieving all accounts"); return _accountService.GetAllAccounts(); } } ``` ## Common Dependency Types [Section titled “Common Dependency Types”](#common-dependency-types) ### Database Collections [Section titled “Database Collections”](#database-collections) MongoDB collections are commonly injected: ```csharp public class Accounts : Controller { readonly IMongoCollection _collection; public Accounts(IMongoCollection collection) { _collection = collection; } [HttpGet] public async Task> GetAccountsAsync() { var result = await _collection.FindAsync(_ => true); return result.ToList(); } } ``` ### Entity Framework DbContext [Section titled “Entity Framework DbContext”](#entity-framework-dbcontext) For Entity Framework Core scenarios: ```csharp public class Accounts : Controller { readonly ApplicationDbContext _dbContext; public Accounts(ApplicationDbContext dbContext) { _dbContext = dbContext; } [HttpGet] public async Task> GetAccountsAsync() { return await _dbContext.Accounts.ToListAsync(); } } ``` ### Business Services [Section titled “Business Services”](#business-services) Inject business logic services: ```csharp public class Accounts : Controller { readonly IAccountService _accountService; readonly ICustomerService _customerService; public Accounts(IAccountService accountService, ICustomerService customerService) { _accountService = accountService; _customerService = customerService; } [HttpGet("{id}/details")] public async Task GetAccountDetails(AccountId id) { var account = await _accountService.GetAccountAsync(id); var customer = await _customerService.GetCustomerAsync(account.Owner); return new AccountDetails(account, customer); } } ``` ### Logging [Section titled “Logging”](#logging) Structured logging with dependency injection: ```csharp public class Accounts : Controller { readonly IMongoCollection _collection; readonly ILogger _logger; public Accounts(IMongoCollection collection, ILogger logger) { _collection = collection; _logger = logger; } [HttpGet("search")] public async Task> SearchAccounts([FromQuery] string term) { _logger.LogInformation("Searching accounts with term: {SearchTerm}", term); var filter = Builders.Filter.Regex( a => a.Name, new BsonRegularExpression(term, "i")); var result = await _collection.FindAsync(filter); var accounts = result.ToList(); _logger.LogInformation("Found {AccountCount} accounts", accounts.Count); return accounts; } } ``` ### Configuration [Section titled “Configuration”](#configuration) Inject configuration objects: ```csharp public class Accounts : Controller { readonly IMongoCollection _collection; readonly AccountQueryOptions _options; public Accounts( IMongoCollection collection, IOptions options) { _collection = collection; _options = options.Value; } [HttpGet] public async Task> GetAccounts() { var result = await _collection.FindAsync(_ => true); return result.Limit(_options.DefaultPageSize).ToList(); } } ``` ## Service Registration [Section titled “Service Registration”](#service-registration) Make sure your dependencies are registered in the service collection: ```csharp // In Program.cs or Startup.cs builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.Configure( builder.Configuration.GetSection("AccountQueries")); ``` ## Multiple Dependencies [Section titled “Multiple Dependencies”](#multiple-dependencies) Controllers can have many dependencies injected: ```csharp public class Accounts : Controller { readonly IAccountService _accountService; readonly ICustomerService _customerService; readonly ICachingService _cache; readonly ILogger _logger; readonly IMapper _mapper; readonly AccountQueryOptions _options; public Accounts( IAccountService accountService, ICustomerService customerService, ICachingService cache, ILogger logger, IMapper mapper, IOptions options) { _accountService = accountService; _customerService = customerService; _cache = cache; _logger = logger; _mapper = mapper; _options = options.Value; } [HttpGet("{id}")] public async Task GetAccount(AccountId id) { var cacheKey = $"account-{id}"; var cached = await _cache.GetAsync(cacheKey); if (cached is not null) { _logger.LogInformation("Returning cached account {AccountId}", id); return cached; } _logger.LogInformation("Loading account {AccountId} from database", id); var account = await _accountService.GetAccountAsync(id); var customer = await _customerService.GetCustomerAsync(account.Owner); var result = _mapper.Map((account, customer)); await _cache.SetAsync(cacheKey, result, _options.CacheExpiry); return result; } } ``` ## Generic Dependencies [Section titled “Generic Dependencies”](#generic-dependencies) You can inject generic types: ```csharp public class GenericQueries : Controller where T : class { readonly IRepository _repository; readonly ILogger> _logger; public GenericQueries(IRepository repository, ILogger> logger) { _repository = repository; _logger = logger; } [HttpGet] public async Task> GetAll() { _logger.LogInformation("Getting all {EntityType}", typeof(T).Name); return await _repository.GetAllAsync(); } } ``` ## Best Practices [Section titled “Best Practices”](#best-practices) 1. **Use readonly fields** - Store injected dependencies as `readonly` fields 2. **Prefer constructor injection** over method injection or service locator patterns 3. **Keep constructors clean** - Don’t perform logic in constructors, just store dependencies 4. **Use appropriate lifetimes** - Register services with appropriate lifetimes (Singleton, Scoped, Transient) 5. **Validate dependencies** - Ensure all required dependencies are registered in the DI container 6. **Use IOptions\** for configuration objects rather than injecting raw configuration ## Avoiding Service Locator [Section titled “Avoiding Service Locator”](#avoiding-service-locator) Don’t use `IServiceProvider` directly in your controllers: ```csharp // ❌ Don't do this - service locator anti-pattern public class BadAccounts : Controller { readonly IServiceProvider _serviceProvider; public BadAccounts(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } [HttpGet] public IEnumerable GetAccounts() { var service = _serviceProvider.GetRequiredService(); return service.GetAllAccounts(); } } // ✅ Do this instead - constructor injection public class GoodAccounts : Controller { readonly IAccountService _accountService; public GoodAccounts(IAccountService accountService) { _accountService = accountService; } [HttpGet] public IEnumerable GetAccounts() { return _accountService.GetAllAccounts(); } } ``` # Observable Queries Observable queries provide real-time data streaming using WebSockets, enabling reactive user experiences where data changes are pushed to clients as they occur. You achieve this by returning `ISubject` from your controller actions. The `ISubject` return type automatically establishes a WebSocket connection between the server and client, enabling real-time data updates. This integrates seamlessly with the [ObservableQuery construct in the frontend](/arc/frontend/react/queries/observable-queries/) through the proxy generator, creating strongly-typed reactive data flows. ## Basic Observable Query [Section titled “Basic Observable Query”](#basic-observable-query) The key to an observable query is to return the `ISubject` produced by the MongoDB `Observe()` extension method. `Observe()` watches the collection and pushes a fresh snapshot every time the data changes — Arc establishes and manages the WebSocket connection for you. The examples below use `_collection`, an injected `IMongoCollection` field on the controller. ```csharp [HttpGet("observable")] public ISubject> AllAccountsObservable() { return _collection.Observe(); } ``` ## Observable with Arguments [Section titled “Observable with Arguments”](#observable-with-arguments) Observable queries can accept arguments just like regular queries: ```csharp [HttpGet("owner/{ownerId}/observable")] public ISubject> GetAccountsByOwnerObservable(CustomerId ownerId) { return _collection.Observe(account => account.Owner == ownerId); } [HttpGet("filtered-observable")] public ISubject> GetFilteredAccountsObservable( [FromQuery] decimal minBalance = 0) { return _collection.Observe(account => account.Balance >= minBalance); } ``` ## Single Object Observable [Section titled “Single Object Observable”](#single-object-observable) For observing changes to a single object: ```csharp [HttpGet("{id}/observable")] public ISubject GetAccountObservable(AccountId id) { return _collection.ObserveSingle(account => account.Id == id); } ``` ## Custom Observable Logic [Section titled “Custom Observable Logic”](#custom-observable-logic) For computed or derived data, build on top of the collection’s observable. `Observe()` returns an `IObservable>` that emits a new snapshot whenever the data changes, so you can use System.Reactive operators such as `Select` to project each snapshot into a computed shape: ```csharp using System.Reactive.Linq; using System.Reactive.Subjects; [HttpGet("summary")] public ISubject GetAccountSummaryObservable() { var summary = new ReplaySubject(1); _collection.Observe() .Select(accounts => new AccountSummary(accounts.Count(), accounts.Sum(a => a.Balance))) .Subscribe(summary); return summary; } ``` ## Multiple Data Source Observables [Section titled “Multiple Data Source Observables”](#multiple-data-source-observables) Observe changes across multiple data sources: ```csharp using System.Reactive.Linq; using System.Reactive.Subjects; public record CombinedData(IEnumerable Accounts, IEnumerable Customers); [HttpGet("combined-observable")] public ISubject GetCombinedDataObservable() { var combined = new ReplaySubject(1); _accountCollection.Observe() .CombineLatest(_customerCollection.Observe(), (accounts, customers) => new CombinedData(accounts, customers)) .Subscribe(combined); return combined; } ``` ## Observable with Computed Values [Section titled “Observable with Computed Values”](#observable-with-computed-values) Create observables that compute derived values: ```csharp using System.Reactive.Linq; using System.Reactive.Subjects; [HttpGet("computed-metrics")] public ISubject GetAccountMetricsObservable() { var metrics = new ReplaySubject(1); _collection.Observe() .Select(accounts => new AccountMetrics( TotalAccounts: accounts.Count(), TotalBalance: accounts.Sum(a => a.Balance), AverageBalance: accounts.Any() ? accounts.Average(a => a.Balance) : 0, ActiveAccounts: accounts.Count(a => a.Balance > 0), HighValueAccounts: accounts.Count(a => a.Balance > 100000))) .Subscribe(metrics); return metrics; } ``` ## Filtered Observables with Dynamic Criteria [Section titled “Filtered Observables with Dynamic Criteria”](#filtered-observables-with-dynamic-criteria) Allow clients to specify filter criteria for observables: ```csharp using System.Reactive.Linq; using System.Reactive.Subjects; public record ObservableFilter( decimal? MinBalance, decimal? MaxBalance, string? NamePattern, CustomerId? OwnerId); [HttpPost("filtered-observable")] public ISubject> GetFilteredObservable([FromBody] ObservableFilter filter) { var predicate = BuildFilterPredicate(filter); var filtered = new ReplaySubject>(1); _collection.Observe() .Select(accounts => accounts.Where(predicate).ToList().AsEnumerable()) .Subscribe(filtered); return filtered; } Func BuildFilterPredicate(ObservableFilter filter) { return account => (!filter.MinBalance.HasValue || account.Balance >= filter.MinBalance.Value) && (!filter.MaxBalance.HasValue || account.Balance <= filter.MaxBalance.Value) && (string.IsNullOrEmpty(filter.NamePattern) || account.Name.Contains(filter.NamePattern, StringComparison.OrdinalIgnoreCase)) && (!filter.OwnerId.HasValue || account.Owner == filter.OwnerId.Value); } ``` ## Throttled Observables [Section titled “Throttled Observables”](#throttled-observables) For high-frequency changes, use the System.Reactive `Sample` operator to emit at most one update per time window, preventing a flood of changes from overwhelming clients: ```csharp using System.Reactive.Linq; using System.Reactive.Subjects; [HttpGet("throttled-observable")] public ISubject> GetThrottledObservable( [FromQuery] int throttleMs = 1000) { var throttled = new ReplaySubject>(1); _collection.Observe() .Sample(TimeSpan.FromMilliseconds(throttleMs)) .Subscribe(throttled); return throttled; } ``` ## Error Handling in Observables [Section titled “Error Handling in Observables”](#error-handling-in-observables) Errors raised while observing the collection propagate through the observable’s error channel automatically. Use the System.Reactive `Do` operator to log them as they flow through, without altering the stream: ```csharp using System.Reactive.Linq; using System.Reactive.Subjects; [HttpGet("robust-observable")] public ISubject> GetRobustObservable() { var observable = new ReplaySubject>(1); _collection.Observe() .Do( onNext: _ => { }, onError: ex => _logger.LogError(ex, "Error observing accounts")) .Subscribe(observable); return observable; } ``` ## Authentication and Authorization [Section titled “Authentication and Authorization”](#authentication-and-authorization) Observable queries support the same authentication and authorization as regular queries: ```csharp [Authorize] [HttpGet("secure-observable")] public ISubject> GetSecureObservable() { // Only authenticated users can subscribe return _collection.Observe(); } [Authorize(Roles = "Admin")] [HttpGet("admin-observable")] public ISubject> GetAdminObservable() { // Only admin users can subscribe return _collection.Observe(); } ``` ## Best Practices for Observable Queries [Section titled “Best Practices for Observable Queries”](#best-practices-for-observable-queries) 1. **Prefer the `Observe()` / `ObserveSingle()` extension methods** — they handle change monitoring, initial data, and cleanup for you 2. **Project with System.Reactive operators** (`Select`, `CombineLatest`, `Sample`) when you need computed, combined, or throttled streams 3. **Use appropriate filters** to minimize unnecessary data transmission 4. **Consider throttling** with `Sample` for high-frequency changes to prevent overwhelming clients 5. **Let errors propagate** through the observable’s error channel; use `Do` to observe them for logging 6. **Use authentication** to control who can subscribe to observable endpoints 7. **Monitor performance** and consider the impact of many concurrent subscriptions ## Connection Management [Section titled “Connection Management”](#connection-management) Arc automatically handles WebSocket connections for observable queries: * **Connection establishment** - Automatic WebSocket upgrade for observable endpoints * **Message serialization** - Automatic JSON serialization of observable data * **Connection cleanup** - Proper disposal of resources when clients disconnect * **Reconnection handling** - Clients can reconnect and resume subscriptions ## Waiting for the First HTTP Result [Section titled “Waiting for the First HTTP Result”](#waiting-for-the-first-http-result) The regular HTTP `GET` endpoint for a controller-based observable query returns the current snapshot as JSON. If the observable has not produced a value yet, add `waitForFirstResult=true` to keep the HTTP request open until the first item arrives. Arc applies a timeout while waiting. By default the timeout is 30 seconds. You can override it with `waitForFirstResultTimeout`, expressed in seconds. ```bash curl "https://localhost:5001/api/observable-controller-queries/observe/delayed-single?waitForFirstResult=true" ``` ```bash curl "https://localhost:5001/api/observable-controller-queries/observe/delayed-single?waitForFirstResult=true&waitForFirstResultTimeout=10" ``` This makes it easy to debug observable controller actions with cURL without switching to WebSockets or SSE. See [Use Observable Queries with cURL](/arc/backend/queries/using-observable-queries-with-curl/) for snapshot, SSE, and long-polling workflows. ## Frontend Integration [Section titled “Frontend Integration”](#frontend-integration) Observable queries integrate seamlessly with frontend frameworks through the proxy generator and the [ObservableQuery construct](/arc/frontend/react/queries/observable-queries/): ```typescript accountsObservable.subscribe(accounts => { // Handle real-time account updates updateUI(accounts); }); ``` The `ISubject` return type automatically establishes and manages WebSocket connections, providing: > **Important**: The `Observe()` and `ObserveSingle()` extension methods manage their own subscriptions and clean up automatically when a client disconnects, so you do not need to write any teardown code. * **Automatic connection management** - WebSocket connections are established and maintained automatically * **Strongly-typed data flow** - Full TypeScript support through the proxy generator * **Reactive integration** - Seamless integration with React hooks like `useObservableQuery()` * **Reconnection handling** - Automatic reconnection and state recovery on connection loss > **Note**: The [proxy generator](/arc/backend/proxy-generation/) automatically creates TypeScript types for your observable queries, making them strongly typed on the frontend as well. # Paging When a controller action returns `IQueryable`, the query pipeline automatically applies server-side paging and sorting. This means you write a simple method that returns a queryable, and the framework handles the rest. ## Why IQueryable matters [Section titled “Why IQueryable matters”](#why-iqueryable-matters) The key to automatic paging is returning `IQueryable` instead of `IEnumerable` or `List`. When the pipeline sees an `IQueryable`, it can append `.Skip()` and `.Take()` *before* the database executes the query — so only the requested page of data travels over the wire. If you return a materialized collection like `List`, the pipeline has no way to apply paging at the database level. All rows are fetched first, defeating the purpose. ```csharp [Route("api/accounts")] public class Accounts : Controller { readonly IMongoCollection _collection; public Accounts(IMongoCollection collection) => _collection = collection; // ✅ Returns IQueryable — paging and sorting are applied automatically [HttpGet] public IQueryable AllAccounts() => _collection.AsQueryable(); } ``` ## How it works [Section titled “How it works”](#how-it-works) When a client sends paging parameters in the query string, the `QueryableQueryRenderer` intercepts the `IQueryable` result and: 1. Counts the total number of matching items 2. Applies sorting based on `sortby` and `sortDirection` 3. Applies `.Skip(page * pageSize)` and `.Take(pageSize)` 4. Returns the page of data wrapped in a `QueryResult` with a `PagingInfo` containing `page`, `size`, `totalItems`, and `totalPages` The client controls paging with these query string parameters: | Parameter | Type | Description | | --------------- | --------------- | ------------------------ | | `page` | `int` | Zero-based page number | | `pageSize` | `int` | Number of items per page | | `sortby` | `string` | Field name to sort by | | `sortDirection` | `asc` or `desc` | Sort direction | ### Example requests [Section titled “Example requests”](#example-requests) ```http GET /api/accounts?page=0&pageSize=25 GET /api/accounts?page=2&pageSize=10&sortby=name&sortDirection=asc ``` When no paging parameters are provided, the full result set is returned without paging. ## Complete example with filtering [Section titled “Complete example with filtering”](#complete-example-with-filtering) Paging works alongside query arguments. The pipeline applies paging *after* your method returns the filtered `IQueryable`: ```csharp [Route("api/accounts")] public class Accounts : Controller { readonly IMongoCollection _collection; public Accounts(IMongoCollection collection) => _collection = collection; [HttpGet] public IQueryable AllAccounts() => _collection.AsQueryable(); [HttpGet("by-owner/{ownerId}")] public IQueryable AccountsByOwner(CustomerId ownerId) => _collection.AsQueryable().Where(a => a.Owner == ownerId); } ``` Both endpoints support paging automatically because they return `IQueryable`. ## Return type comparison [Section titled “Return type comparison”](#return-type-comparison) | Return type | Paging | Sorting | DB-level optimization | | ---------------- | ----------- | ----------- | ------------------------ | | `IQueryable` | ✅ Automatic | ✅ Automatic | ✅ Skip/Take pushed to DB | | `IEnumerable` | ❌ | ❌ | ❌ All rows loaded | | `List` | ❌ | ❌ | ❌ All rows loaded | | `T[]` | ❌ | ❌ | ❌ All rows loaded | ## Manual paging with IQueryContextManager [Section titled “Manual paging with IQueryContextManager”](#manual-paging-with-iquerycontextmanager) If you need full control over how paging is applied — for example, when using the MongoDB driver directly instead of LINQ — inject `IQueryContextManager` and read the paging context manually: ```csharp [Route("api/accounts")] public class Accounts : Controller { readonly IMongoCollection _collection; readonly IQueryContextManager _queryContextManager; public Accounts( IMongoCollection collection, IQueryContextManager queryContextManager) { _collection = collection; _queryContextManager = queryContextManager; } [HttpGet("manual")] public QueryResult ManualPaging() { var context = _queryContextManager.Current; var query = _collection.Find(_ => true); if (context.Sorting != Sorting.None) { query = context.Sorting.Direction == SortDirection.Ascending ? query.SortBy(context.Sorting.Field) : query.SortByDescending(context.Sorting.Field); } var totalItems = (int)query.CountDocuments(); if (context.Paging.IsPaged) { query = query.Skip(context.Paging.Skip).Limit(context.Paging.Size); } var data = query.ToList(); return new QueryResult { Data = data, Paging = new PagingInfo(context.Paging.Page, context.Paging.Size, totalItems) }; } } ``` > Manual paging is rarely needed. Prefer returning `IQueryable` and letting the pipeline handle it. # Query Arguments Controller-based queries can accept arguments to filter, customize, or parameterize the data they return. Arguments can come from route parameters, query strings, or request bodies. > **💡 Proxy Generation**: The [proxy generator](/arc/backend/proxy-generation/) automatically analyzes your query arguments and creates strongly-typed TypeScript interfaces, ensuring type safety between your backend and frontend. ## Route Parameters [Section titled “Route Parameters”](#route-parameters) Route parameters are embedded in the URL path and are typically used for primary identifiers: ```csharp [Route("api/accounts")] public class Accounts : Controller { readonly IMongoCollection _collection; public Accounts(IMongoCollection collection) => _collection = collection; [HttpGet("{id}")] public DebitAccount GetAccountById(AccountId id) { return _collection.Find(a => a.Id == id).FirstOrDefault(); } [HttpGet("owner/{ownerId}")] public IEnumerable GetAccountsByOwner(CustomerId ownerId) { return _collection.Find(a => a.Owner == ownerId).ToList(); } [HttpGet("{id}/balance")] public decimal GetAccountBalance(AccountId id) { var account = _collection.Find(a => a.Id == id).FirstOrDefault(); return account?.Balance ?? 0; } } ``` ## Query String Parameters [Section titled “Query String Parameters”](#query-string-parameters) Query string parameters are appended to the URL after a `?` and are typically used for optional filters or configuration: ```csharp [HttpGet] public IEnumerable GetAccounts([FromQuery] string? nameFilter = null) { var filter = Builders.Filter.Empty; if (!string.IsNullOrEmpty(nameFilter)) { filter = Builders.Filter.Regex( account => account.Name, new BsonRegularExpression(nameFilter, "i")); } return _collection.Find(filter).ToList(); } [HttpGet("search")] public async Task> SearchAccounts( [FromQuery] string? name = null, [FromQuery] decimal? minBalance = null, [FromQuery] decimal? maxBalance = null, [FromQuery] bool includeInactive = false) { var filterBuilder = Builders.Filter; var filters = new List>(); if (!string.IsNullOrEmpty(name)) { filters.Add(filterBuilder.Regex(a => a.Name, new BsonRegularExpression(name, "i"))); } if (minBalance.HasValue) { filters.Add(filterBuilder.Gte(a => a.Balance, minBalance.Value)); } if (maxBalance.HasValue) { filters.Add(filterBuilder.Lte(a => a.Balance, maxBalance.Value)); } if (!includeInactive) { filters.Add(filterBuilder.Gt(a => a.Balance, 0)); } var combinedFilter = filters.Any() ? filterBuilder.And(filters) : filterBuilder.Empty; var result = await _collection.FindAsync(combinedFilter); return result.ToList(); } ``` ## Complex Query Objects [Section titled “Complex Query Objects”](#complex-query-objects) For complex queries with multiple parameters, you can create dedicated query objects: ```csharp public record AccountSearchQuery( string? Name, decimal? MinBalance, decimal? MaxBalance, bool IncludeInactive, string? OwnerName); [HttpGet("advanced-search")] public IEnumerable SearchAccountsAdvanced([FromQuery] AccountSearchQuery query) { var filterBuilder = Builders.Filter; var filters = new List>(); if (!string.IsNullOrEmpty(query.Name)) { filters.Add(filterBuilder.Regex(a => a.Name, new BsonRegularExpression(query.Name, "i"))); } if (query.MinBalance.HasValue) { filters.Add(filterBuilder.Gte(a => a.Balance, query.MinBalance.Value)); } if (query.MaxBalance.HasValue) { filters.Add(filterBuilder.Lte(a => a.Balance, query.MaxBalance.Value)); } if (!query.IncludeInactive) { filters.Add(filterBuilder.Gt(a => a.Balance, 0)); } // Additional complex filtering logic... var combinedFilter = filters.Any() ? filterBuilder.And(filters) : filterBuilder.Empty; return _collection.Find(combinedFilter).ToList(); } ``` ## Observable Query Arguments [Section titled “Observable Query Arguments”](#observable-query-arguments) Observable queries can also accept arguments: ```csharp [HttpGet("owner/{ownerId}/observable")] public ISubject> GetAccountsByOwnerObservable(CustomerId ownerId) { return _collection.Observe(account => account.Owner == ownerId); } [HttpGet("filtered-observable")] public ISubject> GetFilteredAccountsObservable( [FromQuery] decimal? minBalance = null) { if (minBalance.HasValue) { return _collection.Observe(account => account.Balance >= minBalance.Value); } return _collection.Observe(); } ``` ## Argument Types [Section titled “Argument Types”](#argument-types) Arc supports various argument types: ### Primitive Types [Section titled “Primitive Types”](#primitive-types) ```csharp [HttpGet("by-balance")] public IEnumerable GetAccountsByBalance( [FromQuery] decimal balance, [FromQuery] bool exactMatch = false) { return exactMatch ? _collection.Find(a => a.Balance == balance).ToList() : _collection.Find(a => a.Balance >= balance).ToList(); } ``` ### Concept Types [Section titled “Concept Types”](#concept-types) Using concept types (value objects) for stronger typing: ```csharp [HttpGet("by-owner-concept/{ownerId}")] public IEnumerable GetAccountsByOwnerConcept(CustomerId ownerId) { return _collection.Find(a => a.Owner == ownerId).ToList(); } ``` ### Collection Arguments [Section titled “Collection Arguments”](#collection-arguments) ```csharp [HttpGet("by-ids")] public IEnumerable GetAccountsByIds([FromQuery] AccountId[] ids) { return _collection.Find(a => ids.Contains(a.Id)).ToList(); } [HttpGet("by-owners")] public IEnumerable GetAccountsByOwners([FromQuery] List ownerIds) { return _collection.Find(a => ownerIds.Contains(a.Owner)).ToList(); } ``` ### Enums [Section titled “Enums”](#enums) ```csharp public enum AccountStatus { Active, Inactive, Suspended } [HttpGet("by-status")] public IEnumerable GetAccountsByStatus([FromQuery] AccountStatus status) { // Implement status filtering logic return status switch { AccountStatus.Active => _collection.Find(a => a.Balance > 0).ToList(), AccountStatus.Inactive => _collection.Find(a => a.Balance == 0).ToList(), AccountStatus.Suspended => _collection.Find(a => a.Balance < 0).ToList(), _ => _collection.Find(_ => false).ToList() }; } ``` ## Nullable Arguments [Section titled “Nullable Arguments”](#nullable-arguments) Optional arguments should be nullable: ```csharp [HttpGet("flexible-search")] public IEnumerable FlexibleSearch( [FromQuery] string? name = null, [FromQuery] CustomerId? ownerId = null, [FromQuery] decimal? minBalance = null, [FromQuery] decimal? maxBalance = null) { var filterBuilder = Builders.Filter; var filters = new List>(); if (!string.IsNullOrEmpty(name)) filters.Add(filterBuilder.Regex(a => a.Name, new BsonRegularExpression(name, "i"))); if (ownerId.HasValue) filters.Add(filterBuilder.Eq(a => a.Owner, ownerId.Value)); if (minBalance.HasValue) filters.Add(filterBuilder.Gte(a => a.Balance, minBalance.Value)); if (maxBalance.HasValue) filters.Add(filterBuilder.Lte(a => a.Balance, maxBalance.Value)); var combinedFilter = filters.Any() ? filterBuilder.And(filters) : filterBuilder.Empty; return _collection.Find(combinedFilter).ToList(); } ``` ## Default Values [Section titled “Default Values”](#default-values) Provide sensible default values for optional parameters: ```csharp [HttpGet("paged")] public IEnumerable GetPagedAccounts( [FromQuery] int page = 0, [FromQuery] int pageSize = 50, [FromQuery] string sortBy = "name", [FromQuery] bool ascending = true) { var query = _collection.Find(_ => true); // Apply sorting query = ascending ? query.SortBy(sortBy) : query.SortByDescending(sortBy); // Apply paging return query.Skip(page * pageSize).Limit(pageSize).ToList(); } ``` ## Request Body Arguments [Section titled “Request Body Arguments”](#request-body-arguments) For complex input that doesn’t fit well in URLs, use request body parameters: ```csharp public record ComplexSearchCriteria( string[] SearchTerms, Dictionary CustomFilters, DateRange DateRange, SortOptions[] SortBy); [HttpPost("complex-search")] public async Task> ComplexSearch([FromBody] ComplexSearchCriteria criteria) { var filterBuilder = Builders.Filter; var filters = new List>(); // Build filters from complex criteria foreach (var term in criteria.SearchTerms) { filters.Add(filterBuilder.Regex(a => a.Name, new BsonRegularExpression(term, "i"))); } // Apply custom filters, date ranges, etc. var combinedFilter = filters.Any() ? filterBuilder.And(filters) : filterBuilder.Empty; var result = await _collection.FindAsync(combinedFilter); return result.ToList(); } ``` ## Model Binding Attributes [Section titled “Model Binding Attributes”](#model-binding-attributes) Use model binding attributes to control how arguments are bound: ```csharp [HttpGet("mixed-binding/{id}")] public DebitAccount GetAccountMixed( [FromRoute] AccountId id, [FromQuery] bool includeDetails = false, [FromHeader] string acceptLanguage = "en-US") { var account = _collection.Find(a => a.Id == id).FirstOrDefault(); if (includeDetails && account is not null) { // Add additional details based on language preference // Implementation details... } return account; } ``` ## Validation [Section titled “Validation”](#validation) Add validation attributes to ensure argument quality: ```csharp [HttpGet("validated-search")] public IEnumerable ValidatedSearch( [FromQuery] [Required] [MinLength(3)] string searchTerm, [FromQuery] [Range(1, 100)] int pageSize = 20, [FromQuery] [Range(0, int.MaxValue)] int page = 0) { // Validation is automatically applied var filter = Builders.Filter.Regex( a => a.Name, new BsonRegularExpression(searchTerm, "i")); return _collection.Find(filter) .Skip(page * pageSize) .Limit(pageSize) .ToList(); } ``` ## Best Practices [Section titled “Best Practices”](#best-practices) 1. **Use route parameters for identifiers** - Things that identify specific resources 2. **Use query strings for filters** - Optional parameters that modify results 3. **Use request body for complex data** - When you need to send structured data 4. **Provide default values** - Make optional parameters truly optional 5. **Use nullable types** - For optional parameters that might not be provided 6. **Validate input** - Use validation attributes to ensure data quality 7. **Use concepts over primitives** - Leverage value objects for stronger typing 8. **Keep URLs readable** - Don’t overload URLs with too many parameters > **Note**: The [proxy generator](/arc/backend/proxy-generation/) automatically creates TypeScript types for your query arguments, making them strongly typed on the frontend as well. # Return Types Controller-based queries support various data types for return values, giving you flexibility in how you structure your API responses. ## Single Object [Section titled “Single Object”](#single-object) Return a single instance of your read model: ```csharp [HttpGet("{id}")] public DebitAccount GetAccount(AccountId id) { return _collection.Find(a => a.Id == id).FirstOrDefault(); } ``` ## Collections [Section titled “Collections”](#collections) ### IEnumerable\ [Section titled “IEnumerable\”](#ienumerablet) ```csharp [HttpGet] public IEnumerable GetAccounts() { return _collection.Find(_ => true).ToList(); } ``` ### List\ [Section titled “List\”](#listt) ```csharp [HttpGet] public List GetAccountsList() { return _collection.Find(_ => true).ToList(); } ``` ### Arrays [Section titled “Arrays”](#arrays) ```csharp [HttpGet] public DebitAccount[] GetAccountsArray() { return _collection.Find(_ => true).ToArray(); } ``` ## Query Results [Section titled “Query Results”](#query-results) For more control over the response metadata, you can return `QueryResult`: ```csharp [HttpGet] public QueryResult GetAccountsWithMetadata() { var accounts = _collection.Find(_ => true).ToList(); return new QueryResult { Data = accounts, // Additional metadata will be populated automatically }; } ``` ## Async Return Types [Section titled “Async Return Types”](#async-return-types) All return types can be wrapped in `Task` for asynchronous operations: ```csharp [HttpGet] public async Task> GetAccountsAsync() { var result = await _collection.FindAsync(_ => true); return result.ToList(); } [HttpGet("{id}")] public async Task GetAccountAsync(AccountId id) { var result = await _collection.FindAsync(a => a.Id == id); return result.FirstOrDefault(); } ``` ## Custom Response Types [Section titled “Custom Response Types”](#custom-response-types) You can create custom types for complex query results: ```csharp public record AccountSummary(int TotalAccounts, decimal TotalBalance, decimal AverageBalance); [HttpGet("summary")] public AccountSummary GetAccountSummary() { var accounts = _collection.Find(_ => true).ToList(); return new AccountSummary( accounts.Count, accounts.Sum(a => a.Balance), accounts.Count > 0 ? accounts.Average(a => a.Balance) : 0 ); } ``` ## Observable Return Types [Section titled “Observable Return Types”](#observable-return-types) For real-time queries, return `ISubject` or `IObservable`: ```csharp [HttpGet("observable")] public ISubject> GetAccountsObservable() { return _collection.Observe(); } ``` See [Observable Queries](/arc/backend/queries/controller-based/observable-queries/) for more details on real-time data streaming. ## Nullable Return Types [Section titled “Nullable Return Types”](#nullable-return-types) When a query might not return data, use nullable types: ```csharp [HttpGet("{id}")] public DebitAccount? GetAccount(AccountId id) { return _collection.Find(a => a.Id == id).FirstOrDefault(); } ``` ## Best Practices [Section titled “Best Practices”](#best-practices) 1. **Use appropriate collection types** - `IEnumerable` for most cases, `List` when you need specific list operations 2. **Consider nullability** - Use nullable types when queries might return no results 3. **Async for I/O operations** - Always use async methods when dealing with database operations 4. **Custom types for complex data** - Create dedicated response types for complex query results 5. **QueryResult for metadata** - Use `QueryResult` (assigning its `Data` property) when you need to include additional response metadata ## Response Wrappers [Section titled “Response Wrappers”](#response-wrappers) By default, controller-based queries wrap results in a `QueryResult` structure. To bypass this wrapper and return raw results, use the `[AspNetResult]` attribute. For more details, see [Without wrappers](/arc/backend/asp-net-core/without-wrappers/). # Route Templates Controller-based queries use standard ASP.NET Core routing to define URL patterns and bind parameters from the URL path. ## Basic Route Configuration [Section titled “Basic Route Configuration”](#basic-route-configuration) Use the `[Route]` attribute on your controller class to define the base route: ```csharp [Route("api/accounts")] public class Accounts : Controller { readonly IMongoCollection _collection; public Accounts(IMongoCollection collection) => _collection = collection; [HttpGet] public IEnumerable GetAll() { /* ... */ } [HttpGet("{id}")] public DebitAccount GetById(AccountId id) { /* ... */ } } ``` ## Route Parameters [Section titled “Route Parameters”](#route-parameters) ### Single Parameter [Section titled “Single Parameter”](#single-parameter) Route parameters are defined with curly braces in the route template: ```csharp [Route("api/accounts")] public class Accounts : Controller { [HttpGet("{id}")] public DebitAccount GetById(AccountId id) { return _collection.Find(a => a.Id == id).FirstOrDefault(); } [HttpGet("{id}/balance")] public decimal GetBalance(AccountId id) { var account = _collection.Find(a => a.Id == id).FirstOrDefault(); return account?.Balance ?? 0; } } ``` ### Multiple Parameters [Section titled “Multiple Parameters”](#multiple-parameters) Routes can include multiple parameters: ```csharp [HttpGet("owner/{ownerId}/account/{accountId}")] public DebitAccount GetAccountByOwner(CustomerId ownerId, AccountId accountId) { return _collection.Find(a => a.Owner == ownerId && a.Id == accountId).FirstOrDefault(); } [HttpGet("date/{year}/{month}")] public IEnumerable GetAccountsByDate(int year, int month) { // Implementation for date-based filtering return _collection.Find(_ => true).ToList(); } ``` ## Named Routes [Section titled “Named Routes”](#named-routes) You can name routes for URL generation: ```csharp [HttpGet("{id}", Name = "GetAccount")] public DebitAccount GetById(AccountId id) { return _collection.Find(a => a.Id == id).FirstOrDefault(); } ``` ## Route Constraints [Section titled “Route Constraints”](#route-constraints) Add constraints to route parameters to improve matching: ```csharp [Route("api/accounts")] public class Accounts : Controller { // Only match numeric IDs [HttpGet("{id:int}")] public DebitAccount GetByNumericId(int id) { /* ... */ } // Only match GUID format [HttpGet("{id:guid}")] public DebitAccount GetByGuidId(Guid id) { /* ... */ } // Minimum length constraint [HttpGet("name/{name:minlength(3)}")] public IEnumerable GetByName(string name) { /* ... */ } // Range constraint [HttpGet("page/{pageNumber:int:min(1)}")] public IEnumerable GetPage(int pageNumber) { /* ... */ } } ``` ## Optional Parameters [Section titled “Optional Parameters”](#optional-parameters) Make route parameters optional with a question mark: ```csharp [HttpGet("owner/{ownerId}/category/{category?}")] public IEnumerable GetByOwnerAndCategory(CustomerId ownerId, string? category = null) { if (string.IsNullOrEmpty(category)) { return _collection.Find(a => a.Owner == ownerId).ToList(); } // Filter by category if provided return _collection.Find(a => a.Owner == ownerId /* && category filter */).ToList(); } ``` ## Action-Specific Routes [Section titled “Action-Specific Routes”](#action-specific-routes) Override the controller route for specific actions: ```csharp [Route("api/accounts")] public class Accounts : Controller { [HttpGet] public IEnumerable GetAll() { /* ... */ } [HttpGet("search")] public IEnumerable Search([FromQuery] string term) { /* ... */ } [HttpGet("by-owner/{ownerId}")] public IEnumerable GetByOwner(CustomerId ownerId) { /* ... */ } // Complete override of the base route [HttpGet("~/api/special/accounts/summary")] public AccountSummary GetSummary() { /* ... */ } } ``` ## Complex Route Patterns [Section titled “Complex Route Patterns”](#complex-route-patterns) ### Hierarchical Resources [Section titled “Hierarchical Resources”](#hierarchical-resources) Model parent-child relationships in your routes: ```csharp [Route("api/customers/{customerId}/accounts")] public class CustomerAccounts : Controller { [HttpGet] public IEnumerable GetAccountsByCustomer(CustomerId customerId) { return _collection.Find(a => a.Owner == customerId).ToList(); } [HttpGet("{accountId}")] public DebitAccount GetCustomerAccount(CustomerId customerId, AccountId accountId) { return _collection.Find(a => a.Owner == customerId && a.Id == accountId).FirstOrDefault(); } [HttpGet("{accountId}/transactions")] public IEnumerable GetAccountTransactions(CustomerId customerId, AccountId accountId) { // Implementation for getting transactions return new List(); } } ``` ### Multiple Route Templates [Section titled “Multiple Route Templates”](#multiple-route-templates) An action can have multiple route templates: ```csharp [Route("api/accounts")] public class Accounts : Controller { [HttpGet("search")] [HttpGet("find")] // Alternative route public IEnumerable Search([FromQuery] string term) { var filter = Builders.Filter.Regex( a => a.Name, new BsonRegularExpression(term, "i")); return _collection.Find(filter).ToList(); } } ``` ## Route Values and Concepts [Section titled “Route Values and Concepts”](#route-values-and-concepts) When using Cratis concepts (value objects), the route binding works seamlessly: ```csharp // The AccountId concept is automatically bound from the route parameter [HttpGet("{id}")] public DebitAccount GetAccount(AccountId id) { return _collection.Find(a => a.Id == id).FirstOrDefault(); } // Multiple concept parameters [HttpGet("owner/{ownerId}/account/{accountId}")] public decimal GetAccountBalanceForOwner(CustomerId ownerId, AccountId accountId) { var account = _collection.Find(a => a.Owner == ownerId && a.Id == accountId).FirstOrDefault(); return account?.Balance ?? 0; } ``` ## Route Tokens [Section titled “Route Tokens”](#route-tokens) Use route tokens for common patterns: ```csharp // Using [controller] token [Route("api/[controller]")] public class Accounts : Controller { // Matches: /api/accounts [HttpGet("[action]")] public IEnumerable GetAll() { /* ... */ } // Matches: /api/accounts/GetAll } ``` ## Query String vs Route Parameters [Section titled “Query String vs Route Parameters”](#query-string-vs-route-parameters) Choose between route parameters and query strings based on the data’s role: ### Route Parameters (part of the resource identity) [Section titled “Route Parameters (part of the resource identity)”](#route-parameters-part-of-the-resource-identity) ```csharp // Account ID is part of the resource identity [HttpGet("{id}")] public DebitAccount GetAccount(AccountId id) { /* ... */ } // Owner ID identifies a specific subset [HttpGet("owner/{ownerId}")] public IEnumerable GetByOwner(CustomerId ownerId) { /* ... */ } ``` ### Query String Parameters (filtering/options) [Section titled “Query String Parameters (filtering/options)”](#query-string-parameters-filteringoptions) ```csharp // Filtering options [HttpGet("search")] public IEnumerable Search( [FromQuery] string? name = null, [FromQuery] decimal? minBalance = null, [FromQuery] bool includeInactive = false) { // Apply filters based on query parameters return _collection.Find(_ => true).ToList(); } ``` ## Best Practices [Section titled “Best Practices”](#best-practices) 1. **Use meaningful route patterns** - Routes should be intuitive and RESTful 2. **Keep routes simple** - Avoid overly complex route templates 3. **Use constraints** - Add route constraints to improve matching accuracy 4. **Be consistent** - Use consistent naming and structure across your API 5. **Consider hierarchy** - Use hierarchical routes for parent-child relationships 6. **Route parameters for identity** - Use route parameters for resource identifiers 7. **Query strings for filtering** - Use query strings for optional filters and options ## Example: Complete RESTful Route Structure [Section titled “Example: Complete RESTful Route Structure”](#example-complete-restful-route-structure) ```csharp [Route("api/accounts")] public class Accounts : Controller { // GET /api/accounts [HttpGet] public IEnumerable GetAll() { /* ... */ } // GET /api/accounts/{id} [HttpGet("{id}")] public DebitAccount GetById(AccountId id) { /* ... */ } // GET /api/accounts/search?name=john&minBalance=100 [HttpGet("search")] public IEnumerable Search( [FromQuery] string? name = null, [FromQuery] decimal? minBalance = null) { /* ... */ } // GET /api/accounts/owner/{ownerId} [HttpGet("owner/{ownerId}")] public IEnumerable GetByOwner(CustomerId ownerId) { /* ... */ } // GET /api/accounts/{id}/balance [HttpGet("{id}/balance")] public decimal GetBalance(AccountId id) { /* ... */ } } ``` # Model Bound Queries For a more lightweight approach, queries can be their own performers. This is achieved by adorning your read model record with the `[ReadModel]` attribute and implementing static methods for query operations directly on the record type. ```csharp [ReadModel] // The ReadModel attribute is needed public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static IEnumerable GetAllAccounts(IMongoCollection collection) { return collection.Find(_ => true).ToList(); } } ``` > **Note**: If you’re using the Cratis Arc [proxy generator](/arc/backend/proxy-generation/), the method name will become the query name for the generated TypeScript file and class. ## Key Features [Section titled “Key Features”](#key-features) Model-bound queries provide a streamlined approach to querying by: * **Co-locating queries with data models** - Keeping query logic close to the data it operates on * **Eliminating controller boilerplate** - No need for separate controller classes * **Automatic dependency injection** - Dependencies are resolved and injected automatically * **Simple static method pattern** - Clean, straightforward method signatures * **Full async support** - Methods can be asynchronous for database operations * **Multiple query methods** - A single read model can have many query operations * **Flexible return types** - Support for collections, single objects, and observables * **Built-in authorization** - Use standard ASP.NET Core authorization attributes ## When to Use Model-Bound Queries [Section titled “When to Use Model-Bound Queries”](#when-to-use-model-bound-queries) Model-bound queries are ideal when you: * Want to keep query logic close to your data models * Prefer a more functional approach with static methods * Don’t need complex routing scenarios * Want to minimize boilerplate controller code * Have straightforward query operations without complex middleware requirements * Are building simple CRUD-style APIs ## Key Requirements [Section titled “Key Requirements”](#key-requirements) The `[ReadModel]` attribute is required on your record type, and static methods must: * Be `public` and `static` * Can have any descriptive name for the query operation * Can take dependencies as parameters (injected via dependency injection) * Can be async by returning `Task` * Should return the record itself, collections of the record type, or custom result types * Can be observable by returning `ISubject` (do not combine with `Task`) ## Related Topics [Section titled “Related Topics”](#related-topics) * [Static Methods](/arc/backend/queries/model-bound/static-methods/) - Understanding the method requirements and patterns * [Dependency Injection](/arc/backend/queries/model-bound/dependency-injection/) - Method-level dependency injection for services and repositories * [Query Arguments](/arc/backend/queries/model-bound/query-arguments/) - How to handle parameters and input validation * [Return Types](/arc/backend/queries/model-bound/return-types/) - Different ways to return data from your queries * [Authorization](/arc/backend/queries/model-bound/authorization/) - Securing your query methods with roles and policies ## Basic Async Example [Section titled “Basic Async Example”](#basic-async-example) Model-bound queries support asynchronous operations: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static async Task> GetAllAccountsAsync(IMongoCollection collection) { var result = await collection.FindAsync(_ => true); return result.ToList(); } } ``` ## Multiple Query Methods [Section titled “Multiple Query Methods”](#multiple-query-methods) A single read model can contain multiple query methods for different operations: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static IEnumerable GetAllAccounts(IMongoCollection collection) => collection.Find(_ => true).ToList(); public static DebitAccount GetAccountById(AccountId id, IMongoCollection collection) => collection.Find(a => a.Id == id).FirstOrDefault(); public static IEnumerable GetAccountsByOwner(CustomerId ownerId, IMongoCollection collection) => collection.Find(a => a.Owner == ownerId).ToList(); } ``` > **Note**: The [proxy generator](/arc/backend/proxy-generation/) automatically creates TypeScript types for your query methods, making them strongly typed on the frontend as well. # Authorization Model-bound queries support authorization through standard ASP.NET Core authorization attributes as well as the convenient `[Roles]` attribute provided by the Arc. ## Using the Authorize Attribute [Section titled “Using the Authorize Attribute”](#using-the-authorize-attribute) You can secure query methods using the standard `[Authorize]` attribute: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { [Authorize] public static IEnumerable GetAllAccounts(IMongoCollection collection) => collection.Find(_ => true).ToList(); [Authorize(Roles = "Admin,Manager")] public static IEnumerable GetSensitiveAccounts(IMongoCollection collection) => collection.Find(a => a.Balance > 100000).ToList(); } ``` ## Using the Roles Attribute [Section titled “Using the Roles Attribute”](#using-the-roles-attribute) The Arc provides a more convenient `[Roles]` attribute for cleaner syntax when specifying multiple roles: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { [Roles("Admin", "Auditor")] public static IEnumerable GetAdminAccounts(IMongoCollection collection) => collection.Find(_ => true).ToList(); [Roles("Manager")] public static IEnumerable GetManagerAccounts(IMongoCollection collection) => collection.Find(a => a.Owner != CustomerId.Empty).ToList(); } ``` The user needs to have **at least one** of the specified roles to execute the query. ## Read Model-Level Authorization [Section titled “Read Model-Level Authorization”](#read-model-level-authorization) You can apply authorization at the read model level to protect all query methods: ```csharp [ReadModel] [Roles("User")] // All methods require at least "User" role public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static IEnumerable GetAllAccounts(IMongoCollection collection) => collection.Find(_ => true).ToList(); [Roles("Admin")] // Override read model-level authorization public static IEnumerable GetAdminOnlyAccounts(IMongoCollection collection) => collection.Find(a => a.Balance < 0).ToList(); } ``` ## Method-Level Authorization Override [Section titled “Method-Level Authorization Override”](#method-level-authorization-override) Method-level authorization attributes override class-level ones: ```csharp [ReadModel] [Authorize] // Require authentication for all methods public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { // Inherits class-level [Authorize] - requires authentication public static IEnumerable GetUserAccounts(IMongoCollection collection) => collection.Find(_ => true).ToList(); [Roles("Admin", "Manager")] // Overrides class-level, requires specific roles public static IEnumerable GetPrivilegedAccounts(IMongoCollection collection) => collection.Find(a => a.Balance > 50000).ToList(); [AllowAnonymous] // Completely overrides class-level authorization public static int GetTotalAccountCount(IMongoCollection collection) => (int)collection.CountDocuments(_ => true); } ``` ## Policy-Based Authorization [Section titled “Policy-Based Authorization”](#policy-based-authorization) For more complex authorization scenarios, you can use policy-based authorization: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { [Authorize(Policy = "RequireAccountAccess")] public static DebitAccount GetAccountById( AccountId id, IMongoCollection collection) => collection.Find(a => a.Id == id).FirstOrDefault(); [Authorize(Policy = "RequireHighValueAccess")] public static IEnumerable GetHighValueAccounts( IMongoCollection collection) => collection.Find(a => a.Balance > 1000000).ToList(); } ``` ## Context-Dependent Authorization [Section titled “Context-Dependent Authorization”](#context-dependent-authorization) Access user context within query methods for dynamic authorization: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { [Authorize] public static IEnumerable GetMyAccounts( IMongoCollection collection, IHttpContextAccessor httpContextAccessor) { var userId = httpContextAccessor.HttpContext?.User?.FindFirst("sub")?.Value; if (string.IsNullOrEmpty(userId)) return Enumerable.Empty(); var customerId = new CustomerId(Guid.Parse(userId)); return collection.Find(a => a.Owner == customerId).ToList(); } [Authorize] public static DebitAccount? GetAccountIfOwned( AccountId accountId, IMongoCollection collection, IHttpContextAccessor httpContextAccessor) { var userId = httpContextAccessor.HttpContext?.User?.FindFirst("sub")?.Value; if (string.IsNullOrEmpty(userId)) return null; var customerId = new CustomerId(Guid.Parse(userId)); return collection.Find(a => a.Id == accountId && a.Owner == customerId).FirstOrDefault(); } } ``` ## Role Hierarchies [Section titled “Role Hierarchies”](#role-hierarchies) Implement role hierarchies with custom authorization: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { [Roles("User")] // Basic users can see their own accounts public static IEnumerable GetBasicAccounts(IMongoCollection collection) => collection.Find(a => a.Balance >= 0).ToList(); [Roles("Manager", "Admin")] // Managers and admins can see more public static IEnumerable GetManagerAccounts(IMongoCollection collection) => collection.Find(a => a.Balance > -1000).ToList(); [Roles("Admin")] // Only admins can see all accounts including severely overdrawn public static IEnumerable GetAllAccountsIncludingProblematic(IMongoCollection collection) => collection.Find(_ => true).ToList(); } ``` ## Observable Query Authorization [Section titled “Observable Query Authorization”](#observable-query-authorization) Authorization also applies to observable queries: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { [Authorize] public static ISubject> GetAccountsObservable( IMongoCollection collection) => collection.Observe(); [Roles("Admin")] public static ISubject> GetAdminAccountsObservable( IMongoCollection collection) => collection.Observe(a => a.Balance < 0); } ``` ## Authorization with Query Parameters [Section titled “Authorization with Query Parameters”](#authorization-with-query-parameters) Combine authorization with parameter-based filtering: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { [Authorize] public static IEnumerable GetAccountsByOwner( CustomerId ownerId, IMongoCollection collection, IHttpContextAccessor httpContextAccessor) { var currentUserId = httpContextAccessor.HttpContext?.User?.FindFirst("sub")?.Value; var isAdmin = httpContextAccessor.HttpContext?.User?.IsInRole("Admin") == true; // Users can only see their own accounts unless they're admin if (!isAdmin && currentUserId != ownerId.Value.ToString()) { return Enumerable.Empty(); } return collection.Find(a => a.Owner == ownerId).ToList(); } } ``` ## Custom Authorization Attributes [Section titled “Custom Authorization Attributes”](#custom-authorization-attributes) Create custom authorization attributes for domain-specific logic: ```csharp public class RequireAccountOwnershipAttribute : AuthorizeAttribute { public RequireAccountOwnershipAttribute() : base("RequireAccountOwnership") { } } [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { [RequireAccountOwnership] public static DebitAccount GetAccountDetails( AccountId id, IMongoCollection collection) => collection.Find(a => a.Id == id).FirstOrDefault(); } ``` ## Authorization Results [Section titled “Authorization Results”](#authorization-results) When authorization fails, the query pipeline automatically returns an unauthorized result. The query method will not be executed: ```csharp // In your policy handler or middleware public class AccountOwnershipHandler : AuthorizationHandler { protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, AccountOwnershipRequirement requirement) { var userId = context.User.FindFirst("sub")?.Value; // Check if user owns the account being accessed if (userId is not null /* && user owns the account being accessed */) { context.Succeed(requirement); } return Task.CompletedTask; } } ``` ## Anonymous Access [Section titled “Anonymous Access”](#anonymous-access) Use `[AllowAnonymous]` to allow public access to specific query methods. This attribute bypasses all authorization requirements, including class-level `[Authorize]` attributes and role requirements. ### How AllowAnonymous Works [Section titled “How AllowAnonymous Works”](#how-allowanonymous-works) The authorization system evaluates attributes in the following order: 1. **Method-level `[AllowAnonymous]`** - If present on the method, allows anonymous access immediately 2. **Method-level `[Authorize]` or `[Roles]`** - If present on the method, these take precedence over class-level attributes 3. **Class-level `[AllowAnonymous]`** - If present on the class (and no method-level authorization), allows anonymous access 4. **Class-level `[Authorize]` or `[Roles]`** - Applied when no method-level attributes are specified ### Method-Level AllowAnonymous [Section titled “Method-Level AllowAnonymous”](#method-level-allowanonymous) Override class-level authorization for specific query methods: ```csharp [ReadModel] [Authorize] // Require authentication by default public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { [AllowAnonymous] // Override class-level authorization for public data public static int GetTotalAccountCount(IMongoCollection collection) => (int)collection.CountDocuments(_ => true); [AllowAnonymous] public static decimal GetAverageBalance(IMongoCollection collection) { var accounts = collection.Find(_ => true).ToList(); return accounts.Count > 0 ? accounts.Average(a => a.Balance) : 0; } // This method requires authentication (inherits from class) public static IEnumerable GetAllAccounts(IMongoCollection collection) => collection.Find(_ => true).ToList(); } ``` ### Class-Level AllowAnonymous [Section titled “Class-Level AllowAnonymous”](#class-level-allowanonymous) Apply `[AllowAnonymous]` at the class level to make all query methods publicly accessible by default: ```csharp [ReadModel] [AllowAnonymous] // All methods are publicly accessible by default public record PublicStatistics(string Category, int Count) { public static IEnumerable GetAllStatistics( IMongoCollection collection) => collection.Find(_ => true).ToList(); public static PublicStatistics? GetByCategory( string category, IMongoCollection collection) => collection.Find(s => s.Category == category).FirstOrDefault(); [Authorize] // Override class-level: this specific method requires authentication public static IEnumerable GetSensitiveStatistics( IMongoCollection collection) => collection.Find(s => s.Category.StartsWith("Internal")).ToList(); } ``` ### Common Use Cases for AllowAnonymous [Section titled “Common Use Cases for AllowAnonymous”](#common-use-cases-for-allowanonymous) * **Public statistics or counts** - Aggregate data that doesn’t expose sensitive information * **Product catalogs** - Public product listings for e-commerce sites * **Public content** - Blog posts, articles, or documentation * **Health checks** - System status information for monitoring * **Search endpoints** - Public search functionality ## Best Practices [Section titled “Best Practices”](#best-practices) 1. **Apply authorization at the appropriate level** - Use class-level for broad protection, method-level for specific requirements 2. **Use the `[Roles]` attribute** - More convenient than the standard `[Authorize(Roles = "...")]` syntax 3. **Implement defense in depth** - Combine multiple authorization layers when appropriate 4. **Consider user context** - Use `IHttpContextAccessor` to access current user information for dynamic authorization 5. **Test authorization** - Ensure unauthorized users cannot access protected queries 6. **Use policies for complex logic** - Implement custom authorization policies for domain-specific rules 7. **Be explicit about public access** - Use `[AllowAnonymous]` to clearly indicate intentionally public methods 8. **Log authorization failures** - Monitor and log unauthorized access attempts 9. **Keep authorization simple** - Complex authorization logic should be in services, not query methods > **Note**: Authorization is evaluated before the query method is called. If authorization fails, the query will not be executed and the result will indicate the authorization failure. The [proxy generator](/arc/backend/proxy-generation/) automatically creates TypeScript types that respect your authorization constraints, helping prevent unauthorized client-side calls. # Dependency Injection Model-bound queries use method-level dependency injection, where dependencies are resolved and injected as parameters to your static query methods. This approach provides flexibility and testability while keeping the query logic clean and focused. ## How Method-Level Dependency Injection Works [Section titled “How Method-Level Dependency Injection Works”](#how-method-level-dependency-injection-works) Unlike controller-based queries that use constructor injection, model-bound queries inject dependencies directly as method parameters. The Arc framework automatically resolves these dependencies from the service collection based on their parameter types. ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static IEnumerable GetAllAccounts( IMongoCollection collection) // ← Dependency injected as parameter { return collection.Find(_ => true).ToList(); } } ``` ## Common Dependency Types [Section titled “Common Dependency Types”](#common-dependency-types) ### Database Collections [Section titled “Database Collections”](#database-collections) MongoDB collections are the most common dependencies: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static async Task> GetActiveAccountsAsync( IMongoCollection collection) { var result = await collection.FindAsync(a => a.Balance > 0); return result.ToList(); } } ``` ### Entity Framework DbContext [Section titled “Entity Framework DbContext”](#entity-framework-dbcontext) For Entity Framework Core scenarios: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static async Task> GetAccountsFromEFAsync( ApplicationDbContext dbContext) { return await dbContext.DebitAccounts .Where(a => a.Balance >= 0) .ToListAsync(); } } ``` ### Business Services [Section titled “Business Services”](#business-services) Inject domain services for complex business logic: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static async Task GetAccountRiskAssessment( AccountId accountId, IMongoCollection collection, IRiskCalculationService riskService, ITransactionHistoryService transactionService) { var account = await collection.Find(a => a.Id == accountId).FirstOrDefaultAsync(); if (account is null) throw new AccountNotFoundException(accountId); var transactions = await transactionService.GetRecentTransactionsAsync(accountId); var riskScore = await riskService.CalculateRiskAsync(account, transactions); return new AccountRiskAssessment(accountId, riskScore); } } ``` ### Logging [Section titled “Logging”](#logging) Structured logging with dependency injection: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static async Task> SearchAccountsWithLogging( string searchTerm, IMongoCollection collection, ILogger logger) { logger.LogInformation("Searching accounts with term: {SearchTerm}", searchTerm); var filter = Builders.Filter.Regex( a => a.Name, new BsonRegularExpression(searchTerm, "i")); var result = await collection.FindAsync(filter); var accounts = result.ToList(); logger.LogInformation("Found {AccountCount} accounts matching '{SearchTerm}'", accounts.Count, searchTerm); return accounts; } } ``` ### Configuration [Section titled “Configuration”](#configuration) Inject configuration objects using `IOptions` or `IConfiguration`: ```csharp public class AccountQueryOptions { public int MaxSearchResults { get; set; } = 100; public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromMinutes(5); public bool EnableAuditLogging { get; set; } = true; } [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static async Task> GetPagedAccountsWithOptions( int page, int pageSize, IMongoCollection collection, IOptions options, ILogger logger) { var opts = options.Value; var actualPageSize = Math.Min(pageSize, opts.MaxSearchResults); if (opts.EnableAuditLogging) { logger.LogInformation("Retrieving page {Page} with size {PageSize}", page, actualPageSize); } var result = await collection.FindAsync(_ => true); return result.Skip(page * actualPageSize).Limit(actualPageSize).ToList(); } } ``` ### Caching Services [Section titled “Caching Services”](#caching-services) Integrate caching for performance optimization: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static async Task> GetCachedAccountsByOwner( CustomerId ownerId, IMongoCollection collection, IMemoryCache cache, ILogger logger) { var cacheKey = $"accounts-by-owner-{ownerId}"; if (cache.TryGetValue(cacheKey, out IEnumerable? cachedAccounts)) { logger.LogInformation("Returning cached accounts for owner {OwnerId}", ownerId); return cachedAccounts ?? Enumerable.Empty(); } logger.LogInformation("Loading accounts for owner {OwnerId} from database", ownerId); var accounts = await collection.Find(a => a.Owner == ownerId).ToListAsync(); cache.Set(cacheKey, accounts, TimeSpan.FromMinutes(5)); return accounts; } } ``` ## Parameter Order Flexibility [Section titled “Parameter Order Flexibility”](#parameter-order-flexibility) Dependencies can be placed in any position among your method parameters. The framework resolves them by type, not by position: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { // Dependencies first, then query parameters public static async Task> GetAccountsByStatusPattern1( IMongoCollection collection, ILogger logger, AccountStatus status, bool includeInactive) { logger.LogInformation("Getting accounts by status: {Status}", status); // Implementation... return await collection.Find(_ => true).ToListAsync(); } // Query parameters first, then dependencies public static async Task> GetAccountsByStatusPattern2( AccountStatus status, bool includeInactive, IMongoCollection collection, ILogger logger) { logger.LogInformation("Getting accounts by status: {Status}", status); // Implementation... return await collection.Find(_ => true).ToListAsync(); } // Mixed order public static async Task> GetAccountsByStatusPattern3( AccountStatus status, IMongoCollection collection, bool includeInactive, ILogger logger) { logger.LogInformation("Getting accounts by status: {Status}", status); // Implementation... return await collection.Find(_ => true).ToListAsync(); } } ``` ## Multiple Dependencies of Same Type [Section titled “Multiple Dependencies of Same Type”](#multiple-dependencies-of-same-type) When you need multiple dependencies of the same type, use named dependencies or specific implementations: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static async Task GetCrossAccountSummary( IMongoCollection debitCollection, IMongoCollection creditCollection, ILogger logger) { var debitAccounts = await debitCollection.Find(_ => true).ToListAsync(); var creditAccounts = await creditCollection.Find(_ => true).ToListAsync(); logger.LogInformation("Processing {DebitCount} debit and {CreditCount} credit accounts", debitAccounts.Count, creditAccounts.Count); return new CrossAccountSummary( debitAccounts.Sum(a => a.Balance), creditAccounts.Sum(a => a.Balance)); } } ``` ## Generic Dependencies [Section titled “Generic Dependencies”](#generic-dependencies) Use generic dependencies for flexible, reusable patterns: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static async Task> GetAccountsWithGenericRepository( IRepository repository, ILogger logger) { logger.LogInformation("Loading accounts using generic repository"); return await repository.GetAllAsync(); } public static async Task GetAccountByIdWithGenericRepository( AccountId id, IRepository repository, IValidator validator) { var validationResult = await validator.ValidateAsync(id); if (!validationResult.IsValid) { throw new ValidationException(validationResult.Errors); } return await repository.GetByIdAsync(id); } } ``` ## Scoped Dependencies [Section titled “Scoped Dependencies”](#scoped-dependencies) Dependencies are resolved with their registered lifetime (Singleton, Scoped, Transient): ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static async Task> GetAccountsWithScopedServices( IMongoCollection collection, // Scoped ICurrentUserService currentUserService, // Scoped ISystemClock systemClock, // Singleton IAuditService auditService) // Scoped { var currentUser = await currentUserService.GetCurrentUserAsync(); var currentTime = systemClock.UtcNow; await auditService.LogQueryAsync("GetAccountsWithScopedServices", currentUser.Id, currentTime); // Filter based on user permissions var filter = BuildUserFilter(currentUser); return await collection.Find(filter).ToListAsync(); } private static FilterDefinition BuildUserFilter(User user) { if (user.IsAdmin) return Builders.Filter.Empty; return Builders.Filter.Eq(a => a.Owner, user.CustomerId); } } ``` ## Service Registration [Section titled “Service Registration”](#service-registration) Ensure your dependencies are properly registered in the service collection: ```csharp // In Program.cs or Startup.cs builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.Configure( builder.Configuration.GetSection("AccountQueries")); // MongoDB collections are typically registered as: builder.Services.AddScoped>(provider => { var database = provider.GetRequiredService(); return database.GetCollection("debit-accounts"); }); ``` ## Dependency Injection Best Practices [Section titled “Dependency Injection Best Practices”](#dependency-injection-best-practices) 1. **Order parameters logically** - Group related parameters together, but remember that dependency resolution is by type 2. **Use specific interface types** - Prefer `ILogger` over `ILogger`, `IOptions` over `IConfiguration` 3. **Avoid service locator pattern** - Don’t inject `IServiceProvider` and resolve services manually 4. **Keep methods focused** - If you need many dependencies, consider if the method is doing too much 5. **Use appropriate lifetimes** - Understand Singleton, Scoped, and Transient lifetimes for your dependencies 6. **Test with mocked dependencies** - The method-level injection makes unit testing straightforward ## Testing with Dependency Injection [Section titled “Testing with Dependency Injection”](#testing-with-dependency-injection) Method-level dependency injection makes unit testing simple: ```csharp [Fact] public async Task GetAccountsByOwner_Should_Return_Filtered_Accounts() { // Arrange var mockCollection = Substitute.For>(); var mockLogger = Substitute.For>(); var ownerId = new CustomerId(Guid.NewGuid()); var expectedAccounts = new List { new(new AccountId(Guid.NewGuid()), new AccountName("Test Account"), ownerId, 1000m) }; var mockCursor = Substitute.For>(); mockCursor.ToList().Returns(expectedAccounts); mockCollection.FindAsync(Arg.Any>()) .Returns(mockCursor); // Act var result = await DebitAccount.GetAccountsByOwnerWithLogging( ownerId, mockCollection, mockLogger); // Assert result.Should().BeEquivalentTo(expectedAccounts); mockLogger.Received(1).LogInformation( Arg.Is(s => s.Contains("Getting accounts for owner")), ownerId); } ``` ## Error Handling with Dependencies [Section titled “Error Handling with Dependencies”](#error-handling-with-dependencies) Handle dependency-related errors gracefully: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static async Task> GetAccountsWithErrorHandling( IMongoCollection collection, ILogger logger, IHealthCheckService healthCheck) { try { // Check if database is healthy before querying var healthResult = await healthCheck.CheckHealthAsync(); if (healthResult.Status != HealthStatus.Healthy) { logger.LogWarning("Database health check failed: {Status}", healthResult.Status); return Enumerable.Empty(); } var result = await collection.FindAsync(_ => true); return result.ToList(); } catch (MongoException ex) { logger.LogError(ex, "MongoDB error while retrieving accounts"); throw new DataAccessException("Unable to retrieve accounts", ex); } catch (Exception ex) { logger.LogError(ex, "Unexpected error while retrieving accounts"); throw; } } } ``` Method-level dependency injection in model-bound queries provides a clean, testable, and flexible approach to accessing services and repositories while keeping your query logic focused and maintainable. # Observable Queries Observable queries in model-bound scenarios provide the same real-time data streaming capabilities as controller-based queries, but implemented as static methods directly on your read model records. You achieve this by returning `ISubject` from static methods on your `[ReadModel]` decorated record. The `ISubject` return type automatically establishes a WebSocket connection between the server and client, enabling real-time data updates. This integrates seamlessly with the [ObservableQuery construct in the frontend](/arc/frontend/react/queries/observable-queries/) through the proxy generator, creating strongly-typed reactive data flows. ## Basic Observable Query [Section titled “Basic Observable Query”](#basic-observable-query) Define an observable query as a static method on your read model and return the `ISubject` produced by the MongoDB `Observe()` extension method. `Observe()` watches the collection and pushes a fresh snapshot every time the underlying data changes — Arc handles the WebSocket connection and the change monitoring for you: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static ISubject> GetAllAccountsObservable(IMongoCollection collection) { return collection.Observe(); } public static IEnumerable GetAllAccounts(IMongoCollection collection) { return collection.Find(_ => true).ToList(); } } ``` ## Observable with Arguments [Section titled “Observable with Arguments”](#observable-with-arguments) Observable queries can accept arguments just like regular queries. Pass a filter expression to `Observe()` and the observable only streams the matching documents: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static ISubject> GetAccountsByOwnerObservable( CustomerId ownerId, IMongoCollection collection) { return collection.Observe(account => account.Owner == ownerId); } public static ISubject> GetFilteredAccountsObservable( IMongoCollection collection, decimal minBalance = 0) { return collection.Observe(account => account.Balance >= minBalance); } } ``` ## Single Object Observable [Section titled “Single Object Observable”](#single-object-observable) For observing changes to a single object, return `ISubject` (not a collection) and use `ObserveSingle()`, which streams the latest version of the one matching document: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static ISubject GetAccountObservable( AccountId id, IMongoCollection collection) { return collection.ObserveSingle(account => account.Id == id); } } ``` ## Custom Observable Logic [Section titled “Custom Observable Logic”](#custom-observable-logic) For computed or derived data, build on top of the collection’s observable. `Observe()` returns an `IObservable>` that emits a new snapshot whenever the data changes, so you can use System.Reactive operators such as `Select` to project each snapshot into a computed shape. The result is itself an `ISubject` via `ReplaySubject`, so subscribers always receive the most recent computed value: ```csharp using System.Reactive.Linq; using System.Reactive.Subjects; public record AccountSummary(int Count, decimal TotalBalance); [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static ISubject GetAccountSummaryObservable(IMongoCollection collection) { var summary = new ReplaySubject(1); collection.Observe() .Select(accounts => new AccountSummary(accounts.Count(), accounts.Sum(a => a.Balance))) .Subscribe(summary); return summary; } } ``` ## Multiple Data Source Observables [Section titled “Multiple Data Source Observables”](#multiple-data-source-observables) Observe changes across multiple data sources by injecting multiple collections and combining their observables with `CombineLatest`. The combined observable re-emits whenever *either* source changes: ```csharp using System.Reactive.Linq; using System.Reactive.Subjects; public record CombinedData(IEnumerable Accounts, IEnumerable Customers); [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static ISubject GetCombinedDataObservable( IMongoCollection accountCollection, IMongoCollection customerCollection) { var combined = new ReplaySubject(1); accountCollection.Observe() .CombineLatest(customerCollection.Observe(), (accounts, customers) => new CombinedData(accounts, customers)) .Subscribe(combined); return combined; } } ``` ## Observable with Computed Values [Section titled “Observable with Computed Values”](#observable-with-computed-values) Create observables that compute derived values by projecting each snapshot with `Select`. Every change to the underlying collection recomputes and re-emits the metrics: ```csharp using System.Reactive.Linq; using System.Reactive.Subjects; public record AccountMetrics( int TotalAccounts, decimal TotalBalance, decimal AverageBalance, int ActiveAccounts, int HighValueAccounts); [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static ISubject GetAccountMetricsObservable(IMongoCollection collection) { var metrics = new ReplaySubject(1); collection.Observe() .Select(accounts => new AccountMetrics( TotalAccounts: accounts.Count(), TotalBalance: accounts.Sum(a => a.Balance), AverageBalance: accounts.Any() ? accounts.Average(a => a.Balance) : 0, ActiveAccounts: accounts.Count(a => a.Balance > 0), HighValueAccounts: accounts.Count(a => a.Balance > 100000))) .Subscribe(metrics); return metrics; } } ``` ## Filtered Observables with Dynamic Criteria [Section titled “Filtered Observables with Dynamic Criteria”](#filtered-observables-with-dynamic-criteria) Allow clients to specify filter criteria for observables. Observe the whole collection and apply the dynamic predicate to each emitted snapshot with `Select`, so the filtered results stay current as the data changes: ```csharp using System.Reactive.Linq; using System.Reactive.Subjects; public record ObservableFilter( decimal? MinBalance, decimal? MaxBalance, string? NamePattern, CustomerId? OwnerId); [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static ISubject> GetFilteredObservable( ObservableFilter filter, IMongoCollection collection) { var predicate = BuildFilterPredicate(filter); var filtered = new ReplaySubject>(1); collection.Observe() .Select(accounts => accounts.Where(predicate).ToList().AsEnumerable()) .Subscribe(filtered); return filtered; } static Func BuildFilterPredicate(ObservableFilter filter) { return account => (!filter.MinBalance.HasValue || account.Balance >= filter.MinBalance.Value) && (!filter.MaxBalance.HasValue || account.Balance <= filter.MaxBalance.Value) && (string.IsNullOrEmpty(filter.NamePattern) || account.Name.Contains(filter.NamePattern, StringComparison.OrdinalIgnoreCase)) && (!filter.OwnerId.HasValue || account.Owner == filter.OwnerId.Value); } } ``` ## Throttled Observables [Section titled “Throttled Observables”](#throttled-observables) For high-frequency changes, use the System.Reactive `Sample` operator to emit at most one update per time window, preventing a flood of changes from overwhelming clients: ```csharp using System.Reactive.Linq; using System.Reactive.Subjects; [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static ISubject> GetThrottledObservable( IMongoCollection collection, int throttleMs = 1000) { var throttled = new ReplaySubject>(1); collection.Observe() .Sample(TimeSpan.FromMilliseconds(throttleMs)) .Subscribe(throttled); return throttled; } } ``` ## Error Handling in Observables [Section titled “Error Handling in Observables”](#error-handling-in-observables) Errors raised while observing the collection propagate through the observable’s error channel automatically. Use the System.Reactive `Do` operator to log them as they flow through, without altering the stream: ```csharp using System.Reactive.Linq; using System.Reactive.Subjects; [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static ISubject> GetRobustObservable( IMongoCollection collection, ILogger logger) { var observable = new ReplaySubject>(1); collection.Observe() .Do( onNext: _ => { }, onError: ex => logger.LogError(ex, "Error observing accounts")) .Subscribe(observable); return observable; } } ``` ## Authentication and Authorization [Section titled “Authentication and Authorization”](#authentication-and-authorization) Observable queries support the same authentication and authorization as regular queries: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { [Authorize] public static ISubject> GetSecureObservable(IMongoCollection collection) { // Only authenticated users can subscribe return collection.Observe(); } [Authorize(Roles = "Admin")] public static ISubject> GetAdminObservable(IMongoCollection collection) { // Only admin users can subscribe return collection.Observe(); } } ``` ## Dependency Injection [Section titled “Dependency Injection”](#dependency-injection) Model-bound observable queries support the same dependency injection patterns as regular model-bound queries: ```csharp using System.Reactive.Linq; using System.Reactive.Subjects; [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static ISubject> GetAccountsWithBusinessLogic( IMongoCollection collection, IAccountValidator validator) { var valid = new ReplaySubject>(1); collection.Observe() .Select(accounts => accounts.Where(validator.IsValid).ToList().AsEnumerable()) .Subscribe(valid); return valid; } } ``` ## Best Practices for Model-Bound Observable Queries [Section titled “Best Practices for Model-Bound Observable Queries”](#best-practices-for-model-bound-observable-queries) 1. **Prefer the `Observe()` / `ObserveSingle()` extension methods** — they handle change monitoring, initial data, and cleanup for you 2. **Project with System.Reactive operators** (`Select`, `CombineLatest`, `Sample`) when you need computed, combined, or throttled streams 3. **Use appropriate filters** to minimize unnecessary data transmission 4. **Consider throttling** with `Sample` for high-frequency changes to prevent overwhelming clients 5. **Let errors propagate** through the observable’s error channel; use `Do` to observe them for logging 6. **Use authentication** to control who can subscribe to observable endpoints 7. **Monitor performance** and consider the impact of many concurrent subscriptions 8. **Keep observable methods static** and follow the same patterns as regular model-bound queries ## Frontend Integration [Section titled “Frontend Integration”](#frontend-integration) Observable queries integrate seamlessly with frontend frameworks through the proxy generator and the [ObservableQuery construct](/arc/frontend/react/queries/observable-queries/): ```typescript accountsObservable.subscribe(accounts => { // Handle real-time account updates updateUI(accounts); }); ``` The `ISubject` return type automatically establishes and manages WebSocket connections, providing: > **Important**: The `Observe()` and `ObserveSingle()` extension methods manage their own subscriptions and clean up automatically when a client disconnects, so you do not need to write any teardown code. * **Automatic connection management** - WebSocket connections are established and maintained automatically * **Strongly-typed data flow** - Full TypeScript support through the proxy generator * **Reactive integration** - Seamless integration with React hooks like `useObservableQuery()` * **Reconnection handling** - Automatic reconnection and state recovery on connection loss > **Note**: The [proxy generator](/arc/backend/proxy-generation/) automatically creates TypeScript types for your observable queries, making them strongly typed on the frontend as well. ## Waiting for the First HTTP Result [Section titled “Waiting for the First HTTP Result”](#waiting-for-the-first-http-result) The generated HTTP `GET` endpoint for an observable query returns the current snapshot as JSON. If the observable does not have a current value yet, add `waitForFirstResult=true` to keep the HTTP request open until the first item is produced. Arc applies a timeout while waiting. By default the timeout is 30 seconds. You can override it with `waitForFirstResultTimeout`, expressed in seconds. ```bash curl "https://localhost:5001/api/debit-account/observe-single?waitForFirstResult=true" ``` ```bash curl "https://localhost:5001/api/debit-account/observe-single?waitForFirstResult=true&waitForFirstResultTimeout=10" ``` This is useful when debugging observable queries with cURL or any other plain HTTP client and you want the request to block until the first result is available. See [Use Observable Queries with cURL](/arc/backend/queries/using-observable-queries-with-curl/) for snapshot, SSE, and long-polling workflows. ## Connection Management [Section titled “Connection Management”](#connection-management) Arc automatically handles WebSocket connections for observable queries: * **Connection establishment** - Automatic WebSocket upgrade for observable endpoints * **Message serialization** - Automatic JSON serialization of observable data * **Connection cleanup** - Proper disposal of resources when clients disconnect * **Reconnection handling** - Clients can reconnect and resume subscriptions The same connection management applies whether using controller-based or model-bound approaches. # Paging When a static query method on a `[ReadModel]` returns `IQueryable`, the query pipeline automatically applies server-side paging and sorting. You write a simple method that returns a queryable, and the framework handles the rest. ## Why IQueryable matters [Section titled “Why IQueryable matters”](#why-iqueryable-matters) The key to automatic paging is returning `IQueryable` instead of `IEnumerable` or `List`. When the pipeline sees an `IQueryable`, it appends `.Skip()` and `.Take()` *before* the database executes the query — so only the requested page of data travels over the wire. If you return a materialized collection, all rows are fetched first and paging cannot be applied at the database level. ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { // ✅ Returns IQueryable — paging and sorting are applied automatically public static IQueryable AllAccounts(IMongoCollection collection) => collection.AsQueryable(); } ``` ## How it works [Section titled “How it works”](#how-it-works) When a client sends paging parameters in the query string, the `QueryableQueryRenderer` intercepts the `IQueryable` result and: 1. Counts the total number of matching items 2. Applies sorting based on `sortby` and `sortDirection` 3. Applies `.Skip(page * pageSize)` and `.Take(pageSize)` 4. Returns the page of data wrapped in a `QueryResult` with a `PagingInfo` containing `page`, `size`, `totalItems`, and `totalPages` The client controls paging with these query string parameters: | Parameter | Type | Description | | --------------- | --------------- | ------------------------ | | `page` | `int` | Zero-based page number | | `pageSize` | `int` | Number of items per page | | `sortby` | `string` | Field name to sort by | | `sortDirection` | `asc` or `desc` | Sort direction | ### Example requests [Section titled “Example requests”](#example-requests) ```http GET /api/debitaccount/allaccounts?page=0&pageSize=25 GET /api/debitaccount/allaccounts?page=1&pageSize=10&sortby=name&sortDirection=asc ``` When no paging parameters are provided, the full result set is returned without paging. ## Complete example with filtering [Section titled “Complete example with filtering”](#complete-example-with-filtering) Paging works alongside query arguments. The pipeline applies paging *after* your method returns the filtered `IQueryable`: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static IQueryable AllAccounts(IMongoCollection collection) => collection.AsQueryable(); public static IQueryable AccountsByOwner( CustomerId ownerId, IMongoCollection collection) => collection.AsQueryable().Where(a => a.Owner == ownerId); } ``` Both query methods support paging automatically because they return `IQueryable`. ## Return type comparison [Section titled “Return type comparison”](#return-type-comparison) | Return type | Paging | Sorting | DB-level optimization | | ---------------- | ----------- | ----------- | ------------------------ | | `IQueryable` | ✅ Automatic | ✅ Automatic | ✅ Skip/Take pushed to DB | | `IEnumerable` | ❌ | ❌ | ❌ All rows loaded | | `List` | ❌ | ❌ | ❌ All rows loaded | | `T[]` | ❌ | ❌ | ❌ All rows loaded | ## Observable queries with paging [Section titled “Observable queries with paging”](#observable-queries-with-paging) Observable queries that return `ISubject>` also support automatic paging. The pipeline applies paging to each update pushed through the observable: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static ISubject> ObserveAllAccounts( IMongoCollection collection) => collection.Observe(); } ``` ## Frontend integration [Section titled “Frontend integration”](#frontend-integration) The generated TypeScript proxy includes a `useWithPaging` method when the backend query supports paging. See [React Paging](/arc/frontend/react/queries/paging/) for details on using paging in React components. ```tsx const [result, perform, setSorting, setPage, setPageSize] = AllAccounts.useWithPaging(25); // Navigate pages await setPage(result.paging.page + 1); // Change page size await setPageSize(50); // Access paging metadata const { page, size, totalItems, totalPages } = result.paging; ``` # Query Arguments Model-bound queries can accept arguments as method parameters. Arguments are automatically bound from the HTTP request and can include route parameters, query string parameters, or complex objects. ## Method Parameters [Section titled “Method Parameters”](#method-parameters) Arguments are passed as method parameters and are automatically bound from the HTTP request: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static DebitAccount GetAccountById( AccountId id, IMongoCollection collection) { return collection.Find(a => a.Id == id).FirstOrDefault(); } public static IEnumerable SearchAccounts( string nameFilter, decimal? minBalance, IMongoCollection collection, ILogger logger) { logger.LogInformation("Searching accounts with filter: {Filter}", nameFilter); var filterBuilder = Builders.Filter; var filters = new List>(); if (!string.IsNullOrEmpty(nameFilter)) { filters.Add(filterBuilder.Regex(a => a.Name, new BsonRegularExpression(nameFilter, "i"))); } if (minBalance.HasValue) { filters.Add(filterBuilder.Gte(a => a.Balance, minBalance.Value)); } var combinedFilter = filters.Any() ? filterBuilder.And(filters) : filterBuilder.Empty; return collection.Find(combinedFilter).ToList(); } } ``` ## Argument Types [Section titled “Argument Types”](#argument-types) Model-bound queries support various argument types: ### Primitive Types [Section titled “Primitive Types”](#primitive-types) ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static IEnumerable GetAccountsByBalance( decimal balance, bool exactMatch, IMongoCollection collection) { return exactMatch ? collection.Find(a => a.Balance == balance).ToList() : collection.Find(a => a.Balance >= balance).ToList(); } } ``` ### Concept Types [Section titled “Concept Types”](#concept-types) Using concept types (value objects) for stronger typing: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static IEnumerable GetAccountsByOwnerConcept( CustomerId ownerId, IMongoCollection collection) { return collection.Find(a => a.Owner == ownerId).ToList(); } } ``` ### Enums [Section titled “Enums”](#enums) ```csharp public enum AccountStatus { Active, Inactive, Suspended } [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static IEnumerable GetAccountsByStatus( AccountStatus status, IMongoCollection collection) { // Implement status filtering logic return status switch { AccountStatus.Active => collection.Find(a => a.Balance > 0).ToList(), AccountStatus.Inactive => collection.Find(a => a.Balance == 0).ToList(), AccountStatus.Suspended => collection.Find(a => a.Balance < 0).ToList(), _ => collection.Find(_ => false).ToList() }; } } ``` ### Collection Arguments [Section titled “Collection Arguments”](#collection-arguments) ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static IEnumerable GetAccountsByIds( IEnumerable ids, IMongoCollection collection) { return collection.Find(a => ids.Contains(a.Id)).ToList(); } public static IEnumerable GetAccountsByOwners( List ownerIds, IMongoCollection collection) { return collection.Find(a => ownerIds.Contains(a.Owner)).ToList(); } } ``` ## Nullable Arguments [Section titled “Nullable Arguments”](#nullable-arguments) Optional arguments should be nullable: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static IEnumerable FlexibleSearch( string? name, CustomerId? ownerId, decimal? minBalance, decimal? maxBalance, IMongoCollection collection) { var filterBuilder = Builders.Filter; var filters = new List>(); if (!string.IsNullOrEmpty(name)) filters.Add(filterBuilder.Regex(a => a.Name, new BsonRegularExpression(name, "i"))); if (ownerId.HasValue) filters.Add(filterBuilder.Eq(a => a.Owner, ownerId.Value)); if (minBalance.HasValue) filters.Add(filterBuilder.Gte(a => a.Balance, minBalance.Value)); if (maxBalance.HasValue) filters.Add(filterBuilder.Lte(a => a.Balance, maxBalance.Value)); var combinedFilter = filters.Any() ? filterBuilder.And(filters) : filterBuilder.Empty; return collection.Find(combinedFilter).ToList(); } } ``` ## Default Values [Section titled “Default Values”](#default-values) Provide sensible default values for optional parameters: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static IEnumerable GetPagedAccounts( int page = 0, int pageSize = 50, string sortBy = "name", bool ascending = true, IMongoCollection collection) { var query = collection.Find(_ => true); // Apply sorting query = ascending ? query.SortBy(sortBy) : query.SortByDescending(sortBy); // Apply paging return query.Skip(page * pageSize).Limit(pageSize).ToList(); } } ``` ## Complex Query Objects [Section titled “Complex Query Objects”](#complex-query-objects) For complex search criteria, create dedicated parameter objects: ```csharp public record AccountSearchCriteria( string? NamePattern, CustomerId? OwnerId, decimal? MinBalance, decimal? MaxBalance, DateTime? CreatedAfter, DateTime? CreatedBefore, bool IncludeInactive); [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static IEnumerable SearchAccounts( AccountSearchCriteria criteria, IMongoCollection collection) { var filterBuilder = Builders.Filter; var filters = new List>(); if (!string.IsNullOrEmpty(criteria.NamePattern)) filters.Add(filterBuilder.Regex(a => a.Name, new BsonRegularExpression(criteria.NamePattern, "i"))); if (criteria.OwnerId.HasValue) filters.Add(filterBuilder.Eq(a => a.Owner, criteria.OwnerId.Value)); if (criteria.MinBalance.HasValue) filters.Add(filterBuilder.Gte(a => a.Balance, criteria.MinBalance.Value)); if (criteria.MaxBalance.HasValue) filters.Add(filterBuilder.Lte(a => a.Balance, criteria.MaxBalance.Value)); // Add date filters if the model supports them // if (criteria.CreatedAfter.HasValue) // filters.Add(filterBuilder.Gte(a => a.CreatedDate, criteria.CreatedAfter.Value)); if (!criteria.IncludeInactive) filters.Add(filterBuilder.Gt(a => a.Balance, 0)); var combinedFilter = filters.Any() ? filterBuilder.And(filters) : filterBuilder.Empty; return collection.Find(combinedFilter).ToList(); } } ``` ## Parameter Order [Section titled “Parameter Order”](#parameter-order) You can mix query parameters with dependency parameters. Dependencies are resolved by type, so order is flexible: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { // Query parameters first public static IEnumerable GetAccountsByOwnerWithLogging( CustomerId ownerId, bool includeZeroBalance, IMongoCollection collection, ILogger logger) { logger.LogInformation("Getting accounts for owner {OwnerId}, includeZero: {IncludeZero}", ownerId, includeZeroBalance); var filter = includeZeroBalance ? Builders.Filter.Eq(a => a.Owner, ownerId) : Builders.Filter.And( Builders.Filter.Eq(a => a.Owner, ownerId), Builders.Filter.Gt(a => a.Balance, 0)); return collection.Find(filter).ToList(); } // Dependencies first public static IEnumerable GetAccountsByBalanceRange( IMongoCollection collection, ILogger logger, decimal minBalance, decimal maxBalance) { logger.LogInformation("Getting accounts with balance between {Min} and {Max}", minBalance, maxBalance); var filter = Builders.Filter.And( Builders.Filter.Gte(a => a.Balance, minBalance), Builders.Filter.Lte(a => a.Balance, maxBalance)); return collection.Find(filter).ToList(); } } ``` ## Validation Attributes [Section titled “Validation Attributes”](#validation-attributes) Use validation attributes to ensure argument quality: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static IEnumerable SearchWithValidation( [Required] [MinLength(3)] string searchTerm, [Range(1, 100)] int pageSize, [Range(0, int.MaxValue)] int page, IMongoCollection collection) { // Validation is automatically applied by the framework var filter = Builders.Filter.Regex( a => a.Name, new BsonRegularExpression(searchTerm, "i")); return collection.Find(filter) .Skip(page * pageSize) .Limit(pageSize) .ToList(); } } ``` ## Observable Query Arguments [Section titled “Observable Query Arguments”](#observable-query-arguments) Observable queries can also accept arguments: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static ISubject> GetAccountsByOwnerObservable( CustomerId ownerId, IMongoCollection collection) { return collection.Observe(a => a.Owner == ownerId); } public static ISubject GetAccountObservable( AccountId id, IMongoCollection collection) { return collection.ObserveSingle(a => a.Id == id); } } ``` ## URL Binding [Section titled “URL Binding”](#url-binding) Arguments are automatically bound from different parts of the HTTP request: ### Route Parameters [Section titled “Route Parameters”](#route-parameters) Based on the method name and parameter names, route parameters are inferred: ```csharp // This would typically map to: GET /api/debitaccount/getaccountbyid/{id} public static DebitAccount GetAccountById(AccountId id, IMongoCollection collection) { return collection.Find(a => a.Id == id).FirstOrDefault(); } ``` ### Query String Parameters [Section titled “Query String Parameters”](#query-string-parameters) Parameters that aren’t in the route become query string parameters: ```csharp // This would map to: GET /api/debitaccount/searchaccounts?nameFilter=abc&minBalance=100 public static IEnumerable SearchAccounts( string nameFilter, decimal? minBalance, IMongoCollection collection) { // Implementation... return collection.Find(_ => true).ToList(); } ``` ## Best Practices [Section titled “Best Practices”](#best-practices) 1. **Use descriptive parameter names** - They become part of your API contract 2. **Make optional parameters nullable** - Use nullable types for optional arguments 3. **Provide default values** - For commonly used optional parameters 4. **Use concept types** - Leverage value objects for stronger typing 5. **Validate inputs** - Use validation attributes for parameter validation 6. **Keep parameter lists reasonable** - For many parameters, consider using parameter objects 7. **Order parameters logically** - Group related parameters together 8. **Use appropriate types** - Choose the most specific type that makes sense 9. **Handle null inputs gracefully** - Check for null values and handle appropriately ## URL Generation [Section titled “URL Generation”](#url-generation) Arc generates URLs based on your method names and parameters: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { // GET /api/debitaccount/getallaccounts public static IEnumerable GetAllAccounts(IMongoCollection collection) => /* ... */; // GET /api/debitaccount/getaccountbyid/{id} public static DebitAccount GetAccountById(AccountId id, IMongoCollection collection) => /* ... */; // GET /api/debitaccount/searchaccounts?name={name}&minBalance={minBalance} public static IEnumerable SearchAccounts(string? name, decimal? minBalance, IMongoCollection collection) => /* ... */; } ``` > **Note**: The [proxy generator](/arc/backend/proxy-generation/) automatically creates TypeScript types for your query arguments, making them strongly typed on the frontend as well. # Return Types Model-bound queries support various return types, allowing you to structure your API responses according to your application’s needs. ## Collections [Section titled “Collections”](#collections) ### IEnumerable\ [Section titled “IEnumerable\”](#ienumerablet) The most common return type for queries that return multiple records: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static IEnumerable GetAllAccounts(IMongoCollection collection) => collection.Find(_ => true); } ``` ### List\ and Arrays [Section titled “List\ and Arrays”](#listt-and-arrays) You can also return concrete collection types: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static List GetAccountsList(IMongoCollection collection) => collection.Find(_ => true).ToList(); public static DebitAccount[] GetAccountsArray(IMongoCollection collection) => collection.Find(_ => true).ToArray(); } ``` ## Single Objects [Section titled “Single Objects”](#single-objects) Return individual records or null when not found: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static DebitAccount? GetFirstAccount(IMongoCollection collection) => collection.Find(_ => true).FirstOrDefault(); public static DebitAccount GetAccountById(AccountId id, IMongoCollection collection) => collection.Find(a => a.Id == id).FirstOrDefault(); } ``` ## Custom Return Types [Section titled “Custom Return Types”](#custom-return-types) Create custom types for complex query results: ```csharp public record AccountSummary(int TotalAccounts, decimal TotalBalance, decimal AverageBalance); [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static AccountSummary GetAccountSummary(IMongoCollection collection) { var accounts = collection.Find(_ => true).ToList(); return new AccountSummary( accounts.Count, accounts.Sum(a => a.Balance), accounts.Count > 0 ? accounts.Average(a => a.Balance) : 0); } } ``` ## Async Return Types [Section titled “Async Return Types”](#async-return-types) Wrap any return type in `Task` for asynchronous operations: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static async Task> GetAllAccountsAsync(IMongoCollection collection) { var result = await collection.FindAsync(_ => true); return result.ToList(); } public static async Task GetAccountByIdAsync(AccountId id, IMongoCollection collection) { var result = await collection.FindAsync(a => a.Id == id); return result.FirstOrDefault(); } public static async Task GetAccountSummaryAsync(IMongoCollection collection) { var accounts = await collection.Find(_ => true).ToListAsync(); return new AccountSummary( accounts.Count, accounts.Sum(a => a.Balance), accounts.Count > 0 ? accounts.Average(a => a.Balance) : 0); } } ``` ## Observable Return Types [Section titled “Observable Return Types”](#observable-return-types) For real-time queries that push updates to subscribers, return `ISubject`: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static ISubject> GetAccountsObservable( IMongoCollection collection) => collection.Observe(); // Leveraging MongoDB Extension methods public static ISubject GetAccountObservable( AccountId id, IMongoCollection collection) => collection.ObserveSingle(a => a.Id == id); } ``` > **Important**: Do not combine `Task` with `ISubject`. Observable methods should return `ISubject` directly, not `Task>`. ## Complex Business Objects [Section titled “Complex Business Objects”](#complex-business-objects) Return rich domain objects with computed properties: ```csharp public record AccountDetails( AccountId Id, AccountName Name, CustomerId Owner, decimal Balance, decimal AvailableCredit, AccountStatus Status, DateTime LastActivity); public record CustomerInfo(CustomerId Id, string Name, string Email); public record AccountWithCustomerInfo( DebitAccount Account, CustomerInfo Customer, IEnumerable RecentTransactions); [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static AccountDetails GetAccountDetails( AccountId id, IMongoCollection accountCollection, ICreditService creditService, ITransactionService transactionService) { var account = accountCollection.Find(a => a.Id == id).FirstOrDefault(); if (account is null) throw new AccountNotFoundException(id); var availableCredit = creditService.GetAvailableCredit(id); var lastActivity = transactionService.GetLastActivity(id); var status = DetermineAccountStatus(account, lastActivity); return new AccountDetails( account.Id, account.Name, account.Owner, account.Balance, availableCredit, status, lastActivity); } private static AccountStatus DetermineAccountStatus(DebitAccount account, DateTime lastActivity) { if (account.Balance < 0) return AccountStatus.Overdrawn; if (lastActivity < DateTime.UtcNow.AddDays(-90)) return AccountStatus.Dormant; return AccountStatus.Active; } } ``` ## Projection Types [Section titled “Projection Types”](#projection-types) Create projection types for specific views of your data: ```csharp public record AccountListItem(AccountId Id, AccountName Name, decimal Balance); public record AccountSearchResult(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance, double RelevanceScore); [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static IEnumerable GetAccountListItems(IMongoCollection collection) { return collection.Find(_ => true) .Project(a => new AccountListItem(a.Id, a.Name, a.Balance)) .ToEnumerable(); } public static IEnumerable SearchAccountsWithRelevance( string searchTerm, IMongoCollection collection) { // Implement search with relevance scoring var accounts = collection.Find(a => a.Name.Contains(searchTerm)).ToList(); return accounts.Select(a => new AccountSearchResult( a.Id, a.Name, a.Owner, a.Balance, CalculateRelevance(a.Name, searchTerm))); } private static double CalculateRelevance(string accountName, string searchTerm) { // Simple relevance calculation if (accountName.StartsWith(searchTerm, StringComparison.OrdinalIgnoreCase)) return 1.0; if (accountName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)) return 0.8; return 0.5; } } ``` ## Nullable Return Types [Section titled “Nullable Return Types”](#nullable-return-types) Use nullable return types when queries might not find results: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { // Explicitly nullable - might not find account public static DebitAccount? FindAccountById(AccountId id, IMongoCollection collection) { return collection.Find(a => a.Id == id).FirstOrDefault(); } // Non-nullable - should throw if not found public static DebitAccount GetAccountById(AccountId id, IMongoCollection collection) { var account = collection.Find(a => a.Id == id).FirstOrDefault(); return account ?? throw new AccountNotFoundException(id); } } ``` ## Paged Results [Section titled “Paged Results”](#paged-results) For large datasets, return paged results: ```csharp public record PagedResult(IEnumerable Items, int TotalCount, int Page, int PageSize) { public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize); public bool HasNext => Page < TotalPages - 1; public bool HasPrevious => Page > 0; } [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static PagedResult GetPagedAccounts( int page, int pageSize, IMongoCollection collection) { var totalCount = (int)collection.CountDocuments(_ => true); var items = collection.Find(_ => true) .Skip(page * pageSize) .Limit(pageSize) .ToList(); return new PagedResult(items, totalCount, page, pageSize); } } ``` ## Error Responses [Section titled “Error Responses”](#error-responses) Handle errors gracefully and return appropriate types: ```csharp public record QueryResult(T? Data, bool Success, string? ErrorMessage) { public static QueryResult Ok(T data) => new(data, true, null); public static QueryResult Error(string message) => new(default, false, message); } [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static QueryResult TryGetAccountById( AccountId id, IMongoCollection collection, ILogger logger) { try { var account = collection.Find(a => a.Id == id).FirstOrDefault(); return account is not null ? QueryResult.Ok(account) : QueryResult.Error($"Account with ID {id} not found"); } catch (Exception ex) { logger.LogError(ex, "Error retrieving account {AccountId}", id); return QueryResult.Error("An error occurred while retrieving the account"); } } } ``` ## Best Practices [Section titled “Best Practices”](#best-practices) 1. **Use appropriate collection types** - `IEnumerable` for most cases, concrete types when needed 2. **Consider nullability** - Use nullable types when queries might not return results 3. **Use async for I/O** - Wrap return types in `Task` for database operations 4. **Create projection types** - Use specific types for different views of your data 5. **Handle large datasets** - Use paging for queries that might return many results 6. **Don’t mix async and observable** - Use either `Task` or `ISubject`, never both 7. **Use custom types for complex results** - Create dedicated types for rich query responses 8. **Consider performance** - Choose return types that support efficient data access 9. **Be consistent** - Use similar return type patterns across related queries 10. **Document complex return types** - Use XML documentation for non-obvious return structures # Static Methods Model-bound queries are implemented as static methods on your read model records. Understanding the method requirements and patterns is essential for effective query implementation. ## Method Requirements [Section titled “Method Requirements”](#method-requirements) Static methods on your read model record must follow these requirements: * **Must be `public` and `static`** * **Can have any descriptive name** that describes the query operation * **Can take dependencies as parameters** (injected via dependency injection) * **Can be async** by returning `Task` * **Should return appropriate types** - the record itself, collections, or custom result types * **Can be observable** by returning `ISubject` (do not combine with `Task`) ## Basic Static Method Pattern [Section titled “Basic Static Method Pattern”](#basic-static-method-pattern) The simplest query method takes a dependency and returns data: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static IEnumerable GetAllAccounts(IMongoCollection collection) { return collection.Find(_ => true).ToList(); } public static DebitAccount GetAccountById(AccountId id, IMongoCollection collection) { return collection.Find(a => a.Id == id).FirstOrDefault(); } } ``` ## Async Methods [Section titled “Async Methods”](#async-methods) For database operations that support async, return `Task`: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static async Task> GetAllAccountsAsync(IMongoCollection collection) { var result = await collection.FindAsync(_ => true); return result.ToList(); } public static async Task GetAccountByIdAsync(AccountId id, IMongoCollection collection) { var result = await collection.FindAsync(a => a.Id == id); return result.FirstOrDefault(); } } ``` ## Multiple Dependencies [Section titled “Multiple Dependencies”](#multiple-dependencies) Methods can take multiple dependencies from the service collection: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static async Task> SearchAccounts( string searchTerm, IMongoCollection collection, ILogger logger, IConfiguration configuration) { var maxResults = configuration.GetValue("MaxSearchResults", 100); logger.LogInformation("Searching accounts with term: {SearchTerm}", searchTerm); var filter = Builders.Filter.Regex( a => a.Name, new BsonRegularExpression(searchTerm, "i")); var result = await collection.FindAsync(filter); var accounts = result.Limit(maxResults).ToList(); logger.LogInformation("Found {AccountCount} accounts", accounts.Count); return accounts; } } ``` ## Parameter Order Flexibility [Section titled “Parameter Order Flexibility”](#parameter-order-flexibility) Dependencies can be placed in any order - they are resolved by type: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { // Dependencies can come first public static IEnumerable GetAccountsByOwner( IMongoCollection collection, ILogger logger, CustomerId ownerId) { logger.LogInformation("Getting accounts for owner: {OwnerId}", ownerId); return collection.Find(a => a.Owner == ownerId).ToList(); } // Or query parameters can come first public static IEnumerable GetAccountsByBalance( decimal minBalance, bool includeZero, IMongoCollection collection) { var filter = includeZero ? Builders.Filter.Gte(a => a.Balance, minBalance) : Builders.Filter.Gt(a => a.Balance, minBalance); return collection.Find(filter).ToList(); } } ``` ## Generic Dependencies [Section titled “Generic Dependencies”](#generic-dependencies) Methods can use generic dependencies: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static async Task> GetCachedAccounts( IMemoryCache cache, IMongoCollection collection, ILogger logger) { const string cacheKey = "all-accounts"; if (cache.TryGetValue(cacheKey, out IEnumerable? cached)) { logger.LogInformation("Returning cached accounts"); return cached ?? Enumerable.Empty(); } logger.LogInformation("Loading accounts from database"); var accounts = await collection.Find(_ => true).ToListAsync(); cache.Set(cacheKey, accounts, TimeSpan.FromMinutes(5)); return accounts; } } ``` ## Configuration and Options [Section titled “Configuration and Options”](#configuration-and-options) Use `IOptions` or `IConfiguration` for configuration values: ```csharp public class AccountQueryOptions { public int DefaultPageSize { get; set; } = 50; public int MaxPageSize { get; set; } = 200; public TimeSpan CacheExpiry { get; set; } = TimeSpan.FromMinutes(5); } [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static IEnumerable GetPagedAccounts( int page, int? pageSize, IMongoCollection collection, IOptions options) { var opts = options.Value; var actualPageSize = Math.Min(pageSize ?? opts.DefaultPageSize, opts.MaxPageSize); return collection.Find(_ => true) .Skip(page * actualPageSize) .Limit(actualPageSize) .ToList(); } } ``` ## Complex Business Logic [Section titled “Complex Business Logic”](#complex-business-logic) Static methods can implement complex business logic: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static async Task AssessAccountRisk( AccountId accountId, IMongoCollection accountCollection, IMongoCollection transactionCollection, IRiskCalculator riskCalculator, ILogger logger) { logger.LogInformation("Assessing risk for account: {AccountId}", accountId); var account = await accountCollection.Find(a => a.Id == accountId).FirstOrDefaultAsync(); if (account is null) { return new AccountRiskAssessment(accountId, RiskLevel.Unknown, "Account not found"); } var recentTransactions = await transactionCollection .Find(t => t.AccountId == accountId && t.Date > DateTime.UtcNow.AddDays(-30)) .ToListAsync(); var riskScore = riskCalculator.CalculateRisk(account, recentTransactions); logger.LogInformation("Account {AccountId} risk score: {RiskScore}", accountId, riskScore); return new AccountRiskAssessment(accountId, riskScore.Level, riskScore.Reason); } } ``` ## Observable Methods [Section titled “Observable Methods”](#observable-methods) For real-time queries, return `ISubject`: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static ISubject> GetAccountsObservable( IMongoCollection collection) { return collection.Observe(); // MongoDB extension method } public static ISubject GetAccountObservable( AccountId id, IMongoCollection collection) { return collection.ObserveSingle(a => a.Id == id); } } ``` ## Error Handling [Section titled “Error Handling”](#error-handling) Implement proper error handling in your static methods: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { public static async Task GetAccountSafely( AccountId id, IMongoCollection collection, ILogger logger) { try { var result = await collection.FindAsync(a => a.Id == id); return result.FirstOrDefault(); } catch (Exception ex) { logger.LogError(ex, "Error retrieving account {AccountId}", id); return null; } } } ``` ## Method Naming Conventions [Section titled “Method Naming Conventions”](#method-naming-conventions) Use descriptive names that clearly indicate what the method does: ```csharp [ReadModel] public record DebitAccount(AccountId Id, AccountName Name, CustomerId Owner, decimal Balance) { // ✅ Good - descriptive and clear public static IEnumerable GetActiveAccounts(IMongoCollection collection) => collection.Find(a => a.Balance > 0).ToList(); public static IEnumerable SearchAccountsByName(string name, IMongoCollection collection) => collection.Find(a => a.Name.Contains(name)).ToList(); public static AccountSummary GetAccountSummaryByOwner(CustomerId ownerId, IMongoCollection collection) { var accounts = collection.Find(a => a.Owner == ownerId).ToList(); return new AccountSummary(accounts.Count, accounts.Sum(a => a.Balance)); } // ❌ Avoid - too generic or unclear public static IEnumerable Get(IMongoCollection collection) => /* ... */; public static IEnumerable DoSomething(string param, IMongoCollection collection) => /* ... */; } ``` ## Best Practices [Section titled “Best Practices”](#best-practices) 1. **Use descriptive method names** - Make it clear what the query does 2. **Keep methods focused** - Each method should have a single responsibility 3. **Handle dependencies properly** - Order parameters logically (query params first or dependencies first, be consistent) 4. **Use async when appropriate** - For I/O operations that support it 5. **Implement error handling** - Handle exceptions gracefully and log appropriately 6. **Don’t mix async and observable** - Use either `Task` or `ISubject`, not both 7. **Validate inputs** - Check parameters and throw appropriate exceptions for invalid input 8. **Use nullable return types** - When queries might not find results 9. **Leverage dependency injection** - Let the framework resolve your dependencies 10. **Keep business logic in services** - Don’t put complex business logic directly in query methods # Observable Query Demultiplexer The Observable Query Demultiplexer is a composite real-time streaming endpoint that multiplexes multiple observable query subscriptions over a single persistent connection. Rather than creating one WebSocket or SSE connection per query, the demultiplexer provides **two fixed, well-known endpoints** — one for WebSocket and one for Server-Sent Events — that all observable queries in an application route through. ## Endpoints [Section titled “Endpoints”](#endpoints) | Transport | Endpoint | Direction | | ------------------------ | ---------------------- | ------------------------------------------------- | | WebSocket | `/.cratis/queries/ws` | Bidirectional — subscribe, unsubscribe, ping/pong | | Server-Sent Events (SSE) | `/.cratis/queries/sse` | Server → client; one query per connection | Both endpoints are registered automatically by `UseCratisArc()` and require no manual configuration. ## Why a Composite Demultiplexer? [Section titled “Why a Composite Demultiplexer?”](#why-a-composite-demultiplexer) Individual per-query WebSocket endpoints work but come with drawbacks at scale: * Each browser tab opens a separate WebSocket per observable query. * HTTP/1.1 limits the number of concurrent connections per origin. * SSE has the same constraint and typically falls back to polling when the connection limit is reached. The demultiplexer solves this by allowing a single WebSocket to carry updates for many queries simultaneously, and by providing a single, predictable SSE endpoint that clients can connect to for any query. ## Protocol [Section titled “Protocol”](#protocol) All messages exchanged over the demultiplexer share a common envelope: ```json { "type": "", "queryId": "", "payload": { ... }, "timestamp": 1234567890 } ``` | Field | Description | | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `type` | One of the message types listed below. | | `queryId` | Client-assigned identifier that correlates subscriptions with their result updates. Must be unique per subscription within a connection. | | `payload` | Depends on `type` — see the table below. | | `timestamp` | Unix milliseconds. Only populated for `ping` / `pong`. | ### Message Types [Section titled “Message Types”](#message-types) | Type | Direction | Payload | | ------------------ | --------------- | ------------------------------------ | | `subscribe` (0) | Client → Server | `ObservableQuerySubscriptionRequest` | | `unsubscribe` (1) | Client → Server | *(none)* | | `queryResult` (2) | Server → Client | `QueryResult` | | `unauthorized` (3) | Server → Client | *(none)* | | `error` (4) | Server → Client | Error message string | | `ping` (5) | Client → Server | *(timestamp only)* | | `pong` (6) | Server → Client | *(timestamp echoed from ping)* | ### Subscribe Payload — `ObservableQuerySubscriptionRequest` [Section titled “Subscribe Payload — ObservableQuerySubscriptionRequest”](#subscribe-payload--observablequerysubscriptionrequest) ```json { "queryName": "MyApp.Authors.Listing.AllAuthors", "arguments": { "filter": "active" }, "page": 0, "pageSize": 25, "sortBy": "name", "sortDirection": "asc" } ``` | Field | Required | Description | | --------------- | -------- | ------------------------------------------------------------------------------------------------------- | | `queryName` | ✅ | Fully qualified name of the observable query method (e.g. `MyApp.Features.Authors.Listing.AllAuthors`). | | `arguments` | ☐ | Query-string arguments forwarded to the query performer. | | `page` | ☐ | Zero-based page index for paged queries. | | `pageSize` | ☐ | Number of items per page. | | `sortBy` | ☐ | Field name to sort by (case-insensitive). | | `sortDirection` | ☐ | `asc` or `desc`. | ## WebSocket Transport [Section titled “WebSocket Transport”](#websocket-transport) Connect to `/.cratis/queries/ws` and send `subscribe` messages to start receiving updates. ### Subscribe [Section titled “Subscribe”](#subscribe) ```json { "type": 0, "queryId": "authors-list", "payload": { "queryName": "MyApp.Authors.Listing.AllAuthors" } } ``` The server responds with one or more `queryResult` messages whenever the underlying data changes: ```json { "type": 2, "queryId": "authors-list", "payload": { "isSuccess": true, "isAuthorized": true, "data": [ ... ], "validationResults": [] } } ``` ### Unsubscribe [Section titled “Unsubscribe”](#unsubscribe) ```json { "type": 1, "queryId": "authors-list" } ``` ### Keep-alive (Ping / Pong) [Section titled “Keep-alive (Ping / Pong)”](#keep-alive-ping--pong) Send a `ping` with the current Unix timestamp; the server echoes it as a `pong` with the same timestamp for round-trip latency measurement. ```json { "type": 5, "timestamp": 1740000000000 } // Server responds: { "type": 6, "timestamp": 1740000000000 } ``` ### Unauthorized [Section titled “Unauthorized”](#unauthorized) If the current user is not authorized to access the requested query, the server sends an `unauthorized` message and no data stream is established: ```json { "type": 3, "queryId": "authors-list" } ``` ## SSE Transport [Section titled “SSE Transport”](#sse-transport) Connect to `/.cratis/queries/sse` using the `EventSource` API. Pass the fully qualified query name in the `query` query-string parameter. All other query-string parameters are forwarded as query arguments. ```http GET /.cratis/queries/sse?query=MyApp.Authors.Listing.AllAuthors&filter=active ``` The server responds with the standard SSE content type (`text/event-stream`) and streams `data:` frames containing serialized `ObservableQueryHubMessage` envelopes whenever the underlying data changes. Each SSE connection carries a **single query subscription**. To observe multiple queries simultaneously via SSE, open multiple `EventSource` connections — or use the WebSocket transport which multiplexes all subscriptions over one connection. ### Paging and Sorting via SSE [Section titled “Paging and Sorting via SSE”](#paging-and-sorting-via-sse) Pass paging and sorting directly as query-string parameters: ```http GET /.cratis/queries/sse?query=MyApp.Authors.Listing.AllAuthors&page=0&pageSize=20&sortBy=name&sortDirection=asc ``` ## Authorization [Section titled “Authorization”](#authorization) Authorization is enforced for every subscription through the standard query pipeline, including all registered `IQueryFilter` implementations. * If the query performer has an `[Authorize]` attribute and the current user is not authenticated or lacks the required role, the subscription is rejected with an `unauthorized` message. * If the query allows anonymous access (`[AllowAnonymous]`), the subscription is accepted regardless of authentication state. * Authorization is re-evaluated on every new subscription, not cached for the lifetime of the connection. That verdict gates *obtaining* the stream. A subscription can then stay open indefinitely, and nothing ends it when a token expires or a role is revoked. To re-check the verdict while a stream is running, implement an [emission guard](/arc/backend/queries/observable-query-emission-guards/). ## Keep-alive [Section titled “Keep-alive”](#keep-alive) Both WebSocket and SSE transports send automatic keep-alive messages to prevent idle connections from being closed by proxies or firewalls. The server sends a `ping` message only when no other message has been sent within the configured interval. If data is flowing normally (frequent `queryResult` messages), the keep-alive is suppressed — it fires only during periods of inactivity. ### Configuration [Section titled “Configuration”](#configuration) Configure the keep-alive interval in `ArcOptions.Query`: ```csharp builder.Services.Configure(options => { options.Query.KeepAliveInterval = TimeSpan.FromSeconds(30); // default }); ``` Or inline when calling `AddCratisArc()`: ```csharp builder.AddCratisArc(options => { options.Query.KeepAliveInterval = TimeSpan.FromSeconds(45); }); ``` Set `KeepAliveInterval` to `TimeSpan.Zero` or a negative value to disable keep-alive entirely. | Property | Type | Default | Description | | ------------------------- | ---------- | ---------- | ------------------------------------------------------------ | | `Query.KeepAliveInterval` | `TimeSpan` | 30 seconds | How often to send a keep-alive ping when no data is flowing. | ## See also [Section titled “See also”](#see-also) * [Observable Queries (model-bound)](/arc/backend/queries/model-bound/observable-queries/) — How to expose observable queries in the backend. * [Query Pipeline](/arc/backend/queries/query-pipeline/) — How the query pipeline works, including filter hooks. * [Authorization](/arc/backend/core/authorization/) — Role-based authorization for queries and commands. * [Frontend: Observable Query Multiplexing](/arc/frontend/react/queries/observable-query-multiplexing/) — How to configure the frontend to use the hub. # Observable Query Emission Guards Authorization for an observable query runs when the subscription is established. That verdict decides whether the caller may **obtain** the live stream — it says nothing about the minutes or hours the stream then stays open. That is fine for most applications. Subscriptions end when the user navigates away, the tab closes, or the server shuts down. But a subscription has no other reason to end: nothing terminates it when a token expires, a session is signed out, or a role is revoked. Keep-alive pings deliberately stop proxies from culling an idle connection, and the client reconnects and re-subscribes on its own. An **emission guard** closes that gap. Implement `IGuardObservableQueryEmission` and Arc consults it for every emission an observable query is about to write, on every transport — the multiplexed hub and the direct WebSocket/SSE connections alike. ## Writing a guard [Section titled “Writing a guard”](#writing-a-guard) ```csharp public class SessionMustStillBeActive(ISessions sessions) : IGuardObservableQueryEmission { public async Task Guard(ObservableQueryEmissionContext context) { var sessionId = context.Principal?.FindFirst("sid")?.Value; if (sessionId is null) { return ObservableQueryEmissionVerdict.DenyAndTerminate; } return await sessions.IsActive(sessionId) ? ObservableQueryEmissionVerdict.Allow : ObservableQueryEmissionVerdict.DenyAndTerminate; } } ``` That is the whole opt-in. Guards are discovered by convention — no registration, no configuration. An application with no guard pays nothing: no context is built and nothing is dispatched, and emissions take exactly the path they took before. ## The three verdicts [Section titled “The three verdicts”](#the-three-verdicts) | Verdict | What happens | | ------------------ | ---------------------------------------------------------------------------------------------------- | | `Allow` | The emission is written unchanged. | | `Suppress` | This one emission is withheld. The subscription stays live and the next emission is evaluated again. | | `DenyAndTerminate` | Nothing is written, the client is told it is unauthorized, and this subscription is torn down. | `Suppress` does **not** move the delta baseline. The client never saw the withheld emission, so the next delivered `ChangeSet` is still computed against the last state it actually received — nothing goes missing. `DenyAndTerminate` ends only the subscription it was given. Every sibling subscription on the same multiplexed connection keeps streaming. On the hub the client receives an `unauthorized` message for that query id and the subscription is deleted; on a direct connection it receives a final `QueryResult` with `IsAuthorized` false and the stream closes. In both cases the Arc client latches the denial and stops reconnecting — a reconnect would only be denied again, and on a direct SSE connection the browser would otherwise re-establish the stream every few seconds and re-run the whole query for each attempt. ## What the guard is told [Section titled “What the guard is told”](#what-the-guard-is-told) `ObservableQueryEmissionContext` carries the fully qualified query name, the coerced query arguments, the caller’s `ClaimsPrincipal`, the correlation id, whether this is the first emission on the subscription, the subscription’s cancellation token, and the `IServiceProvider` to resolve from. The principal is handed over **explicitly**. Emissions arrive on the producing stream’s own thread, where the request’s `AsyncLocal` context does not flow, so a guard that reached for an ambient accessor would see the wrong identity — or none at all. ## Several guards [Section titled “Several guards”](#several-guards) Every guard is asked, and the most restrictive verdict wins: `DenyAndTerminate` over `Suppress` over `Allow`. The first `DenyAndTerminate` short-circuits the rest. ## Failing closed [Section titled “Failing closed”](#failing-closed) A guard that throws is treated as `DenyAndTerminate`. The failure is logged at `Error` and never rethrown. This is deliberate. A guard that failed open would leave an application believing its stream is protected while it keeps flowing — which is worse than having no guard at all, because nobody goes looking. Know the blast radius before you write one. The aggregator is a process-wide singleton over every discovered guard type, so a single guard that cannot be constructed at all — an open generic, or one whose constructor takes something the subscription scope cannot resolve — denies **every** observable query in the application, on every transport, for as long as the process runs. The only symptom is the `Error` log; clients simply see their subscriptions end as unauthorized. Cover a new guard with a spec that exercises its real constructor. One exception is carved out: if the subscription’s own `CancellationToken` is cancelled while the guard is running, the resulting `OperationCanceledException` is the client going away, not a guard failing. The emission is still withheld and the subscription still ends, but nothing is logged as a failure — otherwise every closed tab would produce an authorization error. ## Cost [Section titled “Cost”](#cost) The guard runs on **every** emission of **every** subscription. Keep it fast: prefer cached state, a local revocation list, or a short-TTL lookup over a network round trip per emission. **The guard instance itself is constructed per emission.** Guards are resolved with `GetServiceOrCreateInstance`, and Arc’s `IFoo → Foo` convention does not match a guard named for what it decides (`SessionMustStillBeActive` implements `IGuardObservableQueryEmission`, not `ISessionMustStillBeActive`), so unless the application registers the type explicitly it is newly constructed — constructor and all — for every emission, and never disposed. Keep the constructor trivial, and put any cache in an injected singleton rather than in a field on the guard, where it would be thrown away and rebuilt each time. Resolve collaborators through the constructor. They come from the subscription’s own scope and are disposed with it, so a guard can safely hold a scoped session store or tenant-aware service. ## Where it does not help [Section titled “Where it does not help”](#where-it-does-not-help) On a WebSocket the principal is frozen at the handshake — the protocol offers no way to present fresh credentials on an established connection. The identity a guard receives on a WebSocket subscription is the one captured when the socket was upgraded, and it does not change for the life of that connection. That does not make the guard useless there; it makes it a lookup rather than a re-read. Take the identity from the context and ask your own source of truth — a session store, a token introspection endpoint, a revocation list — whether that identity is still good. ## See also [Section titled “See also”](#see-also) * [Observable Query Hub](/arc/backend/queries/observable-query-demultiplexer/) — the multiplexed transport and its subscription lifecycle. * [Read Model Interception](/arc/backend/queries/read-model-interception/) — transforming each emitted read model before it is written. * [Authorization](/arc/backend/core/authorization/) — the subscription-time verdict this builds on. # Query health endpoint > Built-in real-time endpoint that exposes the live health of every observable query connection and subscription, with identifiers aligned to the frontend cache. Every running Arc application exposes a live health feed for its observable query subsystem at `/.cratis/queries/health`. The feed is itself an observable query — it pushes a new snapshot every time a connection opens, a subscription changes, or a subscriber disconnects. No polling required. ## Why this endpoint exists [Section titled “Why this endpoint exists”](#why-this-endpoint-exists) When something goes wrong with real-time data — a component freezes, a query never receives its first result, or a user reports stale data — the question is always: *is the issue in the frontend cache, the transport connection, or the backend subscription?* The health endpoint gives you a backend-authoritative answer without attaching a debugger or tailing logs. The query identifiers in the health snapshot use the same fully-qualified format (`{TypeFullName}.{MethodName}`) that the proxy generator writes into the generated TypeScript proxies. You can match a frontend cache entry to a backend subscriber by name alone. ## Subscribing to the feed [Section titled “Subscribing to the feed”](#subscribing-to-the-feed) The endpoint is a standard model-bound observable query. Subscribe to it the same way you subscribe to any other observable query in the frontend: ```typescript import { ObserveHealth } from '/.cratis/queries/QueryHealth'; // generated proxy const [health] = ObserveHealth.use(); // health.data — QueryHealth snapshot, updated in real time ``` The endpoint is anonymous — no authentication is required. ## Response shape [Section titled “Response shape”](#response-shape) A single snapshot has two views of the same subscription state: **connection-centric** (one entry per physical transport connection) and **query-centric** (one entry per distinct query name). Both views are always present in the same snapshot. ### Top-level fields [Section titled “Top-level fields”](#top-level-fields) | Field | Type | Description | | -------------------- | ------------------------------ | ----------------------------------------------------------- | | `connections` | `QueryConnectionHealth[]` | One entry per open transport connection (WebSocket or SSE). | | `totalConnections` | `number` | Total count of open connections. | | `totalSubscriptions` | `number` | Total count of active subscriptions across all connections. | | `querySubscriptions` | `QuerySubscriptionAggregate[]` | Query-centric view — one entry per distinct query name. | ### QueryConnectionHealth [Section titled “QueryConnectionHealth”](#queryconnectionhealth) | Field | Type | Description | | --------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `connectionId` | `string` | Unique connection identifier. WebSocket connections use an incrementing `ws-N` label; SSE connections use a GUID. | | `protocol` | `string` | `WebSocket` or `SSE`. | | `establishedAt` | `Date` | When the connection was opened. | | `subscriptions` | `QuerySubscriptionMetadata[]` | All subscriptions routed through this connection. | ### QuerySubscriptionMetadata [Section titled “QuerySubscriptionMetadata”](#querysubscriptionmetadata) | Field | Type | Description | | -------------------- | ----------------------------- | ----------------------------------------------------------------------------------- | | `subscriptionId` | `string` | The client-generated query ID (`queryId` in the WebSocket protocol). | | `queryIdentifier` | `string` | Fully-qualified query name — matches `queryName` on the generated TypeScript proxy. | | `readModelType` | `string` | Fully-qualified name of the read model type only (without the method). | | `connectedAt` | `Date` | When this subscription was first established. | | `clientInfo` | `QuerySubscriptionClientInfo` | Remote IP, user agent, user identity, and protocol. | | `lastPingSentAt` | `Date?` | Last time a keep-alive ping was sent to this subscriber. | | `lastPongReceivedAt` | `Date?` | Last time a pong was received back. | | `lastDataServedAt` | `Date?` | Last time a data frame was sent to this subscriber. | ### QuerySubscriptionAggregate [Section titled “QuerySubscriptionAggregate”](#querysubscriptionaggregate) The query-centric view groups all physical subscribers for a single query name into one entry. The `queryName` field uses the same identifier format as the frontend proxy’s `queryName` property, making it straightforward to correlate the two sides. | Field | Type | Description | | -------------------- | ------------------- | ---------------------------------------------------------------------------------------- | | `queryName` | `string` | Fully-qualified query name — identical to `queryName` on the generated TypeScript proxy. | | `totalSubscriptions` | `number` | Number of physical subscribers for this query. | | `subscribers` | `QuerySubscriber[]` | One entry per physical connection/subscription pair. | ### QuerySubscriber [Section titled “QuerySubscriber”](#querysubscriber) | Field | Type | Description | | -------------------- | ----------------------------- | --------------------------------------------------- | | `connectionId` | `string` | The parent connection this subscriber belongs to. | | `protocol` | `string` | `WebSocket` or `SSE`. | | `subscriptionId` | `string` | The client-generated subscription identifier. | | `connectedAt` | `Date` | When this subscription was established. | | `clientInfo` | `QuerySubscriptionClientInfo` | Remote IP, user agent, user identity, and protocol. | | `lastPingSentAt` | `Date?` | Last ping sent. | | `lastPongReceivedAt` | `Date?` | Last pong received. | | `lastDataServedAt` | `Date?` | Last data frame sent. | ### QuerySubscriptionClientInfo [Section titled “QuerySubscriptionClientInfo”](#querysubscriptionclientinfo) | Field | Type | Description | | ----------------- | --------- | ------------------------------------ | | `remoteIpAddress` | `string?` | Client IP address. | | `userAgent` | `string?` | Browser or client user-agent string. | | `userId` | `string?` | Authenticated user identity, if any. | | `protocol` | `string` | `WebSocket` or `SSE`. | ## A snapshot in JSON [Section titled “A snapshot in JSON”](#a-snapshot-in-json) ```json { "connections": [ { "connectionId": "ws-1", "protocol": "WebSocket", "establishedAt": "2026-06-10T14:03:00Z", "subscriptions": [ { "subscriptionId": "all-authors-main", "queryIdentifier": "MyApp.Authors.Listing.AllAuthors", "readModelType": "MyApp.Authors.Listing", "connectedAt": "2026-06-10T14:03:01Z", "lastPingSentAt": "2026-06-10T14:04:00Z", "lastPongReceivedAt": "2026-06-10T14:04:00Z", "lastDataServedAt": "2026-06-10T14:03:01Z", "clientInfo": { "protocol": "WebSocket", "remoteIpAddress": "127.0.0.1", "userAgent": "Mozilla/5.0 ...", "userId": "alice@example.com" } } ] } ], "totalConnections": 1, "totalSubscriptions": 1, "querySubscriptions": [ { "queryName": "MyApp.Authors.Listing.AllAuthors", "totalSubscriptions": 1, "subscribers": [ { "connectionId": "ws-1", "protocol": "WebSocket", "subscriptionId": "all-authors-main", "connectedAt": "2026-06-10T14:03:01Z", "lastPingSentAt": "2026-06-10T14:04:00Z", "lastPongReceivedAt": "2026-06-10T14:04:00Z", "lastDataServedAt": "2026-06-10T14:03:01Z", "clientInfo": { "protocol": "WebSocket", "remoteIpAddress": "127.0.0.1", "userAgent": "Mozilla/5.0 ...", "userId": "alice@example.com" } } ] } ] } ``` ## Transport modes [Section titled “Transport modes”](#transport-modes) The health endpoint covers both transport modes equally. When a user opens the application in multiplexed WebSocket mode, their queries appear under a single `ws-N` connection. When they run in direct SSE mode (one connection per query), each query gets its own entry in `connections` with a GUID as its `connectionId`. The `querySubscriptions` array always gives a unified query-level view regardless of which transport the clients are using. That makes it the right starting point when you want to know how many users are subscribed to a specific query — you do not need to filter the `connections` array yourself. ## Cross-stack correlation [Section titled “Cross-stack correlation”](#cross-stack-correlation) The `queryName` in `QuerySubscriptionAggregate` and the `queryIdentifier` in `QuerySubscriptionMetadata` both use the format `{TypeFullName}.{MethodName}` — the same string the proxy generator writes as the `queryName` field on generated TypeScript proxy classes. Because both sides share the same identifier, you can match a frontend cache entry to its backend subscriptions by name. For example, if the frontend diagnostics report that the cache entry for `MyApp.Authors.Listing.AllAuthors` is not subscribed, you can verify in the backend health feed whether a subscription for that name exists at all, which connection carries it, and when data was last served. See [Observable Query Diagnostics](/arc/frontend/react/queries/observable-query-diagnostics/) for how to access the matching frontend diagnostics. ## See also [Section titled “See also”](#see-also) * [Observable Query Demultiplexer](/arc/backend/queries/observable-query-demultiplexer/) — How multiplexed and direct-mode connections work. * [Frontend: Observable Query Diagnostics](/arc/frontend/react/queries/observable-query-diagnostics/) — The frontend diagnostics surface and how to correlate it with this endpoint. # Query Pipeline The query pipeline provides automatic handling of sorting, paging, and advanced query processing through query renderers. This lets query methods stay small while the pipeline handles the cross-cutting query behavior. ## Automatic Sorting and Paging [Section titled “Automatic Sorting and Paging”](#automatic-sorting-and-paging) Arc automatically processes query string parameters for sorting and paging: * `sortby` - Field to sort by * `sortDirection` - `asc` or `desc` * `page` - Page number (0-based) * `pageSize` - Number of items per page ### Query String Examples [Section titled “Query String Examples”](#query-string-examples) ```http GET /api/accounts?sortby=name&sortDirection=asc&page=0&pageSize=50 GET /api/accounts?sortby=balance&sortDirection=desc&page=2&pageSize=25 ``` ### In Controller Actions [Section titled “In Controller Actions”](#in-controller-actions) Your controller actions automatically receive sorting and paging context: ```csharp [Route("api/accounts")] public class Accounts : Controller { readonly IMongoCollection _collection; public Accounts(IMongoCollection collection) => _collection = collection; [HttpGet] public IQueryable GetAccounts() { // Return IQueryable to enable automatic sorting and paging return _collection.AsQueryable(); } } ``` When you return `IQueryable`, the query pipeline automatically: 1. Applies sorting based on the `sortby` and `sortDirection` parameters 2. Applies paging based on the `page` and `pageSize` parameters 3. Wraps the result in a `QueryResult` with paging metadata ## Query Context [Section titled “Query Context”](#query-context) The current query context is available through `IQueryContextManager`: ```csharp public class Accounts : Controller { readonly IMongoCollection _collection; readonly IQueryContextManager _queryContextManager; public Accounts( IMongoCollection collection, IQueryContextManager queryContextManager) { _collection = collection; _queryContextManager = queryContextManager; } [HttpGet("manual")] public QueryResult> GetAccountsManual() { var context = _queryContextManager.Current; var query = _collection.Find(_ => true); // Manual sorting if (context.Sorting != Sorting.None) { query = context.Sorting.Direction == SortDirection.Ascending ? query.SortBy(context.Sorting.Field) : query.SortByDescending(context.Sorting.Field); } // Manual paging var totalItems = (int)query.CountDocuments(); if (context.Paging.IsPaged) { query = query.Skip(context.Paging.Skip).Limit(context.Paging.Size); } var data = query.ToList(); return new QueryResult> { Data = data, Paging = new PagingInfo( context.Paging.Page, context.Paging.Size, totalItems) }; } } ``` ## Query Renderers [Section titled “Query Renderers”](#query-renderers) Query renderers provide a way to implement custom processing for specific data types. They implement the `IQueryRendererFor` interface: ```csharp public class DebitAccountQueryRenderer : IQueryRendererFor> { public QueryRendererResult Execute(IQueryable query, QueryContext queryContext) { var totalItems = query.Count(); // Apply custom business logic query = query.Where(account => account.Balance >= 0); // Only show non-negative balances // Apply sorting if (queryContext.Sorting != Sorting.None) { query = queryContext.Sorting.Field.ToLowerInvariant() switch { "name" => ApplySorting(query, a => a.Name.ToString(), queryContext.Sorting.Direction), "balance" => ApplySorting(query, a => a.Balance, queryContext.Sorting.Direction), "owner" => ApplySorting(query, a => a.Owner.ToString(), queryContext.Sorting.Direction), _ => query }; } // Apply paging if (queryContext.Paging.IsPaged) { query = query.Skip(queryContext.Paging.Skip).Take(queryContext.Paging.Size); } return new QueryRendererResult(totalItems, query.ToList()); } static IQueryable ApplySorting( IQueryable query, Expression> keySelector, SortDirection direction) { return direction == SortDirection.Ascending ? query.OrderBy(keySelector) : query.OrderByDescending(keySelector); } } ``` ### Built-in Renderers [Section titled “Built-in Renderers”](#built-in-renderers) Arc includes built-in query renderers: #### QueryableQueryRenderer [Section titled “QueryableQueryRenderer”](#queryablequeryrenderer) Automatically handles `IQueryable` return types: ```csharp [HttpGet] public IQueryable GetAccountsQueryable() { return _collection.AsQueryable(); } ``` This automatically gets: * Sorting by any field * Paging with proper metadata * Optimized database queries ## MongoDB Extensions [Section titled “MongoDB Extensions”](#mongodb-extensions) Arc provides MongoDB-specific extensions for observable queries: ### Observe() Extension [Section titled “Observe() Extension”](#observe-extension) The `.Observe()` extension method on `IMongoCollection` automatically handles: * Initial data loading * Change stream monitoring * Sorting and filtering * Client disconnection cleanup ```csharp [HttpGet("observable")] public ISubject> GetAccountsObservable() { // Automatic sorting and filtering based on query context return _collection.Observe(); } [HttpGet("observable-filtered")] public ISubject> GetActiveAccountsObservable() { return _collection.Observe(account => account.Balance > 0); } ``` ### Advanced MongoDB Observe [Section titled “Advanced MongoDB Observe”](#advanced-mongodb-observe) ```csharp [HttpGet("observable-advanced")] public ISubject> GetAccountsObservableAdvanced() { var filter = Builders.Filter.And( Builders.Filter.Gt(a => a.Balance, 0), Builders.Filter.Lt(a => a.Balance, 100000) ); return _collection.Observe(filter); } ``` ## Custom Query Providers [Section titled “Custom Query Providers”](#custom-query-providers) For complex scenarios, you can create custom query providers that implement `IQueryRendererFor`: ```csharp public class AccountSummaryRenderer : IQueryRendererFor { readonly IMongoCollection _collection; public AccountSummaryRenderer(IMongoCollection collection) { _collection = collection; } public QueryRendererResult Execute(AccountSummary query, QueryContext queryContext) { // Custom aggregation logic var pipeline = new BsonDocument[] { new("$group", new BsonDocument { { "_id", BsonNull.Value }, { "totalAccounts", new BsonDocument("$sum", 1) }, { "totalBalance", new BsonDocument("$sum", "$balance") }, { "averageBalance", new BsonDocument("$avg", "$balance") } }) }; var result = _collection.Aggregate(pipeline).FirstOrDefault(); if (result is not null) { var summary = new AccountSummary( result["totalAccounts"].AsInt32, result["totalBalance"].AsDecimal(), result["averageBalance"].AsDecimal() ); return new QueryRendererResult(1, summary); } return new QueryRendererResult(0, null); } } ``` ## Query Filters [Section titled “Query Filters”](#query-filters) Query filters execute before query renderers and can perform validation, authorization, logging, and other cross-cutting concerns. They implement the `IQueryFilter` interface: ```csharp public class AccountSecurityFilter : IQueryFilter { readonly ICurrentUser _currentUser; public AccountSecurityFilter(ICurrentUser currentUser) { _currentUser = currentUser; } public async Task OnPerform(QueryContext context) { // Check if user has permission to execute this query if (!await _currentUser.HasPermissionAsync("accounts.read")) { return QueryResult.Unauthorized(context.CorrelationId); } return QueryResult.Success(context.CorrelationId); } } ``` ### Built-in Query Filters [Section titled “Built-in Query Filters”](#built-in-query-filters) Arc includes several built-in query filters that provide essential functionality: | Filter | Description | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `DataAnnotationValidationFilter` | Validates query parameters using data annotations (e.g., `[Required]`, `[Range]`, etc.) applied to query properties | | `FluentValidationFilter` | Validates queries using FluentValidation validators, supporting complex validation scenarios | | `AuthorizationFilter` | Provides authorization for queries using `[Authorize]` and `[Roles]` attributes | #### DataAnnotation Validation Filter [Section titled “DataAnnotation Validation Filter”](#dataannotation-validation-filter) Automatically validates query parameters using DataAnnotations attributes: ```csharp public class GetAccountByIdQuery { [Required] [Range(1, int.MaxValue)] public int Id { get; set; } [MaxLength(100)] public string? Filter { get; set; } } [HttpGet("{id}")] public Task GetAccountById([FromQuery] GetAccountByIdQuery query) { // Validation happens automatically via DataAnnotationValidationFilter return _collection.Find(a => a.Id == query.Id).FirstOrDefaultAsync(); } ``` #### FluentValidation Filter [Section titled “FluentValidation Filter”](#fluentvalidation-filter) For complex validation scenarios using FluentValidation: ```csharp public class GetAccountByIdQueryValidator : AbstractValidator { public GetAccountByIdQueryValidator() { RuleFor(x => x.Id) .GreaterThan(0) .WithMessage("Account ID must be greater than 0"); RuleFor(x => x.Filter) .MaximumLength(50) .When(x => !string.IsNullOrEmpty(x.Filter)) .WithMessage("Filter cannot exceed 50 characters"); } } ``` #### Authorization Filter [Section titled “Authorization Filter”](#authorization-filter) This filter provides query-level authorization using ASP.NET Core authorization attributes. You can use the standard `[Authorize]` attribute: ```csharp [HttpGet("secure-accounts")] [Authorize(Roles = "Admin,Manager")] public Task> GetSecureAccounts() { return _collection.Find(_ => true).ToListAsync(); } ``` Or the convenience `[Roles]` attribute provided by Cratis Arc: ```csharp [HttpGet("admin-accounts")] [Roles("Admin")] public Task> GetAdminAccounts() { return _collection.Find(_ => true).ToListAsync(); } [HttpGet("manager-accounts")] [Roles("Manager", "TeamLead")] // User needs any one of these roles public Task> GetManagerAccounts() { return _collection.Find(_ => true).ToListAsync(); } ``` The authorization filter automatically checks: * User authentication * Required roles (if specified) * Returns `QueryResult.Unauthorized` if authorization fails ### Custom Query Filters [Section titled “Custom Query Filters”](#custom-query-filters) You can create custom filters for cross-cutting concerns: ```csharp public class QueryLoggingFilter : IQueryFilter { readonly ILogger _logger; public QueryLoggingFilter(ILogger logger) { _logger = logger; } public Task OnPerform(QueryContext context) { _logger.LogInformation("Executing query {QueryName} with correlation {CorrelationId}", context.Name, context.CorrelationId); return Task.FromResult(QueryResult.Success(context.CorrelationId)); } } ``` ### Namespace-Based Authorization Filters [Section titled “Namespace-Based Authorization Filters”](#namespace-based-authorization-filters) For cross-cutting authorization, apply one `IQueryFilter` to an entire namespace: ```csharp using Cratis.Arc.Http; using Cratis.Arc.Queries; namespace MyApp.Features.Security; public class NamespaceAuthorizationQueryFilter(IHttpRequestContextAccessor requestContextAccessor) : IQueryFilter { const string ProtectedNamespace = "MyApp.Features.Payments"; const string RequiredRole = "Payments"; public Task OnPerform(QueryContext context) { var isProtectedQuery = context.Name.Value.StartsWith(ProtectedNamespace, StringComparison.Ordinal); if (!isProtectedQuery) { return Task.FromResult(QueryResult.Success(context.CorrelationId)); } var hasRole = requestContextAccessor.Current?.User.IsInRole(RequiredRole) ?? false; return Task.FromResult( hasRole ? QueryResult.Success(context.CorrelationId) : QueryResult.Unauthorized(context.CorrelationId)); } } ``` This lets you keep query methods clean while still enforcing authorization consistently across an entire feature area. All filters are automatically discovered and executed by the query pipeline. They run in registration order, and if any filter returns an unsuccessful result, the query execution stops. ## Query Result Metadata [Section titled “Query Result Metadata”](#query-result-metadata) All queries automatically include metadata in the response: ```json { "data": [...], "paging": { "page": 0, "pageSize": 50, "totalItems": 1337, "hasPrevious": false, "hasNext": true }, "correlationId": "12345678-1234-1234-1234-123456789012", "isSuccess": true, "isAuthorized": true, "isValid": true, "hasExceptions": false, "validationResults": [], "exceptionMessages": [], "exceptionStackTrace": "" } ``` ## Performance Considerations [Section titled “Performance Considerations”](#performance-considerations) ### Efficient Sorting [Section titled “Efficient Sorting”](#efficient-sorting) Use indexed fields for sorting to ensure good performance: ```csharp // Good - if 'name' is indexed GET /api/accounts?sortby=name&sortDirection=asc // Potentially slow - if 'balance' is not indexed GET /api/accounts?sortby=balance&sortDirection=desc ``` ### Efficient Paging [Section titled “Efficient Paging”](#efficient-paging) Use reasonable page sizes to balance performance and user experience: ```csharp // Good GET /api/accounts?page=0&pageSize=50 // Potentially problematic GET /api/accounts?page=0&pageSize=10000 ``` ### Query Optimization [Section titled “Query Optimization”](#query-optimization) Return `IQueryable` when possible to enable database-level optimizations: ```csharp // Good - enables database-level sorting and paging [HttpGet] public IQueryable GetAccounts() { return _collection.AsQueryable(); } // Less efficient - loads all data into memory first [HttpGet] public IEnumerable GetAccountsList() { return _collection.Find(_ => true).ToList(); } ``` ## Best Practices [Section titled “Best Practices”](#best-practices) 1. **Return `IQueryable`** when possible for automatic sorting and paging 2. **Use indexed fields** for sorting to ensure good performance 3. **Implement custom renderers** for complex business logic 4. **Keep page sizes reasonable** (typically 10-100 items) 5. **Use query filters** for cross-cutting concerns like security 6. **Monitor query performance** and optimize slow queries 7. **Test sorting and paging** with realistic data volumes # Read Model Interception Read model interception lets you apply cross-cutting operations to every read model instance before it is served to a client. Common uses include decryption, field masking, localization, and audit enrichment. Interceptors run automatically for all query types — controller-based, model-bound, and observable (WebSocket and SSE). ## How It Works [Section titled “How It Works”](#how-it-works) When a query returns, the framework passes each read model instance through every registered interceptor for that type before serializing the response. For collections every item is intercepted individually. For observable queries the interception happens on every emission. ``` InterceptorFrameworkClientInterceptorFrameworkClientloop[For each item]GET /api/accountsExecute queryIntercept(item)intercepted itemQueryResult with intercepted data ``` ## Implementing an Interceptor [Section titled “Implementing an Interceptor”](#implementing-an-interceptor) Implement `IInterceptReadModel`. No DI registration is required — the framework discovers all implementations in your assemblies automatically via `ITypes` and creates instances on demand, resolving any constructor dependencies from the service provider. ```csharp public class DecryptAccountNumbers : IInterceptReadModel { readonly IEncryptionService _encryption; public DecryptAccountNumbers(IEncryptionService encryption) { _encryption = encryption; } public Task Intercept(AccountSummary readModel) { var decrypted = readModel with { AccountNumber = _encryption.Decrypt(readModel.AccountNumber) }; return Task.FromResult(decrypted); } } ``` > **Note:** The `Intercept` method returns the read model to serve. Read models are typically immutable records, so create a modified copy with a `with` expression and return it rather than mutating the original in place. Arc serves the instance you return. ## Multiple Interceptors [Section titled “Multiple Interceptors”](#multiple-interceptors) You can register any number of interceptors for the same read model type. They run in the order they are discovered. ```csharp public class MaskSensitiveFields : IInterceptReadModel { public Task Intercept(AccountSummary readModel) { var masked = readModel with { AccountNumber = $"****{readModel.AccountNumber[^4..]}" }; return Task.FromResult(masked); } } public class EnrichWithLocale : IInterceptReadModel { readonly ILocalizationService _locale; public EnrichWithLocale(ILocalizationService locale) { _locale = locale; } public Task Intercept(AccountSummary readModel) { var localized = readModel with { FormattedBalance = _locale.FormatCurrency(readModel.Balance) }; return Task.FromResult(localized); } } ``` Both interceptors run for every `AccountSummary` returned by any query. ## Observable Queries [Section titled “Observable Queries”](#observable-queries) Interceptors apply equally to observable (real-time) queries. Each time the observable emits new data, every item passes through the registered interceptors before the payload is sent to the client. ```csharp // No changes needed in your observable query — interception is automatic. [HttpGet("observable")] public ISubject> GetAccountSummaries() { return _collection.Observe(); } ``` ## Type Safety [Section titled “Type Safety”](#type-safety) An interceptor is bound to exactly one read model type through the generic parameter. An interceptor for `AccountSummary` never runs for `TransactionHistory`, even if both are returned by different queries in the same request. # Use Observable Queries with cURL This guide shows you how to work with observable query endpoints by using plain HTTP tools such as `curl`. Use this when you want to: * confirm the current snapshot for an observable query * wait for the first payload before the request returns * follow a live Server-Sent Events (SSE) stream * emulate long polling with repeated HTTP requests The same approach works for both **model-bound** and **controller-based** observable query endpoints. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * You know the observable query URL you want to call. * Your application is running. * The endpoint already returns an observable query result (`ISubject`). ## Get the current snapshot [Section titled “Get the current snapshot”](#get-the-current-snapshot) A normal `GET` request returns the current observable snapshot as JSON. ```bash curl "https://localhost:5001/api/orders/observe-all" ``` Use this when the observable already has a current value and you only want the latest snapshot once. ## Wait for the first payload [Section titled “Wait for the first payload”](#wait-for-the-first-payload) If the observable does not have a current value yet, add `waitForFirstResult=true`. ```bash curl "https://localhost:5001/api/orders/observe-all?waitForFirstResult=true" ``` The request stays open until the observable produces its first payload or the timeout expires. ### Override the timeout [Section titled “Override the timeout”](#override-the-timeout) The default timeout is 30 seconds. Override it with `waitForFirstResultTimeout`, expressed in seconds. ```bash curl "https://localhost:5001/api/orders/observe-all?waitForFirstResult=true&waitForFirstResultTimeout=10" ``` If the timeout expires, Arc returns an HTTP timeout response with a JSON error payload. ## Stream updates over SSE [Section titled “Stream updates over SSE”](#stream-updates-over-sse) To keep the connection open and watch updates continuously, request the endpoint as Server-Sent Events. ```bash curl --no-buffer \ -H "Accept: text/event-stream" \ "https://localhost:5001/api/orders/observe-all" ``` Each update is sent as an SSE `data:` frame that contains a serialized `QueryResult`. Example output: ```text data: {"isSuccess":true,"data":[{"id":"...","status":"ready"}],"changeSet":null} data: {"isSuccess":true,"data":[{"id":"...","status":"shipped"}],"changeSet":null} ``` Use this when you want a live stream instead of a single JSON response. ## Emulate long polling [Section titled “Emulate long polling”](#emulate-long-polling) If you want repeated snapshot requests instead of a continuous stream, call the endpoint in a loop and wait for the first payload each time. ```bash while true; do curl --silent \ "https://localhost:5001/api/orders/observe-all?waitForFirstResult=true&waitForFirstResultTimeout=15" echo done ``` This is effectively **long polling**: * each request waits until data is available or the timeout expires * the server returns a normal JSON payload * the client immediately opens a new request Use this when SSE is not convenient and you still want blocking snapshot requests from plain HTTP tooling. ## Pick the right mode [Section titled “Pick the right mode”](#pick-the-right-mode) | Goal | Request style | | ----------------------------------- | ----------------------------------------------------------------- | | Get the latest snapshot right now | `GET /query` | | Wait until the first payload exists | `GET /query?waitForFirstResult=true` | | Wait with a custom timeout | `GET /query?waitForFirstResult=true&waitForFirstResultTimeout=10` | | Follow live updates continuously | `GET /query` with `Accept: text/event-stream` | | Repeated blocking snapshot requests | Long-poll loop with `waitForFirstResult=true` | ## See also [Section titled “See also”](#see-also) * [Model-bound observable queries](/arc/backend/queries/model-bound/observable-queries/) * [Controller-based observable queries](/arc/backend/queries/controller-based/observable-queries/) * [Observable Query Demultiplexer](/arc/backend/queries/observable-query-demultiplexer/) # Use the HTTP QUERY method By default Arc exposes every query over `GET`, with arguments in the URL query string. That is perfect until a query needs to carry **a lot** of arguments, or **sensitive** ones — a long free-text search, a nested filter, an access token. URLs have length limits, and everything in a URL leaks into server and proxy access logs, browser history, and `Referer` headers. The HTTP `QUERY` method ([RFC 10008](https://www.rfc-editor.org/info/rfc10008/)) solves this: it is a safe, idempotent request — like `GET` — but it carries its arguments in a JSON **request body** instead of the URL. Arc registers **both** verbs for every query endpoint, so `QUERY` is available whenever you want it. `GET` stays the default; you opt into `QUERY` on the client. Use this when you want to: * send query arguments that are too large for a URL * keep sensitive arguments out of URLs, logs, and history * move a growing filter object into a structured body ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Your application is running and exposes at least one query. * You are calling the query from the generated TypeScript proxy, or from a plain HTTP tool. ## Declare the transport in C\# [Section titled “Declare the transport in C#”](#declare-the-transport-in-c) The most direct way is to declare the transport where the query lives — the same place you already put `[Route]` or `[AllowAnonymous]`. Put `[QueryHttpMethod]` on a read model (all its queries) or a single static query method, and the generated proxy defaults to that transport with no client wiring: ```csharp [ReadModel] public record Order(OrderId Id, string Customer) { [QueryHttpMethod(QueryHttpMethod.Query)] public static IEnumerable Search(OrderFilter filter) => /* ... */; } ``` The generated `Search` proxy calls `setHttpMethod(QueryHttpMethod.Query)` in its constructor, so every caller uses QUERY automatically — the backend author’s knowledge that this query takes a large filter flows to the client through proxy generation. A method-level attribute overrides a read-model-level one, callers can still override it at runtime with `setHttpMethod`, and the server accepts both verbs regardless — this only sets the client default. ## Opt in from the client [Section titled “Opt in from the client”](#opt-in-from-the-client) The generated proxies default to `GET`. Switch the default for every query by setting `Globals.queryHttpMethod`: ```typescript import { Globals } from '@cratis/arc'; import { QueryHttpMethod } from '@cratis/arc/queries'; Globals.queryHttpMethod = QueryHttpMethod.Query; ``` To switch a single query without changing the global default, call `setHttpMethod` on the query instance: ```typescript query.setHttpMethod(QueryHttpMethod.Query); ``` Everything else — arguments, paging, sorting, the shape of the result — stays exactly the same. Only the transport changes. ## Let the framework choose with `Auto` [Section titled “Let the framework choose with Auto”](#let-the-framework-choose-with-auto) If you’re not sure every deployment’s network path supports `QUERY` (some corporate proxies, WAFs and gateways don’t recognize it yet), use `QueryHttpMethod.Auto`: ```typescript import { Globals } from '@cratis/arc'; import { QueryHttpMethod } from '@cratis/arc/queries'; Globals.queryHttpMethod = QueryHttpMethod.Auto; ``` Arc sends `QUERY` on the first query. If the server or an intermediary rejects the verb — a `405`/`501` response, or a network/CORS error from `fetch` — it transparently retries the query with `GET` and remembers the outcome **per backend** (origin + API base path) for the rest of the session, so subsequent queries go straight to the working transport and one backend’s lack of support never downgrades another. This also covers the cross-origin case: if the CORS policy doesn’t allow `QUERY`, `Auto` simply settles on `GET`. `Auto` only falls back on **transport-level** failures — an application error (a normal failed `QueryResult`) is never retried as `GET`. Call `resetQueryHttpMethodResolution()` (from `@cratis/arc/queries`) to make the next `Auto` query probe again, for example after a network change. ## Choose the transport per query [Section titled “Choose the transport per query”](#choose-the-transport-per-query) Most queries have small arguments that belong in the URL — only the ones whose arguments overflow it really need `QUERY`. Instead of picking a method for the whole app, set a **resolver** that decides per query. The built-in `lengthBasedQueryHttpMethod` keeps short queries on cacheable `GET` and prefers `QUERY` only when the `GET` URL would exceed a threshold: ```typescript import { Globals } from '@cratis/arc'; import { lengthBasedQueryHttpMethod } from '@cratis/arc/queries'; Globals.queryHttpMethodResolver = lengthBasedQueryHttpMethod({ threshold: 2000 }); ``` When the URL is short the query uses `GET`; when it exceeds the threshold it uses `QUERY` (with `Auto`’s `GET` fallback, so an unsupporting backend still degrades gracefully). The resolver is consulted only when a query has **no** explicit method set via `setHttpMethod` — an explicit per-query choice always wins. You can also write your own policy; it receives the built `GET` URL, the route and the arguments: ```typescript import { QueryHttpMethod } from '@cratis/arc/queries'; Globals.queryHttpMethodResolver = ({ route }) => route.startsWith('/api/reports') ? QueryHttpMethod.Query : QueryHttpMethod.Get; ``` ## The request body [Section titled “The request body”](#the-request-body) With `QUERY`, route parameters stay in the path (they identify the resource); every other argument, plus paging and sorting, moves into a JSON body: ```json { "arguments": { "searchText": "a very long search expression", "status": "active" }, "paging": { "page": 0, "pageSize": 20 }, "sorting": { "field": "name", "direction": "asc" } } ``` `paging` and `sorting` are optional — omit them for an unpaged, unsorted query. ## Call it with cURL [Section titled “Call it with cURL”](#call-it-with-curl) You can exercise a `QUERY` endpoint with any HTTP client. The `Content-Type` must be `application/json` — Arc rejects a request without it. ```bash curl -X QUERY "https://localhost:5001/api/orders/search" \ -H "Content-Type: application/json" \ -d '{ "arguments": { "searchText": "widgets" }, "paging": { "page": 0, "pageSize": 20 } }' ``` The response is the same `QueryResult` JSON you get from the `GET` form of the query. ## Cross-origin calls need CORS to allow QUERY [Section titled “Cross-origin calls need CORS to allow QUERY”](#cross-origin-calls-need-cors-to-allow-query) `QUERY` is not a [simple method](https://developer.mozilla.org/docs/Web/HTTP/CORS#simple_requests), so a browser sends a preflight `OPTIONS` request first. If you call queries from another origin, add `QUERY` to your allowed methods: ```csharp builder.Services.AddCors(options => options.AddDefaultPolicy(policy => policy.WithMethods("GET", "POST", "QUERY").AllowAnyHeader().AllowAnyOrigin())); ``` The `GET` default needs no CORS change, so this only matters once you opt into `QUERY`. ## Turn it off on the server [Section titled “Turn it off on the server”](#turn-it-off-on-the-server) `QUERY` endpoints are registered by default. To restrict query endpoints to `GET` only — for example behind infrastructure that rejects unknown HTTP verbs — disable it: ```csharp builder.Services.Configure(options => options.GeneratedApis.EnableQueryHttpMethod = false); ``` `GET` is unaffected. ## See also [Section titled “See also”](#see-also) * [Use Observable Queries with cURL](/arc/backend/queries/using-observable-queries-with-curl/) * [Configuration](/arc/backend/configuration/) * [Query Pipeline](/arc/backend/queries/query-pipeline/) # Query Validation Query parameters can be validated using Arc’s validation infrastructure. The validation happens in the query pipeline through validation filters before query performers are executed. > **💡 Client-Side Validation**: When using FluentValidation, validation rules are automatically extracted by the [ProxyGenerator](/arc/backend/proxy-generation/validation/) and run on the client before server calls. This provides immediate feedback to users and reduces unnecessary server requests. ## Validation Filters [Section titled “Validation Filters”](#validation-filters) Arc provides two validation filters that automatically validate query parameters: ### DataAnnotationValidationFilter [Section titled “DataAnnotationValidationFilter”](#dataannotationvalidationfilter) Automatically validates query parameters using System.ComponentModel.DataAnnotations attributes: ```csharp // Query performer method [ReadModel] public class Accounts { public static IEnumerable SearchAccounts( [Required][StringLength(50)] string name, [Range(0, double.MaxValue)] decimal? minBalance, [Range(0, double.MaxValue)] decimal? maxBalance, IMongoCollection collection) { // Validation happens automatically in the query pipeline // This method only executes if validation passes var filterBuilder = Builders.Filter; var filters = new List>(); filters.Add(filterBuilder.Regex(a => a.Name, new BsonRegularExpression(name, "i"))); if (minBalance.HasValue) filters.Add(filterBuilder.Gte(a => a.Balance, minBalance.Value)); if (maxBalance.HasValue) filters.Add(filterBuilder.Lte(a => a.Balance, maxBalance.Value)); var combinedFilter = filterBuilder.And(filters); return collection.Find(combinedFilter).ToList(); } } ``` ### Concepts [Section titled “Concepts”](#concepts) A `ConceptValidator` describes whether a value of that concept is well formed. Because that is a property of the type rather than of the operation carrying it, the validator applies wherever the concept appears — on a command property, on a query argument, and at any depth inside either. Declare it once: ```csharp public record EmailAddress(string Value) : ConceptAs(Value) { public static implicit operator string(EmailAddress address) => address.Value; public static implicit operator EmailAddress(string value) => new(value); } public class EmailAddressValidator : ConceptValidator { public EmailAddressValidator() => RuleFor(x => x.Value).EmailAddress().WithMessage("Must be a valid email address"); } ``` Every query taking an `EmailAddress` now rejects a malformed one, with no per-query rule to write or keep in sync. The rule is also carried into the generated client, so the browser rejects it too — see [Proxy Generation Validation](/arc/backend/proxy-generation/validation/). ### FluentValidationFilter [Section titled “FluentValidationFilter”](#fluentvalidationfilter) Automatically validates query parameters using FluentValidation validators. Create validators by inheriting from `QueryValidator`: ```csharp // Define a concept for the query parameter type public record AccountSearchParams(string Name, decimal? MinBalance, decimal? MaxBalance); // Create a validator for the parameter type public class AccountSearchParamsValidator : QueryValidator { public AccountSearchParamsValidator() { RuleFor(x => x.Name) .NotEmpty() .MaximumLength(50) .WithMessage("Account name is required and must be less than 50 characters"); RuleFor(x => x.MinBalance) .GreaterThanOrEqualTo(0) .When(x => x.MinBalance.HasValue) .WithMessage("Minimum balance must be greater than or equal to 0"); RuleFor(x => x.MaxBalance) .GreaterThanOrEqualTo(0) .When(x => x.MaxBalance.HasValue) .WithMessage("Maximum balance must be greater than or equal to 0"); RuleFor(x => x.MinBalance) .LessThanOrEqualTo(x => x.MaxBalance) .When(x => x.MinBalance.HasValue && x.MaxBalance.HasValue) .WithMessage("Minimum balance must be less than or equal to maximum balance"); } } // Use the validated parameter type in query performer [ReadModel] public class Accounts { public static IEnumerable SearchAccountsWithValidation( AccountSearchParams searchParams, IMongoCollection collection) { // FluentValidation happens automatically in the query pipeline var filterBuilder = Builders.Filter; var filters = new List>(); if (!string.IsNullOrEmpty(searchParams.Name)) filters.Add(filterBuilder.Regex(a => a.Name, new BsonRegularExpression(searchParams.Name, "i"))); if (searchParams.MinBalance.HasValue) filters.Add(filterBuilder.Gte(a => a.Balance, searchParams.MinBalance.Value)); if (searchParams.MaxBalance.HasValue) filters.Add(filterBuilder.Lte(a => a.Balance, searchParams.MaxBalance.Value)); var combinedFilter = filters.Any() ? filterBuilder.And(filters) : filterBuilder.Empty; return collection.Find(combinedFilter).ToList(); } } ``` ## How Validation Filters Work [Section titled “How Validation Filters Work”](#how-validation-filters-work) The validation filters operate in the query pipeline: 1. **Parameter Discovery**: Filters use `IQueryPerformerProviders` to discover query parameters and their types 2. **Argument Coercion**: Raw arguments are converted to their declared parameter types, so a validator always sees a real `AccountId` rather than the string it arrived as 3. **Validation**: The arguments are validated — either as a whole, or one at a time (see below) 4. **Validation Results**: Failed validations return a `QueryResult` with validation errors 5. **Pipeline Continuation**: Only successful validations allow the query performer to execute Validation walks the whole object graph of each argument, exactly as command validation walks a command’s properties. A validator is found and run for the argument itself, for anything nested inside it, and for every element of a collection — so a rule never silently fails to apply just because the value it guards sits a level down. ## Whole Argument Set vs. Individual Arguments [Section titled “Whole Argument Set vs. Individual Arguments”](#whole-argument-set-vs-individual-arguments) A query’s arguments are flat, which means there are two ways to describe rules over them. **Individual arguments** is the default. Each argument is validated on its own, and a failing rule reports the argument it belongs to. A rule reported by a validator for a nested value is prefixed with the path to the argument that carried it, so it is attributable to the field the caller supplied. Members are reported the way the client names them: camelCased, and without a concept’s inner member. A `ConceptValidator` declares its rules against the concept’s `Value`, but a concept is a single value — a failure on an `email` argument is reported as `email`, not `Email` or `email.Value`. This is what lets a form match a server rejection to the field that caused it. **The whole argument set** is used when you want rules that span several arguments. Declare a type whose properties mirror the query’s parameters, named `{QueryName}Parameters` or `{ReadModelName}{QueryName}Parameters`, and a `QueryValidator` for it: ```csharp [ReadModel] public class Accounts { public static IEnumerable GetByEmailAndAge(string email, int minAge, IMongoCollection collection) => collection.Find(a => a.Owner.Email == email && a.Owner.Age >= minAge).ToList(); } public class GetByEmailAndAgeParameters { public string Email { get; set; } = string.Empty; public int MinAge { get; set; } } public class GetByEmailAndAgeValidator : QueryValidator { public GetByEmailAndAgeValidator() { RuleFor(x => x.Email).NotEmpty().EmailAddress(); RuleFor(x => x.MinAge).GreaterThanOrEqualTo(0); } } ``` The parameters type is the server-side twin of the arguments object the generated proxy validates, so the same rules run in the browser and at the endpoint, reporting the same member names. Rules declared this way are enforced server-side whether or not the caller went through the proxy — a validator that only ran in the browser would be no validator at all, since the endpoint can be called directly. A type is only accepted as the argument set when it has a property of matching name **and** type for every one of the query’s parameters, so an unrelated type that happens to carry the name is never picked up. Injected dependencies — the `IMongoCollection` above, and any other service the method takes — are ignored: only the parameters the caller actually supplies count. A query that takes no arguments never resolves an argument set. When a query has an argument set, it is validated through that alone, so a failure is never reported twice. If the type does not match, validation falls back to each argument on its own rather than failing. ## Controller-Based Query Validation [Section titled “Controller-Based Query Validation”](#controller-based-query-validation) For controller-based queries (using `[HttpGet]` endpoints), validation works with the standard ASP.NET Core model validation: ```csharp public record AccountSearchQuery( [Required] [StringLength(50)] string Name, [Range(0, double.MaxValue)] decimal? MinBalance, [Range(0, double.MaxValue)] decimal? MaxBalance); [HttpGet("search")] public IEnumerable SearchAccounts([FromQuery] AccountSearchQuery query) { // If validation fails, a 400 Bad Request is returned automatically // This code only executes if validation passes var filterBuilder = Builders.Filter; var filters = new List>(); filters.Add(filterBuilder.Regex(a => a.Name, new BsonRegularExpression(query.Name, "i"))); if (query.MinBalance.HasValue) filters.Add(filterBuilder.Gte(a => a.Balance, query.MinBalance.Value)); if (query.MaxBalance.HasValue) filters.Add(filterBuilder.Lte(a => a.Balance, query.MaxBalance.Value)); var combinedFilter = filterBuilder.And(filters); return _collection.Find(combinedFilter).ToList(); } ``` ## Standard Data Annotations [Section titled “Standard Data Annotations”](#standard-data-annotations) Arc supports all standard validation attributes: ```csharp public record ProductSearchQuery( [Required] [StringLength(100, MinimumLength = 3)] string Name, [Range(0.01, 999999.99)] decimal? MinPrice, [Range(0.01, 999999.99)] decimal? MaxPrice, [RegularExpression(@"^[A-Z]{2,4}$")] string? Category, [EmailAddress] string? ContactEmail, [Url] string? Website); ``` ## Custom Validators [Section titled “Custom Validators”](#custom-validators) For complex validation logic, create custom validators by inheriting from `QueryValidator`: ```csharp public class AccountSearchQueryValidator : QueryValidator { public AccountSearchQueryValidator() { RuleFor(x => x.MinBalance) .LessThanOrEqualTo(x => x.MaxBalance) .When(x => x.MinBalance.HasValue && x.MaxBalance.HasValue) .WithMessage("Minimum balance must be less than or equal to maximum balance"); RuleFor(x => x.Name) .Must(BeValidAccountName) .WithMessage("Account name contains invalid characters"); RuleFor(x => x) .Must(HaveAtLeastOneSearchCriteria) .WithMessage("At least one search criteria must be provided"); } bool BeValidAccountName(string name) { // Custom validation logic return !string.IsNullOrEmpty(name) && name.All(char.IsLetterOrDigit) || name.All(c => char.IsLetterOrDigit(c) || char.IsWhiteSpace(c)); } bool HaveAtLeastOneSearchCriteria(AccountSearchQuery query) { return !string.IsNullOrEmpty(query.Name) || query.MinBalance.HasValue || query.MaxBalance.HasValue; } } ``` ## FluentValidation Support [Section titled “FluentValidation Support”](#fluentvalidation-support) Arc uses FluentValidation internally, giving you access to powerful validation rules: ```csharp public class CustomerQueryValidator : QueryValidator { public CustomerQueryValidator() { RuleFor(x => x.Email) .EmailAddress() .When(x => !string.IsNullOrEmpty(x.Email)); RuleFor(x => x.PhoneNumber) .Matches(@"^\+?[1-9]\d{1,14}$") .When(x => !string.IsNullOrEmpty(x.PhoneNumber)) .WithMessage("Phone number must be in international format"); RuleFor(x => x.Age) .GreaterThanOrEqualTo(0) .LessThanOrEqualTo(150) .When(x => x.Age.HasValue); RuleFor(x => x.Tags) .Must(tags => tags.Count <= 10) .When(x => x.Tags != null) .WithMessage("Maximum 10 tags allowed"); } } ``` ## Cross-Field Validation [Section titled “Cross-Field Validation”](#cross-field-validation) Validate relationships between multiple fields: ```csharp public class DateRangeQueryValidator : QueryValidator { public DateRangeQueryValidator() { RuleFor(x => x.StartDate) .LessThanOrEqualTo(x => x.EndDate) .When(x => x.StartDate.HasValue && x.EndDate.HasValue) .WithMessage("Start date must be before or equal to end date"); RuleFor(x => x.EndDate) .GreaterThanOrEqualTo(DateTime.Today.AddDays(-365)) .When(x => x.EndDate.HasValue) .WithMessage("End date cannot be more than one year in the past"); RuleFor(x => x) .Must(x => !x.StartDate.HasValue || !x.EndDate.HasValue || (x.EndDate.Value - x.StartDate.Value).Days <= 90) .WithMessage("Date range cannot exceed 90 days"); } } ``` ## Async Validation [Section titled “Async Validation”](#async-validation) For validation that requires database lookups or external services: ```csharp public class AccountExistsQueryValidator : QueryValidator { readonly IMongoCollection _collection; public AccountExistsQueryValidator(IMongoCollection collection) { _collection = collection; RuleFor(x => x.AccountId) .MustAsync(AccountExists) .WithMessage("Account does not exist"); RuleFor(x => x.OwnerEmail) .MustAsync(OwnerEmailIsValid) .When(x => !string.IsNullOrEmpty(x.OwnerEmail)) .WithMessage("Owner email is not registered"); } async Task AccountExists(AccountId accountId, CancellationToken cancellationToken) { var count = await _collection.CountDocumentsAsync( a => a.Id == accountId, cancellationToken: cancellationToken); return count > 0; } async Task OwnerEmailIsValid(string email, CancellationToken cancellationToken) { // Call external service or database to validate email // This is just an example await Task.Delay(100, cancellationToken); return email.Contains("@") && email.Contains("."); } } ``` ## Model-Bound Query Validation [Section titled “Model-Bound Query Validation”](#model-bound-query-validation) For model-bound queries with `[ReadModel]`, validation can be applied to the method parameters: ```csharp public record GetAccountsByOwnerQuery( [Required] CustomerId OwnerId, [Range(1, 1000)] int MaxResults = 100); [ReadModel] public class Accounts { public static IEnumerable GetAccountsByOwner( GetAccountsByOwnerQuery query, IMongoCollection collection) { return collection .Find(a => a.Owner == query.OwnerId) .Limit(query.MaxResults) .ToList(); } } // Custom validator for the query public class GetAccountsByOwnerQueryValidator : QueryValidator { public GetAccountsByOwnerQueryValidator() { RuleFor(x => x.OwnerId) .NotNull() .NotEmpty() .WithMessage("Owner ID is required"); RuleFor(x => x.MaxResults) .GreaterThan(0) .LessThanOrEqualTo(1000) .WithMessage("Max results must be between 1 and 1000"); } } ``` ## Validation Error Responses [Section titled “Validation Error Responses”](#validation-error-responses) ### Filter-Based Validation Errors [Section titled “Filter-Based Validation Errors”](#filter-based-validation-errors) When validation fails in the query pipeline (using validation filters), the query returns a `QueryResult` with detailed error information: ```json { "data": null, "paging": { "page": 0, "pageSize": 0, "totalItems": 0 }, "correlationId": "12345678-1234-1234-1234-123456789012", "isSuccess": false, "isAuthorized": true, "isValid": false, "hasExceptions": false, "validationResults": [ { "severity": "Error", "message": "Account name is required and must be less than 50 characters", "members": ["name"] }, { "severity": "Error", "message": "Minimum balance must be greater than or equal to 0", "members": ["minBalance"] } ], "exceptionMessages": [], "exceptionStackTrace": "" } ``` ### Controller-Based Validation Errors [Section titled “Controller-Based Validation Errors”](#controller-based-validation-errors) When validation fails on controller-based queries, the response returns a 400 Bad Request with detailed error information: ```json { "data": null, "paging": { "page": 0, "pageSize": 0, "totalItems": 0 }, "correlationId": "12345678-1234-1234-1234-123456789012", "isSuccess": false, "isAuthorized": true, "isValid": false, "hasExceptions": false, "validationResults": [ { "severity": "Error", "message": "Account name is required", "members": ["name"] }, { "severity": "Error", "message": "Minimum balance must be greater than or equal to 0", "members": ["minBalance"] } ], "exceptionMessages": [], "exceptionStackTrace": "" } ``` ## Ignoring Validation [Section titled “Ignoring Validation”](#ignoring-validation) In some cases, you may want to bypass validation (useful for administrative queries): ```csharp [HttpGet("admin/all-data")] [IgnoreValidation] // Skip validation for this endpoint public IEnumerable GetAllDataForAdmin([FromQuery] AdminQuery query) { // This will execute without validation return _collection.Find(_ => true).ToList(); } ``` ## Conditional Validation [Section titled “Conditional Validation”](#conditional-validation) Apply validation rules conditionally: ```csharp public class ConditionalQueryValidator : QueryValidator { public ConditionalQueryValidator() { // Only validate email if contact method is email RuleFor(x => x.Email) .EmailAddress() .When(x => x.ContactMethod == ContactMethod.Email); // Only validate phone when contact method is phone RuleFor(x => x.PhoneNumber) .Matches(@"^\+?[1-9]\d{1,14}$") .When(x => x.ContactMethod == ContactMethod.Phone); // Require at least one contact method RuleFor(x => x) .Must(x => x.ContactMethod != ContactMethod.None) .WithMessage("A contact method must be specified"); } } ``` ## Complex Object Validation [Section titled “Complex Object Validation”](#complex-object-validation) Validate nested objects and collections: ```csharp public record OrderSearchQuery( string? CustomerName, DateRangeQuery? DateRange, List? ProductCategories, AddressQuery? ShippingAddress); public class OrderSearchQueryValidator : QueryValidator { public OrderSearchQueryValidator() { RuleFor(x => x.DateRange) .SetValidator(new DateRangeQueryValidator()) .When(x => x.DateRange != null); RuleFor(x => x.ShippingAddress) .SetValidator(new AddressQueryValidator()) .When(x => x.ShippingAddress != null); RuleForEach(x => x.ProductCategories) .NotEmpty() .Length(2, 50) .When(x => x.ProductCategories != null); } } ``` ## When to Use Each Validation Approach [Section titled “When to Use Each Validation Approach”](#when-to-use-each-validation-approach) ### Use Filter-Based Validation (Recommended) [Section titled “Use Filter-Based Validation (Recommended)”](#use-filter-based-validation-recommended) * **Model-bound queries**: Using `[ReadModel]` classes with static methods * **Query pipeline**: Working with the query pipeline infrastructure * **Parameter-level validation**: Need to validate individual query parameters * **Consistent validation**: Want validation behavior consistent with commands * **Complex parameter types**: Using concepts or complex objects as parameters ### Use Controller-Based Validation [Section titled “Use Controller-Based Validation”](#use-controller-based-validation) * **HTTP endpoints**: Creating traditional REST API endpoints with `[HttpGet]` * **ASP.NET Core integration**: Leveraging existing ASP.NET Core validation infrastructure * **Simple query objects**: Working with simple DTOs as query parameters * **Web API consistency**: Maintaining consistency with other ASP.NET Core controllers ## Best Practices [Section titled “Best Practices”](#best-practices) 1. **Prefer filter-based validation** for model-bound queries using the query pipeline 2. **Use data annotations** for simple validation rules on individual parameters 3. **Create custom validators** for complex business logic and cross-parameter validation 4. **Validate early** to prevent unnecessary database queries and improve performance 5. **Provide clear error messages** that help users understand how to fix their input 6. **Use async validation sparingly** as it can impact query performance significantly 7. **Test validation rules thoroughly** with edge cases and boundary conditions 8. **Consider performance impact** of validation, especially for high-frequency queries 9. **Document validation requirements** clearly for API consumers 10. **Use conditional validation** to avoid unnecessary validation overhead 11. **Group related validation rules** logically for better maintainability 12. **Validate parameter types** that are concepts or complex objects rather than individual parameters when possible # Tenancy > Serve multiple customers from one application while keeping their data isolated — how Arc resolves the current tenant and carries it through every request. Tenancy means your application serves multiple customers or organizational units while keeping their data and operations isolated. A tenant could be a customer, a business unit, or any logical boundary that must remain separate from others. ## Why Tenancy Matters [Section titled “Why Tenancy Matters”](#why-tenancy-matters) Tenancy keeps data and behavior isolated between customers or organizational units while still sharing the same application deployment. Arc provides tenant resolution, tenant context access, and tenant-aware integrations to help you maintain strict separation and predictable behavior. * **Data isolation**: Each tenant should only see its own data, even when sharing infrastructure. * **Compliance**: Many regulatory requirements demand strict separation and auditable access. * **Operational safety**: Isolation reduces the blast radius of mistakes, queries, and deployments. * **Scalability**: Tenancy enables predictable scaling by segmenting traffic and storage by tenant. Arc helps you establish a clear tenant boundary by resolving a tenant ID for each request, keeping that context available across the application, and wiring tenant-aware integrations to data stores and event streams. ## Best Practices [Section titled “Best Practices”](#best-practices) * Choose a resolver that aligns with your authentication and request flow. * Validate that the requester is authorized to access the resolved tenant. * Include the tenant ID in cache keys, logs, and telemetry. * Keep tenant IDs stable and opaque to avoid enumeration. * Prefer tenant-aware data stores and avoid cross-tenant queries. * Use the development resolver only in local or test environments. ## Security Considerations [Section titled “Security Considerations”](#security-considerations) * Ensure tenant ID resolution happens only after authentication. * Enforce tenant membership checks in application services and policies. * Log tenant access for audits and incident investigation. * Prevent tenant ID spoofing by validating headers, claims, and parameters. * Treat tenant ID as sensitive metadata and avoid exposing it unnecessarily. ## Topics [Section titled “Topics”](#topics) * [Resolving tenant IDs](/arc/backend/tenancy/resolvers/) * [Configuration](/arc/backend/tenancy/configuration/) * [Tenant context access](/arc/backend/tenancy/tenant-context/) * [Database isolation](/arc/backend/tenancy/database-resolvers/) # Tenancy Configuration You can configure tenancy programmatically or through configuration files. Both approaches map to the same options and resolver types. ## Programmatic Configuration [Section titled “Programmatic Configuration”](#programmatic-configuration) ```csharp builder.AddCratisArcCore(options => { options.UseHeaderTenancy("X-Custom-Tenant"); // options.UseQueryTenancy("tenant"); // options.UseClaimTenancy("tenant_id"); // options.UseSubdomainTenancy("myapp.com", "X-Custom-Tenant"); // options.UseFixedTenancy("acme"); // options.UseDevelopmentTenancy("test-tenant"); }); ``` ## Configuration File (appsettings.json) [Section titled “Configuration File (appsettings.json)”](#configuration-file-appsettingsjson) ```json { "Cratis": { "Arc": { "Tenancy": { "ResolverType": "Header", "HttpHeader": "X-Custom-Tenant" } } } } ``` ## Resolver-Specific Settings [Section titled “Resolver-Specific Settings”](#resolver-specific-settings) ### Header Resolver [Section titled “Header Resolver”](#header-resolver) ```json { "Tenancy": { "ResolverType": "Header", "HttpHeader": "X-Custom-Tenant" } } ``` ### Query Resolver [Section titled “Query Resolver”](#query-resolver) ```json { "Tenancy": { "ResolverType": "Query", "QueryParameter": "tenant" } } ``` ### Claim Resolver [Section titled “Claim Resolver”](#claim-resolver) ```json { "Tenancy": { "ResolverType": "Claim", "ClaimType": "tenant_id" } } ``` ### Subdomain Resolver [Section titled “Subdomain Resolver”](#subdomain-resolver) `BaseDomain` is the domain the application itself is served from. A host resolves a tenant only when it is exactly one label in front of it; every other host falls back to `HttpHeader`. `BaseDomain` is **required** and must be the registrable domain you own — at least two letter-digit-hyphen labels, never an address literal. Leaving it out, or setting it to something no host could be matched against, throws `BaseDomainIsNotADomainName` and the host does not start — `UseSubdomainTenancy` throws where you call it, and a value that arrives from configuration is validated while the host is starting. Neither one lets a request through to take its tenant from the client-supplied `HttpHeader` instead. See [the subdomain resolver](/arc/backend/tenancy/resolvers/) for the full rules. ```json { "Tenancy": { "ResolverType": "Subdomain", "BaseDomain": "myapp.com", "HttpHeader": "X-Custom-Tenant" } } ``` ### Fixed Resolver [Section titled “Fixed Resolver”](#fixed-resolver) ```json { "Tenancy": { "ResolverType": "Fixed", "FixedTenantId": "acme" } } ``` ### Development Resolver [Section titled “Development Resolver”](#development-resolver) ```json { "Tenancy": { "ResolverType": "Development", "DevelopmentTenantId": "local-tenant" } } ``` `FixedTenantId` and `DevelopmentTenantId` are two names for the same value, so either key configures either resolver type. Set only one of them - when a configuration source supplies both, whichever key the binder visits last wins. # Database Isolation Arc provides tenant-aware database naming to keep tenant data segregated while sharing infrastructure. ## MongoDB Database Naming [Section titled “MongoDB Database Naming”](#mongodb-database-naming) The default MongoDB resolver appends the tenant ID to the base database name using a `+` separator: ```text {BaseDatabaseName} // when TenantId.NotSet {BaseDatabaseName}+{TenantId} // when tenant ID is set ``` Examples: ```text MyDatabase // when TenantId.NotSet MyDatabase+acme-corp // when tenant ID is "acme-corp" MyDatabase+tenant-123 // when tenant ID is "tenant-123" ``` ## Entity Framework Core Database Naming [Section titled “Entity Framework Core Database Naming”](#entity-framework-core-database-naming) Entity Framework Core follows the same naming pattern by default, ensuring that each tenant maps to its own database name. ## Chronicle Event Store Namespace [Section titled “Chronicle Event Store Namespace”](#chronicle-event-store-namespace) Chronicle uses the tenant ID as the event store namespace when a tenant is resolved. When no tenant is resolved, it uses the default namespace. # Resolving Tenant IDs Arc resolves tenant IDs through pluggable strategies. Each request is evaluated by the configured resolver, and the resulting tenant ID becomes the active tenant context for the request lifecycle. ## Built-In Resolvers [Section titled “Built-In Resolvers”](#built-in-resolvers) ### Header Resolver (Default) [Section titled “Header Resolver (Default)”](#header-resolver-default) Resolves the tenant ID from an HTTP header. ```csharp builder.AddCratisArcCore(options => { options.UseHeaderTenancy("X-Custom-Tenant"); }); ``` Default header name: `x-cratis-tenant-id` ### Query Parameter Resolver [Section titled “Query Parameter Resolver”](#query-parameter-resolver) Resolves the tenant ID from a query string parameter. ```csharp builder.AddCratisArcCore(options => { options.UseQueryTenancy("tenant"); }); ``` Default parameter name: `tenantId` ### Claim Resolver [Section titled “Claim Resolver”](#claim-resolver) Resolves the tenant ID from a claim on the authenticated user. ```csharp builder.AddCratisArcCore(options => { options.UseClaimTenancy("tenant_id"); }); ``` Default claim type: `tenant_id` ### Subdomain Resolver [Section titled “Subdomain Resolver”](#subdomain-resolver) Resolves the tenant ID from the subdomain of a configured base domain, and falls back to the configured HTTP header for every host that does not carry one. ```csharp builder.AddCratisArcCore(options => { options.UseSubdomainTenancy("myapp.com", "X-Custom-Tenant"); }); ``` A host carries a tenant only when it is **exactly one label in front of the base domain**. A request to `acme.myapp.com` resolves the tenant as `acme`. A request to `myapp.com` falls back to the `X-Custom-Tenant` header. This pattern is useful for SaaS applications where each tenant is routed through its own subdomain. The number of labels in a host is never used to decide whether it carries a tenant, so the following all fall back to the header: | Host | Resolved tenant | | ----------------------------------------------------------------------- | --------------- | | `acme.myapp.com` | `acme` | | `myapp.com` (the base domain itself) | the header | | `acme.staging.myapp.com` (more than one label) | the header | | `10.0.0.5`, `[::1]` (IP literals) | the header | | `otherapp.com` (an unrelated domain) | the header | | `evil-myapp.com` (ends with the same text, no label boundary) | the header | | `acme.myapp.com.evil.com` (base domain in the middle) | the header | | `*.myapp.com`, `user@acme.myapp.com`, `a_b.myapp.com` (not a DNS label) | the header | Hosts are normalized before matching, so a trailing dot, a port, mixed casing and an internationalized name all resolve the same tenant: `ACME.MyApp.com.`, `acme.myapp.com:5000` and `acme.myapp.com` all resolve `acme`, and `münchen.myapp.com` resolves the punycode label `xn--mnchen-3ya` — the same tenant as `xn--mnchen-3ya.myapp.com`. Use the base domain to decide what your application’s own host is. If the application is served from `www.myapp.com`, configure that as the base domain and `www.myapp.com` falls back to the header while `acme.www.myapp.com` resolves `acme`. With `myapp.com` as the base domain, `www` is an ordinary tenant label like any other — no host name is treated as special. Default fallback header: `x-cratis-tenant-id` #### The base domain is required [Section titled “The base domain is required”](#the-base-domain-is-required) The base domain is what separates a tenant host from any other host on the internet, so your application refuses to start without a usable one. `UseSubdomainTenancy` throws `BaseDomainIsNotADomainName` when the value is empty, is a single label, is an address literal, or is not made of letter-digit-hyphen labels: ```csharp options.UseSubdomainTenancy("myapp.com"); // fine options.UseSubdomainTenancy("X-Tenant-Id"); // throws — a header name is not a domain options.UseSubdomainTenancy("localhost"); // throws — a single label is not a registrable domain options.UseSubdomainTenancy("192.168.1.10"); // throws — an address identifies no domain ``` Selecting the subdomain resolver in `appsettings.json` gets the same treatment: Arc validates `Tenancy` while the host is starting, so `"ResolverType": "Subdomain"` with a missing or unusable `BaseDomain` throws the same exception out of host startup, before the application accepts its first request. Failing to start is deliberate. Without a base domain no host would ever resolve a tenant, and every request would silently take its tenant from the fallback header instead — which any client can set. Failing on the first request instead would mean a real user finds the mistake, on a process that already reported itself healthy. **Configure the registrable domain your application is served from, and nothing broader.** A bare top-level domain such as `com` is refused because it is a single label, but the check cannot know that `co.uk` is a public suffix: with `co.uk` as the base domain, anyone who registers `evil.co.uk` becomes the tenant `evil`. Pick the domain you actually own. #### The resolved tenant must be a DNS label [Section titled “The resolved tenant must be a DNS label”](#the-resolved-tenant-must-be-a-dns-label) The resolved label becomes the Chronicle namespace and part of the database name, so it is required to be a valid letter-digit-hyphen label — up to 63 characters, starting and ending with a letter or digit. Anything else falls back to the header rather than travelling on as a tenant ID. #### One domain, many spellings [Section titled “One domain, many spellings”](#one-domain-many-spellings) Hosts are matched after IDNA compatibility mapping, which is what lets `münchen.myapp.com` and `xn--mnchen-3ya.myapp.com` be the same tenant. The same mapping means several byte sequences are **the same host**, and all resolve the tenant `admin`: | Written as | Why it is the same | | ------------------------------------------------------- | ---------------------------------------------------------------------------- | | `admin.myapp.com` | the canonical spelling | | `admin.myapp.com` | fullwidth Latin letters map to ASCII | | `admin.myapp.com.` | a root-anchored name | | `admin。myapp.com`, `admin.myapp.com`, `admin。myapp.com` | U+3002, U+FF0E and U+FF61 are label separators | | `admin.myapp.com` | zero width space, soft hyphen, byte order mark and word joiner are ignorable | This is IDNA working as specified — browsers resolve these the same way — so Arc does not reject them. Be aware of the consequence: **a WAF, ingress or router that matches the literal `Host` string sees a different value than Arc does.** If something upstream makes decisions per tenant host, normalize the host there too, or make the decision from Arc’s resolved tenant rather than from the raw header. #### The fallback header is client-supplied [Section titled “The fallback header is client-supplied”](#the-fallback-header-is-client-supplied) Every host that does not carry a tenant falls back to `HttpHeader`, and that header arrives on the request unauthenticated — any caller can set it. **Strip the fallback header at your ingress** so only your own infrastructure can set it, exactly as you would for any other trusted request header. ### Fixed Resolver [Section titled “Fixed Resolver”](#fixed-resolver) Resolves every request to one configured tenant ID. ```csharp builder.AddCratisArcCore(options => { options.UseFixedTenancy("acme"); }); ``` Default tenant ID: `development` The tenant ID is returned regardless of the request and regardless of the hosting environment, which makes this the resolver for a single-tenant deployment: one deployment, one tenant, decided at configuration time rather than per request. Because the resolved tenant drives the Chronicle namespace and the Arc MongoDB database, a fixed tenant is a deployment-wide data-isolation decision - review it before promoting a configuration. ### Development Resolver [Section titled “Development Resolver”](#development-resolver) The same behavior as the Fixed resolver, under its original name. ```csharp builder.AddCratisArcCore(options => { options.UseDevelopmentTenancy("my-test-tenant"); }); ``` Default tenant ID: `development` Despite the name, this resolver has never consulted `IHostEnvironment` - it returns the configured tenant ID in every environment, production included. Prefer `UseFixedTenancy` when the fixed tenant is a deployment constant rather than a local-development convenience; `UseDevelopmentTenancy` remains supported and configures the same tenant ID. # Tenant Context Access Once a tenant ID is resolved, Arc stores it as the current tenant context for the request. You can access this context anywhere through dependency injection. ## Accessing the Current Tenant [Section titled “Accessing the Current Tenant”](#accessing-the-current-tenant) ```csharp using Cratis.Arc.Tenancy; public class CustomerService(ITenantIdAccessor tenantIdAccessor) { public async Task HandleAsync() { var tenantId = tenantIdAccessor.Current; if (tenantId == TenantId.NotSet) { return; } await LoadTenantDataAsync(tenantId); } private static Task LoadTenantDataAsync(TenantId tenantId) => Task.CompletedTask; } ``` ## TenantId.NotSet [Section titled “TenantId.NotSet”](#tenantidnotset) `TenantId.NotSet` signals that no tenant could be resolved from the current context. Handle this case explicitly so you avoid mixing tenant-aware and non-tenant operations. # Testing Arc provides first-class testing support through focused NuGet packages that let you drive commands through the real pipeline infrastructure — validation filters, authorization filters, and command handlers — without an HTTP server or an external database. In Cratis applications, those tests are usually written as [Cratis Specifications](/testing-with-cratis/): a light BDD-style wrapper over xUnit where `Establish()` sets up the context, `Because()` performs the behavior, and `[Fact]` methods assert the outcomes. That lines up with Arc’s model: given a command context, when a command runs, then the command result, appended events, or read model state should look a certain way. The `CommandScenario` class is the single entry point for all command testing. When the Chronicle-specific testing package is also referenced, it automatically extends itself with an in-memory event log. ## Packages [Section titled “Packages”](#packages) | Package | Description | | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `Cratis.Specifications.XUnit` | BDD-style `Specification` base class and `Should*` assertion helpers on top of xUnit. | | `Cratis.Arc.Testing` | Core `CommandScenario` class and `CommandResult` assertion helpers. No event sourcing dependency. | | `Cratis.Arc.Chronicle.Testing` | Automatically extends `CommandScenario` with an in-memory event log when referenced. | | `Cratis.Testing` | Convenience meta-package for full Cratis projects that use both Arc and Chronicle. Use `Cratis.Arc.Testing` when you want Arc without event sourcing. | ## Topics [Section titled “Topics”](#topics) | Topic | Description | | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | [Command Scenarios](/arc/backend/testing/command-scenario/) | How to test commands with `CommandScenario` and the `CommandResult` assertion helpers. | | [Chronicle Extension](/arc/backend/testing/chronicle/) | How the Chronicle in-memory event log activates automatically, how to seed events and read model state, and how to assert against the log. | ## Quick Start [Section titled “Quick Start”](#quick-start) ### 1. Add the package [Section titled “1. Add the package”](#1-add-the-package) ```xml ``` ### 2. Write a spec [Section titled “2. Write a spec”](#2-write-a-spec) Create a `CommandScenario` instance as a field, execute the command in `Because()`, and assert the result from `[Fact]` methods: ```csharp public class when_adding_item_to_cart : Specification { readonly CommandScenario _scenario = new(); CommandResult _result = default!; async Task Because() => _result = await _scenario.Execute(new AddItemToCart("SKU-123", 2)); [Fact] void should_succeed() => _result.ShouldBeSuccessful(); } ``` `Execute` runs the command through the same validation, authorization, and handler pipeline that production uses — no mocking required. The scenario initializes itself lazily on the first `Execute` or `Validate` call. ### 3. Add Chronicle assertions when the command appends events [Section titled “3. Add Chronicle assertions when the command appends events”](#3-add-chronicle-assertions-when-the-command-appends-events) For a Chronicle-backed slice, add the Chronicle testing extension alongside the Arc package: ```xml ``` You can also reference `Cratis.Testing` instead of the two focused packages when the test project is a full Cratis test suite. When `Cratis.Arc.Chronicle.Testing` (or the `Cratis.Testing` meta-package) is referenced, `CommandScenario` automatically gains three extension properties: * `EventScenario` — exposes the full scenario, including `Given` for event seeding * `EventLog` — shortcut to the in-memory event log for assertions * `EventSequence` — the same instance, compatible with Chronicle’s assertion helpers ```csharp public class when_registering_author : Specification { readonly CommandScenario _scenario = new(); readonly EventSourceId _authorId = EventSourceId.New(); CommandResult _result = default!; async Task Because() => _result = await _scenario.Execute(new RegisterAuthor(_authorId, "Jane Austen")); [Fact] void should_succeed() => _result.ShouldBeSuccessful(); [Fact] Task should_have_appended_registered_event() => _scenario.ShouldHaveAppendedEvent( _authorId, e => e.Name == "Jane Austen"); } ``` The command still runs through Arc’s real command pipeline. The Chronicle testing extension captures the events appended during that execution so the spec can assert on the facts without starting a Chronicle server. For the cross-product testing model — Specifications, Arc command scenarios, Chronicle event/read-model/reactor scenarios, and full stack slice specs — see [Testing with Cratis](/testing-with-cratis/). # Chronicle When `Cratis.Arc.Chronicle.Testing` (or the `Cratis.Testing` meta-package) is referenced, `CommandScenario` is automatically extended with an in-memory event scenario. No separate class or base type is needed. The extension is wired via `ChronicleCommandScenarioExtender`, which implements `ICommandScenarioExtender` and is discovered automatically by `CommandScenario` at construction time using the Cratis type discovery system. In a Cratis Specification, that gives you the event-sourced test shape directly: seed prior facts with `_scenario.EventScenario.Given` in `Establish()`, execute the command once in `Because()`, then assert the `CommandResult` and the captured events from `[Fact]` methods. ## Package [Section titled “Package”](#package) ```xml ``` Or via the meta-package: ```xml ``` ## Basic Usage [Section titled “Basic Usage”](#basic-usage) Use the same `CommandScenario` class as for non-Chronicle commands. When the Chronicle testing package is present, these extension properties are available directly on the scenario: | Property | Type | Purpose | | ---------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | | `Given` | `CommandScenarioChronicleGivenBuilder` | Seed the read model state a command observes — `Given.ForEventSource(id).Events(...)` or `.ReadModel(...)` | | `EventScenario` | `EventScenario` | The full event scenario — use `EventScenario.Given` to seed the event log | | `EventLog` | `IEventLog` | The in-memory event log — use for appending and assertions | | `EventSequence` | `IEventSequence` | The same instance as `EventLog` — use with Chronicle’s assertion helpers | | `AppendedEvents` | `IReadOnlyList` | The events captured during command execution | ```csharp public class when_registering_author : Specification { readonly EventSourceId _authorId = EventSourceId.New(); readonly CommandScenario _scenario = new(); CommandResult _result = default!; async Task Because() => _result = await _scenario.Execute(new RegisterAuthor(_authorId, "Jane Austen")); [Fact] void should_succeed() => _result.ShouldBeSuccessful(); [Fact] Task should_have_appended_registered_event() => _scenario.EventLog.ShouldHaveAppendedEvent(_authorId); } ``` The spec executes the command once and then asserts both the Arc result and the Chronicle fact that was recorded. ## Seeding Pre-existing Events with `Given` [Section titled “Seeding Pre-existing Events with Given”](#seeding-pre-existing-events-with-given) Use `_scenario.EventScenario.Given` to append events to the in-memory event log *before* the command runs. Call `ForEventSource` with the event source identifier, then pass the pre-existing events to `Events`: ```csharp public class when_registering_author_with_same_name : Specification { readonly EventSourceId _authorId = EventSourceId.New(); readonly CommandScenario _scenario = new(); CommandResult _result = default!; Task Establish() => _scenario.EventScenario.Given .ForEventSource(_authorId) .Events(new AuthorRegistered("Jane Austen")); async Task Because() => _result = await _scenario.Execute(new RegisterAuthor(_authorId, "Jane Austen")); [Fact] void should_not_succeed() => _result.ShouldNotBeSuccessful(); [Fact] Task should_not_have_appended_a_second_event() => _scenario.EventLog.ShouldHaveTailSequenceNumber(EventSequenceNumber.First); } ``` Seed events before calling `Execute` so they are present when the command handler runs. ## EventLog Assertion Helpers [Section titled “EventLog Assertion Helpers”](#eventlog-assertion-helpers) Chronicle provides a set of assertion helpers that extend `IEventSequence` directly. Call them on `_scenario.EventLog` or `_scenario.EventSequence` after `Execute`. For the full list of available assertions, see the [Chronicle event assertions reference](/chronicle/testing/events/assertions/). ## Transactional Commands in Tests [Section titled “Transactional Commands in Tests”](#transactional-commands-in-tests) The harness runs commands with the same [transactional scope](/arc/backend/commands/transactional-commands/) as production: the events a command returns — and appends through `eventLog.Transactional` — commit atomically when it succeeds and roll back when it fails, including when a unique constraint rejects the commit. Immediate appends through `IEventLog`/`IEventStore.EventLog` land right away and are final, but a failed one fails the command. That gives specs two natural assertions: ```csharp public class when_registering_author_with_taken_name : Specification { readonly EventSourceId _existing = EventSourceId.New(); readonly EventSourceId _author = EventSourceId.New(); readonly CommandScenario _scenario = new(); CommandResult _result = default!; Task Establish() => _scenario.EventScenario.Given .ForEventSource(_existing) .Events(new AuthorRegistered("Jane Austen")); async Task Because() => _result = await _scenario.Execute(new RegisterAuthor(_author, "Jane Austen")); [Fact] void should_not_succeed() => _result.IsSuccess.ShouldBeFalse(); [Fact] void should_surface_the_violation() => _result.ValidationResults.ShouldNotBeEmpty(); [Fact] async Task should_append_nothing_for_the_rejected_author() => (await _scenario.EventLog.HasEventsFor(_author)).ShouldBeFalse(); } ``` Two things to be aware of: * **When events show up in `AppendedEvents` depends on the style.** Immediate appends surface as they happen; the command’s enrolled events — returned events and `Transactional` appends — surface as one batch when the command’s transaction commits. * **Immediate appends are final.** A handler that appends through the plain `IEventLog.Append` (or `IEventStore.EventLog`) writes immediately — a successful append remains in the log even when the command fails afterwards, and a spec can assert exactly that. ## Testing Commands That Use EventForEventSourceId [Section titled “Testing Commands That Use EventForEventSourceId”](#testing-commands-that-use-eventforeventsourceid) When a command handler returns `EventForEventSourceId` or `IEnumerable`, events are appended to different event sources than the command’s own event source id. The standard `EventLog.ShouldHaveAppendedEvent(sequenceNumber)` helpers work against a single sequence and cannot filter by event source id. For these cases use the `CommandScenario`-level assertion helpers, which capture events during execution via the client-side `AppendOperations` observable. | Method | Asserts that… | | --------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `ShouldHaveAppendedEvent(eventSourceId)` | At least one event of type `TEvent` was appended for the given `EventSourceId` | | `ShouldHaveAppendedEvent(eventSourceId, predicate)` | Same, and the event also satisfies the predicate | | `ShouldHaveTailSequenceNumber(expected)` | The highest sequence number among all captured events equals `expected` | ### Example: Single cross-source event [Section titled “Example: Single cross-source event”](#example-single-cross-source-event) ```csharp using Cratis.Arc.Chronicle.Testing.Commands; using Cratis.Arc.Testing.Commands; using Cratis.Chronicle.Events; public class when_migrating_customer_to_new_id : Specification { readonly CommandScenario _scenario = new(); readonly EventSourceId _oldId = EventSourceId.New(); readonly EventSourceId _newId = EventSourceId.New(); CommandResult _result = default!; async Task Because() => _result = await _scenario.Execute(new MigrateCustomerToNewId(_oldId, _newId)); [Fact] void should_succeed() => _result.ShouldBeSuccessful(); [Fact] Task should_have_appended_migrated_event_for_new_id() => _scenario.ShouldHaveAppendedEvent(_newId); [Fact] Task should_reference_old_id_in_event() => _scenario.ShouldHaveAppendedEvent( _newId, e => e.OldCustomerId == _oldId); } ``` ### Example: Multiple cross-source events (fund transfer) [Section titled “Example: Multiple cross-source events (fund transfer)”](#example-multiple-cross-source-events-fund-transfer) ```csharp using Cratis.Arc.Chronicle.Testing.Commands; using Cratis.Arc.Testing.Commands; using Cratis.Chronicle.Events; public class when_transferring_funds : Specification { readonly CommandScenario _scenario = new(); readonly EventSourceId _fromAccount = EventSourceId.New(); readonly EventSourceId _toAccount = EventSourceId.New(); CommandResult _result = default!; async Task Because() => _result = await _scenario.Execute(new TransferFunds(_fromAccount, _toAccount, 250m)); [Fact] void should_succeed() => _result.ShouldBeSuccessful(); [Fact] Task should_have_debited_from_account() => _scenario.ShouldHaveAppendedEvent(_fromAccount); [Fact] Task should_have_credited_to_account() => _scenario.ShouldHaveAppendedEvent(_toAccount); [Fact] Task should_have_debited_correct_amount() => _scenario.ShouldHaveAppendedEvent(_fromAccount, e => e.Amount == 250m); [Fact] Task should_have_appended_two_events() => _scenario.ShouldHaveTailSequenceNumber(1ul); } ``` > **Sequence numbering applies here too**: `ShouldHaveTailSequenceNumber` checks the highest sequence number across all captured events. Two events means a tail of `1` (zero-based). The `AppendedEvents` extension property gives you the raw list if you need to write custom assertions: ```csharp [Fact] void should_have_exactly_two_events() => _scenario.AppendedEvents.Count.ShouldEqual(2); ``` ## Testing Commands That Take Read Model Dependencies [Section titled “Testing Commands That Take Read Model Dependencies”](#testing-commands-that-take-read-model-dependencies) A command handler, `Provide` method, or `CommandValidator` can take a read model as a parameter — Arc resolves it for the command’s event source id exactly as it does at runtime (`IProjectionFor`, `IReducerFor`, and model-bound projections). See [Use current state in a command](/arc/scenarios/use-current-state-in-a-command/) for the production-side pattern. To test such a command you need to control what that read model contains, and the awkward way is to hand-mock `IReadModels`. `_scenario.Given.ForEventSource(id)` does it for you, two ways: seed the **events** the read model is built from, or pin a materialized **instance** directly. ### Seeding Read Model State from Events [Section titled “Seeding Read Model State from Events”](#seeding-read-model-state-from-events) State the events that happened for the event source. Any read model a command injects for that source is materialized from those events through its own reducer or projection — you never name the read model type here, just as you never do in production: ```csharp public class when_withdrawing_with_sufficient_funds : Specification { readonly EventSourceId _accountId = EventSourceId.New(); readonly CommandScenario _scenario = new(); CommandResult _result = default!; void Establish() => _scenario.Given .ForEventSource(_accountId) .Events(new MoneyDeposited(100m), new MoneyDeposited(50m)); async Task Because() => _result = await _scenario.Execute(new Withdraw(_accountId, 120m)); [Fact] void should_succeed() => _result.ShouldBeSuccessful(); } ``` Events are the facts; read models are derived from them. One `Events(...)` call feeds *every* read model built from those events: if the command injects both an `AccountBalance` and an `AccountStatement`, both are materialized from the same events — no read model type appears in the test. This is distinct from `_scenario.EventScenario.Given` above: that seeds the event **log** (prior facts the handler may read or append against); this seeds the **read model state** the command observes through its injected parameters. ### Pinning a Read Model Instance [Section titled “Pinning a Read Model Instance”](#pinning-a-read-model-instance) When you would rather assert against a known value than express the events behind it, pin the instance directly. The read model type is inferred from the value: ```csharp void Establish() => _scenario.Given .ForEventSource(_accountId) .ReadModel(new AccountBalance(150m)); ``` A read model seeded for one event source is not visible to a command targeting another: resolving an unseeded source yields `null`, so a command that injects a nullable read model parameter sees `null`, exactly as in production. ## Multiple Events [Section titled “Multiple Events”](#multiple-events) When a command appends several events, assert each one by its sequence number: ```csharp public class when_completing_order : Specification { readonly CommandScenario _scenario = new(); readonly EventSourceId _orderId = EventSourceId.New(); CommandResult _result = default!; Task Establish() => _scenario.EventScenario.Given .ForEventSource(_orderId) .Events(new OrderPlaced("item-1", 3)); async Task Because() => _result = await _scenario.Execute(new CompleteOrder(_orderId)); [Fact] void should_succeed() => _result.ShouldBeSuccessful(); [Fact] Task should_have_appended_two_events() => _scenario.EventLog.ShouldHaveTailSequenceNumber(new EventSequenceNumber(1)); [Fact] Task should_have_appended_completed_event() => _scenario.EventLog.ShouldHaveAppendedEvent(new EventSequenceNumber(1)); } ``` > **Sequence numbering**: Sequence numbers are zero-based. `EventSequenceNumber.First` is `0`. The second event is `new EventSequenceNumber(1)`, the third `new EventSequenceNumber(2)`, and so on. `ShouldHaveTailSequenceNumber` reports the number of the *last* appended event — so two total events means a tail of `1`. ## What the Extension Provides [Section titled “What the Extension Provides”](#what-the-extension-provides) When `Cratis.Arc.Chronicle.Testing` is referenced, `ChronicleCommandScenarioExtender` registers the following services automatically: * `IEventTypes` → discovered from the assemblies loaded in the test process (same convention used in production) * `IEventLog` → backed by the real in-process Chronicle kernel (no server required) * `IEventSequence` → the same in-process instance * `IReadModels` → resolves a command’s injected read models from the state seeded with the `Given` builder — by projecting seeded events on demand, or from a pinned instance — enabling direct read model dependencies in handlers, validators, and `Provide` methods It also populates the scenario context, exposed through C# 14 extension properties: | Property | Type | Purpose | | ---------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | | `Given` | `CommandScenarioChronicleGivenBuilder` | The given builder for seeding read model state — `ForEventSource(id).Events(...)` or `.ReadModel(...)` | | `EventScenario` | `EventScenario` | The full scenario, including the `Given` builder for seeding the event log | | `EventLog` | `IEventLog` | Shortcut to `EventScenario.EventLog` for Chronicle’s own assertion helpers | | `EventSequence` | `IEventSequence` | Shortcut to `EventScenario.EventSequence` for Chronicle’s assertion helpers | | `AppendedEvents` | `IReadOnlyList` | All events captured during command execution, used by `ShouldHaveAppendedEvent` and `ShouldHaveTailSequenceNumber` | # Command Scenarios `CommandScenario` is a self-contained class for testing any Arc command through the **real** command pipeline — the same infrastructure used in production. Validation filters, authorization filters, and the command handler all execute; nothing is mocked by default. The examples use [Cratis Specifications](/testing-with-cratis/) so the spec reads as given/when/then: `Establish()` registers dependencies, `Because()` runs the command, and each `[Fact]` asserts one outcome. ## Package [Section titled “Package”](#package) ```xml ``` Or via the meta-package: ```xml ``` ## How It Works [Section titled “How It Works”](#how-it-works) `CommandScenario` is a concrete class that you **instantiate** in your test class. Create it as a field, register any additional services via `Services`, then call `Execute` or `Validate` from `Because()` so each `[Fact]` asserts the same behavior. The service provider and pipeline are built lazily on the first `Execute` or `Validate` call so all services registered before that point are available. At construction time `CommandScenario` discovers all `ICommandScenarioExtender` implementations loaded in the test process and calls each one. Extension packages such as `Cratis.Arc.Chronicle.Testing` use this mechanism to register additional services and expose them through C# extension properties — without requiring any base class or explicit setup. ## Basic Usage [Section titled “Basic Usage”](#basic-usage) ```csharp public class when_adding_item_to_cart : Specification { readonly CommandScenario _scenario = new(); CommandResult _result = default!; async Task Because() => _result = await _scenario.Execute(new AddItemToCart("SKU-123", 2)); [Fact] void should_succeed() => _result.ShouldBeSuccessful(); [Fact] void should_be_valid() => _result.ShouldBeValid(); } ``` ## Registering Additional Services [Section titled “Registering Additional Services”](#registering-additional-services) Register mocks or stub implementations in `Establish()` via `scenario.Services`. `Establish()` runs before `Because()`, so all registrations are in place when the pipeline is built: ```csharp public class when_adding_item_to_cart : Specification { readonly IInventoryService _inventory = Substitute.For(); readonly CommandScenario _scenario = new(); CommandResult _result = default!; void Establish() { _inventory.IsInStock("SKU-123").Returns(true); _scenario.Services.AddSingleton(_inventory); } async Task Because() => _result = await _scenario.Execute(new AddItemToCart("SKU-123", 2)); [Fact] void should_succeed() => _result.ShouldBeSuccessful(); } ``` ## Validating Without Executing [Section titled “Validating Without Executing”](#validating-without-executing) Use `Validate` instead of `Execute` to run only the authorization and validation filters without invoking the command handler. This is useful for verifying validation rules in isolation: ```csharp public class when_adding_item_with_empty_sku : Specification { readonly CommandScenario _scenario = new(); CommandResult _result = default!; async Task Because() => _result = await _scenario.Validate(new AddItemToCart(string.Empty, 2)); [Fact] void should_not_be_valid() => _result.ShouldHaveValidationErrors(); [Fact] void should_report_sku_error() => _result.ShouldHaveValidationErrorFor("Sku"); } ``` ## CommandResult Assertion Helpers [Section titled “CommandResult Assertion Helpers”](#commandresult-assertion-helpers) The `CommandResultShouldExtensions` class provides fluent BDD-style assertions for `CommandResult`. All helpers throw `CommandResultAssertionException` with a descriptive message on failure. | Method | Asserts that… | | -------------------------------------------------- | -------------------------------------------------------------------------------- | | `ShouldBeSuccessful()` | `IsSuccess` is `true`; prints all failure reasons on failure | | `ShouldNotBeSuccessful()` | `IsSuccess` is `false` | | `ShouldBeValid()` | `IsValid` is `true`; lists all validation errors on failure | | `ShouldHaveValidationErrors()` | `IsValid` is `false` | | `ShouldHaveValidationErrorFor(message)` | At least one validation error contains the given text | | `ShouldHaveValidationErrorBecauseOf(reason)` | At least one validation error carries the given `ValidationResultReason` | | `ShouldHaveConstraintViolationFor(constraintName)` | At least one validation error is a constraint violation for the named constraint | | `ShouldBeAuthorized()` | `IsAuthorized` is `true` | | `ShouldNotBeAuthorized()` | `IsAuthorized` is `false` | | `ShouldNotHaveExceptions()` | `HasExceptions` is `false` | | `ShouldHaveExceptions()` | `HasExceptions` is `true` | ### Assert the constraint name, not the message [Section titled “Assert the constraint name, not the message”](#assert-the-constraint-name-not-the-message) `ShouldHaveValidationErrorFor(message)` matches against text a human wrote, so the spec stops asserting anything the day someone rewords it — and it cannot tell one constraint from another when two produce similar copy. When a command was rejected by a Chronicle constraint, name the constraint instead. It is the same assertion Chronicle offers on an append result, so a spec says the same thing whether the events reach the store through a command or a raw append. ```csharp [Fact] void should_be_rejected_by_the_uniqueness_constraint() => _result.ShouldHaveConstraintViolationFor(AuthorConstraintNames.UniqueName); ``` ### Example: Validation spec [Section titled “Example: Validation spec”](#example-validation-spec) ```csharp public class when_adding_item_with_zero_quantity : Specification { readonly CommandScenario _scenario = new(); CommandResult _result = default!; async Task Because() => _result = await _scenario.Validate(new AddItemToCart("SKU-123", 0)); [Fact] void should_not_be_valid() => _result.ShouldHaveValidationErrors(); [Fact] void should_have_quantity_error() => _result.ShouldHaveValidationErrorFor("must be greater than zero"); } ``` ### Example: Authorization spec [Section titled “Example: Authorization spec”](#example-authorization-spec) ```csharp public class when_admin_command_executed_by_regular_user : Specification { readonly CommandScenario _scenario = new(); CommandResult _result = default!; void Establish() { // Arc authorization reads the current principal from IHttpRequestContextAccessor. // Supply a request context whose user lacks the "admin" role that DeleteAllOrders // requires via [Authorize(Roles = "admin")]. var requestContext = Substitute.For(); requestContext.User.Returns(new ClaimsPrincipal(new ClaimsIdentity( [new Claim(ClaimTypes.Role, "user")], authenticationType: "test"))); var requestContextAccessor = Substitute.For(); requestContextAccessor.Current.Returns(requestContext); _scenario.Services.AddSingleton(requestContextAccessor); } async Task Because() => _result = await _scenario.Execute(new DeleteAllOrders()); [Fact] void should_not_be_authorized() => _result.ShouldNotBeAuthorized(); } ``` ## What the Scenario Provides [Section titled “What the Scenario Provides”](#what-the-scenario-provides) `CommandScenario` registers logging without a sink — `ILogger` resolves as a no-op — and calls `Services.AddCratisArcCore()` when first initialized, which wires: * Type discovery for all handlers, validators, and filters * The real `ICommandPipeline` * All built-in validation and authorization filters Everything that runs in production runs in the spec — there is no hidden short-circuiting. No log output is produced by default, keeping scenarios lightweight — a console logger would otherwise spawn a background thread per scenario. To see log output while debugging a scenario, opt in before the first `Execute` or `Validate`: ```csharp _scenario.Services.AddLogging(logging => logging.AddConsole()); ``` ## Disposal [Section titled “Disposal”](#disposal) `CommandScenario` implements both `IDisposable` and `IAsyncDisposable`. Disposing it releases the service provider it built and disposes any disposable values extension packages placed in `Context` — the Chronicle extender’s `EventScenario` is cleaned up this way. Disposal is idempotent, and calling `Execute` or `Validate` on a disposed scenario throws `ObjectDisposedException`. With Cratis Specifications, dispose the scenario in `Destroy()`: ```csharp public class when_adding_item_to_cart : Specification { readonly CommandScenario _scenario = new(); void Destroy() => _scenario.Dispose(); // ... } ``` With plain xUnit, implement `IDisposable` (or `IAsyncDisposable`) on the test class and dispose the scenario there — xUnit disposes the test class after each test. Each scenario builds a full service provider on first use, so disposing it per test keeps long spec runs from accumulating providers. # MediatR, MVC, and Arc > If you already build .NET apps with MediatR and ASP.NET Core controllers, here's how those ideas map onto Arc. If you’ve built ASP.NET Core apps with controllers, DTOs, and MediatR, you already know most of the *concepts* in Arc. This page maps those familiar pieces onto Arc’s command/query model. ## The one-paragraph version [Section titled “The one-paragraph version”](#the-one-paragraph-version) In MVC, a controller action takes a request model, validates it, calls a handler, and returns a response. With MediatR, that often becomes an `IRequest` and an `IRequestHandler`. In Arc, the command is a record with a `Handle()` method on it; Arc maps that command to an HTTP endpoint and generates a typed client for your frontend. ## How the pieces map [Section titled “How the pieces map”](#how-the-pieces-map) | You know (MediatR / MVC) | In Arc | | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `IRequest` + `IRequestHandler` | A `[Command]` record with `Handle()` defined **on the record** | | Controller action + routing attributes | Automatic — Arc maps the command/query to HTTP for you | | Request/response DTOs | The command record itself; the result is what `Handle()` returns | | FluentValidation / `ModelState` | A `CommandValidator` discovered by convention | | MediatR pipeline behaviors | The command pipeline and filters | | `INotification` / handlers | A follow-up command, domain service, or optional [Chronicle reactor](/arc/backend/chronicle/) when you are event-sourcing | | A query action returning a DTO from EF | A **query** method on a `[ReadModel]`, served directly over HTTP | | Application-specific `fetch`/HttpClient on the frontend | A **generated TypeScript proxy** | ## What stays the same [Section titled “What stays the same”](#what-stays-the-same) * You still think in commands and queries — the CQRS split you already use with MediatR is first-class here. * You still write small, focused handlers and validators. * You still use dependency injection; constructor-inject collaborators into `Handle()` and into validators. ## What changes [Section titled “What changes”](#what-changes) * **Commands are endpoints.** The command record carries the request shape and the `Handle()` method; Arc maps it to HTTP. * **The handler sits with the intent.** `Handle()` lives on the command record, so the intent and its implementation sit together in one [vertical slice](/arc/) instead of across `Commands/` and `Handlers/` folders. * **The frontend model is generated.** The proxy is generated from your C# types, so command/query shape changes are caught by TypeScript. * **The read side is explicit.** Instead of returning arbitrary DTOs from controllers, you name the read model and expose query methods on it. Those methods are generated into the frontend just like commands. ## A side-by-side [Section titled “A side-by-side”](#a-side-by-side) A “register customer” feature in MediatR + MVC is often a request record, a handler, a validator, a controller action, and a frontend client call. In Arc, the same feature is modeled as a slice: ```csharp [Command] public record RegisterCustomer(CustomerId Id, CompanyName Name) { public Task Handle(IMongoCollection customers) => customers.InsertOneAsync(new Customer(Id, Name)); } [ReadModel] public record Customer([property: Key] CustomerId Id, CompanyName Name) { public static ISubject> AllCustomers(IMongoCollection customers) => customers.Observe(); } ``` Arc exposes both members over HTTP and generates the typed client. Your React component calls the generated command and query proxies; no controller, DTO mapper, or hand-written fetch layer sits between them. ## Where to go next [Section titled “Where to go next”](#where-to-go-next) * Build your first command in the [getting started](/arc/backend/getting-started/) guide. * See the full command and query options under [Backend](/arc/backend/). * Need history, replay, or reactors later? The [Chronicle integration](/arc/backend/chronicle/) shows how Arc adds event sourcing as an optional write-side choice. # Frontend > How Arc turns your C# commands and queries into typed React hooks — call your backend like a local function, with validation and live updates included. Building a React frontend against a backend usually means writing the same plumbing over and over: a `fetch` call here, a hand-written request type there, validation rules copied from the server, and a pile of state to track whether the request is in flight, succeeded, or failed. Every time the backend changes, you go hunting for the frontend code that drifted out of sync. Arc removes that whole layer. When you build a command or query in C#, Arc **generates a typed TypeScript proxy** for it. On the frontend you import that proxy and call its `.use()` hook — the request shape, the response shape, and the validation rules all come along, type-checked end to end. Change the C# and the generated proxy changes with it, so the compiler catches the drift instead of your users. ## How the pieces fit [Section titled “How the pieces fit”](#how-the-pieces-fit) ``` proxy generator .use() hook executes / observes C# command / query Generated TS proxy React component Arc endpoint ``` You write the backend once. The proxy generator runs on build and emits a typed client. Your React code consumes it through each proxy’s static `.use()` hook — and because the types flow across the boundary, there is no DTO to keep in sync and no untyped JSON to second-guess. ## Execute a command from React [Section titled “Execute a command from React”](#execute-a-command-from-react) A command is an intent to change something — *open an account*, *check out a book*. You define it in C#; Arc generates a proxy whose static `.use()` hook gives you a reactive instance, change tracking, and execution. * C# — backend ```csharp // Backend — the command and the read model it serves live together [Command] public record OpenAccount(AccountId Id, AccountHolder Owner) { public Task Handle(IMongoCollection accounts) => accounts.InsertOneAsync(new Account(Id, Owner)); } ``` * TypeScript — frontend ```tsx // Frontend — the generated proxy, driven by its static .use() hook import { OpenAccount } from './api/accounts/OpenAccount'; export const OpenAccountForm = () => { const [command] = OpenAccount.use(); const submit = async () => { const result = await command.execute(); if (result.isSuccess) { /* navigate away, show a toast, … */ } }; return ( <> command.owner = e.target.value} /> ); }; ``` The `.use()` hook returns a tuple — the reactive command instance, then a setter for updating several properties at once. Set a property and the component re-renders; call `command.execute()` and check `result.isSuccess`. The validation you declare on the command is surfaced on the proxy, so invalid input is caught on the client before the request ever leaves the browser. Don't hand-roll forms For real forms, reach for the [Command form](/arc/frontend/react/command-form/) fields and the [Components](/components/) library’s `CommandDialog` instead of wiring inputs by hand — they bind to the command, surface validation, and handle the in-flight state for you. ## Show live data with an observable query [Section titled “Show live data with an observable query”](#show-live-data-with-an-observable-query) A query reads data. An *observable* query keeps reading — its `.use()` hook holds a live connection and re-renders your component whenever the underlying read model changes, so your UI stays current without polling. ```tsx import { AllAccounts } from './api/accounts/AllAccounts'; export const AccountsList = () => { const [accounts] = AllAccounts.use(); // observable: re-renders as the read model changes return (
    {accounts.data.map((account) => (
  • {account.owner}
  • ))}
); }; ``` The result is a `QueryResultWithState` — `accounts.data` holds the rows, with `isPerforming`, `hasData`, and validation state alongside. When a command writes and the `AllAccounts` read model changes, every browser observing this query re-renders with the new list — no refresh, no refetch. That live loop, from a C# command to a React list, is the payoff of building the whole stack on Arc. ## Two ways to build: hooks or MVVM [Section titled “Two ways to build: hooks or MVVM”](#two-ways-to-build-hooks-or-mvvm) Most screens are simplest with the **hooks** shown above — they keep state in the component and read naturally. For complex, stateful screens you can opt into a **Model-View-ViewModel** structure that moves logic into a view model class. | Reach for… | When | | ------------------------------------------ | --------------------------------------------------------------------------------------------- | | [Hooks](/arc/frontend/react/) | The default. Forms, lists, and most screens — less boilerplate, state stays in the component. | | [MVVM for React](/arc/frontend/reactmvvm/) | Complex screens with substantial view logic you want to test and reuse apart from the markup. | Start with hooks; move a screen to MVVM only when its logic outgrows the component. ## Where to go next [Section titled “Where to go next”](#where-to-go-next) [Getting started (React) ](/arc/frontend/getting-started/)Wire Arc into a React app and call your first command and query. [Commands in React ](/arc/frontend/react/commands/)The .use() hook, validation, and command scopes. [Queries in React ](/arc/frontend/react/queries/)Observable, paged, and conditional queries. [Command forms ](/arc/frontend/react/command-form/)Typed form fields that bind straight to a command. [Identity in React ](/arc/frontend/react/identity/)Who the user is and what they're allowed to do. [Components ](/components/)Prebuilt dialogs, forms, and tables that consume these proxies. # Core The core is Arc’s frontend layer **beneath React** — the framework-agnostic TypeScript that the React hooks build on. Most apps consume it indirectly through [React](/arc/frontend/react/); reach for it directly when you’re working outside React (a service layer, a worker, a vanilla-TS integration) or you need lower-level control over how commands and queries execute. | Topic | What it covers | | -------------------------------------------- | ---------------------------------------------------------------------------------- | | [Commands](/arc/frontend/core/commands/) | Instantiating and executing commands without React — the runtime behind the hooks. | | [Queries](/arc/frontend/core/queries/) | Retrieving data and subscribing to observable queries at the core level. | | [Validation](/arc/frontend/core/validation/) | The validation rules and results that flow through commands and queries. | | [Identity](/arc/frontend/core/identity/) | The user-context primitives the higher layers expose. | | [Messaging](/arc/frontend/core/messaging/) | The messaging and communication patterns Arc uses under the hood. | Building UI? You almost certainly want [React](/arc/frontend/react/) instead — it wraps everything here in hooks. # Commands Core commands in Arc are the low-level TypeScript/JavaScript primitives used to execute state-changing operations. This section focuses on command contracts and runtime behavior in `@cratis/arc`. For React-specific ergonomics, see [React Commands](/arc/frontend/react/commands/). ## Capabilities [Section titled “Capabilities”](#capabilities) | Capability | What It Covers | Learn More | | ---------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------- | | Command contracts | `ICommand`, route metadata, execution shape, and command state model | [Command Contracts](/arc/frontend/core/commands/contracts/) | | Runtime configuration | Microservice routing, API base path, and global Arc configuration | [Configuration](/arc/frontend/core/commands/configuration/) | | Validation and results | Client-side validation, `CommandResult`, and error categories | [Validation And Results](/arc/frontend/core/commands/validation-and-results/) | | Backend integration | Controller-based/model-bound backend mapping and proxy generation | [Backend Integration](/arc/frontend/core/commands/integration/) | ## Related Documentation [Section titled “Related Documentation”](#related-documentation) * [Command Result](/arc/frontend/core/commands/command-result/) * [Validation](/arc/frontend/core/commands/validation/) * [Validation](/arc/frontend/core/validation/) * [Backend Commands](/arc/backend/commands/) # CommandResult When a command is executed, it returns a `CommandResult` that provides comprehensive information about the execution outcome. The result includes success/failure status, validation errors, authorization status, exceptions, and optional response data. ## Structure [Section titled “Structure”](#structure) The `CommandResult` interface provides the following properties: ```typescript interface ICommandResult { readonly correlationId: Guid; readonly isSuccess: boolean; readonly isAuthorized: boolean; readonly isValid: boolean; readonly hasExceptions: boolean; readonly validationResults: ValidationResult[]; readonly exceptionMessages: string[]; readonly exceptionStackTrace: string; readonly response?: TResponse; } ``` ## Status Properties [Section titled “Status Properties”](#status-properties) Understanding the different status properties is crucial for proper error handling: ### isSuccess [Section titled “isSuccess”](#issuccess) Indicates whether the command executed successfully **overall**. This is the primary indicator of whether the operation completed as intended. * `true`: The command executed without any issues (authorized, valid, and no exceptions) * `false`: The command failed for one or more reasons (could be authorization, validation, or exceptions) **Use this when:** * You want to know if the overall operation succeeded * You need a single boolean check for success/failure * You’re implementing simple success/failure UI feedback ```typescript const result = await command.execute(); if (result.isSuccess) { // Show success message } else { // Handle failure (check other properties for details) } ``` ### isAuthorized [Section titled “isAuthorized”](#isauthorized) Indicates whether Arc’s command pipeline authorized this command. * `true`: The user has permission to execute the command * `false`: The generated command endpoint returned an authorization failure, typically HTTP 403. An upstream authentication challenge can return HTTP 401 before a `CommandResult` payload is produced. **Use this when:** * You need to distinguish authorization failures from other errors * You want to show specific “access denied” messages * You need to redirect users to login or show permission requests ```typescript const result = await command.execute(); if (!result.isAuthorized) { // Show "Access Denied" message // Redirect to login or request permissions } ``` ### isValid [Section titled “isValid”](#isvalid) Indicates whether the command payload passed validation rules. * `true`: All validation rules passed * `false`: One or more validation rules failed (check `validationResults` for details) **Use this when:** * You need to display validation errors to users * You want to highlight invalid form fields * You’re implementing client-side validation feedback ```typescript const result = await command.execute(); if (!result.isValid) { // Display validation errors from result.validationResults result.validationResults.forEach(error => { console.log(`${error.members.join('.')}: ${error.message}`); }); } ``` ### hasExceptions [Section titled “hasExceptions”](#hasexceptions) Indicates whether any exceptions occurred during command execution. * `true`: One or more exceptions were thrown * `false`: No exceptions occurred **Use this when:** * You need to catch unexpected server errors * You want to log errors for debugging * You need to show generic error messages for system failures ```typescript const result = await command.execute(); if (result.hasExceptions) { // Log exception details console.error('Exceptions:', result.exceptionMessages); console.error('Stack trace:', result.exceptionStackTrace); // Show user-friendly error message showErrorMessage('An unexpected error occurred. Please try again.'); } ``` ## Understanding the Relationship [Section titled “Understanding the Relationship”](#understanding-the-relationship) The status properties work together to provide a complete picture: ```typescript // Scenario 1: Complete success // isSuccess = true, isAuthorized = true, isValid = true, hasExceptions = false // Scenario 2: Authorization failure // isSuccess = false, isAuthorized = false, isValid = true, hasExceptions = false // Scenario 3: Validation failure // isSuccess = false, isAuthorized = true, isValid = false, hasExceptions = false // Scenario 4: Exception occurred // isSuccess = false, isAuthorized = true, isValid = true, hasExceptions = true // Scenario 5: Multiple failures (validation + exception) // isSuccess = false, isAuthorized = true, isValid = false, hasExceptions = true ``` **Key principle:** If `isSuccess` is `false`, check the other properties to determine **why** it failed. ## Accessing Data [Section titled “Accessing Data”](#accessing-data) ### Response Data [Section titled “Response Data”](#response-data) When the command returns data, access it through the `response` property: ```typescript interface CreateAccountResponse { accountId: string; accountNumber: string; } const result = await createAccountCommand.execute(); if (result.isSuccess && result.response) { const { accountId, accountNumber } = result.response; console.log(`Account created: ${accountNumber}`); } ``` The response is typed according to the command’s generic parameter, providing full type safety. ### Validation Results [Section titled “Validation Results”](#validation-results) Access detailed validation errors through the `validationResults` array: ```typescript const result = await command.execute(); if (!result.isValid) { result.validationResults.forEach(error => { console.log(`Severity: ${error.severity}`); console.log(`Message: ${error.message}`); console.log(`Members: ${error.members.join('.')}`); console.log(`State: ${JSON.stringify(error.state)}`); console.log(`Reason: ${error.reason}`); }); } ``` Each `ValidationResult` contains: * `severity`: The severity level of the validation error * `message`: A developer-facing message. Only meant to be shown to a user when `reason` is `rule` * `members`: Array of property names that failed validation * `state`: Additional context, set by whoever authored the rule (FluentValidation’s `WithState`) * `reason`: What composed the result — see below * `reasonDetail`: Which specific thing within `reason` produced the result — the name of the violated constraint for `constraintViolation`. `undefined` when the reason carries no finer identity ### Telling one kind of rejection from another [Section titled “Telling one kind of rejection from another”](#telling-one-kind-of-rejection-from-another) A rejection your own rules produced and one the framework composed on your behalf arrive in the same array, in the same shape. `reason` is what separates them, so you never have to match the message text: | `reason` | What happened | What to do | | ---------------------- | ---------------------------------------------------- | ---------------------------------------------------- | | `rule` | A rule you authored rejected the input. The default. | Show `message` — it is your copy | | `concurrencyViolation` | The event source moved on since it was read | Offer a retry; `state` carries the violation | | `constraintViolation` | A constraint on the event store rejected the append | Show your own copy for that constraint | | `validatorFailed` | A validator threw; nothing the author wrote survives | Show your own generic copy, and check the server log | ```typescript import { ValidationResultReason } from '@cratis/arc'; const result = await command.execute(); if (result.validationResults.some(_ => _.reason === ValidationResultReason.ConcurrencyViolation)) { // Someone else changed this while the form was open. Re-read and resubmit — the // input was never the problem, so do not put an error on any field. return retry(); } ``` ### Telling one constraint from another [Section titled “Telling one constraint from another”](#telling-one-constraint-from-another) `reason` says a constraint on the event store rejected the append; `reasonDetail` says **which** one. Two uniqueness constraints on the same command produce the same `reason`, so branching on it alone cannot pick the copy that belongs to each — and the message is developer text you should not be matching on either. Branch on the constraint name: ```typescript const result = await command.execute(); const rejectedBy = (constraint: string) => result.validationResults.some(_ => _.reason === ValidationResultReason.ConstraintViolation && _.reasonDetail === constraint); if (rejectedBy('UniqueOrganizationNumber')) { return setFieldError('organizationNumber', 'That organization number is already registered.'); } ``` The value is the constraint’s own name as Chronicle reports it on the violation — the same name a backend spec asserts with `ShouldHaveConstraintViolationFor`. Note `reason` is an open set, not an enum — Arc, Chronicle and your own code can all mint values. Treat an unrecognized value the way you treat `rule`, and never `switch` over it exhaustively. Only `rule` means the message is yours to show. Everything else is Cratis-authored developer text: it is in English, it is not localized, and it describes framework state rather than the user’s situation. Map those to your own copy. ### Exception Details [Section titled “Exception Details”](#exception-details) Access exception information when `hasExceptions` is `true`: ```typescript const result = await command.execute(); if (result.hasExceptions) { // Array of exception messages result.exceptionMessages.forEach(msg => { console.error('Exception:', msg); }); // Full stack trace for debugging console.error('Stack trace:', result.exceptionStackTrace); } ``` ## Chaining Callbacks [Section titled “Chaining Callbacks”](#chaining-callbacks) `CommandResult` supports a fluent API for handling different outcomes: ```typescript const result = await command.execute(); result .onSuccess((response) => { console.log('Success!', response); }) .onFailed((commandResult) => { console.log('Failed:', commandResult); }) .onUnauthorized(() => { console.log('Not authorized'); }) .onValidationFailure((validationResults) => { console.log('Validation errors:', validationResults); }) .onException((messages, stackTrace) => { console.error('Exception:', messages, stackTrace); }); ``` Each callback method returns the `CommandResult` instance, allowing you to chain multiple handlers. ## Best Practices [Section titled “Best Practices”](#best-practices) ### 1. Check isSuccess First [Section titled “1. Check isSuccess First”](#1-check-issuccess-first) Always start by checking `isSuccess` for the overall outcome: ```typescript const result = await command.execute(); if (result.isSuccess) { // Handle success } else { // Check specific failure reasons if (!result.isAuthorized) { /* ... */ } if (!result.isValid) { /* ... */ } if (result.hasExceptions) { /* ... */ } } ``` ### 2. Provide Specific Error Messages [Section titled “2. Provide Specific Error Messages”](#2-provide-specific-error-messages) Use the specific status properties to give users meaningful feedback: ```typescript const result = await command.execute(); if (!result.isSuccess) { if (!result.isAuthorized) { showMessage('You do not have permission to perform this action.'); } else if (!result.isValid) { showValidationErrors(result.validationResults); } else if (result.hasExceptions) { showMessage('An unexpected error occurred. Please contact support.'); } } ``` ### 3. Log Exceptions for Debugging [Section titled “3. Log Exceptions for Debugging”](#3-log-exceptions-for-debugging) Always log exception details when `hasExceptions` is `true`: ```typescript if (result.hasExceptions) { logger.error('Command execution failed', { messages: result.exceptionMessages, stackTrace: result.exceptionStackTrace, correlationId: result.correlationId }); } ``` ### 4. Use Correlation ID for Tracking [Section titled “4. Use Correlation ID for Tracking”](#4-use-correlation-id-for-tracking) The `correlationId` helps track command executions across the system: ```typescript console.log(`Command executed with correlation ID: ${result.correlationId}`); ``` This is especially useful for debugging and support scenarios. ## Related Topics [Section titled “Related Topics”](#related-topics) * [Commands](/arc/frontend/core/commands/) - Core command concepts and usage * [React Commands](/arc/frontend/react/commands/) - Using commands in React components * [Validation](/arc/backend/commands/validation/) - Understanding validation rules # Command Configuration Command routing and endpoint settings can be configured per command instance, but are usually configured once through the React `` root component. ## Microservice Routing [Section titled “Microservice Routing”](#microservice-routing) Per command: ```typescript command.setMicroservice('user-service'); ``` Recommended global setup: * Configure `microservice` on `` so every command and query uses the same service routing strategy. * See [Arc Configuration](/arc/frontend/react/arc/#microservice-support). ## API Base Path [Section titled “API Base Path”](#api-base-path) Per command: ```typescript command.setApiBasePath('/api/v1'); ``` Recommended global setup: * Configure `apiBasePath` on `` for consistent routing across the application. * See [Arc Configuration](/arc/frontend/react/arc/#configuration-options). ## See Also [Section titled “See Also”](#see-also) * [Command Contracts](/arc/frontend/core/commands/contracts/) * [Backend Integration](/arc/frontend/core/commands/integration/) # Command Contracts Core command support in `@cratis/arc` is built around typed command classes generated from backend command definitions. ## ICommand [Section titled “ICommand”](#icommand) The core command interface provides execution and state-management capabilities: ```typescript interface ICommand extends ICanBeConfigured { readonly route: string; readonly roles: string[]; readonly propertyDescriptors: PropertyDescriptor[]; execute( allowedSeverity?: ValidationResultSeverity, ignoreWarnings?: boolean ): Promise>; validate(): Promise>; clear(): void; setInitialValues(values: TCommandContent): void; setInitialValuesFromCurrentValues(): void; revertChanges(): void; readonly hasChanges: boolean; propertyChanged(property: string): void; onPropertyChanged(callback: PropertyChanged, thisArg: object): void; } ``` ## Change Tracking [Section titled “Change Tracking”](#change-tracking) Commands track property changes automatically. * `hasChanges` indicates whether current values differ from the baseline. * `setInitialValues()` sets an explicit baseline. * `setInitialValuesFromCurrentValues()` snapshots current values as baseline. * `revertChanges()` restores baseline values. * `validate()` runs the server-side authorization and validation path without executing the handler. ## Property Change Notifications [Section titled “Property Change Notifications”](#property-change-notifications) Commands expose property-change callbacks for reactive flows: ```typescript command.onPropertyChanged((property: string) => { console.log(`Property ${property} changed`); }); ``` ## See Also [Section titled “See Also”](#see-also) * [Validation And Results](/arc/frontend/core/commands/validation-and-results/) * [Configuration](/arc/frontend/core/commands/configuration/) * [React Commands](/arc/frontend/react/commands/) # Backend Integration Core commands are designed to align with backend command endpoints and generated proxies. ## Backend Command Styles [Section titled “Backend Command Styles”](#backend-command-styles) Arc supports both backend styles: * [Controller-based Commands](/arc/backend/commands/controller-based/) * [Model-bound Commands](/arc/backend/commands/model-bound/) Both styles produce strongly typed frontend command proxies. ## Proxy Generation Benefits [Section titled “Proxy Generation Benefits”](#proxy-generation-benefits) * Compile-time type safety * IDE IntelliSense and navigation * Automatic regeneration when backend contracts change * No manual HTTP request wiring For setup and configuration, see [Backend Proxy Generation](/arc/backend/proxy-generation/). ## React Layer [Section titled “React Layer”](#react-layer) For higher-level usage patterns in React components, see [React Commands](/arc/frontend/react/commands/). # Command Validation Command validation enables pre-flight validation of commands without executing them. This provides early feedback to users before performing potentially expensive or state-changing operations. ## Purpose [Section titled “Purpose”](#purpose) The validation mechanism allows you to check authorization and validation rules without executing the command handler. This is essential for: * **Early User Feedback**: Show validation errors before the user submits a form * **UX Improvements**: Enable/disable submit buttons based on validation state * **Authorization Checks**: Verify user permissions without side effects * **Progressive Validation**: Validate fields as users interact with forms ## How It Works [Section titled “How It Works”](#how-it-works) When you validate a command, the request is sent to the backend validation endpoint where: 1. All command filters run (authorization, validation) 2. The command handler is **not** executed 3. A `CommandResult` is returned with validation and authorization status 4. No side effects occur on the system For details on the backend validation pipeline, see [Backend Command Validation](/arc/backend/commands/command-validation/). ## Command.validate() Method [Section titled “Command.validate() Method”](#commandvalidate-method) All generated TypeScript command proxies include a `validate()` method alongside the `execute()` method: ```typescript interface ICommand { /** * Validate the command without executing it. * Returns validation and authorization status. */ validate(): Promise>; /** * Execute the command. */ execute(): Promise>; } ``` ## Basic Usage [Section titled “Basic Usage”](#basic-usage) ```typescript import { CreateOrder } from './generated/commands'; async function validateOrder() { const command = new CreateOrder(); command.orderNumber = 'ORD-12345'; command.customerId = '550e8400-e29b-41d4-a716-446655440000'; // Validate without executing const result = await command.validate(); if (result.isSuccess) { console.log('Command is valid and authorized'); } else { if (!result.isAuthorized) { console.log('User not authorized'); } if (!result.isValid) { console.log('Validation errors:', result.validationResults); } } } ``` ## CommandResult Structure [Section titled “CommandResult Structure”](#commandresult-structure) Both `execute()` and `validate()` return the same `CommandResult` structure: ```typescript interface CommandResult { correlationId: string; isSuccess: boolean; // Overall success (authorized + valid + no exceptions) isAuthorized: boolean; // Authorization status isValid: boolean; // Validation status hasExceptions: boolean; // Whether exceptions occurred validationResults: ValidationResult[]; exceptionMessages: string[]; exceptionStackTrace: string; response?: TResponse; // Only populated on execute() } interface ValidationResult { message: string; members: string[]; severity: 'Error' | 'Warning' | 'Info'; } ``` **Important**: The `response` property will be `null` or `undefined` when using `validate()` since the handler is not executed. ## Validation Filters [Section titled “Validation Filters”](#validation-filters) The `validate()` method runs all registered command filters on the backend: * **AuthorizationFilter**: Checks user permissions * **DataAnnotationValidationFilter**: Validates data annotations * **FluentValidationFilter**: Runs FluentValidation validators For more information, see [Backend Command Filters](/arc/backend/commands/command-filters/). ## Best Practices [Section titled “Best Practices”](#best-practices) ### When to Use Validate [Section titled “When to Use Validate”](#when-to-use-validate) ✅ **Good Use Cases:** * Form validation as users type or blur fields * Enabling/disabling submit buttons based on validation state * Showing validation messages before submission * Checking authorization before showing UI elements ❌ **Avoid:** * Calling validate() immediately before execute() (execute already validates) * Over-validating (don’t validate on every keystroke for performance) * Using validate() as a substitute for client-side validation ### Performance Considerations [Section titled “Performance Considerations”](#performance-considerations) * Validation makes a server round-trip, so use judiciously * Consider debouncing validation calls for real-time feedback * Client-side validation is still important for immediate feedback * Server validation ensures security and data integrity ### Example: Debounced Validation [Section titled “Example: Debounced Validation”](#example-debounced-validation) ```typescript let validationTimeout: NodeJS.Timeout; function debounceValidation(command: ICommand, onResult: (result: CommandResult) => void) { clearTimeout(validationTimeout); validationTimeout = setTimeout(async () => { const result = await command.validate(); onResult(result); }, 500); } // Usage const command = new CreateOrder(); command.orderNumber = 'ORD-12345'; debounceValidation(command, (result) => { if (!result.isSuccess) { console.log('Validation errors:', result.validationResults); } }); ``` ## Security Considerations [Section titled “Security Considerations”](#security-considerations) * Validation endpoints run the same authorization filters as execute endpoints * Unauthorized users receive 401/403 responses from validation endpoints * Validation does not expose sensitive data since handlers aren’t executed * Validation results may reveal authorization policies (by design) ## Framework-Specific Usage [Section titled “Framework-Specific Usage”](#framework-specific-usage) For React-specific patterns and hooks, see [React Command Validation](/arc/frontend/react/commands/validation/). # Validation And Results Commands return `CommandResult` and perform validation before execution. ## Client-Side Validation [Section titled “Client-Side Validation”](#client-side-validation) Validation metadata is generated from backend FluentValidation rules by the proxy generator. ```typescript const command = new CreateUserCommand(); command.email = ''; command.age = 15; const result = await command.execute(); // result.isValid === false // result.validationResults contains validation errors ``` For deeper validation behavior, see [Validation](/arc/frontend/core/validation/) and [Validation](/arc/frontend/core/commands/validation/). ## Result Shape [Section titled “Result Shape”](#result-shape) A command result includes status and diagnostics such as: * Success/failure status * Validation details * Response payload (when present) * Exception details For full `CommandResult` details, see [Command Result](/arc/frontend/core/commands/command-result/). ## Error Categories [Section titled “Error Categories”](#error-categories) Typical failure categories include: * Validation errors * Network and timeout failures * HTTP status failures * Domain/application exceptions ## See Also [Section titled “See Also”](#see-also) * [Command Result](/arc/frontend/core/commands/command-result/) * [Validation](/arc/frontend/core/commands/validation/) * [React Commands](/arc/frontend/react/commands/) # Identity The frontend identity is based on information it gets from a cookie called `.cratis-identity`. The purpose of this is to be able to provide identity information to the client at the first render. This allows for a better developer and user experience, as there is no need to call the backend for details about the user. While in development mode on your local machine, if this cookie does not exist it will call the `.cratis/me` endpoint from the frontend itself. This makes it possible to work without having to simulate the entire production environment locally. > 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 principals](/arc/general/generating-principal/). This information found in the cookie is a base64 encoded string containing the JSON structure that is expected. ## Identity provider [Section titled “Identity provider”](#identity-provider) Identity is a read only feature in the frontend. It can’t be manipulated, as it is owned by the backend or the ingress. All access to identity goes through what is called `IdentityProvider`. The `IdentityProvider` provides functionality for getting the current identity. ```typescript import { IdentityProvider } from '@cratis/arc/identity'; const identity = await IdentityProvider.getCurrent(); console.log(`Hello '${identity.name}'`); ``` > Note that the `getCurrent()` method is an asynchronous operation that returns a promise. The reason for this is that if the cookie is not found, it will call the `.cratis/me` endpoint to try to get the identity. ## Details [Section titled “Details”](#details) Part of the identity can hold details that are beyond what the identity provider provides. These details are application specific and something that your application or ingress should be responsible for filling out. Details can be considered optional, as that might not be a requirement for your application. The `getCurrent()` method takes a generic parameter that allows you to specify the type of the details object. ```typescript import { IdentityProvider } from '@cratis/arc/identity'; type IdentityDetails = { department: string, age: number }; const identity = await IdentityProvider.getCurrent(); console.log(`Hello '${identity.name}' from ´${identity.details.department}`); ``` ## IIdentity [Section titled “IIdentity”](#iidentity) The return type coming from `getCurrent()` looks like the following: | Name | Type | Description | | -------- | ------------------------- | ------------------------------------------------------------------ | | id | string | The unique identifier from the identity provider | | name | string | The user name | | roles | string\[] | Array of roles the identity is in | | details | any / type | Any additional identity details with type given, defaults to `any` | | isInRole | (role: string) => boolean | Method to check if the identity is in a specific role | ## Role checking [Section titled “Role checking”](#role-checking) The identity includes information about the roles assigned to the user. You can check if a user is in a specific role using the `isInRole()` method: ```typescript import { IdentityProvider } from '@cratis/arc/identity'; const identity = await IdentityProvider.getCurrent(); if (identity.isInRole('Admin')) { console.log('User is an admin'); } ``` You can also access the roles array directly: ```typescript import { IdentityProvider } from '@cratis/arc/identity'; const identity = await IdentityProvider.getCurrent(); console.log(`User roles: ${identity.roles.join(', ')}`); ``` ## Refresh [Section titled “Refresh”](#refresh) In some scenarios you might need to refresh the identity. Typically if the user has been granted more access or details has been updated. Rather than having your user log out and back in again, you can issue a refresh. Since the cookie is there and not governed by the frontend, it needs to call the backend or ingress to perform the refresh. The refresh calls the `.cratis/me` endpoint which returns the identity and details. This endpoint should also be responsible for updating the cookie so that any call to `getCurrent()` on the `IdentityProvider` gives you the correct identity and details. To refresh the identity you can call the `refresh()` method on the identity object itself. ```typescript import { IdentityProvider } from '@cratis/arc/identity'; let identity = await IdentityProvider.getCurrent(); identity = await identity.refresh(); ``` The identity object is designed to be immutable, leading to the `refresh()` method having to return a new instance. This means that the original `identity` instance won’t be updated and you would have to replace it if you have it as a variable. # Messaging Arc core provides a typed publish/subscribe messenger through `@cratis/arc/messaging`. ## IMessenger [Section titled “IMessenger”](#imessenger) `IMessenger` publishes and subscribes by runtime type: ```ts import { IMessenger } from '@cratis/arc/messaging'; class UserSelected { constructor(readonly userId: string) { } } messenger.subscribe(UserSelected, message => { console.log(message.userId); }); messenger.publish(new UserSelected('42')); ``` ## Scoped hierarchy [Section titled “Scoped hierarchy”](#scoped-hierarchy) `Messenger` can be created with a parent messenger: ```ts import { Messenger } from '@cratis/arc/messaging'; const root = new Messenger(); const child = new Messenger(root); ``` Default behavior: * Messages published in a child stay in that branch (no bubbling to parent). * Messages published in a parent trickle down to all descendants recursively. ### Override properties [Section titled “Override properties”](#override-properties) Each messenger has two properties: * `bubbleToParent` (default `false`) — bubble published messages to parent. * `trickleDownToChildren` (default `true`) — forward messages to child messengers. # Queries Cratis Arc provides comprehensive TypeScript/JavaScript support for queries, enabling seamless integration between your frontend and backend through type-safe, automatically generated proxies. Queries retrieve data from your backend and are executed as HTTP GET operations against your backend controllers. ## Overview [Section titled “Overview”](#overview) The frontend query system provides: * **Type-safe interfaces** for queries * **HTTP integration** using the Fetch API * **Automatic parameter validation** for queries * **Configuration flexibility** for different environments * **Microservice support** for distributed architectures * **Built-in sorting and paging** support * **Request cancellation** for improved performance ## IQuery Interface [Section titled “IQuery Interface”](#iquery-interface) The base query interface provides common functionality: ```typescript interface IQuery { sorting: Sorting; paging: Paging; } ``` ## IQueryFor Interface [Section titled “IQueryFor Interface”](#iqueryfor-interface) The specific query interface adds execution capabilities: ```typescript interface IQueryFor extends IQuery { readonly route: string; defaultValue: TDataType; parameters: TParameters | undefined; perform(args?: TParameters): Promise>; } ``` ## Key Features [Section titled “Key Features”](#key-features) ### Client-Side Validation [Section titled “Client-Side Validation”](#client-side-validation) Queries automatically validate parameters before executing server calls. Validation rules are defined on the backend using FluentValidation and automatically extracted by the ProxyGenerator: ```typescript const query = new SearchUsersQuery(); query.parameters = { searchTerm: 'ab', minAge: -5 }; // Invalid const result = await query.perform(); // Validation runs client-side before server call // result.isValid === false // result.validationResults contains error details ``` For more information about validation, see [Validation](/arc/frontend/core/validation/). ### Parameter Validation [Section titled “Parameter Validation”](#parameter-validation) Queries automatically validate required parameters before execution, preventing unnecessary network requests for incomplete data. ### Sorting and Paging [Section titled “Sorting and Paging”](#sorting-and-paging) Built-in support for: * **Sorting**: Field-based sorting with ascending/descending direction * **Paging**: Page number and page size management ### Request Cancellation [Section titled “Request Cancellation”](#request-cancellation) Queries support automatic request cancellation when new requests are made, preventing race conditions and unnecessary processing. ## Integration with Backend [Section titled “Integration with Backend”](#integration-with-backend) The frontend query system is designed to work seamlessly with the backend through: ### Controller-Based Queries [Section titled “Controller-Based Queries”](#controller-based-queries) Backend queries are implemented as controller actions that handle HTTP GET endpoints to retrieve data. For detailed information about implementing backend queries, see [Backend Queries](/arc/backend/queries/). ### Automatic Proxy Generation [Section titled “Automatic Proxy Generation”](#automatic-proxy-generation) The most powerful feature of this system is the automatic generation of TypeScript proxies from your backend controllers. This eliminates the need for: * Manual HTTP client code * Type definitions that can become out of sync * Consulting API documentation for parameter requirements **Key Benefits:** * **Compile-time type safety**: Catch integration errors at build time * **IntelliSense support**: Get autocomplete and parameter hints in your IDE * **Automatic updates**: Proxies regenerate when backend changes * **Zero maintenance**: No manual synchronization between frontend and backend For comprehensive information about setting up and configuring proxy generation, see [Proxy Generation](/arc/backend/proxy-generation/). ## Configuration [Section titled “Configuration”](#configuration) Queries support configuration for different deployment scenarios: ### Microservice Configuration [Section titled “Microservice Configuration”](#microservice-configuration) ```typescript query.setMicroservice('inventory-service'); ``` ### API Base Path Configuration [Section titled “API Base Path Configuration”](#api-base-path-configuration) ```typescript query.setApiBasePath('/api/v1'); ``` ### Observable Query Transport (`queryDirectMode`) [Section titled “Observable Query Transport (queryDirectMode)”](#observable-query-transport-querydirectmode) By default, observable queries route through the centralized hub endpoints. Set `Globals.queryDirectMode = true` to connect each observable query directly to its own WebSocket URL, bypassing the hub. This setting is typically managed via the `` React component prop `queryDirectMode` (see [React query integration](/arc/frontend/react/queries/)), but can also be set directly on `Globals` for non-React environments: ```typescript import { Globals } from '@cratis/arc'; // Connect each observable query directly (bypass hub) Globals.queryDirectMode = true; ``` | Value | Description | | ----------------- | ------------------------------------------------------------------------------------ | | `false` (default) | Route through the centralized hub (`/.cratis/queries/sse` or `/.cratis/queries/ws`). | | `true` | Connect directly to each query’s own per-query WebSocket URL. | ## Error Handling [Section titled “Error Handling”](#error-handling) The system provides comprehensive error handling for queries: ### Query Errors [Section titled “Query Errors”](#query-errors) * **Parameter validation**: Client-side validation before request * **Network failures**: Automatic fallback to default values * **Timeout handling**: Request cancellation and retry logic ## Best Practices [Section titled “Best Practices”](#best-practices) ### Query Usage [Section titled “Query Usage”](#query-usage) 1. **Set default values** to prevent undefined states 2. **Use parameters** consistently for reusable queries 3. **Implement proper loading states** during execution 4. **Handle empty results** appropriately ### Performance Considerations [Section titled “Performance Considerations”](#performance-considerations) 1. **Leverage automatic request cancellation** for queries 2. **Implement proper error boundaries** for network failures 3. **Cache query results** when appropriate ## Next Steps [Section titled “Next Steps”](#next-steps) * Learn about [React query integration](/arc/frontend/react/queries/) for React-specific query patterns * Explore [Commands](/arc/frontend/core/commands/) for state modification operations * Understand [MVVM patterns](/arc/frontend/reactmvvm/) for more sophisticated frontend architectures * Set up [Proxy Generation](/arc/backend/proxy-generation/) to automatically generate your query proxies # Query Configuration Queries can be configured for service routing and endpoint behavior in both React and non-React environments. ## Microservice Routing [Section titled “Microservice Routing”](#microservice-routing) Per query: ```typescript query.setMicroservice('inventory-service'); ``` In React applications, configure this globally with ``. ## API Base Path [Section titled “API Base Path”](#api-base-path) Per query: ```typescript query.setApiBasePath('/api/v1'); ``` In React applications, set this globally with ``. ## Observable Query Transport (`queryDirectMode`) [Section titled “Observable Query Transport (queryDirectMode)”](#observable-query-transport-querydirectmode) By default, observable queries use centralized hub endpoints. Set `Globals.queryDirectMode = true` to bypass the hub and connect directly per query. ```typescript import { Globals } from '@cratis/arc'; Globals.queryDirectMode = true; ``` | Value | Description | | ----------------- | -------------------------------------------------------------- | | `false` (default) | Route through `/.cratis/queries/sse` or `/.cratis/queries/ws`. | | `true` | Connect directly to each observable query WebSocket URL. | In React, this is usually configured through ``. See [React query integration](/arc/frontend/react/queries/). ## Query Cache Retention (`queryCacheRetentionMs`) [Section titled “Query Cache Retention (queryCacheRetentionMs)”](#query-cache-retention-querycacheretentionms) Controls how long (in milliseconds) the query cache keeps an entry alive after the last subscriber releases it. The default is `30 000` ms (30 seconds). ```typescript import { Globals } from '@cratis/arc'; // Keep cached data for 60 seconds after the last subscriber unmounts. Globals.queryCacheRetentionMs = 60_000; // Restore immediate eviction. Globals.queryCacheRetentionMs = 0; ``` In React applications, set this through `` instead of modifying `Globals` directly. See [React query configuration](/arc/frontend/react/queries/configuration/). ## See Also [Section titled “See Also”](#see-also) * [Observable Queries](/arc/frontend/react/queries/observable-queries/) * [Observable Query Multiplexing](/arc/frontend/react/queries/observable-query-multiplexing/) # Query Contracts Core query support in `@cratis/arc` is built on typed query classes and generated proxies. ## IQuery [Section titled “IQuery”](#iquery) The base query interface holds cross-cutting query concerns: ```typescript interface IQuery extends ICanBeConfigured { get sorting(): Sorting; set sorting(value: Sorting); get paging(): Paging; set paging(value: Paging); } ``` ## IQueryFor [Section titled “IQueryFor”](#iqueryfor) `IQueryFor` adds route, typed parameters, default value, and execution: ```typescript interface IQueryFor extends IQuery, IHaveParameters { readonly route: string; readonly requiredRequestParameters: string[]; readonly defaultValue: TDataType; readonly roles: string[]; get parameters(): TParameters | undefined; set parameters(value: TParameters); perform(args?: TParameters): Promise>; } ``` ## Built-in Concerns [Section titled “Built-in Concerns”](#built-in-concerns) Query contracts include: * Typed parameters and responses * Required route/request parameter metadata * Sorting and paging metadata * Required roles for UI decisions * Default values for predictable initialization * Execution through `perform()` ## See Also [Section titled “See Also”](#see-also) * [Validation And Behavior](/arc/frontend/core/queries/validation-and-behavior/) * [React Queries](/arc/frontend/react/queries/) # Backend Integration Core queries map directly to backend query endpoints and generated query proxies. ## Backend Query Styles [Section titled “Backend Query Styles”](#backend-query-styles) Arc supports both backend styles: * [Controller-based Queries](/arc/backend/queries/controller-based/) * [Model-bound Queries](/arc/backend/queries/model-bound/) Both styles generate equivalent frontend proxy ergonomics. ## Proxy Generation Benefits [Section titled “Proxy Generation Benefits”](#proxy-generation-benefits) * Compile-time contract safety * IntelliSense for query parameters and results * Automatic sync with backend contract changes * Consistent wrapper/result handling For setup details, see [Backend Proxy Generation](/arc/backend/proxy-generation/). ## React Layer [Section titled “React Layer”](#react-layer) For React hook-level patterns (`use`, Suspense, observable, paging), see [React Queries](/arc/frontend/react/queries/). # Queries Core queries in Arc are the low-level TypeScript/JavaScript primitives used to retrieve data from backend query endpoints. This section focuses on contracts and runtime behavior in `@cratis/arc`. For React hook-based usage, see [React Queries](/arc/frontend/react/queries/). ## Capabilities [Section titled “Capabilities”](#capabilities) | Capability | What It Covers | Learn More | | ----------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------ | | Query contracts | `IQuery`, `IQueryFor`, sorting, paging, and typed execution | [Query Contracts](/arc/frontend/core/queries/contracts/) | | Runtime configuration | Microservice routing, API base path, and observable transport mode | [Configuration](/arc/frontend/core/queries/configuration/) | | Validation and behavior | Client-side validation, request behavior, and error categories | [Validation And Behavior](/arc/frontend/core/queries/validation-and-behavior/) | | Backend integration | Controller-based/model-bound mapping and proxy generation | [Backend Integration](/arc/frontend/core/queries/integration/) | ## Related Documentation [Section titled “Related Documentation”](#related-documentation) * [Validation](/arc/frontend/core/validation/) * [Backend Queries](/arc/backend/queries/) * [React Queries](/arc/frontend/react/queries/) # Validation And Behavior Core queries validate input and provide predictable execution behavior. ## Client-Side Validation [Section titled “Client-Side Validation”](#client-side-validation) Validation metadata is generated from backend FluentValidation rules through the proxy generator. ```typescript const query = new SearchUsersQuery(); query.parameters = { searchTerm: 'ab', minAge: -5 }; const result = await query.perform(); // result.isValid === false // result.validationResults contains validation errors ``` Observable queries validate the same way. `perform()` returns an invalid result, and `subscribe()` delivers one to your callback instead of opening a connection: ```typescript const query = new ObserveUsers(); query.subscribe(result => { // result.isValid === false when the arguments were rejected, // which is distinct from a valid result that simply has no data yet }, { minAge: -5 }); ``` Because `subscribe()` validates the arguments it is given, a subscription started before its arguments are available is rejected rather than left open. Gate it with `ObservableQueryWhen` so the subscription only starts once the arguments exist: ```tsx {/* subscribes only once authorId has a value */} ``` Client-side validation is a convenience, not a gate — every rule it applies is also enforced by the server, so calling an endpoint directly gains nothing. Server rejections report member names the same way the client does: camelCased, and attributed to the field rather than to a concept’s inner value. For general validation docs, see [Validation](/arc/frontend/core/validation/). ## Sorting And Paging [Section titled “Sorting And Paging”](#sorting-and-paging) Queries include native sorting and paging primitives via `Sorting` and `Paging`. For React usage patterns and generated hooks, see [Paging](/arc/frontend/react/queries/paging/). ## Request Cancellation [Section titled “Request Cancellation”](#request-cancellation) When a newer request supersedes an active one, Arc cancels stale work to reduce race conditions and unnecessary processing. ## Error Categories [Section titled “Error Categories”](#error-categories) Typical query failure categories include: * Parameter validation errors * Network failures * Timeouts and cancellations * Server exceptions ## See Also [Section titled “See Also”](#see-also) * [Query Contracts](/arc/frontend/core/queries/contracts/) * [React Queries](/arc/frontend/react/queries/) # Validation Core validation in Arc provides shared TypeScript/JavaScript primitives and generated rules that run before command and query requests. This page is an overview of validation capabilities. Use the pages below for specific implementation details. ## Capabilities [Section titled “Capabilities”](#capabilities) | Capability | What It Covers | Learn More | | ------------------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------- | | Fluent rules API | Programmatic rule definition, built-in rules, and custom messages | [Rules And Fluent API](/arc/frontend/core/validation/rules/) | | Command/query integration | Automatic pre-flight validation behavior for commands and queries | [Command And Query Integration](/arc/frontend/core/validation/integration/) | | Validation payloads | Result shape, severities, and diagnostics | [Validation Results](/arc/frontend/core/validation/results/) | | Severity-based execution gating | Warning/information filtering and confirmation flows | [Severity Filtering](/arc/frontend/core/validation/severity-filtering/) | ## Related Topics [Section titled “Related Topics”](#related-topics) * [Core Commands](/arc/frontend/core/commands/) * [Core Queries](/arc/frontend/core/queries/) * [Backend Command Validation](/arc/backend/commands/validation/) * [Backend Query Validation](/arc/backend/queries/validation/) * [Proxy Generation Validation](/arc/backend/proxy-generation/validation/) # Command And Query Integration Validation runs automatically before command execution and query performance. ## Commands [Section titled “Commands”](#commands) ```typescript const command = new CreateUserCommand(); command.email = ''; command.age = 15; const result = await command.execute(); // result.isValid === false ``` ## Queries [Section titled “Queries”](#queries) ```typescript const query = new SearchUsersQuery(); query.parameters = { searchTerm: 'ab', minAge: -5 }; const result = await query.perform(); // result.isValid === false ``` ## Backend-Governed Validation [Section titled “Backend-Governed Validation”](#backend-governed-validation) Rules are defined on the backend and extracted by proxy generation: * Single source of truth for validation rules * Consistent frontend/backend behavior * Type-safe generated validators See [Backend Command Validation](/arc/backend/commands/validation/) and [Backend Query Validation](/arc/backend/queries/validation/). ## Related [Section titled “Related”](#related) * [Rules And Fluent API](/arc/frontend/core/validation/rules/) * [Validation Results](/arc/frontend/core/validation/results/) * [Severity Filtering](/arc/frontend/core/validation/severity-filtering/) # Validation Results Validation failures are returned in command/query results using a shared validation-result shape. ## Result Item Shape [Section titled “Result Item Shape”](#result-item-shape) ```typescript interface ValidationResult { severity: ValidationResultSeverity; message: string; members: string[]; state: any; } ``` Example: ```typescript { severity: ValidationResultSeverity.Error, message: 'Email address is required', members: ['email'], state: null } ``` ## Severity Levels [Section titled “Severity Levels”](#severity-levels) * `Error` * `Warning` * `Information` * `Unknown` For command execution filtering by severity, see [Severity Filtering](/arc/frontend/core/validation/severity-filtering/). ## Related [Section titled “Related”](#related) * [Command Result](/arc/frontend/core/commands/command-result/) * [Validation](/arc/frontend/core/commands/validation/) # Rules And Fluent API Arc validation for frontend core provides a fluent API that mirrors common FluentValidation patterns. ## Programmatic Rule Definition [Section titled “Programmatic Rule Definition”](#programmatic-rule-definition) Use `ruleFor()` with a chainable API: ```typescript class CreateUserCommandValidator extends CommandValidator { constructor() { super(); this.ruleFor(c => c.email).notEmpty().emailAddress(); this.ruleFor(c => c.age).greaterThanOrEqual(18); this.ruleFor(c => c.name).minLength(2).maxLength(50); } } ``` ## Built-in Rules [Section titled “Built-in Rules”](#built-in-rules) * `notEmpty()`, `notNull()` * `minLength()`, `maxLength()`, `length()` * `emailAddress()` * `matches(regex)` * `greaterThan()`, `greaterThanOrEqual()`, `lessThan()`, `lessThanOrEqual()` ## Custom Messages [Section titled “Custom Messages”](#custom-messages) Use `withMessage()` to override defaults: ```typescript this.ruleFor(c => c.email) .notEmpty() .withMessage('Email address is required'); ``` ## Supported FluentValidation Mappings [Section titled “Supported FluentValidation Mappings”](#supported-fluentvalidation-mappings) | FluentValidation | TypeScript | | ------------------------- | ----------------------- | | `NotEmpty()` | `notEmpty()` | | `NotNull()` | `notNull()` | | `EmailAddress()` | `emailAddress()` | | `MinimumLength(n)` | `minLength(n)` | | `MaximumLength(n)` | `maxLength(n)` | | `Length(min, max)` | `length(min, max)` | | `Matches(regex)` | `matches(regex)` | | `GreaterThan(n)` | `greaterThan(n)` | | `GreaterThanOrEqualTo(n)` | `greaterThanOrEqual(n)` | | `LessThan(n)` | `lessThan(n)` | | `LessThanOrEqualTo(n)` | `lessThanOrEqual(n)` | ## Note [Section titled “Note”](#note) Complex custom predicates such as `.Must()` are not supported for generated client-side execution. # Validation Severity Filtering Validation severity filtering allows you to control which validation results block command execution based on their severity level. This enables user-friendly workflows where warnings and informational messages can be shown to users for confirmation before allowing execution to proceed. ## Overview [Section titled “Overview”](#overview) When validating commands, validation results can have different severity levels: * **Error** (`ValidationResultSeverity.Error = 3`): Critical issues that must be fixed * **Warning** (`ValidationResultSeverity.Warning = 2`): Potential issues that should be reviewed * **Information** (`ValidationResultSeverity.Information = 1`): Informational messages that don’t prevent execution * **Unknown** (`ValidationResultSeverity.Unknown = 0`): Unclassified validation results By default, only **Error** severity results block command execution. Warnings and Information results are filtered out and don’t prevent the command from executing. ## Purpose [Section titled “Purpose”](#purpose) Severity filtering enables better user experiences by allowing you to: * **Show Warnings to Users**: Display validation warnings without blocking execution * **Require User Confirmation**: Let users review and acknowledge warnings before proceeding * **Progressive Execution**: First validate with strict rules, then allow users to override warnings * **Flexible Validation**: Apply different validation strictness based on user roles or contexts ## How It Works [Section titled “How It Works”](#how-it-works) ### Default Behavior [Section titled “Default Behavior”](#default-behavior) Without specifying an allowed severity, only **Error** level validation results block execution: ```typescript const command = new CreateOrder(); command.orderNumber = 'ORD-12345'; // Default: only errors block execution const result = await command.execute(); if (!result.isSuccess) { // Only errors are present - warnings and info were filtered out console.log('Errors:', result.validationResults); } ``` ### Allowing Warnings [Section titled “Allowing Warnings”](#allowing-warnings) To allow warnings (only errors block execution): ```typescript const command = new CreateOrder(); command.orderNumber = 'ORD-12345'; // Allow warnings - only errors block execution const result = await command.execute(ValidationResultSeverity.Warning); if (!result.isSuccess) { // Only errors are present console.log('Errors:', result.validationResults); } ``` ### Allowing Information [Section titled “Allowing Information”](#allowing-information) To allow both warnings and information (only errors block): ```typescript const command = new CreateOrder(); command.orderNumber = 'ORD-12345'; // Allow information - only errors block execution const result = await command.execute(ValidationResultSeverity.Information); if (!result.isSuccess) { // Only errors are present console.log('Errors:', result.validationResults); } ``` ## Common Patterns [Section titled “Common Patterns”](#common-patterns) ### Warning Confirmation Workflow [Section titled “Warning Confirmation Workflow”](#warning-confirmation-workflow) A typical pattern is to first execute without allowing warnings, show them to the user, and then re-execute with warnings allowed if the user confirms: ```typescript import { CreateOrder } from './generated/commands'; import { ValidationResultSeverity } from '@cratis/arc/validation'; async function createOrderWithWarningConfirmation() { const command = new CreateOrder(); command.orderNumber = 'ORD-12345'; command.customerId = '550e8400-e29b-41d4-a716-446655440000'; // First attempt: default behavior (only errors block) let result = await command.execute(); if (!result.isSuccess) { // Check if we have only warnings (no errors) const hasOnlyWarnings = result.validationResults.every( v => v.severity === ValidationResultSeverity.Warning ); if (hasOnlyWarnings) { // Show warnings to user const warnings = result.validationResults.map(v => v.message).join('\n'); const userConfirmed = await showWarningDialog( 'Warning', `The following warnings were found:\n\n${warnings}\n\nDo you want to proceed anyway?` ); if (userConfirmed) { // User confirmed - re-execute allowing warnings result = await command.execute(ValidationResultSeverity.Warning); } else { // User cancelled return; } } else { // We have errors - show them to user const errors = result.validationResults.map(v => v.message).join('\n'); showErrorDialog('Validation Errors', errors); return; } } if (result.isSuccess) { console.log('Order created successfully!'); } } function showWarningDialog(title: string, message: string): Promise { // Implementation depends on your UI framework // Return true if user confirms, false if user cancels } function showErrorDialog(title: string, message: string): void { // Implementation depends on your UI framework } ``` ### Helper Function [Section titled “Helper Function”](#helper-function) You can create a reusable helper function for the warning confirmation pattern: ```typescript import { ICommand, CommandResult } from '@cratis/arc/commands'; import { ValidationResultSeverity } from '@cratis/arc/validation'; interface ConfirmationOptions { title?: string; allowInformation?: boolean; } async function executeWithConfirmation( command: ICommand, confirmCallback: (messages: string[]) => Promise, options?: ConfirmationOptions ): Promise> { const allowedSeverity = options?.allowInformation ? ValidationResultSeverity.Information : ValidationResultSeverity.Warning; // First attempt with default behavior let result = await command.execute(); if (!result.isSuccess && !result.hasExceptions) { // Check if all validation results are warnings/info const maxSeverity = Math.max(...result.validationResults.map(v => v.severity)); if (maxSeverity <= allowedSeverity) { // Only warnings/info present const messages = result.validationResults.map(v => v.message); const confirmed = await confirmCallback(messages); if (confirmed) { // Re-execute with allowed severity result = await command.execute(allowedSeverity); } } } return result; } // Usage const command = new CreateOrder(); const result = await executeWithConfirmation( command, async (messages) => { return confirm(`Warnings:\n${messages.join('\n')}\n\nProceed anyway?`); } ); ``` ## React Integration [Section titled “React Integration”](#react-integration) For React applications, you can create a custom hook: ```typescript import { useState } from 'react'; import { ICommand, CommandResult } from '@cratis/arc/commands'; import { ValidationResultSeverity } from '@cratis/arc/validation'; interface UseCommandWithConfirmationResult { execute: () => Promise; result?: CommandResult; warnings?: string[]; isLoading: boolean; confirmAndExecute: () => Promise; } export function useCommandWithConfirmation( command: ICommand ): UseCommandWithConfirmationResult { const [result, setResult] = useState>(); const [warnings, setWarnings] = useState(); const [isLoading, setIsLoading] = useState(false); const execute = async () => { setIsLoading(true); try { const cmdResult = await command.execute(); setResult(cmdResult); if (!cmdResult.isSuccess && !cmdResult.hasExceptions) { const hasOnlyWarnings = cmdResult.validationResults.every( v => v.severity === ValidationResultSeverity.Warning ); if (hasOnlyWarnings) { setWarnings(cmdResult.validationResults.map(v => v.message)); } } } finally { setIsLoading(false); } }; const confirmAndExecute = async () => { setIsLoading(true); try { const cmdResult = await command.execute(ValidationResultSeverity.Warning); setResult(cmdResult); setWarnings(undefined); } finally { setIsLoading(false); } }; return { execute, result, warnings, isLoading, confirmAndExecute }; } // Usage in a component function CreateOrderForm() { const command = new CreateOrder(); const { execute, result, warnings, isLoading, confirmAndExecute } = useCommandWithConfirmation(command); const handleSubmit = async () => { await execute(); }; return (
{warnings && (

Warnings

    {warnings.map((w, i) =>
  • {w}
  • )}
)} {result?.isSuccess &&
Order created successfully!
}
); } ``` ## Backend Implementation [Section titled “Backend Implementation”](#backend-implementation) The severity filtering happens both on the client and server: ### Client-Side Filtering [Section titled “Client-Side Filtering”](#client-side-filtering) Before sending the request, the client filters validation results to determine if execution should proceed: 1. Client-side validators run (if configured) 2. Validation results are filtered based on `allowedSeverity` 3. If only allowed severities remain, the request proceeds to the server 4. The `X-Allowed-Severity` header is sent with the request ### Server-Side Filtering [Section titled “Server-Side Filtering”](#server-side-filtering) The backend also filters validation results: 1. The `CommandEndpointMapper` reads the `X-Allowed-Severity` header 2. The `CommandPipeline` runs all validation filters 3. Validation results are filtered based on the allowed severity 4. Only validation results with severity > `allowedSeverity` block execution For backend implementation details, see [Backend Validation Severity Filtering](/arc/backend/commands/validation-severity-filtering/). ## API Reference [Section titled “API Reference”](#api-reference) ### ValidationResultSeverity Enum [Section titled “ValidationResultSeverity Enum”](#validationresultseverity-enum) ```typescript enum ValidationResultSeverity { Unknown = 0, Information = 1, Warning = 2, Error = 3 } ``` ### ICommand.execute() [Section titled “ICommand.execute()”](#icommandexecute) ```typescript interface ICommand { /** * Execute the command. * @param allowedSeverity Optional maximum severity level to allow. * Validation results with severity higher than this will cause the command to fail. * If not specified, only Error severity blocks execution. */ execute( allowedSeverity?: ValidationResultSeverity, ignoreWarnings?: boolean ): Promise>; } ``` ### Filtering Logic [Section titled “Filtering Logic”](#filtering-logic) The filtering logic works as follows: * `allowedSeverity` not specified (default): Only `Error` severity results block execution * `allowedSeverity = ValidationResultSeverity.Warning`: Only `Error` severity results block execution (warnings allowed) * `allowedSeverity = ValidationResultSeverity.Information`: Only `Error` and `Warning` severity results block execution (information allowed) Results with severity **greater than** the `allowedSeverity` will block execution. ## Best Practices [Section titled “Best Practices”](#best-practices) ### When to Use Severity Filtering [Section titled “When to Use Severity Filtering”](#when-to-use-severity-filtering) ✅ **Good Use Cases:** * Orders with non-critical warnings (e.g., “Low stock” warning) * Forms with recommendations that users can override * Operations with soft validation rules * Multi-step wizards where some warnings can be deferred ❌ **Avoid:** * Critical business rules (always use Error severity) * Security-related validations * Data integrity checks * Regulatory compliance requirements ### Security Considerations [Section titled “Security Considerations”](#security-considerations) * Never use severity filtering to bypass security validations * Always use `Error` severity for authorization failures * Critical business rules should always be `Error` severity * Don’t rely on client-side severity filtering for security (server validates too) ### Performance Tips [Section titled “Performance Tips”](#performance-tips) * Severity filtering doesn’t add significant overhead * The same validation runs regardless of allowed severity * Use appropriate severity levels in your validators * Consider caching validation results if executing multiple times ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Warnings Still Block Execution [Section titled “Warnings Still Block Execution”](#warnings-still-block-execution) **Cause**: Not specifying `allowedSeverity` parameter or validators returning `Error` severity. **Solution**: * Pass `ValidationResultSeverity.Warning` to `execute()` * Check that validators are using `Warning` severity for non-critical issues * Verify that custom validators set the correct severity level ### Errors Are Allowed Through [Section titled “Errors Are Allowed Through”](#errors-are-allowed-through) **Cause**: Using too high of an `allowedSeverity` value or validators incorrectly assigning severity. **Solution**: * Never use `ValidationResultSeverity.Error` as `allowedSeverity` * Verify validators are using `Error` severity for critical issues * Check backend validator implementations ### Server Returns Different Results [Section titled “Server Returns Different Results”](#server-returns-different-results) **Cause**: Server-side and client-side validators may have different rules. **Solution**: * Server always has final authority on validation * Ensure client-side validators match server-side rules * Use FluentValidation with proxy generation for consistency ## Related Documentation [Section titled “Related Documentation”](#related-documentation) * [Validation](/arc/frontend/core/commands/validation/) - Pre-flight validation without execution * [Validation](/arc/frontend/core/validation/) - Client-side validation rules * [Backend Validation Severity Filtering](/arc/backend/commands/validation-severity-filtering/) - Server implementation * [Command Filters](/arc/backend/commands/command-filters/) - Backend validation pipeline # Getting started (React) > From zero to a React screen that runs a type-safe query and command against your Arc backend — using the generated proxies, no hand-written API client. You finished the [backend slice](/arc/backend/getting-started/your-first-command/): a `RegisterAuthor` command and an `AllAuthors` query, and `dotnet build` turned both into typed TypeScript proxies. Now for the other half — a React screen that lists authors and adds one. Normally this is where the type safety ends. You’d hand-write a `fetch` wrapper, redeclare the request and response shapes in TypeScript, and hope the two sides stay in sync. Arc skips all of that: the proxies are generated *from your C#*, so the frontend already knows the exact types — and the compiler catches you the moment they drift. Let’s read data and run a command through them. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * **The backend slice from [Your first command and query](/arc/backend/getting-started/your-first-command/)** — its `RegisterAuthor` command and `AllAuthors` query are what this screen calls. * **A successful `dotnet build`** — that’s what generates the TypeScript proxies the frontend imports. * **A Vite + React app** with `@cratis/arc.react` and `@cratis/components` installed — the `dotnet new cratis` template scaffolds exactly that. Backend first Proxies appear when you **build the backend** — until `dotnet build` succeeds, the proxy files don’t exist. If you scaffolded with `dotnet new cratis`, the frontend and a sample feature are already wired; this page explains what’s happening so you can add your own. ``` dotnet build C# command + query — RegisterAuthor · AllAuthors generated TS proxies AllAuthors.use() — reads, live CommandDialog — executes your React screen ``` ## Wire it up [Section titled “Wire it up”](#wire-it-up) 1. **Initialize the bindings and mount the providers.** The `cratis` template gives you a Vite + React app with `@cratis/arc.react` and `@cratis/components` installed. Two things happen at startup — the generated bindings learn how to reach the backend, and the app is wrapped in the Cratis providers: App.tsx ```tsx import { Bindings } from './Bindings'; // generated import { CratisComponentsProvider } from '@cratis/components'; Bindings.initialize(); export const App = () => ( ); ``` 2. **Read data with a query.** The query you wrote in C# is generated as a typed proxy. Because `AllAuthors` is an **observable** query, the `.use()` hook re-renders the component whenever the underlying read model changes — live updates, no polling: Authors.tsx ```tsx import { AllAuthors } from './Authors/Author'; // generated proxy export const Authors = () => { const [authors] = AllAuthors.use(); return (
    {authors.data.map(a =>
  • {a.name}
  • )}
); }; ``` 3. **Execute a command.** `CommandDialog` runs a generated command — it instantiates it, renders the form fields and the confirm/cancel buttons, and disables confirm while it executes: AddAuthor.tsx ```tsx import { CommandDialog } from '@cratis/components/CommandDialog'; import { InputTextField } from '@cratis/components/CommandForm'; import { RegisterAuthor } from './Authors/RegisterAuthor'; // generated proxy export const AddAuthor = () => ( command={RegisterAuthor} title="Add author" okLabel="Add"> value={i => i.name} title="Name" /> ); ``` ## Where the type safety pays off [Section titled “Where the type safety pays off”](#where-the-type-safety-pays-off) Look at the field accessor `i => i.name`. It isn’t a string you typed and hope matches — it’s a property on the generated `RegisterAuthor` type. Rename `Name` in the C# command, rebuild, and `i => i.name` stops compiling until you update it. The same goes for `a.name` on the query side. There’s no second source of truth to drift, and no runtime surprise when a shape changes: the mismatch is a build error, on your machine, before anyone runs it. And the form is wired to the live list for nothing extra — `AllAuthors.use()` is subscribed to the read model, so the moment the command writes, the list re-renders. You didn’t write any of that plumbing. ## Recap [Section titled “Recap”](#recap) You initialized the generated **bindings**, read a read model through a typed, observable **query proxy**, and executed a typed **command** with `CommandDialog` — and the whole screen stays type-checked against the C# it came from. That round trip, C# to React and back, all typed, is the thing Arc exists to give you. ## Where to go next [Section titled “Where to go next”](#where-to-go-next) * **[Build a full-stack feature](/build-a-full-app/)** — the same slice, end to end, backend and frontend together. * Go deeper on the [React](/arc/frontend/react/) integration, or the [MVVM with React](/arc/frontend/reactmvvm/) approach for larger screens. * Browse the building blocks in [Components](/components/). # React This is the heart of Arc on the frontend: a small set of hooks and helpers that turn your generated command and query proxies into idiomatic React. You import a proxy, call its `.use()` hook, and get a typed, reactive instance — no API client, no DTOs, no manual loading state. Start with [Commands](/arc/frontend/react/commands/) and [Queries](/arc/frontend/react/queries/); the rest of the pages here refine those two with validation, scopes, and identity. ## Working with commands [Section titled “Working with commands”](#working-with-commands) | Topic | What it covers | | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | [Commands](/arc/frontend/react/commands/) | The `.use()` hook — instantiate, bind, validate, and execute a command from a component. | | [Command Validation](/arc/frontend/react/commands/validation/) | Pre-flight validation: catch invalid input before the request leaves the browser. | | [Command Scopes](/arc/frontend/react/commands/scope/) | A scope that captures changes, validation, and errors across several commands in one UI. | ## Working with queries [Section titled “Working with queries”](#working-with-queries) | Topic | What it covers | | --------------------------------------- | ----------------------------------------------------------------------------------------- | | [Queries](/arc/frontend/react/queries/) | Reading data with `.use()` — including live, observable queries that re-render on change. | ## Around the edges [Section titled “Around the edges”](#around-the-edges) | Topic | What it covers | | -------------------------------------------------- | ----------------------------------------------------------------------------- | | [Configure Arc](/arc/frontend/react/arc/) | Point the React app at your backend and set transport, headers, and identity. | | [Identity](/arc/frontend/react/identity/) | Who the user is, and what they’re allowed to see and do. | | [Dialogs](/arc/frontend/react/dialogs/) | Consistent dialog handling for command and data-entry flows. | | [Proxy Generation](/arc/backend/proxy-generation/) | How the typed proxies you import here are generated from C#. | | [Storybook](/arc/frontend/react/storybook/) | The Storybook for the components Arc exposes. | | [Story Components](/arc/frontend/react/stories/) | Building good-looking, consistent stories. | Prefer a structured, testable approach for complex screens? See [MVVM with React](/arc/frontend/reactmvvm/). # Arc As with the backend, you can mix and match from the features you want to use. But there is a convenience wrapper that will help you configure it all in the form of a custom component that provides the `ArcContext` and configures other Arc contexts in one go. However, if you’re looking to use some of the microservice capabilities, you will have to use the `ArcContext` to provide the name of the currently running microservice. Internally, Arc uses this information to add the correct headers / query string parameters to distinguish one microservice from the other in a composition with a single ingress in front of it. To add Arc, you simply add the following to your application: ```tsx export const App = () => { return ( {/* Your application */} ); }; ``` ## Configuration Options [Section titled “Configuration Options”](#configuration-options) The `` component provides centralized configuration for all commands and queries in your application. These settings apply cross-cuttingly to all operations, eliminating the need to configure individual commands or queries. | Option | Type | Purpose | | --------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | microservice | String | Name of the microservice, which will add necessary HTTP headers on Commands and Queries | | development | Boolean | Whether or not we’re running in development, defaults to false | | origin | String | Url for where the APIs are located, defaults to empty string and makes them relative to the documents location | | basePath | String | Base path for the application | | apiBasePath | String | Base path prepended to all Command and Query requests | | httpHeadersCallback | Function | Optional callback function that returns additional HTTP headers to include with all commands, queries, and identity requests (e.g., for including cookies or authentication tokens) | | eventSourceFactory | `(url: string) => EventSource` | Optional factory for creating the `EventSource` instances used by SSE observable query connections. Falls back to the global `EventSource` constructor when not set — see [Custom EventSource Factory](/arc/frontend/react/queries/configuration/#custom-eventsource-factory) | | queryTransportMethod | `QueryTransportMethod` | Transport used by observable queries (`ServerSentEvents` or `WebSocket`) | | queryDirectMode | Boolean | Whether observable queries bypass the central hub and connect directly per-query | | queryConnectionCount | Number | Number of observable query hub connection slots | | observableQueryTransferMode | `ObservableQueryTransferMode` | Transfer mode used by `useChangeStream` | | queryCacheRetentionMs | Number | How long (ms) to keep query data in cache after the last subscriber unmounts — defaults to `30000` | | observableQueryDiagnostics | `IObservableQueryDiagnostics` | Live diagnostics stream for query cache, transport, and ownership state | Example: ```tsx export const App = () => { return ( {/* Your application */} ); }; ``` ## Microservice Support [Section titled “Microservice Support”](#microservice-support) In microservice architectures, multiple services often share a single ingress point (e.g., an API gateway or reverse proxy). The `microservice` property enables the Arc to route requests to the correct backend service automatically. ### How It Works [Section titled “How It Works”](#how-it-works) When you specify a microservice name, the Arc adds this information to all HTTP requests (via headers or query parameters), allowing the ingress to route requests appropriately: ```tsx export const App = () => { return ( {/* Your application */} ); }; ``` All commands and queries within this application will automatically include the microservice identifier, ensuring they reach the correct backend service. ### Development Configuration [Section titled “Development Configuration”](#development-configuration) For local development, you can use environment variables to dynamically configure the microservice and API paths: ```tsx export const App = () => { const microserviceName = import.meta.env.VITE_MICROSERVICE_NAME || 'user-service'; const apiBasePath = import.meta.env.VITE_API_BASE_PATH || '/api'; const isDevelopment = import.meta.env.DEV; return ( {/* Your application */} ); }; ``` **.env.development example:** ```env VITE_MICROSERVICE_NAME=user-service VITE_API_BASE_PATH=/api ``` **.env.production example:** ```env VITE_MICROSERVICE_NAME=user-service VITE_API_BASE_PATH=/api/v1 ``` This approach allows you to: * Run different microservices locally with different configurations * Switch between local development and remote APIs * Configure different API paths for different environments * Test microservice routing without modifying code ### Multiple Microservices in One Frontend [Section titled “Multiple Microservices in One Frontend”](#multiple-microservices-in-one-frontend) If your frontend application needs to communicate with multiple microservices, you can override the microservice configuration for specific sections of your application using nested `` components: ```tsx export const App = () => { return ( {/* This section communicates with a different microservice */} ); }; ``` ## Query Configuration [Section titled “Query Configuration”](#query-configuration) Query-related Arc configuration (transport, direct mode, connection count, transfer mode, and headers) is documented in [Queries Configuration](/arc/frontend/react/queries/configuration/). ## Observable Query Diagnostics [Section titled “Observable Query Diagnostics”](#observable-query-diagnostics) Arc exposes a diagnostics service through `ArcContext.observableQueryDiagnostics` for tooling and internal dashboards. The service provides both a snapshot API and a live observable stream for the current query cache, transport, and ownership state. See [Observable Query Diagnostics](/arc/frontend/react/queries/observable-query-diagnostics/) for usage examples. ## HTTP Headers Callback [Section titled “HTTP Headers Callback”](#http-headers-callback) The `httpHeadersCallback` property allows you to provide additional HTTP headers that will be automatically included with all HTTP requests made by commands, queries, and identity operations. This is particularly useful for including authentication cookies, authorization tokens, or other dynamic headers. ```tsx export const App = () => { const getHeaders = () => { return { 'X-Custom-Header': 'custom-value', 'Authorization': `Bearer ${getAuthToken()}`, // Include cookies or other dynamic headers }; }; return ( {/* Your application */} ); }; ``` The callback function should return a `HeadersInit` object (compatible with the Fetch API headers) that contains the additional headers to include with each request. ## Reconnecting Queries [Section titled “Reconnecting Queries”](#reconnecting-queries) When authentication state changes at runtime — for example when a user logs in or logs out — the existing WebSocket or Server-Sent Events connections may no longer carry the correct credentials. The `reconnectQueries()` method on the Arc context tears down all active observable query subscriptions, disposes the shared transport connections, and signals every `useObservableQuery` hook to re-subscribe through fresh connections that pick up the current cookies and headers. ### Accessing reconnectQueries [Section titled “Accessing reconnectQueries”](#accessing-reconnectqueries) Use `useContext(ArcContext)` inside any component rendered within ``: ```tsx import { useContext } from 'react'; import { ArcContext } from '@cratis/arc.react'; export const LoginButton = () => { const arc = useContext(ArcContext); const handleLogin = () => { // Set authentication cookie or token first document.cookie = 'auth-token=abc123; path=/; SameSite=Lax'; // Then reconnect queries so new connections carry the credential arc.reconnectQueries?.(); }; return ; }; ``` ### Login and logout flow [Section titled “Login and logout flow”](#login-and-logout-flow) A typical authentication transition follows this pattern: 1. **Login**: Set the authentication cookie, then call `reconnectQueries()`. New transport connections are established with the cookie attached, and the identity provider picks up the authenticated user on refresh. 2. **Logout**: Call `identity.clearIdentity()` to reset the client-side identity state and remove the identity cookie, then call `reconnectQueries()` so queries reconnect as anonymous. ```tsx import { useContext } from 'react'; import { ArcContext } from '@cratis/arc.react'; import { useIdentity } from '@cratis/arc.react/identity'; export const AuthControls = () => { const arc = useContext(ArcContext); const identity = useIdentity(); const login = () => { // Set your auth cookie document.cookie = 'auth=...; path=/; SameSite=Lax'; arc.reconnectQueries?.(); }; const logout = () => { // Clear cookie document.cookie = 'auth=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'; // Clear client-side identity identity.clearIdentity(); // Reconnect queries as anonymous arc.reconnectQueries?.(); }; return (
{identity.isSet ? : }
); }; ``` ### What happens internally [Section titled “What happens internally”](#what-happens-internally) Calling `reconnectQueries()` performs three steps in order: 1. **Tear down subscriptions** — Every cached query entry has its subscription callback torn down without evicting the cache entry. This allows hooks to detect the unsubscribed state and re-subscribe. 2. **Reset the shared multiplexer** — The module-level multiplexer singleton (WebSocket or SSE) is disposed, so the next subscription creates a fresh transport connection. 3. **Bump the query version** — An internal version counter increments, which is included in the dependency array of every `useObservableQuery` effect. React re-runs the effects, each hook re-subscribes, and new connections are established with the current credentials. # CommandForm The `CommandForm` component provides a declarative way to build forms for Arc commands with built-in validation, error handling, and field management. ## Overview [Section titled “Overview”](#overview) CommandForm simplifies working with Arc commands in React by: * Automatically managing command state * Providing type-safe field bindings * Handling validation integration * Supporting flexible customization * Managing form lifecycle ## Basic Usage [Section titled “Basic Usage”](#basic-usage) ```tsx import { CommandForm } from '@cratis/arc.react/commands'; import { InputTextField } from '@cratis/arc.react/commands'; class UserCommand extends Command { name = ''; email = ''; } function MyForm() { return ( value={c => c.name} title="Name" placeholder="Enter your name" /> value={c => c.email} title="Email" type="email" placeholder="Enter your email" /> ); } ``` > **Note**: Field components require an explicit generic type parameter (e.g., `>`) to ensure the `value` accessor function is properly typed. This provides full IntelliSense and type safety when writing `c => c.propertyName`. ## Props Reference [Section titled “Props Reference”](#props-reference) ### CommandFormProps [Section titled “CommandFormProps”](#commandformprops) | Property | Type | Default | Description | | ------------------------- | ----------------------------------------------------------------- | ---------------------- | -------------------------------------------------------------- | | `command` | `Constructor` | Required | The command class to use for the form | | `initialValues` | `Partial` | `undefined` | Initial values for the form fields | | `currentValues` | `Partial` | `undefined` | Current values that will be merged with initial values | | `showTitles` | `boolean` | `true` | Whether to show field titles automatically | | `showErrors` | `boolean` | `true` | Whether to show field error messages automatically | | `fieldContainerComponent` | `React.ComponentType` | `undefined` | Custom component for rendering field containers | | `fieldDecoratorComponent` | `React.ComponentType` | `undefined` | Custom component for decorating fields with icons and tooltips | | `errorDisplayComponent` | `React.ComponentType` | `undefined` | Custom component for rendering validation errors | | `tooltipComponent` | `React.ComponentType` | `undefined` | Custom component for rendering tooltips on field descriptions | | `errorClassName` | `string` | `'p-error'` | CSS class name for error message elements | | `iconAddonClassName` | `string` | `'p-inputgroup-addon'` | CSS class name for icon addon containers | | `onFieldValidate` | `(command, fieldName, oldValue, newValue) => string \| undefined` | `undefined` | Custom field validation function | | `onFieldChange` | `(command, fieldName, oldValue, newValue) => void` | `undefined` | Callback when field value changes | | `onBeforeExecute` | `(values) => values` | `undefined` | Transform command values before execution | | `onSuccess` | `(response: TResponse) => void` | `undefined` | Called when command executes successfully | | `onFailed` | `(commandResult: CommandResult) => void` | `undefined` | Called when command execution fails | | `onException` | `(messages: string[], stackTrace: string) => void` | `undefined` | Called when command throws an exception | | `onUnauthorized` | `() => void` | `undefined` | Called when user is not authorized | | `onValidationFailure` | `(validationResults: ValidationResult[]) => void` | `undefined` | Called when command fails validation | ## Initial Values [Section titled “Initial Values”](#initial-values) Set initial values for the form: ```tsx value={c => c.name} title="Name" /> value={c => c.email} title="Email" /> value={c => c.role} title="Role" options={roles} /> ``` ## Children [Section titled “Children”](#children) CommandForm accepts any React elements as children. The following are treated specially: * **Field components** - Components with `displayName` of `'CommandFormField'` are automatically bound to the command * **CommandForm.Column** - Used for multi-column layouts * **Other elements** - Headings, buttons, divs, etc. are rendered as-is in order ## See Also [Section titled “See Also”](#see-also) * [Field Types](/arc/frontend/react/command-form/field-types/) - Available field components * [Validation](/arc/frontend/react/command-form/validation/) - Integration with Arc validation * [Customization](/arc/frontend/react/command-form/customization/) - Custom titles, errors, and containers * [Advanced Usage](/arc/frontend/react/command-form/advanced-patterns/) - Layouts, hooks, and async data # Advanced Patterns Advanced techniques and patterns for complex CommandForm scenarios. ## Computed Fields [Section titled “Computed Fields”](#computed-fields) Add computed or derived fields: ```tsx function InvoiceForm() { const command = useCommandInstance(CreateInvoice); const subtotal = (command.items || []).reduce((sum, item) => sum + (item.quantity * item.price), 0 ); const tax = subtotal * 0.1; // 10% tax const total = subtotal + tax; return ( {/* Item fields */}
Subtotal: ${subtotal.toFixed(2)}
Tax (10%): ${tax.toFixed(2)}
Total: ${total.toFixed(2)}
); } ``` ## Integration with External Libraries [Section titled “Integration with External Libraries”](#integration-with-external-libraries) Integrate CommandForm with UI component libraries: ```tsx // Example with a modal library import { Modal } from 'some-modal-library'; function EditUserModal({ userId, onClose }: { userId: string, onClose: () => void }) { const command = useCommandInstance(UpdateUser, { id: userId }); const [open, setOpen] = useState(true); useEffect(() => { // Load user data const loadUser = async () => { const user = await fetchUser(userId); Object.assign(command, user); }; loadUser(); }, [userId]); const handleSubmit = async () => { const result = await command.execute(); if (result.isSuccess) { setOpen(false); onClose(); } }; return (

Edit User

value={c => c.name} title="Name" required /> value={c => c.email} type="email" title="Email" required />
); } ``` ## Best Practices [Section titled “Best Practices”](#best-practices) 1. **Separation of Concerns**: Keep business logic in command handlers, UI logic in components 2. **Error Handling**: Always handle errors from `execute()` and `validate()` 3. **Performance**: Use `React.memo` for expensive custom field components 4. **Cleanup**: Clear timeouts and cancel async operations in useEffect cleanup 5. **Type Safety**: Leverage TypeScript for command definitions and props 6. **Testing**: Test forms with various states (empty, invalid, valid, submitting) 7. **Accessibility**: Ensure custom layouts maintain proper tab order and focus management ## See Also [Section titled “See Also”](#see-also) * [CommandForm Overview](/arc/frontend/react/command-form/) * [Layouts](/arc/frontend/react/command-form/layouts/) * [Working with Hooks](/arc/frontend/react/command-form/hooks/) * [Form Lifecycle](/arc/frontend/react/command-form/form-lifecycle/) # Auto Server Validation CommandForm can automatically trigger server-side validation when all client-side validations pass. This provides real-time feedback for server-side validation rules (like checking if a username is already taken) without requiring an explicit form submission. ## Overview [Section titled “Overview”](#overview) Auto server validation is designed for scenarios where you need to validate data against server-side business rules while users are filling out a form. Instead of waiting for form submission to discover that a username is taken or an email is already registered, users get immediate feedback as they type. **Key features**: * Automatically calls `command.validate()` when all client validations pass * Configurable throttling to prevent excessive server calls * Seamlessly integrates with CommandForm’s existing validation system * Works with any backend validation endpoint ## Basic Usage [Section titled “Basic Usage”](#basic-usage) Enable auto server validation by setting the `autoServerValidate` prop: ```tsx value={c => c.username} title="Username" required /> value={c => c.email} type="email" title="Email" required /> ``` When enabled, the form will automatically call the command’s `validate()` method (which makes an HTTP request to `/api/{command-route}/validate`) whenever: 1. All client-side validations have passed 2. The command data changes 3. Any pending throttle timer has expired (if configured) ## Validation Flow [Section titled “Validation Flow”](#validation-flow) ```flow User types → Client validation runs → All fields valid? ↓ Yes Throttle timer starts (if configured) → Timer expires → HTTP POST /validate ↓ Server validates → Returns errors → Form displays errors ``` If the user continues typing before the throttle timer expires, the timer resets and starts over. ## Throttling [Section titled “Throttling”](#throttling) To avoid excessive server calls during rapid typing, use the `autoServerValidateThrottle` prop to delay validation: ```tsx value={c => c.username} title="Username" required /> value={c => c.email} type="email" title="Email" required /> ``` ### Throttle Values [Section titled “Throttle Values”](#throttle-values) The throttle value is in milliseconds: * **500 (default)**: Good balance between responsiveness and server load - recommended for most forms * **300**: More responsive but more server calls * **1000**: Less responsive but fewer server calls * **0**: No throttle, validation occurs immediately (not recommended for `validateOn="change"`) ### How Throttling Works [Section titled “How Throttling Works”](#how-throttling-works) 1. User makes a change that passes all client validations 2. Timer starts counting down from the throttle value 3. If user makes another change before timer expires: * Previous timer is cancelled * New timer starts from the beginning 4. When timer expires without interruption: * Server validation is triggered 5. If server returns errors: * Errors are displayed in the form * Timer resets for next validation **Example**: With a 500ms throttle, typing “” character by character: * Without throttle: 17 server calls (one per character) * With throttle: 1 server call (after user stops typing for 500ms) ## Backend Implementation [Section titled “Backend Implementation”](#backend-implementation) For auto server validation to work, your backend must implement a validation endpoint that returns validation errors without executing the command. ### C# Example [Section titled “C# Example”](#c-example) ```csharp public record RegisterUser(string Username, string Email, string Password); public class RegisterUserValidator : CommandValidator { private readonly IUserRepository _users; public RegisterUserValidator(IUserRepository users) { _users = users; RuleFor(x => x.Username) .NotEmpty() .MinLength(3) .MaxLength(20) .MustAsync(async (username, ct) => !await _users.UsernameExists(username, ct)) .WithMessage("This username is already taken"); RuleFor(x => x.Email) .NotEmpty() .EmailAddress() .MustAsync(async (email, ct) => !await _users.EmailExists(email, ct)) .WithMessage("This email address is already registered"); RuleFor(x => x.Password) .NotEmpty() .MinLength(8) .WithMessage("Password must be at least 8 characters"); } } ``` The framework automatically exposes the validation endpoint at `/api/register-user/validate`. When called, it: 1. Deserializes the command from the request 2. Runs all validation rules 3. Returns errors without executing the command handler ### Validation Response [Section titled “Validation Response”](#validation-response) The validation endpoint returns a `CommandResult` with validation errors: ```json { "isSuccess": false, "validationErrors": { "username": ["This username is already taken"], "email": ["This email address is already registered"] } } ``` ## Frontend Integration [Section titled “Frontend Integration”](#frontend-integration) The frontend automatically handles the validation response and displays errors: ```tsx value={c => c.username} title="Username" placeholder="Choose a username" required minLength={3} maxLength={20} /> {/* Error appears here: "This username is already taken" */} value={c => c.email} type="email" title="Email" placeholder="Enter your email" required /> {/* Error appears here: "This email address is already registered" */} value={c => c.password} type="password" title="Password" placeholder="Min 8 characters" required minLength={8} /> {/* Error appears here: "Password must be at least 8 characters" */} ``` ## When to Use Auto Server Validation [Section titled “When to Use Auto Server Validation”](#when-to-use-auto-server-validation) ### Good Use Cases [Section titled “Good Use Cases”](#good-use-cases) Auto server validation is ideal for: * **Uniqueness checks**: Username, email, phone number availability * **External validation**: Coupon codes, invite codes, API keys * **Domain validation**: Domain name availability for SaaS products * **Real-time quotas**: Check if user has reached limits * **Complex business rules**: Rules that require database lookups or external services ### When to Avoid [Section titled “When to Avoid”](#when-to-avoid) Consider manual validation (validate on submit) instead when: * **Slow endpoints**: Validation takes more than 1 second * **High server load**: Validation is expensive (complex queries, external APIs) * **Many interdependent fields**: Better to validate entire form at once * **Sequential validation**: One field’s validation depends on another’s result * **Privacy concerns**: You don’t want to reveal information before submission ## Performance Considerations [Section titled “Performance Considerations”](#performance-considerations) ### 1. Use Appropriate Throttle Values [Section titled “1. Use Appropriate Throttle Values”](#1-use-appropriate-throttle-values) ```tsx // Good: Balanced throttle for username checking // Avoid: No throttle with validateOn="change" // Consider: Longer throttle for expensive validation ``` ### 2. Optimize Backend Validators [Section titled “2. Optimize Backend Validators”](#2-optimize-backend-validators) Keep validation queries fast: ```csharp // Good: Indexed lookup public async Task UsernameExists(string username, CancellationToken ct) { return await _db.Users .Where(u => u.Username == username) // Username column should be indexed .AnyAsync(ct); } // Avoid: Full table scan or complex joins public async Task UsernameExists(string username, CancellationToken ct) { var allUsers = await _db.Users.Include(u => u.Profile).ToListAsync(ct); return allUsers.Any(u => u.Username == username); } ``` ### 3. Implement Rate Limiting [Section titled “3. Implement Rate Limiting”](#3-implement-rate-limiting) Protect your validation endpoints from abuse: ```csharp [HttpPost("validate")] [RateLimitAttribute(MaxRequests = 10, TimeWindowSeconds = 60)] public async Task Validate([FromBody] RegisterUser command) { // Validation logic } ``` ### 4. Cache Validation Results [Section titled “4. Cache Validation Results”](#4-cache-validation-results) For expensive validations, consider caching: ```csharp public class RegisterUserValidator : CommandValidator { private readonly IUserRepository _users; private readonly IMemoryCache _cache; public RegisterUserValidator(IUserRepository users, IMemoryCache cache) { _users = users; _cache = cache; RuleFor(x => x.Username) .MustAsync(async (username, ct) => { var cacheKey = $"username_exists_{username}"; if (_cache.TryGetValue(cacheKey, out bool exists)) { return !exists; } exists = await _users.UsernameExists(username, ct); _cache.Set(cacheKey, exists, TimeSpan.FromMinutes(5)); return !exists; }) .WithMessage("This username is already taken"); } } ``` ### 5. Combine with Client Validation [Section titled “5. Combine with Client Validation”](#5-combine-with-client-validation) Always validate client-side first to reduce unnecessary server calls: ```tsx c.username} required // Client-side required check minLength={3} // Client-side length check maxLength={20} // Client-side length check pattern="[a-zA-Z0-9_]+" // Client-side pattern check /> {/* Server validation only runs after client validation passes */} ``` ## Complete Example [Section titled “Complete Example”](#complete-example) Here’s a full registration form with auto server validation: ```tsx import { CommandForm, InputTextField } from '@cratis/arc.react/commands'; import { RegisterUser } from './commands'; function RegistrationForm() { return (

Create Your Account

{ console.log('Registration successful!'); // Redirect to dashboard }} > value={c => c.username} title="Username" placeholder="Choose a unique username" required minLength={3} maxLength={20} pattern="[a-zA-Z0-9_]+" /> 3-20 characters, letters, numbers, and underscores only value={c => c.email} type="email" title="Email Address" placeholder="your.email@example.com" required /> We'll send a verification email value={c => c.password} type="password" title="Password" placeholder="Min 8 characters" required minLength={8} /> At least 8 characters

Your username and email will be checked for availability as you type. Please wait for validation to complete before submitting.

); } ``` ### What Happens [Section titled “What Happens”](#what-happens) 1. User types username: `"john"` * Client validation: ✓ Passes (3 chars, valid pattern) * Throttle: 500ms timer starts 2. User continues typing: `"john_"` * Client validation: ✓ Passes (5 chars, valid pattern) * Throttle: Previous timer cancelled, new 500ms timer starts 3. User continues typing: `"john_smith"` * Client validation: ✓ Passes (10 chars, valid pattern) * Throttle: Previous timer cancelled, new 500ms timer starts 4. User stops typing for 500ms * Server validation: HTTP POST /api/register-user/validate * Response: `{ "validationErrors": { "username": ["This username is already taken"] } }` * Form: Error displayed below username field 5. User changes username to `"john_smith_2024"` * Client validation: ✓ Passes * Throttle: 500ms timer starts * User stops typing for 500ms * Server validation: HTTP POST /api/register-user/validate * Response: `{ "isSuccess": true }` * Form: No errors, username is available ## Integration with Other Validation Props [Section titled “Integration with Other Validation Props”](#integration-with-other-validation-props) Auto server validation works alongside other validation props: ```tsx {/* fields */} ``` **Interaction**: * `validateOn` controls when client validation runs * `autoServerValidate` controls whether server validation runs automatically * Server validation only triggers when ALL client validations pass * `autoServerValidateThrottle` delays server validation, not client validation ## Best Practices [Section titled “Best Practices”](#best-practices) 1. **Set reasonable throttle values**: 300-500ms for most forms, 1000ms+ for expensive validation 2. **Optimize backend validators**: Use database indexes, caching, and efficient queries 3. **Always validate client-side first**: Reduce server load by catching simple errors client-side 4. **Provide visual feedback**: Show loading state during server validation (see [Customization](/arc/frontend/react/command-form/customization/)) 5. **Handle errors gracefully**: Display clear, actionable error messages 6. **Implement rate limiting**: Protect validation endpoints from abuse 7. **Consider user experience**: Don’t validate too aggressively - let users finish typing 8. **Test throttle behavior**: Verify throttle works correctly with rapid typing 9. **Monitor server load**: Track validation endpoint performance and adjust throttle if needed 10. **Document validation rules**: Make it clear what server-side rules exist ## See Also [Section titled “See Also”](#see-also) * [Validation](/arc/frontend/react/command-form/validation/) - Client-side validation options * [Backend Command Validation](/arc/backend/commands/validation/) - Implementing validators * [Customization](/arc/frontend/react/command-form/customization/) - Customizing validation display * [Form Lifecycle](/arc/frontend/react/command-form/form-lifecycle/) - Understanding form state # Creating Custom Fields While CommandForm provides built-in field components for common scenarios, you can easily create your own custom fields to integrate with any UI library or implement specialized input controls. ## Overview [Section titled “Overview”](#overview) Custom fields are created using the `asCommandFormField` higher-order component (HOC), which handles all the integration with CommandForm automatically, including: * Value synchronization with the command instance * Change event handling * Validation state management * Error message display * Required field handling ## Basic Anatomy [Section titled “Basic Anatomy”](#basic-anatomy) A custom field consists of two parts: 1. **Your component** - Receives `WrappedFieldProps` and renders the UI 2. **Field configuration** - Specifies default value and how to extract values from change events ```tsx import { asCommandFormField, WrappedFieldProps } from '@cratis/arc.react/commands'; // 1. Define your component props (extends WrappedFieldProps) interface MyFieldProps extends WrappedFieldProps { placeholder?: string; // Add any custom props here } // 2. Create the field using asCommandFormField export const MyField = asCommandFormField( // Your component implementation (props) => ( ), // Configuration { defaultValue: '', extractValue: (e: unknown) => { const event = e as React.ChangeEvent; return event.target.value; }, }, ); ``` ## WrappedFieldProps [Section titled “WrappedFieldProps”](#wrappedfieldprops) Your component receives these props automatically from CommandForm: | Prop | Type | Description | | ---------- | ------------------------------------------- | ------------------------------------------------- | | `value` | `TValue` | The current field value from the command instance | | `onChange` | `(valueOrEvent: TValue \| unknown) => void` | Callback to update the value | | `invalid` | `boolean` | Whether the field has validation errors | | `required` | `boolean` | Whether the field is required | | `errors` | `string[]` | Array of error messages for this field | ## Configuration Object [Section titled “Configuration Object”](#configuration-object) The second parameter to `asCommandFormField` is a configuration object: | Property | Type | Required | Description | | -------------- | ---------------------------- | -------- | ---------------------------------------------------------------------------------------------------- | | `defaultValue` | `TValue` | Yes | Default value when the field is empty/undefined | | `extractValue` | `(event: unknown) => TValue` | No | Function to extract the value from change events. If omitted, the event itself is used as the value. | ## How a field is recognized [Section titled “How a field is recognized”](#how-a-field-is-recognized) `asCommandFormField` marks the component it returns so `CommandForm` recognizes and binds it. The marker is a static `isCommandFormField` property, and the component’s `displayName` is set to `CommandFormField` as a compatibility fallback for older consuming packages. The wrapped field can also bind itself when an opaque layout component creates or transforms it at render time. This allows custom layouts to keep the original field element type, clone it, and change presentation or binding props before the field registers with the form. Binding is tracked by a private framework marker rather than by the presence of an `onValueChange` prop. A layout may therefore supply or wrap `onValueChange` without disabling command binding. CommandForm updates the command first and then invokes that consumer callback. Do not use `onValueChange` as a signal that a field has already been bound. Caution Do not strip static properties from a command form field. Storybook and other transforms may rewrite `displayName`, but the `isCommandFormField` marker preserves recognition. If both markers are removed, the component no longer participates in CommandForm binding. In development builds, CommandForm warns once when a recognized field cannot resolve a command property from its accessor. Production builds remain non-throwing and do not emit this diagnostic. If you build a field by hand rather than through `asCommandFormField`, use `withCommandFormFieldBinding`. It marks the component and lets it bind itself when rendered behind an opaque custom layout: ```tsx import { CommandFormFieldProps, withCommandFormFieldBinding, } from '@cratis/arc.react/commands'; const MyHandRolledField = withCommandFormFieldBinding((props: CommandFormFieldProps) => ( props.onValueChange?.(event.target.value)} /> )); ``` `markAsCommandFormField` remains supported for compatibility. Marker-only fields can be discovered through visible child trees, while `withCommandFormFieldBinding` is the preferred choice for fields that may cross custom component boundaries. ## Example: PrimeReact InputText [Section titled “Example: PrimeReact InputText”](#example-primereact-inputtext) Here’s a complete example of creating a custom field using PrimeReact’s `InputText` component: ```tsx import React from 'react'; import { InputText, InputTextProps } from 'primereact/inputtext'; import { asCommandFormField, WrappedFieldProps } from '@cratis/arc.react/commands'; // Define the props your field accepts, combining WrappedFieldProps with PrimeReact's InputTextProps interface PrimeInputTextFieldProps extends WrappedFieldProps { placeholder?: InputTextProps['placeholder']; maxLength?: InputTextProps['maxLength']; keyfilter?: InputTextProps['keyfilter']; size?: InputTextProps['size']; variant?: InputTextProps['variant']; } // Create the field component export const PrimeInputTextField = asCommandFormField( (props) => { const { value, onChange, invalid, required, errors, placeholder, maxLength, keyfilter, size, variant, ...rest } = props; return (
{errors.length > 0 && (
{errors.map((error, idx) => ( {error} ))}
)}
); }, { defaultValue: '', extractValue: (e: unknown) => { if (e && typeof e === 'object' && 'target' in e) { const event = e as React.ChangeEvent; return event.target.value; } return String(e || ''); }, }, ); ``` ### Usage [Section titled “Usage”](#usage) ```tsx import { CommandForm } from '@cratis/arc.react/commands'; import { PrimeInputTextField } from './fields/PrimeInputTextField'; interface UserCommand { name: string; email: string; phone: string; } function UserForm() { return ( value={(c) => c.name} title='Full Name' placeholder='Enter your name' required /> value={(c) => c.email} title='Email Address' placeholder='you@example.com' keyfilter='email' required /> value={(c) => c.phone} title='Phone Number' placeholder='+1 (555) 123-4567' keyfilter='int' /> ); } ``` ## Advanced Examples [Section titled “Advanced Examples”](#advanced-examples) ### Complex Component with Multiple Elements [Section titled “Complex Component with Multiple Elements”](#complex-component-with-multiple-elements) ```tsx interface RichTextFieldProps extends WrappedFieldProps { maxLength?: number; showCharCount?: boolean; } export const RichTextField = asCommandFormField( (props) => { const { value, onChange, invalid, required, errors, maxLength, showCharCount } = props; const charCount = value.length; return (