diff --git a/.claude/commands/coreex-docs-sync.md b/.claude/commands/coreex-docs-sync.md
new file mode 100644
index 00000000..43f0709d
--- /dev/null
+++ b/.claude/commands/coreex-docs-sync.md
@@ -0,0 +1,87 @@
+---
+description: "Fetch the CoreEx sample architecture docs and all per-package AI usage guides from GitHub and cache them locally under .github/docs/coreex/ for offline, faster expert guidance."
+allowed-tools: [Read, Write, Glob, Grep, WebFetch]
+---
+
+Sync the CoreEx docs to `.github/docs/coreex/`. Follow these steps exactly.
+
+## Step 1 — Detect current CoreEx version
+
+Search for the `CoreEx` NuGet package version in this order, stopping at the first match:
+1. `Directory.Packages.props` — look for ``
+2. Any `*.csproj` file — look for ``
+3. `Directory.Build.props`
+
+Record the version (or `unknown` if not found). Use it in the manifest.
+
+## Step 2 — Detect referenced CoreEx packages (for manifest only)
+
+Scan `Directory.Packages.props` and all `*.csproj` files for `PackageVersion` or `PackageReference` entries whose `Include` attribute starts with `CoreEx`. Collect the distinct package names. This list goes into the manifest so the CoreEx Expert knows which packages the project currently uses — it does not limit which guides are synced.
+
+## Step 3 — Create cache directories
+
+Ensure both directories exist. Create them if absent:
+- `.github/docs/coreex/`
+- `.github/docs/coreex/agents/`
+
+## Step 4 — Fetch and write the sample architecture docs
+
+Fetch each URL below and write to the corresponding local path. Report each as it completes.
+
+| URL | Local path |
+|---|---|
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/samples/docs/local-dev.md` | `.github/docs/coreex/local-dev.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/samples/docs/layers.md` | `.github/docs/coreex/layers.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/samples/docs/patterns.md` | `.github/docs/coreex/patterns.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/samples/docs/contracts-layer.md` | `.github/docs/coreex/contracts-layer.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/samples/docs/domain-layer.md` | `.github/docs/coreex/domain-layer.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/samples/docs/application-layer.md` | `.github/docs/coreex/application-layer.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/samples/docs/infrastructure-layer.md` | `.github/docs/coreex/infrastructure-layer.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/samples/docs/hosts-layer.md` | `.github/docs/coreex/hosts-layer.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/samples/docs/testing.md` | `.github/docs/coreex/testing.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/samples/docs/tooling.md` | `.github/docs/coreex/tooling.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/samples/docs/aspire.md` | `.github/docs/coreex/aspire.md` |
+
+## Step 5 — Fetch and write all per-package guides
+
+Fetch the `AGENTS.md` for every CoreEx package listed below — regardless of whether the project currently references them. This allows the CoreEx Expert to guide on and recommend any package, including ones not yet adopted.
+
+| URL | Local path |
+|---|---|
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/src/CoreEx/AGENTS.md` | `.github/docs/coreex/agents/CoreEx.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/src/CoreEx.AspNetCore/AGENTS.md` | `.github/docs/coreex/agents/CoreEx.AspNetCore.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/src/CoreEx.AspNetCore.NSwag/AGENTS.md` | `.github/docs/coreex/agents/CoreEx.AspNetCore.NSwag.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/src/CoreEx.Azure.Messaging.ServiceBus/AGENTS.md` | `.github/docs/coreex/agents/CoreEx.Azure.Messaging.ServiceBus.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/src/CoreEx.Caching.FusionCache/AGENTS.md` | `.github/docs/coreex/agents/CoreEx.Caching.FusionCache.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/src/CoreEx.CodeGen/AGENTS.md` | `.github/docs/coreex/agents/CoreEx.CodeGen.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/src/CoreEx.Data/AGENTS.md` | `.github/docs/coreex/agents/CoreEx.Data.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/src/CoreEx.Database/AGENTS.md` | `.github/docs/coreex/agents/CoreEx.Database.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/src/CoreEx.Database.Postgres/AGENTS.md` | `.github/docs/coreex/agents/CoreEx.Database.Postgres.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/src/CoreEx.Database.SqlServer/AGENTS.md` | `.github/docs/coreex/agents/CoreEx.Database.SqlServer.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/src/CoreEx.DomainDriven/AGENTS.md` | `.github/docs/coreex/agents/CoreEx.DomainDriven.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/src/CoreEx.EntityFrameworkCore/AGENTS.md` | `.github/docs/coreex/agents/CoreEx.EntityFrameworkCore.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/src/CoreEx.Events/AGENTS.md` | `.github/docs/coreex/agents/CoreEx.Events.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/src/CoreEx.RefData/AGENTS.md` | `.github/docs/coreex/agents/CoreEx.RefData.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/src/CoreEx.UnitTesting/AGENTS.md` | `.github/docs/coreex/agents/CoreEx.UnitTesting.md` |
+| `https://raw.githubusercontent.com/Avanade/CoreEx/main/src/CoreEx.Validation/AGENTS.md` | `.github/docs/coreex/agents/CoreEx.Validation.md` |
+
+If any fetch fails, record the failure, skip that file, and continue.
+
+## Step 6 — Write the manifest
+
+Write `.github/docs/coreex/.manifest` with this exact format:
+
+```
+synced: YYYY-MM-DD
+coreex-version:
+referenced-packages:
+```
+
+## Step 7 — Report
+
+Summarise:
+- How many architecture docs were written successfully.
+- How many package guides were written successfully (out of 16).
+- Any files that failed to fetch (with the error).
+- The CoreEx version and referenced packages recorded in the manifest.
+- A reminder: *"Re-run `/coreex-docs-sync` after bumping the CoreEx NuGet version or when the CoreEx Expert suggests the cache is stale."*
diff --git a/.claude/commands/coreex-expert.md b/.claude/commands/coreex-expert.md
new file mode 100644
index 00000000..286e8183
--- /dev/null
+++ b/.claude/commands/coreex-expert.md
@@ -0,0 +1,8 @@
+---
+description: "CoreEx Expert — architecture guidance, pattern decisions, and sample-aligned advice for projects using CoreEx NuGet packages."
+allowed-tools: [Read, Glob, Grep, WebFetch, WebSearch, Edit, Write]
+---
+
+Read `.github/agents/coreex-expert.agent.md` and follow the instructions in that file.
+
+The user's question or request follows. Apply the CoreEx Expert role, operating rules, and response format defined in the agent file.
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
new file mode 100644
index 00000000..b1dbbf04
--- /dev/null
+++ b/.config/dotnet-tools.json
@@ -0,0 +1,12 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-reportgenerator-globaltool": {
+ "version": "5.5.10",
+ "commands": [
+ "reportgenerator"
+ ]
+ }
+ }
+}
diff --git a/.dockerignore b/.dockerignore
deleted file mode 100644
index f2dfd3bc..00000000
--- a/.dockerignore
+++ /dev/null
@@ -1,37 +0,0 @@
-**/.classpath
-**/.dockerignore
-**/.env
-**/.git
-**/.gitignore
-**/.project
-**/.settings
-**/.toolstarget
-**/.vs
-**/.vscode
-**/*.*proj.user
-**/*.dbmdl
-**/*.jfm
-**/azds.yaml
-**/bin
-**/charts
-**/docker-compose*
-**/Dockerfile*
-**/node_modules
-**/npm-debug.log
-**/obj
-**/secrets.dev.yaml
-**/values.dev.yaml
-LICENSE
-README.md
-
-# Build results
-[Dd]ebug/
-[Dd]ebugPublic/
-[Rr]elease/
-[Rr]eleases/
-x64/
-x86/
-bld/
-[Bb]in/
-[Oo]bj/
-[Ll]og/
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..643a1302
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,197 @@
+root = true
+
+# ── Universal ────────────────────────────────────────────────────────────────
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 4
+trim_trailing_whitespace = true
+insert_final_newline = true
+end_of_line = lf
+
+# ── C# source files ──────────────────────────────────────────────────────────
+
+[*.cs]
+indent_style = space
+indent_size = 4
+
+# ── Formatting ───────────────────────────────────────────────────────────────
+
+# Allman-style braces — opening brace always on its own line.
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation
+csharp_indent_case_contents = true
+csharp_indent_switch_labels = true
+csharp_indent_labels = flush_left
+
+# Spacing
+csharp_space_after_cast = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_around_binary_operators = before_and_after
+
+# Wrapping
+csharp_preserve_single_line_statements = false
+csharp_preserve_single_line_blocks = true
+
+# Using directives — System.* first; no blank lines between groups.
+dotnet_sort_system_directives_first = false
+dotnet_separate_import_directive_groups = false
+
+# ── Language rules — standards (IDE warning; not a build error without EnforceCodeStyleInBuild) ──
+
+# File-scoped namespaces required.
+csharp_style_namespace_declarations = file_scoped:warning
+
+# Null-forgiving operator: prefer is-null checks over reference equality.
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
+
+# ── Language rules — modernisation (IDE suggestion) ──────────────────────────
+
+# Braces: omit on single-line if/else/for/while bodies; require when multi-line.
+csharp_prefer_braces = when_multiline:none
+
+# Expression-bodied members: prefer => when the entire body is a single expression.
+csharp_style_expression_bodied_methods = when_on_single_line:none
+csharp_style_expression_bodied_properties = true:none
+csharp_style_expression_bodied_accessors = when_on_single_line:none
+csharp_style_expression_bodied_indexers = when_on_single_line:none
+csharp_style_expression_bodied_operators = when_on_single_line:none
+csharp_style_expression_bodied_lambdas = when_on_single_line:none
+csharp_style_expression_bodied_local_functions = when_on_single_line:none
+csharp_style_expression_bodied_constructors = when_on_single_line:none
+
+# var: prefer explicit type for built-ins; prefer var when type is obvious from RHS.
+csharp_style_var_for_built_in_types = false:none
+csharp_style_var_when_type_is_apparent = true:none
+csharp_style_var_elsewhere = true:none
+
+# Null handling and pattern matching.
+dotnet_style_null_propagation = true:none
+dotnet_style_coalesce_expression = true:none
+csharp_style_prefer_null_check_over_type_check = true:none
+csharp_style_prefer_is_not_expression = true:none
+csharp_style_prefer_pattern_matching = true:none
+csharp_style_prefer_switch_expression = true:none
+
+# Object and collection initialisers.
+dotnet_style_object_initializer = true:none
+dotnet_style_collection_initializer = true:none
+
+# Modern using declarations (using var x = ... instead of using (var x = ...)).
+csharp_prefer_simple_using_statement = true:none
+
+# Auto-properties over explicit backing fields where no extra logic exists.
+dotnet_style_prefer_auto_properties = true:none
+
+# Throw expressions (x ?? throw ...).
+csharp_style_throw_expression = true:none
+
+# Conditional delegate invocation (handler?.Invoke() vs if (handler != null) handler()).
+csharp_style_conditional_delegate_call = true:none
+
+# Tuple/anonymous type member names.
+dotnet_style_prefer_inferred_tuple_names = true:none
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:none
+
+# Accessibility modifiers — always explicit, even when the default would apply.
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:none
+
+# Preferred modifier order.
+csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:none
+
+# ── Naming conventions ───────────────────────────────────────────────────────
+# Rules are evaluated top-to-bottom; the first match wins.
+
+## Symbol groups
+
+dotnet_naming_symbols.private_fields.applicable_kinds = field
+dotnet_naming_symbols.private_fields.applicable_accessibilities = private,private_protected
+
+dotnet_naming_symbols.non_private_fields.applicable_kinds = field
+dotnet_naming_symbols.non_private_fields.applicable_accessibilities = public,internal,protected,protected_internal
+
+dotnet_naming_symbols.constants.applicable_kinds = field,local
+dotnet_naming_symbols.constants.required_modifiers = const
+
+dotnet_naming_symbols.interfaces.applicable_kinds = interface
+
+dotnet_naming_symbols.type_parameters.applicable_kinds = type_parameter
+
+dotnet_naming_symbols.async_methods.applicable_kinds = method
+dotnet_naming_symbols.async_methods.required_modifiers = async
+
+## Naming styles
+
+# _camelCase
+dotnet_naming_style.prefix_underscore.capitalization = camel_case
+dotnet_naming_style.prefix_underscore.required_prefix = _
+
+# PascalCase
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+# IPascalCase
+dotnet_naming_style.i_prefix_pascal_case.capitalization = pascal_case
+dotnet_naming_style.i_prefix_pascal_case.required_prefix = I
+
+# TPascalCase
+dotnet_naming_style.t_prefix_pascal_case.capitalization = pascal_case
+dotnet_naming_style.t_prefix_pascal_case.required_prefix = T
+
+# PascalCaseAsync
+dotnet_naming_style.pascal_case_async.capitalization = pascal_case
+dotnet_naming_style.pascal_case_async.required_suffix = Async
+
+## Rules
+
+# Private fields → _camelCase (warning — agreed standard)
+dotnet_naming_rule.private_fields_underscore.symbols = private_fields
+dotnet_naming_rule.private_fields_underscore.style = prefix_underscore
+dotnet_naming_rule.private_fields_underscore.severity = warning
+
+# Non-private fields → PascalCase (suggestion — rare in this codebase)
+dotnet_naming_rule.non_private_fields_pascal.symbols = non_private_fields
+dotnet_naming_rule.non_private_fields_pascal.style = pascal_case
+dotnet_naming_rule.non_private_fields_pascal.severity = suggestion
+
+# Constants → PascalCase (suggestion)
+dotnet_naming_rule.constants_pascal.symbols = constants
+dotnet_naming_rule.constants_pascal.style = pascal_case
+dotnet_naming_rule.constants_pascal.severity = suggestion
+
+# Interfaces → IPascalCase (warning — agreed standard)
+dotnet_naming_rule.interfaces_i_prefix.symbols = interfaces
+dotnet_naming_rule.interfaces_i_prefix.style = i_prefix_pascal_case
+dotnet_naming_rule.interfaces_i_prefix.severity = warning
+
+# Type parameters → TPascalCase (suggestion)
+dotnet_naming_rule.type_parameters_t_prefix.symbols = type_parameters
+dotnet_naming_rule.type_parameters_t_prefix.style = t_prefix_pascal_case
+dotnet_naming_rule.type_parameters_t_prefix.severity = suggestion
+
+# Async methods → PascalCaseAsync (warning — agreed standard)
+dotnet_naming_rule.async_methods_async_suffix.symbols = async_methods
+dotnet_naming_rule.async_methods_async_suffix.style = pascal_case_async
+dotnet_naming_rule.async_methods_async_suffix.severity = warning
+
+# ── Structured data files ─────────────────────────────────────────────────────
+
+[*.{json,jsn,xml,yaml,yml,props,csproj,sln,sql,pgsql}]
+indent_style = space
+indent_size = 2
+
+# ── Markdown ──────────────────────────────────────────────────────────────────
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.filenesting.json b/.filenesting.json
new file mode 100644
index 00000000..e21bbefa
--- /dev/null
+++ b/.filenesting.json
@@ -0,0 +1,14 @@
+{
+ "help": "https://go.microsoft.com/fwlink/?linkid=866610",
+ "dependentFileProviders": {
+ "add": {
+ "pathSegment": {
+ "add": {
+ ".g.cs": [
+ ".cs"
+ ]
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..2bbf8c1f
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,6 @@
+* text=auto eol=lf
+
+*.bat text eol=crlf
+*.cmd text eol=crlf
+
+*.sh text eol=lf
\ No newline at end of file
diff --git a/.github/AI-WORKFLOWS.md b/.github/AI-WORKFLOWS.md
new file mode 100644
index 00000000..03fcd96a
--- /dev/null
+++ b/.github/AI-WORKFLOWS.md
@@ -0,0 +1,51 @@
+# AI Workflow Set
+
+This folder contains the AI artefacts that give GitHub Copilot and Claude Code authoritative knowledge of CoreEx patterns, conventions, and architecture. They can be used directly in the CoreEx repository or copied into a consuming project.
+
+## What's here
+
+| Artefact | Path | Purpose |
+|----------|------|---------|
+| Global instructions | `copilot-instructions.md` | Auto-injected project-wide context: repo shape, conventions, house rules, generated-file ownership. Applied to every chat interaction automatically. |
+| Area instructions | `instructions/*.instructions.md` | Scoped context injected automatically when editing a matching file type (contracts, services, repositories, controllers, tests, etc.). |
+| Agent | `agents/coreex-expert.agent.md` | Dedicated expert for CoreEx architecture and pattern guidance — explains conventions, reviews designs, and routes to the right command. |
+| Prompts | `prompts/*.prompt.md` | Deterministic, file-driven commands invoked with `/` in chat. |
+| Skills | `skills/*/SKILL.md` | Reasoning-based commands for open-ended tasks. Invoked with `/` in Claude Code; attach the `SKILL.md` via `#file:` in Copilot. |
+| Authoring guides | `INSTRUCTION_AUTHORING.md`, `SKILL_AUTHORING.md` | Standards for writing new instruction files and skills. |
+
+## Agent
+
+**`coreex-expert`** — invoke when you need to explain a CoreEx concept, choose between patterns, review a design, or get architecture guidance aligned to the sample implementations.
+
+- Claude Code: `@coreex-expert`
+- Copilot Chat: switch to **Agent** mode and select **CoreEx Expert**
+
+The agent uses a local doc cache (populated by `/coreex-docs-sync`) to avoid live GitHub fetches on every question. It covers all 16 CoreEx packages, distinguishing those already in the project from ones the project could adopt. See the [agent README](./agents/README.md) for the resolution flowchart, cache structure, and adoption guide.
+
+## Instructions
+
+Instructions are passive — no action is needed to activate them. The global file applies to every session; area files are injected automatically based on what you are editing.
+
+| File | Injected when editing |
+|------|-----------------------|
+| `coreex-conventions.instructions.md` | All `.cs` files — naming, nullability, expression bodies, `ConfigureAwait`, house rules |
+| `coreex-contracts.instructions.md` | Contract files — `[Contract]`, `[ReferenceData]`, source generation |
+| `coreex-application-services.instructions.md` | Application services — `TransactionAsync`, validation, event enqueuing |
+| `coreex-validators.instructions.md` | Validator files — `Validator`, rule chains |
+| `coreex-repositories.instructions.md` | Repository files — `EfDbModel`, mappers, `QueryArgsConfig`, paging |
+| `coreex-api-controllers.instructions.md` | Controller files — `WebApi` helpers, `[IdempotencyKey]`, PATCH |
+| `coreex-event-subscribers.instructions.md` | Subscriber files — `[Subscribe]`, `SubscribedManager`, error handling |
+| `coreex-host-setup.instructions.md` | `Program.cs` files — middleware order, service registration, outbox relay |
+| `coreex-tooling.instructions.md` | CodeGen and Database projects — `ref-data.yaml`, DbEx, generated-file ownership |
+| `coreex-tests.instructions.md` | Test files — UnitTestEx, NUnit, AwesomeAssertions, outbox/event assertions |
+| `coreex-domain.instructions.md` | Domain files — aggregates, mutation guards, `Result` pipelines |
+
+## Prompts, Skills, and Templates
+
+| Command | Type | What it does |
+|---------|------|-------------|
+| [`CoreEx.Template`](../src/CoreEx.Template/README.md) | Template pack | Deterministic `dotnet new` scaffolding for a CoreEx solution plus API, relay, and subscriber hosts. Use `dotnet new install CoreEx.Template` and then the `coreex*` templates in a terminal. |
+| [`/acquire-codebase-knowledge`](./skills/acquire-codebase-knowledge/README.md) | Skill | Maps an unfamiliar codebase and produces seven structured onboarding documents. |
+| [`/coreex-scaffold`](./skills/solution-scaffolder/README.md) | Skill-backed prompt | Guides greenfield solution scaffolding, chooses the smallest safe CoreEx.Template shape, and runs the matching `dotnet new coreex*` commands. |
+| [`/coreex-docs-sync`](./skills/coreex-docs-sync/README.md) | Skill | Fetches and caches CoreEx architecture docs and all per-package AI guides locally under `.github/docs/coreex/`. |
+| [`/aspire`](./skills/aspire/README.md) | Skill | Orchestrates Aspire distributed apps locally: start, stop, logs, debug. |
diff --git a/.github/INSTRUCTION_AUTHORING.md b/.github/INSTRUCTION_AUTHORING.md
new file mode 100644
index 00000000..187eafe4
--- /dev/null
+++ b/.github/INSTRUCTION_AUTHORING.md
@@ -0,0 +1,246 @@
+---
+applyTo: "**/*.instructions.md"
+description: "Standards for authoring and maintaining Copilot instruction files in this repository"
+tags: ["authoring", "standards", "instructions", "documentation"]
+---
+
+# Instruction File Authoring Standards
+
+Instruction files (`.instructions.md`) are **context-window injections for a code model**. When Copilot generates or edits a file that matches the `applyTo` glob, the instruction file is automatically prepended to the context. This has two practical consequences that drive every rule below:
+
+- **Every token costs.** Instruction file content displaces actual code context. Brevity and precision matter.
+- **Code examples outperform prose rules.** Copilot is a code model. Showing it the exact pattern to follow produces better results than listing `MUST`/`MUST NOT` directives.
+
+Write instruction files as you would a concise internal coding guide for a capable new team member, not as a policy document.
+
+---
+
+## Principles
+
+- **Show, don't tell.** A real, working code example is worth more than five imperative rules.
+- **Be specific.** Use actual type names, package names, and method names from this codebase.
+- **Be brief.** If content cannot fit on one screen, split into a separate file or move detail to `Further Reading`.
+- **Do not restate global rules.** If `.github/copilot-instructions.md` already covers it, do not repeat it here.
+- **Explicit negation matters.** Copilot's training data contains many common patterns that are wrong for this repo. State anti-patterns explicitly with a `## Do Not` section.
+- **Scope tightly.** An instruction that is always injected regardless of context wastes tokens. Use the narrowest `applyTo` glob that is still correct.
+- **Never direct Copilot toward generated files.** Instruction files must never include guidance, examples, or `applyTo` globs that would cause Copilot to create or modify `*.g.cs`, `*.g.sql`, `*.g.pgsql`, or any other generated-output file. All generated files are owned exclusively by their corresponding tooling (Roslyn source generator, `*.Database` project, `*.CodeGen` project). Changes must be made to the source templates or generation configuration, not to the output. See [Generated Code](#generated-code) below.
+
+---
+
+## Required Format
+
+```markdown
+---
+applyTo: ""
+description: ""
+tags: ["tag1", "tag2"]
+---
+
+# Conventions
+
+## NuGet / Project References
+
+| Package | Key types provided |
+|---|---|
+| `CoreEx.X` | `TypeA`, `TypeB`, `TypeC` |
+
+##
+
+
+
+```csharp
+// real example matching what the codebase actually looks like
+```
+
+##
+
+...
+
+## Do Not
+
+- Do not use `SomeWrongType` — use `CorrectType` instead.
+- Do not call `SomeAntiPattern()` directly; delegate to the application service.
+
+## Further Reading
+
+- [`samples/docs/.md`](../../samples/docs/.md) — layer-level walkthrough with sample code references.
+- [`src/CoreEx.X/README.md`](../../src/CoreEx.X/README.md) — full API reference for the primary package.
+```
+
+---
+
+## Frontmatter Rules
+
+- Field order: `applyTo` → `description` → `tags`.
+- `applyTo` must be the narrowest glob that correctly captures all relevant files. Examples:
+ - `**/Contracts/**/*.cs` — contract DTOs
+ - `**/Application/**/*.cs` — application services
+ - `**/Infrastructure/**/*.cs` — repositories and adapters
+ - `**/Controllers/**/*.cs` — API controllers
+ - `**/Subscribe/**/*.cs` — event subscriber classes
+ - `**/Program.cs` — host entry points
+ - `**/*Validator*.cs` — validator classes
+ - `**/*.Test*/**/*.cs` — test projects
+ - `**/*.Database/**/*.sql` — database project SQL scripts
+- `description` must be one sentence, concrete, and specific to the area governed.
+- `tags` must reflect the instruction's domain (e.g., `["validation", "application-layer", "unit-of-work"]`).
+- Do not use `applyTo: "**"` unless the file is genuinely repository-wide.
+
+---
+
+## Section Guidance
+
+### `## NuGet / Project References` — required, always first
+
+List every package the generated code will need, with the key types Copilot should use from each. This is the highest-value, lowest-token section: it anchors imports and prevents Copilot from inventing types.
+
+```markdown
+| Package | Key types provided |
+|---|---|
+| `CoreEx` | `[ScopedService]`, `IUnitOfWork`, `NotFoundException`, `.ThrowIfNull()` |
+| `CoreEx.Validation` | `Validator`, `.ValidateAndThrowAsync()` |
+```
+
+### Pattern sections — one section per distinct concept
+
+Name sections after the actual coding concept, not a rule number. Each section should contain one or two orienting sentences followed by a real code example. Avoid long prose; if a concept needs a paragraph of explanation, it belongs in `Further Reading`.
+
+### `## Do Not` — required when anti-patterns exist
+
+List things Copilot must not generate for this area. Be specific: name the wrong type, wrong library, or wrong pattern, and say what to use instead. This section counteracts Copilot's training data for common-but-wrong patterns.
+
+```markdown
+## Do Not
+
+- Do not use `AutoMapper` — use explicit mapping helpers or classes.
+- Do not inject `IUnitOfWork` into controllers — delegate to the application service.
+- Do not inherit from `Controller` — inherit from `ControllerBase`.
+```
+
+### `## Further Reading` — required
+
+Link to the layer-level `samples/docs/` walkthrough and any directly relevant `src/*/README.md` files. Copilot will fetch these when it needs deeper context, keeping the instruction file itself lean.
+
+```markdown
+## Further Reading
+
+- [`samples/docs/application-layer.md`](../../samples/docs/application-layer.md) — application service patterns with sample code.
+- [`src/CoreEx/Results/README.md`](../../src/CoreEx/Results/README.md) — `Result` pipeline API reference.
+```
+
+---
+
+## Conflict Resolution
+
+- `.github/copilot-instructions.md` defines repository-wide defaults. Never restate them in a scoped file.
+- When two instruction files could apply to the same file, the narrower `applyTo` glob wins.
+- If a new file would duplicate policy from an existing one, extend the existing file instead.
+
+## Non-Instruction Files in `.github/instructions/`
+
+Not every file in this folder is an instruction file. `namespace_readme_template.md` is a copy/paste template used by skills and prompts — it has no `applyTo` frontmatter and is never auto-injected by Copilot. Do not add `applyTo` to it or restructure it to follow the instruction file format.
+
+---
+
+## Generated Code
+
+Generated files are owned exclusively by their tooling and must never be touched by Copilot or by hand. There are three distinct generators in this repo, each with its own input and output:
+
+### Roslyn source generator (`CoreEx.Generator`)
+
+Triggered at compile time. Reads classes decorated with `[Contract]` or `[ReferenceData]` and emits companion `.g.cs` files in the same project.
+
+| Output pattern | What to change instead |
+|---|---|
+| `*.g.cs` alongside a `[Contract]` class | The decorated partial class itself (add/remove properties, change attributes) |
+| `*.g.cs` alongside a `[ReferenceData]` class | The decorated partial class itself |
+
+### CoreEx.CodeGen (`*.CodeGen` project)
+
+A development-time console tool. Reads a single `ref-data.yaml` file (validated against `schema/coreex-refdata.json`) and generates a complete reference-data implementation — contract, controller, service, repository interface, repository, and mapper — across four project directories in one run.
+
+| Output pattern | Target layer | What to change instead |
+|---|---|---|
+| `Contracts/**/*.g.cs` | Contracts | `ref-data.yaml` entity/property config |
+| `**/Controllers/**/*.g.cs` | Api | `ref-data.yaml` route/entity config |
+| `**/Services/**/*.g.cs` | Application | `ref-data.yaml` entity config |
+| `**/Repositories/**/*.g.cs` | Infrastructure | `ref-data.yaml` repository/mapper config |
+| `**/Mappers/**/*.g.cs` | Infrastructure | `ref-data.yaml` property config or `excludeMapper: true` |
+
+The entry point is a one-line `Program.cs` in the `*.CodeGen` project. Templates live in `CoreEx.CodeGen/RefData/Templates/` (embedded in the NuGet package) and are the only place structural changes to the generated shape belong.
+
+### DbEx (`*.Database` project)
+
+A development-time migration and generation tool. Reads YAML configuration and SQL migration scripts to produce database schema, outbox infrastructure, and EF Core scaffolding.
+
+| Output pattern | What to change instead |
+|---|---|
+| `*.g.sql`, `*.g.pgsql` | The YAML configuration or SQL migration scripts in the `*.Database` project |
+| `*DbContext.g.cs`, `Persistence/*.g.cs` | The DbEx YAML config in the `*.Database` project |
+
+### Rules for instruction file authors
+
+- Do not write guidance that instructs Copilot to create or modify any `*.g.*` file.
+- Do not include `*.g.*` files in `applyTo` globs — they must never match a generated output.
+- When showing examples that reference generated types (e.g. a persistence model or a ref-data controller), show the YAML or source class that drives generation, not the generated output.
+- If a user asks Copilot to edit a generated file, identify which generator owns it from the table above and redirect to the correct input source.
+
+---
+
+## Worked Example
+
+A well-formed instruction file for API controllers:
+
+```markdown
+---
+applyTo: "**/Controllers/**/*.cs"
+description: "API controller conventions for CoreEx: inheritance, routing, WebApi helper usage, and CQRS separation"
+tags: ["controllers", "api", "routing", "dependency-injection"]
+---
+
+# API Controller Conventions
+
+## NuGet / Project References
+
+| Package | Key types provided |
+|---|---|
+| `CoreEx.AspNetCore` | `WebApi`, `[IdempotencyKey]`, `[ProducesNotFoundProblem]`, `[Query]`, `[Paging]` |
+| `CoreEx.AspNetCore.NSwag` | `[OpenApiTag]` |
+
+## Structure
+
+Inherit from `ControllerBase`. Decorate with `[ApiController]`, `[Route]`, and `[OpenApiTag]`. Inject `WebApi`
+and the relevant service interface via primary constructor, guarded with `.ThrowIfNull()`. Split read and
+write operations into separate controller classes following CQRS conventions.
+
+```csharp
+[ApiController, Route("/api/products"), OpenApiTag("Products")]
+public class ProductController(WebApi webApi, IProductService service) : ControllerBase
+{
+ private readonly WebApi _webApi = webApi.ThrowIfNull();
+ private readonly IProductService _service = service.ThrowIfNull();
+}
+```
+
+## Action Methods
+
+Return `Task` via the `WebApi` helper. Do not return typed `ActionResult` directly.
+
+```csharp
+[HttpPost, IdempotencyKey]
+public Task CreateAsync([FromBody] Product product) =>
+ _webApi.PostAsync(Request, () => _service.CreateAsync(product), statusCode: HttpStatusCode.Created);
+```
+
+## Do Not
+
+- Do not inherit from `Controller` — that pulls in View support; use `ControllerBase`.
+- Do not return `ActionResult` directly — use the `WebApi` helper for consistent error translation.
+- Do not inject `IUnitOfWork` into controllers — it belongs in the application service.
+- Do not put business logic in controllers — delegate immediately to the application service.
+
+## Further Reading
+
+- [`samples/docs/hosts-layer.md`](../../samples/docs/hosts-layer.md) — API host composition and controller patterns.
+- [`src/CoreEx.AspNetCore/README.md`](../../src/CoreEx.AspNetCore/README.md) — `WebApi` helper API reference.
+```
diff --git a/.github/SKILL_AUTHORING.md b/.github/SKILL_AUTHORING.md
new file mode 100644
index 00000000..18653ed8
--- /dev/null
+++ b/.github/SKILL_AUTHORING.md
@@ -0,0 +1,149 @@
+---
+description: "Standards for creating and maintaining SKILL.md files"
+applyTo: "**/.github/skills/**/SKILL.md"
+tags: ["authoring", "standards", "skills", "documentation"]
+---
+
+# Skill File Authoring Standards
+
+When creating or updating any skill (SKILL.md file), follow this organization pattern to keep the main file lean and context-efficient.
+
+## Purpose
+
+Skill files guide AI agents through complex, multi-step tasks. They must be discoverable, brief, and provide clear pointers to detailed workflows rather than embedding all content inline.
+
+## Skill Directory Structure
+
+```
+skills/{skill-name}/
+ SKILL.md # Main entry point (lean, <300 lines)
+ references/
+ workflow.md # Detailed step-by-step workflows
+ checklists.md # Completion gates, validation criteria
+ patterns.md # Code patterns, templates, conventions
+ troubleshooting.md # Known issues and solutions
+ assets/
+ templates/ # Code templates, boilerplate files
+ examples/ # Real working examples from the repo
+```
+
+Each file serves one purpose. Keep files focused and scannable.
+
+## SKILL.md Content Rules
+
+The main SKILL.md must include:
+
+1. **YAML frontmatter** with `name`, `description`, `argument-hint`, `tags`
+2. **One-sentence purpose statement** — what the skill does and when to use it
+3. **"When to Use" section** — bullet points, not prose; concrete triggers
+4. **"When Not to Use" section** — prevents misuse and clarifies boundaries
+5. **Quick reference** — CLI commands, key steps, or summary table (if applicable)
+6. **Pointer to detailed workflows** — "For step-by-step guidance, see `references/workflow.md`"
+7. **Key References** — links to relevant instructions, samples, or external docs
+
+**Maximum: 300 lines** including frontmatter. If you exceed this, move content to `references/`.
+
+## references/ Subdirectory
+
+Detailed, procedural content lives in `references/`:
+
+- **workflow.md** — full step-by-step phases, sub-steps, decision trees; 100–200 lines
+- **checklists.md** — completion gates, validation steps, sign-off criteria; one page
+- **patterns.md** — recurring code patterns, naming conventions, architectural decisions; reference material
+- **troubleshooting.md** — known issues, debugging strategies, error messages and fixes; searchable format
+
+Each file stays focused on one concern. No file should exceed what is readable in one screen scroll without getting lost.
+
+## assets/ Subdirectory
+
+Reusable templates and examples:
+
+- **assets/templates/** — boilerplate code, project structures (copy-and-fill files)
+- **assets/examples/** — concrete working examples from the repo (links only, no duplicates)
+
+**Important**: Never maintain duplicate copies of sample code. Always link to the canonical source in `samples/` or other repository locations.
+
+## Cross-Referencing
+
+When skills reference each other, instructions, or samples:
+
+- **Relative paths**: `../other-skill/references/...` (for other skills)
+- **Absolute workspace paths**: `/.github/instructions/coreex-host-setup.instructions.md`, `/samples/src/Contoso.Products.Api/Program.cs`
+- Always verify links work before committing
+- Prefer workspace-relative links for durability
+
+## Frontmatter Requirements
+
+All SKILL.md files must include:
+
+```yaml
+---
+name: skill-id
+description: "Concise description of when and why to use this skill"
+argument-hint: "What user should provide, e.g. 'Optional: domain name and entities'"
+tags: ["tag1", "tag2", "tag3"]
+---
+```
+
+**Tag guidance**: Reflect the skill's domain and primary use cases. Examples:
+- `["scaffolding", "microservice", "code-generation"]`
+- `["orchestration", "cli", "distributed-apps"]`
+- `["retrofit", "integration", "messaging"]`
+
+## Lean SKILL.md Example
+
+```yaml
+---
+name: coreex-design-review
+description: "Review a proposed CoreEx design against repository conventions and samples"
+argument-hint: "Feature, domain, or host shape to review"
+tags: ["design-review", "architecture", "coreex"]
+---
+
+# CoreEx Design Review
+
+Reviews a proposed feature or service shape against current CoreEx guidance. Focuses on layering, host responsibilities, validation, persistence, and messaging choices.
+
+## When to Use
+
+- Reviewing a proposed API, subscriber, relay, or orchestration shape
+- Comparing multiple implementation options before coding
+- Checking whether a design aligns to existing samples and instructions
+- Creating a small evidence-backed plan for a change
+
+## When Not to Use
+
+- Deterministic project scaffolding — use the `CoreEx.Template` `dotnet new` templates instead
+- Repository onboarding — use `/acquire-codebase-knowledge`
+- Running the local distributed app — use `/aspire`
+
+## Workflow Overview
+
+1. **Inspect Context** — examine the current domain, hosts, and nearby sample
+2. **Identify Shape** — decide API-only, API plus relay, API plus subscriber, or orchestration
+3. **Check Conventions** — validate layering, instructions, and naming expectations
+4. **Assess Risks** — call out migration, compatibility, and operational tradeoffs
+5. **Recommend Next Steps** — provide the smallest safe implementation path
+
+For detailed step-by-step guidance, see [`references/workflow.md`](references/workflow.md).
+
+## Key References
+
+- [Application Services Instructions](/.github/instructions/coreex-application-services.instructions.md)
+- [Contracts Instructions](/.github/instructions/coreex-contracts.instructions.md)
+- [Host Setup Instructions](/.github/instructions/coreex-host-setup.instructions.md)
+- [CoreEx Template Pack](../src/CoreEx.Template/README.md)
+- [CoreEx Capabilities](./docs/capabilities.md)
+```
+
+## Quality Gates
+
+Before completing a skill:
+
+- [ ] SKILL.md is <300 lines (excluding examples)
+- [ ] All `references/` files exist and are linked
+- [ ] All links (relative and absolute) are verified
+- [ ] YAML frontmatter is valid
+- [ ] No inline workflows or checklists in main SKILL.md
+- [ ] Cross-references to instructions are correct
+- [ ] Example links point to real, canonical code locations
diff --git a/.github/agents/README.md b/.github/agents/README.md
new file mode 100644
index 00000000..25a0963e
--- /dev/null
+++ b/.github/agents/README.md
@@ -0,0 +1,131 @@
+# CoreEx Expert Agent
+
+The `coreex-expert` agent gives GitHub Copilot and Claude Code authoritative, CoreEx-idiomatic guidance — architecture decisions, pattern selection, layer design, and implementation advice — all aligned to the Contoso sample implementations.
+
+It is defined once in `coreex-expert.agent.md` and works in both tools from the same file.
+
+| Tool | How to invoke |
+|------|--------------|
+| GitHub Copilot Chat | Switch to **Agent** mode and select **CoreEx Expert** |
+| Claude Code | `@coreex-expert` |
+
+---
+
+## How the agent resolves guidance
+
+The agent relies on a local doc cache rather than making live GitHub fetches on every question. The cache is populated by `/coreex-docs-sync` and stored under `.github/docs/coreex/`.
+
+```
+Developer asks a CoreEx question
+ │
+ ▼
+ Read .github/docs/coreex/.manifest
+ │
+ ┌─────┴─────┐
+ │ │
+ manifest no manifest
+ present │
+ │ └──► offer to run /coreex-docs-sync first
+ │ (never runs silently — always asks first)
+ │
+ check staleness
+ │
+ ┌────┴────┐
+ │ │
+ fresh stale
+ │ (>30 days OR coreex-version in manifest
+ │ differs from version in project files)
+ │ │
+ │ └──► recommend running /coreex-docs-sync
+ │
+ ▼
+Use local cache (always preferred over live GitHub fetches)
+ │
+ ├── .github/docs/coreex/*.md ← 10 architecture docs
+ │
+ └── .github/docs/coreex/agents/*.md ← 16 per-package AI guides
+ │
+ read manifest referenced-packages
+ to distinguish:
+ • package already in project → guide on current usage
+ • package not yet in project → recommend adopting it
+```
+
+The `referenced-packages` field in the manifest lets the agent distinguish between a package the project already uses and one it would need to add — without restricting which guides get synced.
+
+---
+
+## The local doc cache
+
+`/coreex-docs-sync` fetches from the CoreEx GitHub repository and writes two folders:
+
+**`.github/docs/coreex/`** — 10 architecture docs:
+
+| File | Content |
+|------|---------|
+| `layers.md` | Full layer dependency diagram and design-time tooling overview |
+| `patterns.md` | Pattern catalog — every architectural, application, messaging, and testing pattern |
+| `contracts-layer.md` | Generated contracts, `[Contract]`, `[ReferenceData]`, source generation |
+| `domain-layer.md` | Aggregates, mutation guards, integration-event accumulation, `Result` pipelines |
+| `application-layer.md` | Service orchestration, `TransactionAsync`, validators, policies, adapters |
+| `infrastructure-layer.md` | EF Core repositories, mappers, outbox wiring, relay publisher |
+| `hosts-layer.md` | API, Subscribe, and Outbox Relay `Program.cs` shapes, middleware ordering |
+| `testing.md` | Unit, integration, API, Subscribe, and Relay test patterns |
+| `tooling.md` | CodeGen and Database project run order, generated-file ownership |
+| `aspire.md` | Aspire orchestration for local distributed development and E2E testing |
+
+**`.github/docs/coreex/agents/`** — 16 per-package AI usage guides, one per CoreEx NuGet package. All 16 are synced unconditionally so the agent can guide on any package — including ones the project hasn't adopted yet.
+
+**`.github/docs/coreex/.manifest`** — records `synced` date, `coreex-version`, and `referenced-packages`.
+
+### Staleness triggers
+
+| Trigger | Agent action |
+|---------|-------------|
+| Cache absent | Offers to run `/coreex-docs-sync` before the first GitHub fetch |
+| `synced` date > 30 days | Recommends a refresh |
+| `coreex-version` in manifest ≠ version in project files | Recommends a refresh |
+
+---
+
+## Why sync all 16 package guides unconditionally
+
+An earlier design synced only the packages the project already references. This was changed because:
+
+- The agent cannot recommend adopting a package (e.g. `CoreEx.Caching.FusionCache`) if it has no knowledge of what that package offers.
+- All 16 guides are small markdown files — the total download is negligible.
+- Syncing all unconditionally removes the need to re-run after adding a new package.
+- The `referenced-packages` manifest field preserves the "in project vs. not yet" distinction without making it a gate on what gets synced.
+
+---
+
+## Adopting the agent in a consuming project
+
+Copy the following from this repository into any project that references CoreEx NuGet packages:
+
+```
+.github/
+ copilot-instructions.md
+ agents/
+ coreex-expert.agent.md
+ instructions/
+ coreex-conventions.instructions.md
+ coreex-contracts.instructions.md
+ coreex-application-services.instructions.md
+ coreex-validators.instructions.md
+ coreex-repositories.instructions.md
+ coreex-api-controllers.instructions.md
+ coreex-event-subscribers.instructions.md
+ coreex-host-setup.instructions.md
+ coreex-tooling.instructions.md
+ coreex-tests.instructions.md
+ coreex-domain.instructions.md
+ skills/
+ coreex-docs-sync/
+ SKILL.md
+ acquire-codebase-knowledge/ # optional — repo onboarding docs
+```
+
+On first use, run `/coreex-docs-sync` to populate the local cache. Re-run whenever the CoreEx NuGet version is bumped.
+
+For deterministic project scaffolding, use the [CoreEx.Template](../src/CoreEx.Template/README.md) `dotnet new` template pack rather than an agent skill.
diff --git a/.github/agents/coreex-expert.agent.md b/.github/agents/coreex-expert.agent.md
new file mode 100644
index 00000000..7d86786a
--- /dev/null
+++ b/.github/agents/coreex-expert.agent.md
@@ -0,0 +1,120 @@
+---
+name: CoreEx Expert
+description: "Use when you need to explain, understand, or decide how CoreEx works in your project. Triggers: explain CoreEx, how does CoreEx, which pattern, which capability, which shape, plan a feature, review a design, compare samples, architecture guidance, coding patterns, layering, host setup, validation, repository conventions, eventing, outbox relay, subscriber design, sample-aligned decisions."
+tools: [read/readFile, read/problems, search/codebase, search/fileSearch, search/textSearch, search/listDirectory, search/usages, search/changes, web/fetch, web/githubRepo, web/githubTextSearch, edit/editFiles, edit/createFile]
+user-invocable: true
+argument-hint: Ask for CoreEx pattern guidance, architecture decisions, or sample-aligned implementation advice.
+---
+You are the CoreEx Expert.
+
+Your mission:
+- Provide authoritative guidance on CoreEx architecture, patterns, and practices.
+- Prefer CoreEx-native primitives and conventions over generic .NET advice.
+- Keep recommendations aligned with the established layering, sample implementations, and consumer-facing AI guides.
+- Apply equally whether working in the CoreEx repository itself or a consuming project.
+
+## Primary sources of truth
+
+### Locally present
+
+These files are present when the CoreEx AI workflow set has been copied into the project:
+
+- `.github/copilot-instructions.md` — project-wide guidelines, repository shape, key conventions, and house rules.
+- `.github/instructions/coreex-contracts.instructions.md` — entity contracts, `[Contract]`, `[ReferenceData]`, source generation.
+- `.github/instructions/coreex-domain.instructions.md` — DDD aggregates, `Entity`, mutation guards, `Result` pipelines.
+- `.github/instructions/coreex-application-services.instructions.md` — service shape, `TransactionAsync`, validation-before-transaction, event enqueuing.
+- `.github/instructions/coreex-validators.instructions.md` — `Validator`, rule chains, `CommonValidator`, `ValidateAndThrowAsync`.
+- `.github/instructions/coreex-repositories.instructions.md` — `EfDbModel`, `IBiDirectionMapper`, `QueryArgsConfig`, paging.
+- `.github/instructions/coreex-api-controllers.instructions.md` — controller shape, `WebApi` helpers, `[IdempotencyKey]`, PATCH.
+- `.github/instructions/coreex-event-subscribers.instructions.md` — subscriber classes, `[Subscribe]`, `SubscribedManager`, error handling.
+- `.github/instructions/coreex-host-setup.instructions.md` — `Program.cs` shape, middleware order, service registration, outbox relay hosts.
+- `.github/instructions/coreex-tooling.instructions.md` — `*.CodeGen` and `*.Database` projects, `ref-data.yaml`, DbEx, generated-file ownership.
+- `.github/instructions/coreex-tests.instructions.md` — `UnitTestEx`, `NUnit`, `AwesomeAssertions`, outbox/event expectations, seed data.
+
+### Per-package AI usage guides
+
+Check `.github/docs/coreex/agents/` for locally cached guides first (see [Local doc cache](#local-doc-cache)). `/coreex-docs-sync` caches guides for **all** CoreEx packages — check the manifest's `referenced-packages` field to distinguish packages already in the project from ones the project would need to add.
+
+If a guide is not cached locally, fetch from GitHub:
+
+- [CoreEx](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx/AGENTS.md) — exceptions, `ExecutionContext`, `Result`, entity contracts, `Runtime.UtcNow`, DI attributes.
+- [CoreEx.AspNetCore](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.AspNetCore/AGENTS.md) — `WebApi`, middleware, health checks, idempotency.
+- [CoreEx.AspNetCore.NSwag](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.AspNetCore.NSwag/AGENTS.md) — NSwag/OpenAPI integration.
+- [CoreEx.Azure.Messaging.ServiceBus](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.Azure.Messaging.ServiceBus/AGENTS.md) — Service Bus publisher, subscribers, error handling.
+- [CoreEx.Caching.FusionCache](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.Caching.FusionCache/AGENTS.md) — `IHybridCache`, Redis backplane, idempotency provider.
+- [CoreEx.CodeGen](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.CodeGen/AGENTS.md) — `CodeGenConsole`, `ref-data.yaml`, generated-file ownership.
+- [CoreEx.Data](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.Data/AGENTS.md) — `IUnitOfWork`, `TransactionAsync`, `QueryArgsConfig`, `DataResult`.
+- [CoreEx.Database](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.Database/AGENTS.md) — `IDatabase`, `DatabaseCommand`, outbox relay base types.
+- [CoreEx.Database.Postgres](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.Database.Postgres/AGENTS.md) — PostgreSQL `IDatabase`, outbox, error-code conventions.
+- [CoreEx.Database.SqlServer](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.Database.SqlServer/AGENTS.md) — SQL Server `IDatabase`, session context, outbox, error-code conventions.
+- [CoreEx.DomainDriven](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.DomainDriven/AGENTS.md) — `Entity`, `Aggregate`, `PersistenceState`.
+- [CoreEx.EntityFrameworkCore](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.EntityFrameworkCore/AGENTS.md) — `EfDb`, `EfDbModel`, dynamic query, `ValueConverterBridge`.
+- [CoreEx.Events](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.Events/AGENTS.md) — `EventData`, `IEventFormatter`, `IEventPublisher`, `SubscribedManager`.
+- [CoreEx.RefData](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.RefData/AGENTS.md) — `ReferenceData`, `ReferenceDataHybridCache`, `ReferenceDataOrchestrator`.
+- [CoreEx.UnitTesting](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.UnitTesting/AGENTS.md) — outbox/event expectations, `JsonDataReader`, `AwesomeAssertions`.
+- [CoreEx.Validation](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.Validation/AGENTS.md) — `Validator`, rule catalogue, `ValidateAndThrowAsync`.
+
+### Sample architecture docs
+
+Check `.github/docs/coreex/` for a local cache first (see [Local doc cache](#local-doc-cache)). If local copies are present, prefer them. Otherwise fetch from GitHub:
+
+- [Local Development Setup](https://github.com/Avanade/CoreEx/blob/main/samples/docs/local-dev.md) — infrastructure services (Docker/Podman), connection strings, Service Bus emulator config, startup sequences, and Aspire E2E guide.
+- [Layer Dependencies](https://github.com/Avanade/CoreEx/blob/main/samples/docs/layers.md) — full layer dependency diagram, design-time tooling overview, dependency rules.
+- [Pattern Catalog](https://github.com/Avanade/CoreEx/blob/main/samples/docs/patterns.md) — error handling, railway-oriented flows, outbox, adapters, policies, testing.
+- [Contracts Layer](https://github.com/Avanade/CoreEx/blob/main/samples/docs/contracts-layer.md) — generated contracts, interfaces, reference data code properties.
+- [Domain Layer](https://github.com/Avanade/CoreEx/blob/main/samples/docs/domain-layer.md) — aggregates, mutation guards, integration-event accumulation, `Result` pipelines.
+- [Application Layer](https://github.com/Avanade/CoreEx/blob/main/samples/docs/application-layer.md) — service orchestration, `TransactionAsync`, `IUnitOfWork.Events`, validators, policies, adapters.
+- [Infrastructure Layer](https://github.com/Avanade/CoreEx/blob/main/samples/docs/infrastructure-layer.md) — EF Core repositories, `IBiDirectionMapper`, outbox table wiring, relay publisher.
+- [Hosts Layer](https://github.com/Avanade/CoreEx/blob/main/samples/docs/hosts-layer.md) — API, Subscribe, and Relay `Program.cs` shapes, middleware ordering, Service Bus wiring.
+- [Testing](https://github.com/Avanade/CoreEx/blob/main/samples/docs/testing.md) — unit, integration, API, Subscribe, and Relay test patterns with concrete examples.
+- [Tooling](https://github.com/Avanade/CoreEx/blob/main/samples/docs/tooling.md) — `*.CodeGen` and `*.Database` project run order, generated-file ownership, schema generation.
+- [Aspire](https://github.com/Avanade/CoreEx/blob/main/samples/docs/aspire.md) — Aspire orchestration for local distributed development and E2E testing.
+
+## Local doc cache
+
+`/coreex-docs-sync` populates two local folders. Prefer local copies over GitHub URLs or fetches whenever they are present.
+
+| Folder | Contents |
+|---|---|
+| `.github/docs/coreex/` | 10 sample architecture docs (layers, patterns, each layer walkthrough, testing, tooling, Aspire) |
+| `.github/docs/coreex/agents/` | AI usage guides for **all** CoreEx packages — available for guidance even on packages not yet adopted by this project |
+
+A manifest at `.github/docs/coreex/.manifest` records the sync date, CoreEx version, and which packages are currently referenced in the project.
+
+**When you are about to consult a sample architecture doc or a per-package guide:**
+
+1. Check for the file under `.github/docs/coreex/` or `.github/docs/coreex/agents/` respectively.
+2. If found, use the local copy. Then read `.github/docs/coreex/.manifest` and check:
+ - `synced` date: if older than 30 days, recommend running `/coreex-docs-sync`.
+ - `4.0.0-preview-1`: scan `*.csproj`, `Directory.Packages.props`, and `Directory.Build.props` for the `CoreEx` package version; if it differs from the manifest, recommend running `/coreex-docs-sync`.
+3. If no local cache exists and you are about to fetch a GitHub URL, offer first: *"I can run `/coreex-docs-sync` to cache the CoreEx docs and all package guides locally — this avoids repeated GitHub fetches. Want me to do that first?"*
+
+**At the start of a session involving CoreEx guidance**, read `.github/docs/coreex/.manifest` if it exists. The `referenced-packages` field lists which CoreEx packages this project currently uses — distinguish between guiding on an **already-referenced** package and recommending a **new** one the project would need to add.
+
+Do not set up the local cache silently — always offer and wait for confirmation.
+
+## Operating rules
+
+- Always inspect current code before recommending changes.
+- Give sample-backed guidance where possible; cite the specific doc or file that supports the recommendation.
+- Favor smallest safe change and preserve existing structure.
+- Separate explanation, plan, and implementation guidance clearly.
+- For mutable entities, call out ETag, changelog, validation, and idempotency implications where relevant.
+- For messaging, explicitly distinguish API-only, API plus outbox relay, API plus subscriber, and full orchestration shapes.
+- Never recommend editing `*.g.cs`, `*.g.sql`, or `*.g.pgsql` files — direct the user to the owning generator instead (Roslyn source generator for `*.g.cs`; `*.Database` project for `*.g.sql`/`*.g.pgsql`).
+
+## Decision routing
+
+These skills are part of the CoreEx AI workflow set and live in `.github/skills/`. They can be copied from the [CoreEx repository](https://github.com/Avanade/CoreEx/tree/main/.github/skills) into a consuming project:
+
+- Greenfield domain or host scaffolding → advise using the [CoreEx.Template](../../src/CoreEx.Template/README.md) `dotnet new` templates.
+- Retrofit capability on an existing domain → inspect the current code and recommend the smallest manual changes aligned to the samples and instructions.
+- Repo mapping or onboarding documentation → advise using `/acquire-codebase-knowledge`.
+
+## Response format
+
+1. **Recommendation** — the CoreEx-idiomatic answer.
+2. **Why this fits CoreEx** — pattern or design principle it follows.
+3. **Evidence** — specific file/doc/sample that backs it up.
+4. **Risks and tradeoffs** — anything the user should weigh.
+5. **Minimal next steps** — actionable and ordered.
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 00000000..09c7dea0
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,185 @@
+---
+# applyTo is intentionally omitted — this file is applied globally by VS Copilot convention for copilot-instructions.md.
+description: "Project-wide guidelines and conventions for CoreEx development"
+tags: ["guidelines", "conventions", "comments"]
+---
+
+# Copilot Instructions
+
+## Purpose
+CoreEx is a modular .NET framework for enterprise APIs and distributed services. Favor CoreEx-native primitives, patterns, and extensions over ad-hoc implementations.
+
+## Repository Shape
+- `CoreEx.sln`: main solution for framework + samples.
+- `src\`: reusable CoreEx libraries (AspNetCore, Database, EntityFrameworkCore, Events, Validation, DomainDriven, RefData, Caching, etc.).
+- `gen\CoreEx.Generator\`: Roslyn source generator for contracts.
+- `tests\`: framework-level tests.
+- `samples\src\Contoso.*\`: sample domains split by layer/host.
+- `samples\aspire\AppHost.cs`: orchestration entrypoint.
+- `coreex-starter\`: separate starter template repo — ignore unless user wants starter changes.
+
+## Build, Test, and Run
+- **Build**: `dotnet build CoreEx.sln`
+- **Test**: `dotnet test CoreEx.sln` or target specific projects.
+- **Single test**: `dotnet test --filter "FullyQualifiedName~"`
+- **Samples**: docker-compose infrastructure + dotnet run for Database projects + Aspire AppHost.
+- **Linting**: No separate `dotnet format`. Build is the lint pass (nullable, LangVersion=preview, TreatWarningsAsErrors in `src\Directory.Build.props`).
+- **Formatting**: 4 spaces for `*.cs`, 2 spaces for `*.json|*.xml|*.yaml|*.props|*.csproj|*.sln|*.sql` per `.editorconfig`.
+
+## Local Development Infrastructure
+
+All sample hosts depend on containerised infrastructure. Start it before running any host or integration test:
+
+```bash
+podman compose -f docker-compose.yml up -d # Podman preferred; `docker compose` also works
+```
+
+| Service | Port(s) | Purpose |
+|---|---|---|
+| `db-sql-server` | 1433 | Shopping domain database; Service Bus emulator backing store |
+| `db-postgres` | 5432 | Products domain database |
+| `redis-cache` | 6379 | FusionCache Redis backplane (all domains) |
+| `servicebus-emulator` | 5672 AMQP, 5300 mgmt | Azure Service Bus emulator; namespace `sbemulatorns`; topic `contoso` with subscriptions `products` and `shopping` (both session-enabled); config at `servicebus/Config.json` |
+| `dts-emulator` | 8080, 8082 | Azure Durable Task Scheduler emulator; task hubs `default` and `order` |
+| `aspire-dashboard` | 18888 UI, 4317 OTLP | Standalone OpenTelemetry dashboard; usable without running the full Aspire AppHost |
+
+Connection strings for each service in development are in each host's `appsettings.Development.json` under the `Aspire:` configuration key hierarchy. See [`samples/docs/local-dev.md`](../samples/docs/local-dev.md) for full detail, connection string patterns, and startup sequences.
+
+## Architecture
+- **Two roles**: framework packages (`src\`) + sample reference implementations (`samples\`).
+- **Business layers** (strict inward dependency — inner layers have no knowledge of outer): `*.Contracts` → `*.Application` → `*.Domain` (optional) → `*.Infrastructure`.
+- **Host layers** (composition roots, no business logic): `*.Api`, `*.Relay`, `*.Subscribe`.
+- **Design-time tooling** (no runtime presence): `*.CodeGen` (generates reference-data layer from `ref-data.yaml`) and `*.Database` (schema, seeding, outbox infrastructure via DbEx).
+- **Sample flow**: Controllers → `WebApi` helpers → Application services (validate + `IUnitOfWork`) → Infrastructure repositories (EF + explicit mappers) → transactional outbox → relay publishes to Service Bus → subscribers consume.
+- **Polyglot data**: Products uses PostgreSQL (`CoreEx.Database.Postgres` + `CoreEx.EntityFrameworkCore`); Shopping uses SQL Server (`CoreEx.Database.SqlServer` + `CoreEx.EntityFrameworkCore`). Layers above Infrastructure are database-agnostic.
+- **Primary domains**: Products and Shopping complete; Orders WIP. See `samples\README.md` for topology.
+- **Aspire**: orchestrates all sample hosts in `samples\aspire\Contoso.Aspire\AppHost.cs` for local distributed development and E2E testing.
+
+## Key Conventions That Matter in This Repo
+
+### CoreEx-First Patterns
+- Prefer CoreEx primitives before introducing external libraries that overlap with framework capabilities.
+- Prefer CoreEx exception types (`NotFoundException`, `ValidationException`, `BusinessException`, `ConcurrencyException`, etc.) and CoreEx `Result`/`Result` flows over custom error wrappers.
+- Do not introduce AutoMapper in any repository unless the repository maintainer explicitly requests it. Repositories and services use explicit mapping helpers/classes.
+
+### Contracts and Source Generation
+- Contracts are commonly declared as `[Contract] public partial class ...`.
+- Mutable contracts often implement `IIdentifier`, `IETag`, and `IChangeLog`.
+- Use `[ReadOnly(true)]` for server-managed fields and `[ReferenceData]` for reference-data-backed code properties.
+- Canonical casing transformations belong in property setters when already established by the model (for example `Sku` uppercasing in `ProductBase`).
+- Favor the existing source-generation approach; do not hand-write members that are meant to be generated.
+
+### Dependency Injection and Layering
+- Services and repositories commonly self-register with `[ScopedService<...>]`.
+- Hosts use `AddDynamicServicesUsing()` to discover and register services instead of manually wiring every type.
+- Keep interface/implementation layering intact:
+ - application interfaces live in `Application\Interfaces\`, `Application\Repositories\`, `Application\Adapters\`, or `Application\Policies\`;
+ - infrastructure implementations live in `Infrastructure\`.
+- There are two distinct mapping layers — do not conflate them:
+ - **Application-level** (`Application\Mapping\`): Domain aggregate → Contract, using `Mapper`; present only in domains with a Domain layer (e.g. Shopping).
+ - **Infrastructure-level** (`Infrastructure\Mapping\`): Contract ↔ Persistence model, using `BiDirectionMapper`; present in all domains.
+
+### Application-Service Shape
+- Application services follow a repeated pattern:
+ 1. guard/normalize inputs;
+ 2. validate with CoreEx validators;
+ 3. load current state where needed;
+ 4. wrap mutations **and** event publication together inside `_unitOfWork.TransactionAsync(...)` — both the database write and the outbox event are committed atomically or not at all;
+ 5. add `EventData` to `_unitOfWork.Events` inside that same transactional scope.
+- Use exception-based flows for straightforward CRUD-style services.
+- Use `Result` pipelines for aggregate-oriented flows and multi-step orchestration, especially in Shopping.
+
+### Adapters (Anti-Corruption Layer)
+- When a domain needs to interact with another domain or external service, define an **adapter interface** in `Application\Adapters\`. The Application layer depends on this domain-idiomatic abstraction — never on the remote API's schema or transport directly.
+- Infrastructure implements the adapter using a **typed HTTP client** (`Infrastructure\Clients\`) for the transport concern, keeping client and orchestration in separate focused classes.
+- Two adapter roles appear in Shopping:
+ - **Synchronous adapter** (`IProductAdapter`) — real-time calls (e.g. inventory reservation at checkout); the HTTP client is called live inside the unit of work.
+ - **Sync/replication adapter** (`IProductSyncAdapter`) — event-driven data replication; receives published domain events and maintains a local eventually-consistent copy in the domain's own store.
+- Do not call `HttpClient` directly from services — always go through the adapter interface.
+
+### Policies
+- Policies (`Application\Policies\`) encapsulate **domain-level guard logic** that requires I/O — adapter or repository calls. A policy provides a named, independently testable home for rules that depend on external state and cannot be expressed in a validator alone (synchronous) or in the domain model (no async I/O). Policies return `Result` or `Result` and can be called from any point in service orchestration where the condition needs to be verified.
+- Use a policy when an invariant cannot be expressed in a validator alone (e.g. confirming a referenced entity exists before allowing a mutation).
+- Policies return `Result` or `Result` and compose naturally into `Result` service pipelines via `.GoAsync()` / `.ThenAsAsync()`.
+
+### Host Composition
+- `Program.cs` files follow a predictable CoreEx host shape:
+ - `builder.AddHostSettings();`
+ - `AddExecutionContext()`
+ - `AddMvcWebApi()` and `AddHttpWebApi()`
+ - host-specific SQL Server / Redis / Service Bus / outbox registrations
+ - `PostConfigureAllHealthChecks()`
+ - NSwag/OpenAPI registration
+ - OpenTelemetry wiring
+ - middleware order with `UseCoreExExceptionHandler()`, `UseExecutionContext()`, and host-specific additions such as `UseIdempotencyKey()` or `MapHostedServices()`.
+- API hosts, subscriber hosts, and outbox relay hosts intentionally have different startup shapes. Do not collapse them into one generic startup unless the user explicitly asks for that refactor.
+
+### Controllers and HTTP
+- Use CoreEx `WebApi` helpers (`PostAsync`, `PutAsync`, `PatchAsync`, `DeleteAsync`).
+- PATCH: `application/merge-patch+json`.
+- POST: use `[IdempotencyKey]`.
+- OpenAPI/health endpoints standard in hosts.
+
+### Data and Messaging
+- Transactional outbox + Azure Service Bus are first-class messaging patterns across all domains.
+- **Products** uses PostgreSQL; **Shopping** uses SQL Server. Do not assume SQL Server when working on Products.
+- Shopping: synchronous HTTP inventory reservation + transactional outbox + async event publishing. Preserve this split.
+- Both domains use `CoreEx.Caching.FusionCache` (hybrid in-process + Redis backplane cache) for reference data and idempotency. Register via `AddFusionCache()` / `AddFusionHybridCache()` in `Program.cs`; clear via `Test.ClearFusionCacheAsync()` in test `[OneTimeSetUp]`.
+
+### Testing
+- Framework: UnitTestEx + NUnit + AwesomeAssertions (the `AwesomeAssertions` NuGet package — not FluentAssertions).
+- Sample: `WithGenericTester` (unit) or `WithApiTester` (API/Subscribe/Relay).
+- Integration tests: per-class named seed files `Data\read-data.seed.yaml` / `Data\mutate-data.seed.yaml` (Products), or a single `Data\data.yaml` (Orders/Shopping), in Test.Common + `Resources\` JSON expectations.
+- **Intra-domain dependencies are real; inter-domain dependencies are always mocked.** Own database, cache, and outbox are started and seeded in `[OneTimeSetUp]`. Cross-domain HTTP calls and direct broker publishes are replaced with `MockHttpClientFactory` / `UseExpectedAzureServiceBusPublisher()`.
+- Outbox assertion helpers are database-specific: `UseExpectedPostgresOutboxPublisher()` for Products; `UseExpectedSqlServerOutboxPublisher()` for Shopping. Do not use the SQL Server helper in Products tests.
+- Mock downstream HTTP calls; do not assume live APIs.
+
+### House Rules
+- Code comments end with a period/full stop.
+- Always use `.ConfigureAwait(false)` in service/repository code.
+- `enable` and `enable` are set in `Directory.Build.props` — treat nullable warnings as errors, never suppress them with `!` without justification.
+- Every project has a single `GlobalUsing.cs` at the project root. All `using` statements go there — never in individual source files. The code generator (`*.CodeGen`) emits no `using` statements and depends on this.
+- File-scoped namespace declarations only: `namespace Foo.Bar;` — never block-scoped.
+- Single-line `if` bodies do not need braces: `if (x) return;`
+- Use expression-bodied syntax (`=>`) when the entire method or property body is a single expression.
+- Private instance fields are always prefixed with `_`.
+
+### Generated Code
+Never create or edit `*.g.cs`, `*.g.sql`, or `*.g.pgsql` files directly. Each generator owns its outputs:
+
+| File pattern | Generator | Change instead |
+|---|---|---|
+| `*.g.cs` (contracts, ref-data) | Roslyn source generator (`CoreEx.Generator`) | The `[Contract]`- or `[ReferenceData]`-decorated partial class |
+| `*.g.cs` (ref-data layer — controller, service, repository, mapper) | `*.CodeGen` project (CoreEx.CodeGen + `ref-data.yaml`) | `ref-data.yaml` config or the Handlebars templates in `CoreEx.CodeGen/RefData/Templates/` |
+| `*.g.sql`, `*.g.pgsql`, `*DbContext.g.cs`, `Persistence/*.g.cs` | `*.Database` project (DbEx) | DbEx YAML config or SQL migration scripts |
+
+See [INSTRUCTION_AUTHORING.md](INSTRUCTION_AUTHORING.md#generated-code) for full generator ownership detail.
+
+## Key Docs to Read Before Large Changes
+- `README.md` — repo-level positioning and top-level commands.
+- `samples\README.md` — runnable Contoso architecture and local setup.
+- `docs\capabilities.md` — deeper CoreEx capability and pattern explanations.
+- `samples\docs\layers.md` — full layer diagram, dependency rules, and design-time tooling overview.
+- `samples\docs\patterns.md` — pattern catalog with links to layer-specific detail for every architectural, application, messaging, and testing pattern used in the samples.
+- `samples\docs\.md` — detailed walkthrough for each layer: `contracts-layer.md`, `application-layer.md`, `domain-layer.md`, `infrastructure-layer.md`, `hosts-layer.md`, `testing.md`, `tooling.md`.
+- `.github\instructions\*.instructions.md` — area-specific rules auto-injected when editing matching files (`Program.cs`, contracts, application services, repositories, validators, subscribers, tests).
+
+## Agent Customizations (Prompts, Skills, and Templates)
+
+The following prompts, skills, and templates are available in this repository. Type `/` in chat to invoke prompts and skills. Use `dotnet new` in a terminal for templates.
+
+| Command | Type | When to use |
+|---------|------|-------------|
+| `CoreEx.Template` | Template pack | Deterministic `dotnet new` scaffolding for solution, API, relay, and subscriber shapes. Use `dotnet new install CoreEx.Template` and then run `dotnet new coreex`, `coreex-api`, `coreex-relay`, or `coreex-subscribe` as needed. |
+| `CoreEx Expert` | Agent | Architecture guidance, pattern recommendations, and design review aligned to the samples and repo instructions. |
+| `/init` | Prompt | Initialize a new CoreEx solution or workspace. |
+| `/setup` | Prompt | Configure an existing CoreEx solution with standard tooling and settings. |
+
+## Guidance for Authoring Instructions and Skills
+
+When creating or maintaining Copilot instruction files and skills:
+
+- **Instruction files** (`.instructions.md`) — see [INSTRUCTION_AUTHORING.md](./INSTRUCTION_AUTHORING.md) for standards on YAML frontmatter, section order, and content rules.
+- **Skill files** (`SKILL.md`) — see [SKILL_AUTHORING.md](./SKILL_AUTHORING.md) for the directory structure pattern (`references/`, `assets/`), lean main file rules (<300 lines), and cross-referencing guidelines.
+
+Both documents define durable patterns for creating guidance that is discoverable, maintainable, and context-efficient.
\ No newline at end of file
diff --git a/.github/instructions/coreex-api-controllers.instructions.md b/.github/instructions/coreex-api-controllers.instructions.md
new file mode 100644
index 00000000..e8248f6b
--- /dev/null
+++ b/.github/instructions/coreex-api-controllers.instructions.md
@@ -0,0 +1,330 @@
+---
+applyTo: "**/Controllers/**/*.cs"
+description: "API conventions for CoreEx: MVC ControllerBase and Minimal API approaches, WebApi integration, routing, CQRS separation"
+tags: ["controllers", "api", "routing", "cqrs", "dependency-injection", "minimal-api"]
+---
+
+# API Conventions
+
+> **Precondition — the Api host must exist.** Controllers live in the `*.Api` host, which is **not** part of the base `coreex` solution. Before authoring a controller, confirm an Api host is present (`**/*.Api/*.Api.csproj`); if it is absent, run the scaffolding workflow first (see `coreex-host-setup.instructions.md` → "Scaffolding an API host") — which confirms creation, generates it via the `coreex-api` template using the recorded solution options, and adds the new projects to the solution (`dotnet sln add`) as the final in-session step. Also ensure the entity's application service exists (create per *Service Operations — Confirm Scope* in `coreex-application-services.instructions.md` if not).
+
+> **Maintain the API tests alongside the controller.** When you create or change a controller's operations, **offer to create or update the matching `XxxReadTests` / `XxxMutateTests`** (per-entity, one partial file per operation) in the `*.Test.Api` project — see `coreex-tests.instructions.md` → "API Tests — Structure & Generation". If accepted, co-design the seed data, tests, and `.res.json`/`.req.json` resources together; if declined, proceed but note the coverage gap.
+
+CoreEx.AspNetCore supports two approaches for exposing HTTP endpoints. Choose one per host — they can coexist in the same application when needed.
+
+| Approach | Registration | Returns | Best for |
+|---|---|---|---|
+| **MVC Controllers** | `AddMvcWebApi()` | `IActionResult` | Familiar controller model; NSwag/OpenAPI attributes |
+| **Minimal APIs** | `AddHttpWebApi()` | `IResult` | Lightweight; less ceremony; endpoint groups in `Program.cs` |
+
+Both use the same `WebApi` helper — method names, `WithResult` variants, `ro.WithLocationUri`, `.Required()`, and `.Adjust(...)` are identical in both approaches.
+
+## NuGet / Project References
+
+| Package | Key types provided |
+|---|---|
+| `CoreEx.AspNetCore` | `WebApi`, `[IdempotencyKey]`, `[Accepts]`, `[ProducesNotFoundProblem]`, `[Query]`, `[Paging]`, `HttpNames`; Minimal API: `.WithQuery()`, `.WithPaging()`, `.Accepts()`, `.ProducesNotFoundProblem()`, `.ProducesNoContent()`, `.ProducesCreated()`, `.WithIdempotencyKey()` |
+| `CoreEx.AspNetCore.NSwag` | `[OpenApiTag]` |
+| `CoreEx` | `.Required()`, `.Adjust(...)` |
+
+---
+
+## MVC Controllers
+
+### Structure
+
+- Inherit from `ControllerBase`. Never inherit from `Controller` (that brings View support).
+- Decorate with `[ApiController]` and `[Route("...")]` on the class.
+- Inject `WebApi` and the relevant service interface via primary constructor. Guard with `.ThrowIfNull()`.
+- **Mirror the service CQRS split:** a **`XxxController`** exposes the **mutating** endpoints (POST/PUT/PATCH/DELETE) and injects `IXxxService`; a **`XxxReadController`** exposes the **read** endpoints (GET/query) and injects `IXxxReadService`.
+- **Unify them in OpenAPI with a shared `[OpenApiTag("Xxx")]`.** Put the **same** tag on both controllers so Swagger/OpenAPI presents one logical "Xxx" group — CQRS is an **internal** structuring concern, not something the external API surface should expose. (A tag may also be placed on an individual action to cross-tag it.)
+
+```csharp
+// Mutating endpoints
+[ApiController, Route("/api/products"), OpenApiTag("Products")]
+public class ProductController(WebApi webApi, IProductService service) : ControllerBase
+{
+ private readonly WebApi _webApi = webApi.ThrowIfNull();
+ private readonly IProductService _service = service.ThrowIfNull();
+}
+
+// Read endpoints — same route base and same OpenApiTag so they appear as one "Products" group
+[ApiController, Route("/api/products"), OpenApiTag("Products")]
+public class ProductReadController(WebApi webApi, IProductReadService service) : ControllerBase
+{
+ private readonly WebApi _webApi = webApi.ThrowIfNull();
+ private readonly IProductReadService _service = service.ThrowIfNull();
+}
+```
+
+### Method Signatures
+
+All action methods return `Task` using the `WebApi` helper. Do not return typed `ActionResult` directly.
+
+**Every action method takes a trailing `CancellationToken cancellationToken = default` and flows it through.** MVC binds it to `HttpContext.RequestAborted` automatically. Pass it to the `WebApi` helper via the named `cancellationToken:` argument, and use the **lambda's** `ct` parameter (`(ro, ct) => …`) when calling the service — never discard it with `(ro, _)`:
+
+```csharp
+public Task PostAsync(CancellationToken cancellationToken = default) => _webApi.PostAsync(Request, (ro, ct) =>
+{
+ ro.WithLocationUri(e => new Uri($"/api/employees/{e.Id}", UriKind.Relative));
+ return _service.CreateAsync(ro.Value, ct);
+}, cancellationToken: cancellationToken);
+```
+
+This is an instance of the universal rule — **every `async`/`Task`-returning method takes a `CancellationToken` and passes it on** (see `coreex-conventions.instructions.md`). The examples below all follow it.
+
+#### Standard (exception-based services)
+
+| HTTP Verb | WebApi helper | Notes |
+|---|---|---|
+| `GET` / `HEAD` | `_webApi.GetAsync(...)` | Use both attributes together |
+| `POST` | `_webApi.PostAsync(...)` | Add `[IdempotencyKey]` for safe POST |
+| `PUT` | `_webApi.PutAsync(...)` | Include ETag via `IF-MATCH` header |
+| `PATCH` | `_webApi.PatchAsync(...)` | Requires `get:` and `put:` lambdas |
+| `DELETE` | `_webApi.DeleteAsync(...)` | Returns 204 No Content |
+
+#### Result-based (`Result` pipeline services)
+
+When the service returns `Result`, use the `WithResult` variants. The controller code is equally thin.
+
+| HTTP Verb | WebApi helper | Notes |
+|---|---|---|
+| `GET` | `_webApi.GetWithResultAsync(...)` | |
+| `POST` (single out) | `_webApi.PostWithResultAsync(...)` | |
+| `POST` (in + out) | `_webApi.PostWithResultAsync(...)` | Use when body maps to a different output type |
+| `PUT` (single out) | `_webApi.PutWithResultAsync(...)` | |
+| `PUT` (in + out) | `_webApi.PutWithResultAsync(...)` | |
+| `DELETE` (typed) | `_webApi.DeleteWithResultAsync(...)` | Use when delete returns the deleted resource |
+
+### Route Parameters
+
+Use `.Required()` to validate route parameters at the point of first use. It **returns the value** when non-default, or throws a `ValidationException` when the value is null/default — which the `WebApi` error handler translates to a **400 validation response** (not a 500). This is the correct treatment: a missing or empty route parameter is a caller error, not a programming error.
+
+```csharp
+[HttpGet("{id}"), HttpHead("{id}")]
+public Task GetAsync(string id, CancellationToken cancellationToken = default) =>
+ _webApi.GetAsync(Request, (_, ct) => _service.GetAsync(id.Required(), ct), cancellationToken: cancellationToken);
+```
+
+Do not use `.ThrowIfNull()` / `.ThrowIfNullOrEmpty()` on route parameters — those throw `ArgumentNullException`, which results in a 500 rather than a 400.
+
+### POST — Create with Location Header
+
+Use `ro.WithLocationUri(...)` to set the `Location` response header:
+
+```csharp
+[HttpPost]
+[Accepts]
+[ProducesResponseType(201)]
+[IdempotencyKey]
+public Task PostAsync(CancellationToken cancellationToken = default) => _webApi.PostAsync(Request, (ro, ct) =>
+{
+ ro.WithLocationUri(p => new Uri($"/api/products/{p.Id}", UriKind.Relative));
+ return _service.CreateAsync(ro.Value, ct);
+}, cancellationToken: cancellationToken);
+```
+
+> **Agent instruction — confirm idempotency for every POST.** When adding a `POST`, ask whether the operation should be **idempotent** (safe to retry without creating a duplicate). A general **create-style** POST is a strong candidate — default to offering it. If confirmed, decorate the action with **`[IdempotencyKey]`** (MVC) or `.WithIdempotencyKey()` (Minimal API): a retried request carrying the same key then returns the original result instead of creating a second resource. Omit it only when the user confirms the POST is **not** idempotent (e.g. a deliberately non-repeatable command). This applies to `POST` specifically; `PUT`/`PATCH`/`DELETE` are inherently idempotent and do not take the attribute.
+
+For a **full-entity update**, expose **both** endpoints by default — they share the same write `UpdateAsync`:
+- **`PUT`** — full replace.
+- **`PATCH`** — merge-patch (RFC 7396) over the current entity.
+
+Only implement **specialized/partial** update or patch endpoints when the user **explicitly** asks; the default is the PUT + PATCH pair.
+
+```csharp
+[HttpPut("{id}")]
+[Accepts]
+public Task UpdateAsync(string id, CancellationToken cancellationToken = default) => _webApi.PutAsync(Request,
+ (ro, ct) => _service.UpdateAsync(ro.Value.Adjust(p => p.Id = id), ct), cancellationToken: cancellationToken);
+```
+
+`PATCH` always supplies both `get:` and `put:` delegates: it **fetches** the current entity, merges the patch document over it, then calls `put`. The fetch uses the write service's own primary `GetAsync` (`XxxService` exposes a by-id `GetAsync` for exactly this), so the mutating controller depends on a **single** service — no need to also inject `IXxxReadService`:
+
+```csharp
+[HttpPatch("{id}")]
+[Accepts(HttpNames.MergePatchJsonMediaTypeName)]
+public Task PatchAsync(string id, CancellationToken cancellationToken = default) => _webApi.PatchAsync(Request,
+ get: (ro, ct) => _service.GetAsync(id.Required(), ct),
+ put: (ro, ct) => _service.UpdateAsync(ro.Value.Adjust(p => p.Id = id), ct),
+ cancellationToken: cancellationToken);
+```
+
+Because the **write** service backs the `get:` fetch, **`IXxxService` must declare a by-id `GetAsync`** (alongside the mutators) — and `XxxService` must implement it. This `GetAsync` lives on the **write** interface even though it reads; it is the controller's single dependency for PATCH (and for a write-side `GET` by id). Do **not** route the PATCH fetch through `IXxxReadService`:
+
+```csharp
+public interface IProductService
+{
+ Task GetAsync(string id, CancellationToken ct = default); // ← required by PATCH's get: and the write GET
+ Task CreateAsync(Product value, CancellationToken ct = default);
+ Task UpdateAsync(Product value, CancellationToken ct = default);
+ Task DeleteAsync(string id, CancellationToken ct = default);
+}
+```
+
+### Query Endpoints
+
+Expose `QueryArgs` and `PagingArgs` via `[Query]` and `[Paging]` action attributes. Access them via the request options object (`ro`):
+
+```csharp
+[HttpGet]
+[Query(supportsOrderBy: true), Paging(supportsCount: true)]
+public Task QueryAsync(CancellationToken cancellationToken = default) =>
+ _webApi.GetAsync(Request, (ro, ct) => _service.QueryAsync(ro.QueryArgs, ro.PagingArgs, ct), cancellationToken: cancellationToken);
+```
+
+### Reference Data Endpoints
+
+Delegate to `ReferenceDataOrchestrator.Current.GetWithFilterAsync()`. Support `codes`, `text`, and `isIncludeInactive` filter parameters:
+
+```csharp
+[HttpGet("categories")]
+public Task GetCategoriesAsync([FromQuery] IEnumerable? codes = default, string? text = default, CancellationToken cancellationToken = default)
+ => _webApi.GetAsync(Request, (ro, ct) => ReferenceDataOrchestrator.Current.GetWithFilterAsync(codes, text, ro.IsIncludeInactive, ct), cancellationToken: cancellationToken);
+```
+
+### Response Metadata Attributes
+
+Decorate actions with standard response metadata attributes:
+
+- `[ProducesResponseType(statusCode)]` — preferred generic form for new code.
+- `[ProducesResponseType(typeof(T), statusCode)]` — equivalent non-generic form; either is acceptable.
+- `[ProducesNotFoundProblem()]` — shorthand for `[ProducesResponseType(typeof(ProblemDetails), 404)]`; use on GET/PUT/PATCH/DELETE where not-found is expected.
+- `[Accepts]` — documents the consumed media type.
+
+### Query Schema Endpoint
+
+Read controllers that expose a `QueryAsync` should also expose a `$query` schema endpoint. This returns the JSON schema for the supported query/filter parameters:
+
+```csharp
+[HttpGet("$query")]
+[ProducesResponseType(typeof(JsonElement), 200)]
+public Task QuerySchemaAsync(CancellationToken cancellationToken = default) =>
+ _webApi.GetAsync(Request, (ro, ct) => _service.QuerySchemaAsync(ct), cancellationToken: cancellationToken);
+```
+
+### Result-Based Services
+
+When the service returns `Result`, use the `WithResult` variants:
+
+```csharp
+[HttpPost("{basketId}/checkout")]
+[ProducesResponseType(typeof(Basket), StatusCodes.Status200OK)]
+[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
+public Task CheckoutAsync(string basketId, CancellationToken cancellationToken = default) =>
+ _webApi.PostWithResultAsync(Request, (_, ct) =>
+ _service.CheckoutAsync(basketId.Required(), ct), HttpStatusCode.OK, cancellationToken: cancellationToken);
+
+[HttpPost("{basketId}/items")]
+[IdempotencyKey]
+[Accepts]
+[ProducesResponseType(typeof(Basket), StatusCodes.Status200OK)]
+public Task ItemAddAsync(string basketId, CancellationToken cancellationToken = default) =>
+ _webApi.PostWithResultAsync(Request, (ro, ct) =>
+ _service.ItemAddAsync(basketId.Required(), ro.Value, ct), HttpStatusCode.OK, cancellationToken: cancellationToken);
+```
+
+---
+
+## Minimal APIs
+
+Register the HTTP variant in `Program.cs` and map endpoints directly — no controller class required. `WebApi` is injected into the handler lambda alongside the service:
+
+```csharp
+// Program.cs
+builder.Services.AddHttpWebApi(); // or alongside AddMvcWebApi() if both are needed
+```
+
+### Attribute → RouteHandlerBuilder Equivalents
+
+MVC action attributes have direct `RouteHandlerBuilder` extension equivalents — chain them after `app.MapGet/Post/etc.`:
+
+| MVC attribute | Minimal API equivalent |
+|---|---|
+| `[Query(supportsOrderBy: true)]` | `.WithQuery(supportsOrderBy: true)` |
+| `[Paging(supportsCount: true)]` | `.WithPaging(supportsCount: true)` |
+| `[Accepts]` | `.Accepts()` |
+| `[ProducesNotFoundProblem]` | `.ProducesNotFoundProblem()` |
+| `[IdempotencyKey]` | `.WithIdempotencyKey()` |
+
+### Examples
+
+**GET by id:**
+```csharp
+app.MapGet("api/products/{id}",
+ (HttpRequest request, WebApi webApi, IProductReadService service, string id, CancellationToken cancellationToken)
+ => webApi.GetWithResultAsync(request, (_, ct) => service.GetAsync(id.Required(), ct), cancellationToken: cancellationToken))
+ .Produces().ProducesNotFoundProblem();
+```
+
+**POST — create with Location header:**
+```csharp
+app.MapPost("api/products",
+ (HttpRequest request, WebApi webApi, IProductService service, CancellationToken cancellationToken)
+ => webApi.PostWithResultAsync(request, async (ro, ct) =>
+ {
+ ro.WithLocationUri(p => new Uri($"api/products/{p.Id}", UriKind.Relative));
+ return await service.CreateAsync(ro.Value, ct).ConfigureAwait(false);
+ }, cancellationToken: cancellationToken))
+ .Accepts().ProducesCreated().WithIdempotencyKey();
+```
+
+**PUT:**
+```csharp
+app.MapPut("api/products/{id}",
+ (HttpRequest request, WebApi webApi, IProductService service, string id, CancellationToken cancellationToken)
+ => webApi.PutWithResultAsync(request, (ro, ct) =>
+ service.UpdateAsync(ro.Value.Adjust(p => p.Id = id), ct), cancellationToken: cancellationToken))
+ .Accepts().Produces().ProducesNotFoundProblem();
+```
+
+**PATCH — JSON Merge-Patch:**
+```csharp
+app.MapPatch("api/products/{id}",
+ (HttpRequest request, WebApi webApi, IProductService service, string id, CancellationToken cancellationToken)
+ => webApi.PatchWithResultAsync(request,
+ get: (_, ct) => service.GetAsync(id.Required(), ct),
+ put: (ro, ct) => service.UpdateAsync(ro.Value.Adjust(p => p.Id = id), ct),
+ cancellationToken: cancellationToken))
+ .Accepts(HttpNames.MergePatchJsonMediaTypeName).Produces().ProducesNotFoundProblem();
+```
+
+**DELETE:**
+```csharp
+app.MapDelete("api/products/{id}",
+ (HttpRequest request, WebApi webApi, IProductService service, string id, CancellationToken cancellationToken)
+ => webApi.DeleteWithResultAsync(request, (_, ct) => service.DeleteAsync(id.Required(), ct), cancellationToken: cancellationToken))
+ .ProducesNoContent();
+```
+
+**Query with filtering and paging:**
+```csharp
+app.MapGet("api/products",
+ (HttpRequest request, WebApi webApi, IProductReadService service, CancellationToken cancellationToken)
+ => webApi.GetWithResultAsync(request, (ro, ct) => service.QueryAsync(ro.QueryArgs, ro.PagingArgs, ct), cancellationToken: cancellationToken))
+ .Produces().WithQuery(supportsOrderBy: true).WithPaging(supportsCount: true);
+```
+
+All the same rules apply as for MVC controllers: no business logic in the handler, delegate immediately to the application service, use `.Required()` on route parameters, and **take a `CancellationToken` handler parameter** (ASP.NET injects it) that is passed to the `WebApi` helper (`cancellationToken:`) and on to the service via the lambda's `ct`.
+
+---
+
+## Do Not
+
+- Do not inherit from `Controller` — that pulls in View support; use `ControllerBase`.
+- Do not return `ActionResult` directly — use the `WebApi` helper for consistent error translation and status-code mapping.
+- Do not inject `IUnitOfWork` into controllers or endpoint handlers — it belongs in the application service.
+- Do not put business logic in controllers or endpoint handlers — delegate immediately to the application service.
+- Do not call `HttpClient` or adapters directly from controllers — go through the application service.
+- Do not put mutating and read endpoints in one controller — split into `XxxController` (mutations, `IXxxService`) and `XxxReadController` (reads, `IXxxReadService`).
+- Do not expose the CQRS split externally — give both controllers the **same** `[OpenApiTag("Xxx")]` so they surface as one OpenAPI group; do not use distinct tags/route bases per controller.
+- Do not omit the `PATCH` when exposing a full `PUT` update — offer both by default; add specialized/partial update or patch endpoints only on explicit request.
+- Do not add a `POST` without confirming idempotency — apply `[IdempotencyKey]` (`.WithIdempotencyKey()` for Minimal APIs) when it is idempotent (create-style POSTs almost always are); omit only when the user confirms it is not.
+- Do not omit or discard the `CancellationToken` — **every** action/handler takes `CancellationToken cancellationToken = default` (MVC) or a `CancellationToken` handler parameter (Minimal API), passes it to the `WebApi` helper via `cancellationToken:`, and calls the service with the **lambda's** `ct` (`(ro, ct) => …`). Do not write `(ro, _) => _service.XxxAsync(…)` (drops the token) or call the service without a token argument.
+
+## Further Reading
+
+- [Hosts Layer Guide](https://github.com/Avanade/CoreEx/blob/main/samples/docs/hosts-layer.md) — API host composition, controller patterns, and `Program.cs` shape.
+- [CoreEx.AspNetCore README](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.AspNetCore/README.md) — `WebApi` helper API reference.
+- [CoreEx.AspNetCore Mvc README](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.AspNetCore/Mvc/README.md) — MVC `WebApi` (`IActionResult`-returning), action attributes, and controller patterns.
+- [CoreEx.AspNetCore Http README](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.AspNetCore/Http/README.md) — Minimal API `WebApi` (`IResult`-returning) and `RouteHandlerBuilder` extensions.
diff --git a/.github/instructions/coreex-application-services.instructions.md b/.github/instructions/coreex-application-services.instructions.md
new file mode 100644
index 00000000..baf88c48
--- /dev/null
+++ b/.github/instructions/coreex-application-services.instructions.md
@@ -0,0 +1,480 @@
+---
+applyTo: "**/Application/**/*.cs"
+description: "Application service conventions: ScopedService registration, dependency injection, validation, unit of work, CQRS, policies, adapters, and Result pipelines"
+tags: ["services", "application-layer", "dependency-injection", "validation", "unit-of-work", "cqrs", "policies", "adapters"]
+---
+
+# Application Service Conventions
+
+## NuGet / Project References
+
+| Package | Key types provided |
+|---|---|
+| `CoreEx` | `[ScopedService]`, `Runtime`, `NotFoundException`, `BusinessException`, `ValidationException`, `.ThrowIfNull()`, `.ThrowIfNullOrEmpty()`, `QueryArgs`, `PagingArgs`, `ItemsResult`, `Result`, `Result.GoAsync()`, `.ThenAs()`, `.ThenAsAsync()` |
+| `CoreEx.Data` | `IUnitOfWork`, `DataResult` |
+| `CoreEx.Events` | `EventData`, `EventAction` |
+| `CoreEx.Validation` | `Validator`, `Validator`, `.ValidateAndThrowAsync()`, `.ValidateWithResultAsync()` |
+| `CoreEx.RefData` | `ReferenceDataOrchestrator` |
+
+## Structure
+
+- Define a public interface (e.g., `IProductService`) in the Application project, typically under an `Interfaces/` sub-folder — not a hard requirement, but a clean convention that keeps the public surface of the Application layer easy to navigate.
+- Implement with `[ScopedService]` attribute so it registers itself via dynamic DI — no manual registration required.
+- Inject dependencies via primary constructor and guard every injected parameter with `.ThrowIfNull()`.
+
+```csharp
+[ScopedService]
+public class ProductService(IUnitOfWork unitOfWork, IProductRepository repository) : IProductService
+{
+ private readonly IUnitOfWork _unitOfWork = unitOfWork.ThrowIfNull();
+ private readonly IProductRepository _repository = repository.ThrowIfNull();
+}
+```
+
+> **Do not inject validators (or mappers/policies) into the service constructor.** A `Validator` is invoked via its static `Default` singleton, so it is never a constructor parameter.
+>
+> ```csharp
+> // ❌ Wrong — validator injected
+> public class EmployeeService(IUnitOfWork unitOfWork, IEmployeeRepository repository, EmployeeValidator validator) : IEmployeeService
+>
+> // ✅ Correct — only the unit of work and repository (validator called via EmployeeValidator.Default)
+> public class EmployeeService(IUnitOfWork unitOfWork, IEmployeeRepository repository) : IEmployeeService
+> ```
+
+## Service Operations — Confirm Scope
+
+Before generating a service, confirm which operations it should expose — never silently assume the full set.
+
+> **Agent instruction:**
+> - When asked to create a service **without** specifying the operations, confirm the standard CRUD set with the user — **Get**, **Create**, **Update**, **Delete** — presenting each as **default-selected** so the user can deselect any that are not wanted.
+> - Even when the user asks for "CRUD" (or "the usual"), still confirm the four operations (Get / Create / Update / Delete, each default-selected) rather than assuming all four — they may want only a subset.
+> - **Never add a Query (collection/search) operation unless it is explicitly requested.** Querying needs deliberate, additional design — `QueryArgsConfig` filter/order fields, paging, and a purpose-built read shape (see [CQRS — Read Services](#cqrs--read-services)) — so it must not be inferred from a generic "create a service"/"CRUD" request. If querying is asked for, gather those specifics first.
+
+## Guard Clauses
+
+`.ThrowIfNull()` and `.ThrowIfNullOrEmpty()` **return the guarded value** when the check passes, so they can be used inline at the point of first use rather than as separate pre-checks. This keeps code tight without sacrificing safety:
+
+```csharp
+// Constructor injection — the assignment is the first use; guard inline
+private readonly IProductRepository _repository = repository.ThrowIfNull();
+
+// Inline at point of first use in a method body
+var current = await _repository.GetAsync(product.Id.ThrowIfNullOrEmpty(), cancellationToken).ConfigureAwait(false);
+
+// Guards chain — each returns the value if it passes, so further checks can follow
+public BasketStatus Status { get; private set => field = value.ThrowIfNull().ThrowIfInactive(); }
+```
+
+Use a top-of-method pre-check (non-inline) only when the value is not immediately consumed:
+
+```csharp
+public async Task UpdateAsync(Product product, CancellationToken cancellationToken = default)
+{
+ product.ThrowIfNull(); // checked here; not passed anywhere yet
+ await ProductValidator.Default.ValidateAndThrowAsync(product, cancellationToken).ConfigureAwait(false);
+ var current = await _repository.GetAsync(product.Id.ThrowIfNullOrEmpty(), cancellationToken).ConfigureAwait(false);
+ // ...
+}
+```
+
+## Validation
+
+Validators live in `Application/Validators/` and are **not registered in DI** — they are not injected into services (see [DI Registration Principle](#di-registration-principle) below). Choose the base class based on whether the validator needs injected dependencies:
+
+**`Validator`** — use when no constructor injection is required. Exposes a static `Default` singleton; always call via the singleton:
+
+```csharp
+public class ProductValidator : Validator
+{
+ public ProductValidator()
+ {
+ Property(p => p.Sku).Mandatory().MaximumLength(50);
+ Property(p => p.SubCategory).Mandatory().IsValid(); // typed ref-data nav property, not SubCategoryCode
+ Property(p => p.Price).PrecisionScale(null, 2).GreaterThanOrEqualTo(0, _ => "zero");
+ }
+}
+
+// Call via Default singleton — never use new ProductValidator() at the call site:
+await ProductValidator.Default.ValidateAndThrowAsync(product, cancellationToken);
+```
+
+**Choose the invocation that matches the flow — never bare `ValidateAsync`:**
+
+| Flow | Method | Behaviour |
+|---|---|---|
+| Exception style (non-ROP) | `ValidateAndThrowAsync(value)` | Throws `ValidationException` on failure — stops execution |
+| `Result` pipeline (ROP) | `ValidateWithResultAsync(value)` | Returns a `Result` to compose / short-circuit on |
+
+`ValidateAsync(value)` merely **returns the validation result without throwing** — calling it and ignoring the return *swallows the errors and continues*. Do **not** use it for fail-fast validation. In a non-ROP service use `ValidateAndThrowAsync`; in a `Result` pipeline use `ValidateWithResultAsync`.
+
+```csharp
+// ❌ Wrong — does not throw; the failure is swallowed and execution continues
+await EmployeeValidator.Default.ValidateAsync(employee, cancellationToken).ConfigureAwait(false);
+
+// ✅ Correct (non-ROP) — throws ValidationException on failure
+await EmployeeValidator.Default.ValidateAndThrowAsync(employee, cancellationToken).ConfigureAwait(false);
+```
+
+**`Validator`** — use when constructor injection is required (e.g., a repository for async I/O). No singleton; instantiate directly at the call site using dependencies already in scope in the service:
+
+```csharp
+public class MovementRequestValidator : Validator
+{
+ private readonly IProductRepository _repository;
+
+ public MovementRequestValidator(IProductRepository repository)
+ {
+ _repository = repository.ThrowIfNull();
+ Property(x => x.Id).Mandatory().MaximumLength(50);
+ // ... declarative rules
+ }
+
+ protected async override Task OnValidateAsync(ValidationContext context, CancellationToken cancellationToken)
+ {
+ if (context.HasErrors) return; // fail fast — skip I/O if declarative phase found errors
+
+ var ids = context.Value.Products!.Select(kvp => kvp.Key).ToArray();
+ var products = await _repository.GetForReservationAsync(ids, cancellationToken).ConfigureAwait(false);
+
+ await context.ValidateFurtherAsync(c => c
+ .HasProperty(x => x.Products, c => c.Dictionary(c => c
+ .WithKeyValidator("Product", k => k
+ .NotFound().WhenValue(v => !products.ContainsKey(v))))),
+ cancellationToken).ConfigureAwait(false);
+ }
+}
+
+// Instantiate directly — _repository is already injected into the service:
+await new MovementRequestValidator(_repository).ValidateAndThrowAsync(request, cancellationToken);
+```
+
+Both phases apply to both base classes. For `Result` pipelines, use `ValidateWithResultAsync` instead of `ValidateAndThrowAsync`:
+
+```csharp
+var result = await Result.GoAsync(() => MyValidator.Default.ValidateWithResultAsync(value, cancellationToken));
+if (result.IsFailure) return result.AsResult();
+```
+
+## Not Found Handling
+
+After loading an entity, throw immediately if it does not exist:
+
+```csharp
+var current = await _repository.GetAsync(id, cancellationToken).ConfigureAwait(false);
+NotFoundException.ThrowIfDefault(current);
+```
+
+## Business Rule Exceptions
+
+Use `BusinessException` for domain rule violations that are the caller's fault but are not validation errors:
+
+```csharp
+if (!product.IsInactive)
+ throw new BusinessException("A product must first be deactivated before it can be deleted.");
+```
+
+`BusinessException` (and all CoreEx exceptions that extend `ExtendedException`) support optional fluent extension methods that enrich the error with machine-readable context. All methods return the exception so they can be chained directly on the `throw` expression:
+
+| Method | Purpose |
+|---|---|
+| `.WithErrorCode(string)` | Adds a machine-readable code the caller can key on (e.g. `"product-not-inactive"`) |
+| `.WithKey(object)` | Attaches the entity key — surfaces in the problem-details response under `key` |
+| `.WithDetail(string)` | Adds extended human-readable detail beyond the main message |
+| `.WithStatusCode(HttpStatusCode)` | Overrides the default HTTP status code (use sparingly) |
+| `.WithExtension(string, object)` | Adds arbitrary key/value metadata to `extensions` in the problem-details response |
+| `.AsTransient(TimeSpan?)` | Marks the error as transient so retry infrastructure knows it is safe to retry |
+
+```csharp
+// Minimal — message only
+if (!product.IsInactive)
+ throw new BusinessException("A product must first be deactivated before it can be deleted.");
+
+// With machine-readable error code and entity key
+if (!product.IsInactive)
+ throw new BusinessException("A product must first be deactivated before it can be deleted.")
+ .WithErrorCode("product-not-inactive")
+ .WithKey(product.Id);
+
+// With additional detail
+if (basket.HasExpiredItems)
+ throw new BusinessException("Basket cannot be checked out.")
+ .WithErrorCode("basket-has-expired-items")
+ .WithDetail("One or more items in the basket have expired and must be removed before checkout.");
+```
+
+## Assigning the identifier on Create
+
+The **service** assigns the new identifier on `Create` — the database does **not** generate it (the migration scaffold's value-generation default is dropped unless a key is explicitly DB-generated; see the tooling guidance). Set it from the ambient `Runtime` **after validation, before the transaction**, alongside any other server-controlled fields:
+
+```csharp
+public async Task CreateAsync(Product product, CancellationToken cancellationToken = default)
+{
+ product.ThrowIfNull();
+ await ProductValidator.Default.ValidateAndThrowAsync(product, cancellationToken).ConfigureAwait(false);
+
+ product.Id = Runtime.NewId(); // service-assigned identity — never left to the caller or the DB
+ product.CategoryCode = product.SubCategory!.CategoryCode; // derive any other server-set fields here
+ product.IsInactive = true;
+
+ return await _unitOfWork.TransactionAsync(async ct =>
+ {
+ var dr = await _repository.CreateAsync(product, ct).ConfigureAwait(false);
+ return dr.WhereMutated(v =>
+ _unitOfWork.Events.Add(EventData.CreateEventWith(v, EventAction.Created)));
+ }, cancellationToken).ConfigureAwait(false);
+}
+```
+
+- **Match the generator to the identifier type:** `string` key → `Runtime.NewId()`; `Guid` key → `Runtime.NewGuid()`. Never use `Guid.NewGuid()` / `Guid.NewGuid().ToString()` directly — always go through `Runtime` so the clock/GUID source stays test-controllable.
+- **Always assign on Create** so the value is deterministic and present before the event is published — don't rely on the caller-supplied `Id` and don't defer to the database.
+- **Exception — DB-generated keys only:** if a key is explicitly an identity/sequence column (the rare, explicitly-requested case), the database assigns it; the service does **not** set `Id` and reads it back from the create result instead.
+
+## Unit of Work and Events
+
+Wrap all side-effectful database operations in `_unitOfWork.TransactionAsync(...)`. Both the database write and the outbox event publication are committed atomically inside this scope — events are only dispatched if the transaction commits successfully.
+
+```csharp
+return await _unitOfWork.TransactionAsync(async ct =>
+{
+ var dr = await _repository.CreateAsync(product, ct).ConfigureAwait(false);
+ return dr.WhereMutated(v =>
+ _unitOfWork.Events.Add(EventData.CreateEventWith(v, EventAction.Created)));
+}, cancellationToken).ConfigureAwait(false);
+```
+
+**When eventing is enabled, every mutating operation must publish its event inside the transaction.** `Create`, `Update`, and `Delete` each add their event (`EventAction.Created` / `Updated` / `Deleted`) via `_unitOfWork.Events.Add(...)` within the `TransactionAsync` scope — a transaction that writes but adds no event is a bug. (Eventing is enabled when the solution was scaffolded with it, or whenever the domain publishes domain events; if a domain is genuinely event-free, omit it.)
+
+```csharp
+// ❌ Wrong — writes inside the transaction but never adds the event
+return await _unitOfWork.TransactionAsync(async ct =>
+{
+ var result = await _repository.CreateAsync(employee, ct).ConfigureAwait(false);
+ return result.Value!;
+}, cancellationToken).ConfigureAwait(false);
+
+// ✅ Correct — event added within the same transaction, only on mutation
+return await _unitOfWork.TransactionAsync(async ct =>
+{
+ var dr = await _repository.CreateAsync(employee, ct).ConfigureAwait(false);
+ return dr.WhereMutated(v =>
+ _unitOfWork.Events.Add(EventData.CreateEventWith(v, EventAction.Created)));
+}, cancellationToken).ConfigureAwait(false);
+```
+
+- `WhereMutated(action)` — executes `action` only when the data result records a mutation; add the event inside this callback. **Mind the overload:** `DataResult` (from `Create`/`Update`) carries the value, so use `WhereMutated(v => ...)`; `DataResult` (from `Delete`) has **no value**, so use the parameterless `WhereMutated(() => ...)` — not `WhereMutated(_ => ...)`.
+- `EventData.CreateEventWith(value, action)` — a typed event **carrying the entity value** (Create/Update only — pass the **real** mutated value `v`). For a **no-value** event (Delete), use `EventData.CreateEvent(action).WithKey(id)` — the type + action + key, **no value**. **Never fabricate a value to feed `CreateEventWith` on a delete** (e.g. `CreateEventWith(new Employee { Id = id }, …)` or `CreateEventWith(default, …)`): a delete event must have **no body** — a synthetic entity wrongly serialises a near-empty value, adds a version suffix that delete events must not have, and buries the id in the body instead of the metadata key. The id belongs in `.WithKey(id)`.
+- `EventAction.Created`, `EventAction.Updated`, `EventAction.Deleted` — use the standard constants.
+
+For delete the `DataResult` has no value, so use the parameterless `WhereMutated(() => ...)` and carry the identity via `.WithKey(id)`:
+
+```csharp
+public async Task DeleteAsync(string id, CancellationToken cancellationToken = default)
+{
+ await _unitOfWork.TransactionAsync(async ct =>
+ {
+ var dr = await _repository.DeleteAsync(id.ThrowIfNullOrEmpty(), ct).ConfigureAwait(false);
+
+ // ❌ Wrong — fabricates a throwaway value to carry the id; attaches a (near-empty) body,
+ // adds a version suffix delete must not have, and puts the id in the body, not the key.
+ dr.WhereMutated(() =>
+ _unitOfWork.Events.Add(EventData.CreateEventWith(new Contracts.Employee { Id = id }, EventAction.Deleted)));
+
+ // ✅ Correct — no value; type + action + key only.
+ dr.WhereMutated(() => // () — no value on a delete DataResult
+ _unitOfWork.Events.Add(
+ EventData.CreateEvent(EventAction.Deleted).WithKey(id)));
+ }, cancellationToken).ConfigureAwait(false);
+}
+```
+
+## Result<T> Pipeline Style
+
+Using `Result` chains is a developer choice — it is not restricted to DDD aggregate services. It can be applied to any service method where explicit, composable failure propagation is preferred over exceptions. Compose with `Result.GoAsync`, `.ThenAs`, `.ThenAsAsync`. The unit of work is still `TransactionAsync`:
+
+```csharp
+public Task> CreateAsync(string customerId, CancellationToken cancellationToken = default)
+{
+ var aggregate = Domain.Basket.CreateNew(customerId.ThrowIfNullOrEmpty());
+
+ return _unitOfWork.TransactionAsync(async ct =>
+ {
+ var br = await _repository.CreateAsync(aggregate, ct).ConfigureAwait(false);
+ return br.ThenAs(b =>
+ {
+ var contract = BasketMapper.Map(b);
+ _unitOfWork.Events.Add(EventData.CreateEventWith(contract, EventAction.Created));
+ return contract;
+ });
+ }, cancellationToken);
+}
+```
+
+For multi-step orchestration with early exit on the first failure:
+
+```csharp
+var pr = await Result.GoAsync(() => SomeValidator.Default.ValidateWithResultAsync(input, cancellationToken))
+ .ThenAsAsync(v => _someAdapter.EnsureExistsAsync(v.Id!, cancellationToken));
+
+if (pr.IsFailure)
+ return pr.AsResult();
+```
+
+#### Operator reference and the `As` / `Async` naming convention
+
+The pipeline operators come in **families**, each with consistent modifier suffixes. **Read the suffix to know what an operator does:**
+
+- **`As`** — the operation **changes the result type** (`Result` → `Result`, `Result` → `Result`, or `Result` → `Result`). The non-`As` form keeps the same type. This is **by design**: you must explicitly opt into a type change, so a `T` flowing through unchanged uses `Then`, while producing a different type uses `ThenAs`. If the compiler complains a delegate returns the "wrong" type, you almost certainly want the `As` variant.
+- **`Async`** — the supplied delegate is asynchronous (returns a `Task`).
+- **`AsAsync`** — both: an async delegate that also changes the type.
+
+| Family | Runs the delegate when… | Same-type / type-changing |
+|---|---|---|
+| `Then` | result is **success** | `Then` / `ThenAs` |
+| `When` | success **and** a condition holds | `When` / `WhenAs` |
+| `Any` | **always** (success or failure) | `Any` / `AnyAs` |
+| `OnFailure` | result is **failure** | `OnFailure` / `OnFailureAs` |
+| `Match` | branches on success vs failure, returning a value | `Match` / `MatchAs` |
+
+Each has `Async` and `AsAsync` variants too (e.g. `ThenAsync`, `ThenAsAsync`). Start a pipeline with `Result.Go(...)` / `Result.GoAsync(...)`; also available: `Bind`, `Combine`, and `.AsResult()` to drop a `Result` to a `Result`.
+
+Failure factories (return a failed result of the matching type): `Result.ValidationError(...)`, `NotFoundError(...)`, `BusinessError(...)`, `ConflictError(...)`, `ConcurrencyError(...)`, `DuplicateError(...)`, `AuthenticationError(...)`, `AuthorizationError(...)`, `TransientError(...)`. Success factories: `Result.Success`, `Result.Ok(value)`. See the [CoreEx Results README](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx/Results/README.md) for the full set.
+
+## CQRS — Read Services
+
+Split a domain's service operations **by mutation** — this is the convention:
+
+- **Mutating** operations — `Create`, `Update`, `Delete`, and any other state-changing operation — live in **`XxxService`** (`IXxxService`). These own validation, the unit of work, and event publication. `XxxService` **also** exposes a primary by-id `GetAsync(id)` to support its own mutation flows (the PATCH pre-fetch, fetch-then-update, concurrency/not-found checks).
+- **Query and read-model** operations — `QueryAsync` (collections/search) and other purpose-built read shapes — live in **`XxxReadService`** (`IXxxReadService`), which also exposes the by-id `GetAsync` for the read API.
+
+This is the surface expression of CQRS: the write model (mutations + events) and the read model (queries returning purpose-built shapes) are designed and scaled independently. Both interfaces live in `Interfaces/` side by side (`IProductService.cs`, `IProductReadService.cs`); both implementations live together in the service folder.
+
+A primary by-id `GetAsync` therefore appears on **both** services — this is intentional. Each is a single line delegating to the shared `IXxxRepository.GetAsync`, so there is no real logic duplication; having `XxxService` own a `GetAsync` keeps the mutating controller dependent on **one** service (no need to also resolve `IXxxReadService` just for the PATCH fetch). The meaningful divergence — queries and read-optimized shapes — stays exclusive to `XxxReadService`.
+
+```csharp
+[ScopedService]
+public class ProductReadService(IProductRepository repository) : IProductReadService
+{
+ private readonly IProductRepository _repository = repository.ThrowIfNull();
+
+ public Task GetAsync(string id, CancellationToken cancellationToken = default) => _repository.GetAsync(id, cancellationToken);
+ public Task> QueryAsync(QueryArgs? query, PagingArgs? paging, CancellationToken cancellationToken = default)
+ => _repository.QueryAsync(query, paging, cancellationToken);
+}
+```
+
+**The repository stays singular — CQRS is a service-layer split, not a data-layer one.** Both `XxxService` and `XxxReadService` inject the **same** `IXxxRepository` when they share a data source (e.g. the one SQL database) — do **not** split the repository to mirror the services. Only when a specific operation targets a **different** data source (e.g. a read served from a separate store or search index) is an additional repository introduced; the owning service injects and calls the appropriate repository per operation.
+
+## Anti-Corruption Layer (Adapters)
+
+When a service needs to call another domain's API, inject an adapter interface (e.g., `IProductAdapter`) rather than calling `HttpClient` directly. Implement the adapter in the Infrastructure layer using a typed HTTP client. The interface surface should be domain-idiomatic — not a mirror of the remote API.
+
+Adapter interfaces live in `Application/Adapters/` (one interface per external domain). The Infrastructure implementation lives in `Infrastructure/Adapters/`.
+
+```csharp
+// Application/Adapters/IProductAdapter.cs — interface only (domain-idiomatic, not a mirror of the remote API)
+public interface IProductAdapter
+{
+ Task> GetAsync(string id, CancellationToken cancellationToken = default);
+ Task ReserveInventoryAsync(Domain.Basket basket, CancellationToken cancellationToken = default);
+ Task CancelReservationAsync(Domain.Basket basket, CancellationToken cancellationToken = default);
+}
+
+// Infrastructure layer — implementation
+[ScopedService]
+public class ProductAdapter(ProductsHttpClient httpClient) : IProductAdapter { ... }
+```
+
+A second adapter interface (`IXxxSyncAdapter`) handles **event-driven data replication** — receiving published events from another domain and maintaining a local eventually-consistent copy in the consuming domain's own store.
+
+## Policies
+
+Policies (`Application/Policies/`) encapsulate **domain-level guard logic** that requires I/O (adapter or repository calls). They provide a named, independently testable home for rules that depend on external state and cannot be expressed in a validator alone (synchronous) or enforced directly in the domain model (no async I/O). A policy can be called from any point in service orchestration where the condition needs to be verified.
+
+Policies are **not registered in DI** — they are instantiated directly at the call site using dependencies already injected into the calling service (see [DI Registration Principle](#di-registration-principle) below).
+
+Policies return `Result` or `Result` and compose naturally into `Result` pipelines via `.GoAsync()` / `.ThenAsAsync()`:
+
+```csharp
+// Application/Policies/ProductPolicy.cs
+public class ProductPolicy(IProductAdapter productAdapter)
+{
+ private readonly IProductAdapter _productAdapter = productAdapter.ThrowIfNull();
+
+ public Task> EnsureExistsAsync(string productId, CancellationToken cancellationToken = default) => Result
+ .GoAsync(() => _productAdapter.GetAsync(productId, cancellationToken))
+ .OnFailure(r => r.IsNotFoundError
+ ? Result.ValidationError(MessageItem.CreateErrorMessage(nameof(productId), "Product was not found."))
+ : r);
+}
+
+// In the calling service — _productAdapter is already injected into the service:
+var result = await new ProductPolicy(_productAdapter).EnsureExistsAsync(productId, cancellationToken);
+```
+
+## Application-Level Mapping
+
+When a domain has a Domain layer, an `Application/Mapping/` sub-folder holds mappers that translate between the **Domain aggregate** and the **Contract**. This mapping is an Application-layer concern because it sits at the public surface boundary — it is not tied to any persistence technology.
+
+Use `Mapper` (uni-directional). Mappers are **not registered in DI** — call them via the static `Map()` method directly at the point of use (see [DI Registration Principle](#di-registration-principle) below):
+
+```csharp
+// Application/Mapping/BasketMapper.cs
+public class BasketMapper : Mapper
+{
+ protected override Contracts.Basket OnMap(Domain.Basket source) => new()
+ {
+ Id = source.Id,
+ StatusCode = source.Status,
+ Items = [.. source.Items.Select(i => BasketItemMapper.Map(i))]
+ };
+}
+
+// Call via static Map() — no injection, no new():
+var contract = BasketMapper.Map(aggregate);
+```
+
+Infrastructure-level mapping (Contract ↔ Persistence model) uses `BiDirectionMapper` and lives in `Infrastructure/Mapping/`. Do not conflate the two layers.
+
+## DI Registration Principle
+
+Only register a type in DI when there is a current, concrete intent to mock or replace it. Applying YAGNI, the following Application-layer types are **not** DI-registered — they are called or instantiated directly at the point of use:
+
+| Type | How to use |
+|---|---|
+| `Validator` | Call via static `Default` singleton: `MyValidator.Default.ValidateAndThrowAsync(...)` |
+| `Validator` | Instantiate directly with already-injected deps: `new MyValidator(_repo).ValidateAndThrowAsync(...)` |
+| `Mapper` | Call via static `Map()` method: `MyMapper.Map(source)` |
+| Policy classes | Instantiate directly with already-injected deps: `new MyPolicy(_adapter).EnsureExistsAsync(...)` |
+
+Keeping these out of DI avoids bloating service constructors with dependencies that are not realistic substitution points, and defers that complexity until there is a real need for it.
+
+## ConfigureAwait
+
+Always call `.ConfigureAwait(false)` on every `await` inside service and repository methods.
+
+## Do Not
+
+- Do not publish events outside of `_unitOfWork.TransactionAsync(...)` — events must be committed atomically with the database write.
+- Do not put **query/collection or read-model** operations in `XxxService`, nor mutating operations in `XxxReadService` — queries belong in `XxxReadService`. (A primary by-id `GetAsync` legitimately appears on **both**: the write service uses it to support its own mutations, e.g. the PATCH pre-get.)
+- Do not split the repository to mirror the CQRS services — both share one `IXxxRepository` per data source; add another repository only for a genuinely different data source.
+- Do not call `HttpClient` directly from services — always go through an adapter interface.
+- Do not reference Infrastructure assemblies from the Application layer — all persistence and transport concerns are reached through interfaces.
+- Do not implement rules in `OnValidateAsync` that require I/O without first guarding with `if (context.HasErrors) return;`.
+- Do not add business logic to controllers — services own all use-case orchestration.
+- Do not register Validators, Mappers, or Policies in DI or inject them into service constructors — call or instantiate them directly at the point of use (YAGNI: refactor to DI only when there is a real need to mock or replace them).
+- Do not use `new ProductValidator()` at the call site when `Validator` provides a `Default` singleton — use `ProductValidator.Default`.
+- Do not inject a validator into a service constructor — a `Validator` is invoked via its `Default` singleton, never as a constructor dependency.
+- Do not call bare `ValidateAsync(...)` for fail-fast validation — it returns the result without throwing, silently swallowing errors. Use `ValidateAndThrowAsync` (non-ROP) or `ValidateWithResultAsync` (ROP).
+- Do not perform a mutating `TransactionAsync` without adding its event (`_unitOfWork.Events.Add(...)`) when eventing is enabled — the write and the event must be committed together.
+- Do not use `WhereMutated(_ => ...)` on a delete — `DataResult` (delete) has no value; use the parameterless `WhereMutated(() => ...)`. The value-carrying `WhereMutated(v => ...)` is only for `DataResult` (create/update).
+- Do not reach for a non-`As` Result operator when the delegate changes the result type — use the `As` variant (e.g. `ThenAs`/`ThenAsAsync`); the `As` suffix exists to make the type change explicit.
+- Do not generate service operations the user did not confirm — confirm the CRUD set (Get/Create/Update/Delete, each default-selected) when operations are unspecified, and never add a Query operation without an explicit request.
+
+## Further Reading
+
+- [Application Layer Guide](https://github.com/Avanade/CoreEx/blob/main/samples/docs/application-layer.md) — full walkthrough of services, validators, adapters, policies, mapping, and the unit-of-work pattern.
+- [Pattern Catalog](https://github.com/Avanade/CoreEx/blob/main/samples/docs/patterns.md) — CQRS, Service, Unit of Work, Validator, Policy, Adapter, and Event patterns with cross-links.
+- [Layer Dependencies](https://github.com/Avanade/CoreEx/blob/main/samples/docs/layers.md) — layer dependency rules: Application depends inward only on Contracts and its own interfaces.
+- [CoreEx.Validation README](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.Validation/README.md) — `Validator`, rule set, `OnValidateAsync`, and `ValidateFurtherAsync`.
+- [CoreEx README](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx/README.md) — `IUnitOfWork`, `Result`, `[ScopedService]`, and CoreEx exception types.
+- [CoreEx Results README](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx/Results/README.md) — `Result` type, pipeline operators (`.GoAsync`, `.ThenAs`, `.ThenAsAsync`), and error propagation semantics.
diff --git a/.github/instructions/coreex-contracts.instructions.md b/.github/instructions/coreex-contracts.instructions.md
new file mode 100644
index 00000000..b2891d69
--- /dev/null
+++ b/.github/instructions/coreex-contracts.instructions.md
@@ -0,0 +1,410 @@
+---
+applyTo: "**/Contracts/**/*.cs"
+description: "Contract (DTO) conventions: source generation, marker attributes, reference data, ETag, and ChangeLog support"
+tags: ["contracts", "dto", "source-generation", "reference-data", "etag"]
+---
+
+# Contract (DTO) Conventions
+
+## File Placement
+
+Contracts live **flat in the root of the `*.Contracts` project** — both hand-authored (`Product.cs`) and generated (`Product.g.cs`) files. **Do not create sub-folders** such as `Entities/`, `Models/`, or `RefData/` to group them; the samples place every contract at the project root regardless of whether it is a root entity, subordinate, request/response DTO, or reference-data type. Only introduce a sub-folder if the **user explicitly asks** for one. The same flat-root convention applies to the matching files in the other layers (validators, services, repositories, mappers) unless their instructions state otherwise.
+
+## NuGet / Project References
+
+| Package | Key types provided |
+|---|---|
+| `CoreEx` | `[Contract]`, `IIdentifier`, `ICompositeKey`, `IETag`, `IChangeLog`, `ChangeLog`, `[ReadOnly]`, `[Localization]`; includes the `CoreEx.Generator` Roslyn source generator — no separate package reference required |
+| `CoreEx.RefData` | `ReferenceData`, `ReferenceDataCollection`, `[ReferenceData]`, `[ReferenceData]`, `ReferenceDataSortOrder` |
+
+```xml
+
+
+
+
+```
+
+## Root vs. Subordinate (contract or entity)
+
+Note that the terms Contract and Entity are interchangeable in this context. The conventions below apply to all DTOs that represent a resource, whether they are persisted entities or plain request/response models.
+
+If the user specifies that it is Reference Data then treat as defined by [Reference Data Contracts](#reference-data-contracts).
+
+Before generating any contract, determine whether it is a **root** or a **subordinate** contract.
+Always ask the user if this is not explicit in their request.
+
+| Concern | Root | Subordinate |
+|---|---|---|
+| `IIdentifier` | ✅ Always (unless explicitly requested otherwise). The user may also specify a different type | ❌ Omit |
+| `IETag` | ✅ Always (unless explicitly requested otherwise) | ❌ Omit |
+| `IChangeLog` | ✅ When audit trail is needed (ask) | ❌ Omit |
+| `[ReadOnly(true)]` on `Id`, `ETag`, `ChangeLog` | ✅ Required | N/A |
+| `[Contract]` + `partial` | ✅ Always for generated members | Only if generated members are needed |
+
+**Root** — owns its own identity, is persisted independently, and is retrieved/mutated via its own API endpoint (e.g. `Product`, `Order`, `Person`).
+
+**Subordinate** — a child, line-item, value object, or request/response DTO that is generally accessed through a root (e.g. `OrderLine`, `Address`, `BasketItemAddRequest`).
+
+> **Agent instruction:** When asked to create a contract and the category is not explicit,
+> ask: *"Is `{Name}` a root contract (it has its own identity) or a subordinate contract (accessed only through a parent)?"*
+> Do not assume root. Do not generate `IIdentifier`, `IETag`, or `IChangeLog` until confirmed.
+
+> **Identifier type — do not substitute.** The identifier type is `IIdentifier` by **default**. Use `string?` unless the user **explicitly** states a different type (e.g. "use a `Guid` id", "the key is an `int`"). **Never** silently change it — in particular, do **not** default to `Guid` because it is common elsewhere. The chosen type is authoritative and must flow through unchanged to every downstream artefact: the `ref-data.yaml` `idType`, the persistence model, and the database primary-key column type (a `string?` id → `NVARCHAR(50)`/`VARCHAR(50)`, **not** `UNIQUEIDENTIFIER`/`UUID`). If a plan or prior step states one type, the implementation must match it exactly — flag any discrepancy rather than resolving it by changing the type.
+
+## Unified API and Messaging Surface
+
+The same contract type is used for both the HTTP API response **and** the event message payload. A `Product` returned from `GET /api/products/{id}` is the same `Contracts.Product` type published as a `product.created` event body. Do not split a resource into separate API and event DTOs.
+
+When a domain **consumes** events from another domain, declare a local internal representation (e.g., `Application\Adapters\Products\Product`) rather than taking a dependency on the publishing domain's Contracts assembly. Keep the shape consistent with the published contract, but own it locally to preserve the anti-corruption boundary.
+
+## Source Generation
+
+Mark entity contract classes with the `[Contract]` attribute and declare them `partial`. The `CoreEx` package ships with a bundled Roslyn source generator ([`CoreEx.Generator`](https://github.com/Avanade/CoreEx/tree/main/gen/CoreEx.Generator)) that activates automatically — no extra package reference is needed. It emits serialization, equality, and change-tracking code into a paired `*.g.cs` file at compile time. Never manually implement those generated members.
+
+> Both `[Contract]` and `[ReferenceData]` trigger this Roslyn source generator. For reference data types the flow is two-stage: the `*.CodeGen` project (OnRamp/Handlebars) first generates the class decorated with `[ReferenceData]`, then the Roslyn generator processes that attribute at compile time to emit additional members -- see [Reference Data Contracts](#reference-data-contracts).
+
+```csharp
+[Contract]
+public partial class Product : ProductBase, IETag, IChangeLog { }
+```
+
+### How the generator runs (do not chase phantom build problems)
+
+The Roslyn source generator runs **automatically as part of compilation** — every `dotnet build` and every IDE design-time build. You do **not** trigger it manually, and there is no separate step to "make it generate".
+
+Common misconceptions to avoid:
+- **Missing generated members before a build is normal — not an error to fix by hand.** If a `partial` property or class has no visible implementation, it is because the project simply hasn't been built yet. Build it; do **not** hand-author the generated partial implementation or create the `.g.cs` file to "unblock" things.
+- **Other compilation errors do not stop the generator.** Roslyn runs source generators on the parsed compilation regardless of unrelated errors; there is **no build-ordering requirement and no "circular dependency"** whereby errors elsewhere prevent generation. Do not invent such a dependency — fix the actual reported errors and rebuild.
+- **If a member is still missing after a clean build, the contract declaration is malformed**, not the build process. Check that the class has `[Contract]` and is `partial`, the property is `partial`, and the attributes are correct (see below) — then rebuild. Never substitute a hand-written implementation for the generated one.
+
+Plain value-object or request contracts that do not need generated members (equality, cloning, etc.) can be declared as ordinary, non-`partial` classes without `[Contract]`:
+
+```csharp
+// No [Contract] needed — no generated members required.
+public class BasketItemAddRequest
+{
+ public string? ProductId { get; set; }
+ public decimal Quantity { get; set; }
+}
+```
+
+## Interfaces
+
+Implement the appropriate CoreEx marker interfaces depending on the entity's behavior:
+
+| Interface | When to use |
+|---|---|
+| `IIdentifier` | Entity has a single primary key |
+| `ICompositeKey` | Entity has a multi-part key |
+| `IETag` | Entity participates in optimistic concurrency / IF-MATCH |
+| `IChangeLog` | Entity records created/updated audit metadata |
+
+All three are typically combined on mutable entities:
+
+```csharp
+[Contract]
+public partial class Product : ProductBase, IETag, IChangeLog
+{
+ [ReadOnly(true)]
+ public string? Id { get; set; }
+
+ [ReadOnly(true)]
+ public string? ETag { get; set; }
+
+ [ReadOnly(true)]
+ public ChangeLog? ChangeLog { get; set; }
+}
+```
+
+## Documentation Comments
+
+Give **every contract property a ``** (and the contract class itself). The standard `Id`/`ETag`/`ChangeLog` members — which implement `IIdentifier`/`IETag`/`IChangeLog` — may use `` instead. See [XML Documentation Comments](#) in the conventions. (Common mistake: leaving the contract properties undocumented — they each need a summary.)
+
+```csharp
+/// Represents the Employee contract.
+[Contract]
+public partial class Employee : IIdentifier, IETag, IChangeLog
+{
+ ///
+ [ReadOnly(true)]
+ public string? Id { get; set; }
+
+ /// Gets or sets the employee's first name.
+ public string? FirstName { get; set; }
+
+ /// Gets or sets the employee's gender (reference data).
+ [ReferenceData]
+ public partial string? GenderCode { get; set; }
+
+ ///
+ [ReadOnly(true)]
+ public string? ETag { get; set; }
+
+ ///
+ [ReadOnly(true)]
+ public ChangeLog? ChangeLog { get; set; }
+}
+```
+
+## ReadOnly Properties
+
+Decorate server-assigned properties with `[ReadOnly(true)]` to signal that clients cannot supply them. Common examples: `Id`, `ETag`, `ChangeLog`, `CategoryCode` (derived from SubCategory). NSwag/OpenAPI automatically excludes these from inbound request schemas.
+
+## Reference Data Properties
+
+Use `[ReferenceData]` on code properties that back a reference data relationship. Two conditions must both be met for the source generator to emit the navigation accessor:
+
+1. The **class** is decorated with `[Contract]` and declared `partial`.
+2. The **property** is declared `partial`.
+
+```csharp
+[Contract]
+public partial class ProductBase : IIdentifier
+{
+ [ReferenceData]
+ [Localization("Sub-category")]
+ public partial string? SubCategoryCode { get; set; }
+
+ [ReferenceData]
+ [Localization("Unit-of-measure")]
+ public partial string? UnitOfMeasureCode { get; set; }
+}
+```
+
+The generated code exposes a strongly-typed `SubCategory` property alongside the raw `SubCategoryCode` string. If either `[Contract]` or `partial` is missing from the class, the navigation property will not be generated and the code will not compile correctly.
+
+> **Only `[ReferenceData]` properties are `partial` — not every property in the class.** The class is `partial` (it has a generated `.g.cs` part), but among its **properties** *only* the `[ReferenceData]`-decorated code properties are declared `partial` (the generator supplies their implementation part — the typed navigation). Every other property is an **ordinary auto-property** with no `partial` keyword. Do **not** assume that because the class is `partial` its properties must be too — marking a plain property `partial` with no generated implementation fails to compile with **CS9248** *("partial property … must have an implementation part")*.
+>
+> ```csharp
+> public partial class Employee : IIdentifier
+> {
+> public string? FirstName { get; set; } // ✅ plain — NOT partial
+> public decimal Salary { get; set; } // ✅ plain — NOT partial
+>
+> [ReferenceData]
+> public partial string? GenderCode { get; set; } // ✅ partial — generator emits the `Gender` navigation
+>
+> // public partial string? FirstName { get; set; } // ❌ CS9248 — no generated implementation for a non-ref-data property
+> }
+> ```
+
+> **Agent instruction — property type resolution:** When generating contract properties, apply this hierarchy for every property first, then generate the complete contract in a single pass. Do not ask about individual properties mid-list.
+>
+> **Step 1 — Honour explicit types**
+> If the user specifies a CLR type (e.g. `string Gender`, `int Rating`), use it as-is. No lookup. No question.
+>
+> **Step 2 — Infer obvious primitives by name pattern (silent — no question)**
+>
+> | Name pattern | Inferred type |
+> |---|---|
+> | `First*`, `Last*`, `*Name`, `*Description`, `*Text`, `*Notes`, `*Comment`, `Sku`, `Email`, `Phone`, `Url` | `string?` |
+> | `Is*`, `Has*`, `Can*`, `Allow*` | `bool` |
+> | `*Price`, `*Amount`, `*Cost`, `*Rate`, `*Total`, `*Balance`, `*Percentage` | `decimal` |
+> | `*Date`, `*On`, `*At`, `Created*`, `Updated*`, `Deleted*` | `DateTime?` |
+> | `*Quantity`, `*Qty` | `decimal` |
+> | `*Count`, `*Number`, `*Sequence` | `int` |
+>
+> **Step 3 — Check `ref-data.yaml` for any remaining untyped noun properties**
+> Search `entities:` in `tools/[domain].CodeGen/ref-data.yaml` for each unresolved property name:
+> - **Found** — wire up silently as `[ReferenceData]` `public partial string? {Name}Code { get; set; }`. No question.
+> - **Not found** — add to the candidates list for Step 4.
+>
+> **Step 4 — Ask once, for all remaining candidates, at the end**
+> After processing every property, if any candidates remain unresolved, ask a single question:
+> *"The following properties could be reference data types — which should I add to `ref-data.yaml`? (select any, or none to treat as plain properties): `Gender`, `Status`, `Priority`"*
+> Never ask per-property. Never interrupt before all properties have been analysed.
+>
+> **Step 5 — Single batch edit and one CodeGen run**
+> For all properties the user confirms as reference data:
+> 1. Add **all** confirmed types to `ref-data.yaml` under `entities:` in a **single edit**.
+> 2. Offer to run `dotnet run` from the `*.CodeGen` directory **once** to generate all of them in one pass.
+> 3. On success, summarise the generated artefacts; on failure relay the **complete output verbatim** then fix `ref-data.yaml` and offer to re-run. Do not create `.g.cs` files manually.
+>
+> **Step 6 — Generate the complete contract in one pass**
+> Once all types are resolved and CodeGen has run (if needed), emit the full contract:
+> - Reference data properties: `[ReferenceData]` `public partial string? {Name}Code { get; set; }` — the property name is always `{Name}Code`; the navigation property `{Name}` is Roslyn-generated and must not be hand-authored.
+> - Plain properties: use the inferred or explicit CLR type.
+> - Apply `[Localization("Human label")]` **only** where the auto-derived label would be wrong/undesired (e.g. `SubCategoryCode` → `"Sub-category"`). Do **not** add it when the value would equal the default (e.g. `[Localization("Salary")]` on `Salary` is redundant).
+> - For any property confirmed as plain (Step 4, user selected none or a subset), use `string?` as the default if no better type can be inferred.
+
+## Localization Labels
+
+CoreEx automatically derives a human-friendly label from the property name (the PascalCase name is split into words — e.g. `DateOfBirth` → "Date Of Birth"). **Only** decorate a property with `[Localization("Human label")]` when that default would be wrong or undesired — typically to drop a `Code` suffix or hyphenate (e.g. `SubCategoryCode` → "Sub-category").
+
+```csharp
+// ✅ Needed — default "Sub Category Code" is undesired
+[Localization("Sub-category")]
+public partial string? SubCategoryCode { get; set; }
+// Validation error: "Sub-category is required." (not "Sub Category Code is required.")
+
+// ❌ Redundant — the default already yields "Salary"; do not annotate
+[Localization("Salary")]
+public decimal Salary { get; set; }
+```
+
+Omit `[Localization]` whenever the attribute value would equal the auto-derived label — it is noise. Add it only to change the label.
+
+## Inheritance for Shared Fields
+
+Extract shared fields into an abstract `XxxBase` class when multiple contracts share the same core properties. This keeps validation and mapping code DRY.
+
+A projection subclass that adds no source-generated behavior (no `IETag`, `IChangeLog`, etc.) does **not** need `[Contract]` or `partial`:
+
+```csharp
+[Contract]
+public abstract partial class ProductBase : IIdentifier
+{
+ public string? Id { get; set; }
+ public string? Sku { get; set; }
+ public string? Text { get; set; }
+ public decimal Price { get; set; }
+}
+
+[Contract]
+public partial class Product : ProductBase, IETag, IChangeLog { /* additions only */ }
+
+// Projection — plain class, no generated members needed.
+public class ProductLite : ProductBase
+{
+ public decimal QtyOnHand { get; set; }
+}
+```
+
+## Reference Data Contracts
+
+Reference data contracts are **generated, not hand-authored**. The source of truth is the `entities:` section of `ref-data.yaml` in the domain's `*.CodeGen` project. Running the CodeGen generates all artefacts across every layer -- contract class, API endpoint, service method, repository interface, repository implementation, and mapper -- as `.g.cs` files that must never be edited directly.
+
+> **Agent instruction:** When asked to create or modify a reference data type:
+> 1. Edit `ref-data.yaml` in `tools/[domain].CodeGen/` -- add or update the entry under `entities:`.
+> 2. Offer to run `dotnet run` from the CodeGen directory on the user's behalf.
+> 3. If confirmed, execute it and summarise the generated artefacts on success; on failure relay the **complete output verbatim** — it provides the diagnostic needed to fix the entry.
+> 4. On failure, fix the issue in `ref-data.yaml` and offer to re-run -- do not create or edit `.g.cs` files to work around a generation error.
+> 5. If the user declines, remind them to run `dotnet run` from the `*.CodeGen` directory before the new types are available.
+>
+> If the user **explicitly requests** hand-authoring instead of CodeGen, use the pattern shown in [Hand-authored contracts (explicit request only)](#hand-authored-contracts-explicit-request-only) below.
+
+### `ref-data.yaml` -- entity definition
+
+The standard `IReferenceData` properties (`Id`, `Code`, `Text`, `Description`, `SortOrder`, `IsActive`, `StartsOn`, `EndsOn` etc.) are automatically included in every generated type -- do not declare them under `properties:`. Only additional domain-specific columns need to be listed, and most reference data entities require none at all.
+
+```yaml
+entities:
+- name: Brand # minimal form -- no extra properties needed
+- name: Category # same; just name is sufficient for most entities
+- name: SubCategory
+ properties:
+ - name: CategoryCode
+ type: ^Category # ^ prefix = ref-data navigation property (typed accessor generated)
+- name: UnitOfMeasure
+ plural: UnitsOfMeasure # override irregular pluralization
+ idType: Guid # override identifier type; defaults to string
+ properties:
+ - name: Scale
+ type: int # additional stored column (not part of IReferenceData)
+ - name: DiscountPercentage
+ type: decimal
+ excludeContract: true # exclude from generated contract (persistence model only)
+```
+
+Key `entities:` options:
+
+| Key | Required | Default | Purpose |
+|---|---|---|---|
+| `name` | Yes | -- | Entity name (PascalCase) |
+| `plural` | No | Auto-pluralized | Override when pluralization is irregular |
+| `idType` | No | `string` | Identifier type override (e.g. `Guid`, `int`) |
+| `properties[].name` | Yes (if any) | -- | Additional stored property name |
+| `properties[].type` | Yes (if any) | -- | CLR type; prefix `^` for a ref-data navigation accessor |
+| `properties[].excludeContract` | No | `false` | Exclude from the generated contract (persistence only) |
+
+### Hand-authored extensions (optional)
+
+After CodeGen runs, an optional hand-authored `partial` class in the same namespace can add constants or computed members. Do not redeclare `[ReferenceData]`, the base class, or the collection class -- those are owned by the generator.
+
+```csharp
+// MovementKind.cs -- hand-authored extension; MovementKind.g.cs is generated, do not edit.
+public partial class MovementKind
+{
+ public const string Adjust = "A";
+ public const string Issue = "I";
+ public const string Receive = "R";
+}
+
+// UnitOfMeasure.cs -- computed property derived from a generated stored field.
+public partial class UnitOfMeasure
+{
+ [JsonIgnore]
+ public int Precision => 16 - Scale; // Scale is a generated stored field
+}
+```
+
+### Hand-authored contracts (explicit request only)
+
+If the user explicitly requests a hand-authored reference data contract (i.e., without CodeGen), declare the type and its paired collection class directly:
+
+```csharp
+[ReferenceData]
+public partial class Category : ReferenceData;
+
+public class CategoryCollection() : ReferenceDataCollection(ReferenceDataSortOrder.Code);
+```
+
+Use `ReferenceData` when a non-string identifier type is required:
+
+```csharp
+[ReferenceData]
+public partial class Priority : ReferenceData;
+
+public class PriorityCollection() : ReferenceDataCollection(ReferenceDataSortOrder.Code);
+```
+
+## Casing Transformations
+
+Apply casing transforms in the property setter, not in the validator, when a field has a canonical form:
+
+```csharp
+public string? Sku { get => field; set => field = value?.ToUpper(); }
+```
+
+## JsonIgnore
+
+Use `[JsonIgnore]` for computed or internal properties that must not appear in the API response or request body:
+
+```csharp
+[JsonIgnore]
+public bool IsQuantityValidForKind => KindCode switch { ... };
+```
+
+## No Business Logic in Contracts
+
+Contracts are data transfer objects. Keep them free of domain rules, validation logic, and service calls. Read-only computed helpers (like `IsQuantityValidForKind` above) are acceptable shorthands but must not mutate state.
+
+## Generated Code
+
+Never create or edit `*.g.cs` files directly.
+
+| File pattern | Generator | Change instead |
+|---|---|---|
+| `*.g.cs` (ref-data types, cross-layer) | `*.CodeGen` project (`ref-data.yaml` + Handlebars/OnRamp) | Edit `ref-data.yaml` and re-run `dotnet run` |
+| `*.g.cs` (contract members) | Roslyn source generator (`CoreEx.Generator`) | The `[Contract]`-decorated partial class |
+
+## Do Not
+
+- Do not reference another domain's Contracts assembly to consume its events — declare a local adapter model instead.
+- Do not add `[Contract]` or `partial` to plain value-object or request classes that need no generated members.
+- Do not implement members that the Roslyn source generator emits (equality, cloning, serialization helpers).
+- Do not place domain rules, validators, or service calls in contract classes.
+- Do not leave contract properties without a `` — every property gets one (standard `Id`/`ETag`/`ChangeLog` may use ``). See the *Documentation Comments* section.
+- Do not add a redundant `[Localization]` attribute whose value equals the auto-derived label (e.g. `[Localization("Salary")]` on `Salary`) — only annotate to change the label.
+- Do not edit `*.g.cs` files directly — regenerate via the appropriate tooling.
+- Do not hand-author a generated partial implementation (or create its `.g.cs`) because it is "missing" — it appears after a build; the generator runs automatically during compilation.
+- Do not invent a build-ordering or "circular dependency" excuse for missing generated code — Roslyn runs the generator even when other errors exist; fix the real errors and rebuild.
+- Do not emit `#nullable enable` or `#nullable restore` pragma directives — nullable is enabled project-wide via `Directory.Build.props`.
+- Do not create a sub-folder (e.g. `Entities/`, `Models/`, `RefData/`) to house contracts — place every contract flat in the `*.Contracts` root (see *File Placement*); only nest when the user explicitly asks.
+
+## Further Reading
+
+- [Contracts Layer Guide](https://github.com/Avanade/CoreEx/blob/main/samples/docs/contracts-layer.md) — unified API/event surface, source generation, reference data, and internal adapter models.
+- [CoreEx.Generator](https://github.com/Avanade/CoreEx/tree/main/gen/CoreEx.Generator) — Roslyn source generator that processes `[Contract]` and `[ReferenceData]` annotations.
+- [CoreEx.RefData README](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.RefData/README.md) — reference data types, collections, and sort order.
+- [Tooling Guide](https://github.com/Avanade/CoreEx/blob/main/samples/docs/tooling.md) — `*.CodeGen` project usage and `ref-data.yaml` configuration.
diff --git a/.github/instructions/coreex-conventions.instructions.md b/.github/instructions/coreex-conventions.instructions.md
new file mode 100644
index 00000000..947a16d2
--- /dev/null
+++ b/.github/instructions/coreex-conventions.instructions.md
@@ -0,0 +1,213 @@
+---
+applyTo: "**/*.cs"
+description: "Universal C# coding conventions: nullable, implicit usings, GlobalUsing.cs, file-scoped namespaces, brace style, expression bodies, and private field naming"
+tags: ["conventions", "style", "nullable", "usings", "naming"]
+---
+
+# C# Coding Conventions
+
+## Project Configuration
+
+Every project must have `Nullable` and `ImplicitUsings` enabled. For consuming projects these are typically set once in a root `Directory.Build.props`:
+
+```xml
+
+ enable
+ enable
+
+```
+
+Nullable warnings are treated as errors. Never suppress a nullable warning with the null-forgiving operator (`!`) without a clear reason in a comment.
+
+## Global Usings
+
+Every project has a single `GlobalUsing.cs` at the project root that declares all namespace imports. Do not add `using` statements to individual source files.
+
+```csharp
+// GlobalUsing.cs — all usings for the project declared here (sorted alphabetically)
+global using Contoso.Products.Application.Interfaces;
+global using CoreEx;
+global using Microsoft.Extensions.Logging;
+global using System;
+global using System.Collections.Generic;
+global using System.Threading;
+global using System.Threading.Tasks;
+```
+
+**Always re-sort after editing**: Whenever you add or change an entry in `GlobalUsing.cs`, re-sort the **entire** file alphabetically (ordinal) so the order stays deterministic — all namespaces sorted equally, with no special grouping for `System.*`, one `global using` per line. Never simply append a new entry at the end.
+
+**Why this matters for code generation**: The `*.CodeGen` project emits no `using` statements in generated files. Every namespace referenced by generated code must already be declared in `GlobalUsing.cs`, or the generated output will not compile. When adding a new namespace dependency, add it to `GlobalUsing.cs` — not to the generated file.
+
+**Global usings follow the code (the scaffold ships clean).** A fresh `coreex` scaffold deliberately does **not** pre-declare `global using`s for its own still-empty project namespaces (`{Solution}.Contracts`, `{Solution}.Application`, `{Solution}.Application.Repositories`) — a `global using` of a namespace with no types yet is a **CS0234** compile error, and shipping placeholder/marker types just to avoid that is an anti-pattern. Instead, **add each `global using` to the consuming project's `GlobalUsing.cs` at the moment you create the code that needs it** — the first contract, the first service, or immediately after CodeGen emits the repository interfaces. This keeps the scaffold an honest, compiling starting point and is a normal, expected step when adding code. (The one exception is the `AssemblyMarker` types in Application/Infrastructure, which exist for `AddDynamicServicesUsing` assembly anchoring — not for namespace population.)
+
+## File-Scoped Namespaces
+
+Use file-scoped namespace declarations. Never use block-scoped namespaces.
+
+```csharp
+// Correct — file-scoped
+namespace Contoso.Products.Application;
+
+public class ProductService { }
+```
+
+```csharp
+// Wrong — do not use block-scoped
+namespace Contoso.Products.Application
+{
+ public class ProductService { }
+}
+```
+
+## Braces on `if` Statements
+
+Single-line `if` bodies do not require braces.
+
+```csharp
+// Correct — no braces needed
+if (product == null) return null;
+
+// Correct — no braces needed - prefer muli-line bodies even when they fit on one line
+if (context.HasErrors)
+ return;
+
+// Correct — braces required when body spans multiple lines
+if (condition)
+{
+ DoSomething();
+ DoSomethingElse();
+}
+```
+
+## Expression-Bodied Members
+
+Use `=>` syntax whenever the entire body is a single expression — methods, properties, constructors, operators, and accessors. Use a block body when there are multiple statements. The choice is entirely the developer's; the IDE makes no suggestion in either direction.
+
+```csharp
+// Method delegation — use =>
+public Task GetAsync(string id, CancellationToken cancellationToken = default) => _repository.GetAsync(id, cancellationToken);
+
+// Multi-line single expression — use =>
+public Task> QueryAsync(QueryArgs? query, PagingArgs? paging, CancellationToken cancellationToken = default)
+ => _repository.QueryAsync(query, paging, cancellationToken);
+
+// Constructor with single expression — use =>
+public DataResult(bool wasMutated) => WasMutated = wasMutated;
+
+// Computed property — use =>
+public string DisplayName => $"{First} {Last}";
+
+// Multiple statements — block body required
+public async Task UpdateAsync(Product product, CancellationToken cancellationToken = default)
+{
+ product.ThrowIfNull();
+ await ProductValidator.Default.ValidateAndThrowAsync(product, cancellationToken).ConfigureAwait(false);
+ return await _repository.UpdateAsync(product, cancellationToken).ConfigureAwait(false);
+}
+```
+
+## Line Length and Method Declarations
+
+Keep declarations and statements on a **single line**. Do not wrap a method (or constructor/delegate) declaration across multiple lines by placing each parameter on its own line — keep the signature on one line even when it has several parameters. Only break a line when it would otherwise exceed **250 characters**.
+
+```csharp
+// Correct — single line, even with multiple parameters
+protected override Task OnValidateAsync(ValidationContext context, CancellationToken cancellationToken)
+
+// Incorrect — needlessly split across lines while under 250 characters
+protected override Task OnValidateAsync(
+ ValidationContext context,
+ CancellationToken cancellationToken)
+```
+
+## Ambient Runtime (Clock and GUIDs)
+
+Obtain the current time and new `Guid` values from CoreEx's ambient `Runtime` (`CoreEx` namespace) rather than the BCL statics. `Runtime` is `ExecutionContext`-aware and provider-backed (`TimeProvider` / `IdentifierGenerator`), making values consistent across a request and substitutable in tests.
+
+```csharp
+DateTimeOffset now = Runtime.UtcNow; // DateTimeOffset
+DateTime utc = Runtime.UtcNow.UtcDateTime; // DateTime (when a DateTime is required)
+Guid id = Runtime.NewGuid(); // new Guid
+```
+
+- Need a `DateTimeOffset` → `Runtime.UtcNow` (**never** `DateTimeOffset.UtcNow`).
+- Need a `DateTime` → `Runtime.UtcNow.UtcDateTime` (**never** `DateTime.UtcNow`).
+- Need a `Guid` → `Runtime.NewGuid()` (**never** `Guid.NewGuid()`).
+
+## Cancellation Tokens
+
+**Every `async`/`Task`-returning method takes a `CancellationToken` and passes it on.** Add it as the **last parameter** — `CancellationToken cancellationToken = default` on public/library methods (default so callers aren't forced to supply one). Flow it through to **every** downstream awaitable call (repositories, `HttpClient`, EF Core, `_unitOfWork`, other services) — never call an overload that accepts a token without passing it, and never silently drop it.
+
+```csharp
+public async Task CreateAsync(Employee value, CancellationToken cancellationToken = default)
+{
+ await EmployeeValidator.Default.ValidateAndThrowAsync(value, cancellationToken).ConfigureAwait(false);
+ return await _repository.CreateAsync(value, cancellationToken).ConfigureAwait(false); // pass it on
+}
+```
+
+- **Controllers / Minimal-API handlers** take a `CancellationToken` parameter (ASP.NET binds/injects it), pass it to the `WebApi` helper via `cancellationToken:`, and use the helper lambda's `ct` for the service call — see `coreex-api-controllers.instructions.md`.
+- **Interfaces** declare the parameter too, so implementations and callers can honour it (`Task GetAsync(string id, CancellationToken cancellationToken = default);`).
+- **Tests** are the exception — `Test.Scoped(...)` / the `WebApi` lambda supply the token; you don't manufacture one.
+
+## XML Documentation Comments
+
+Document the public surface with XML doc comments, and **never duplicate** a description that an interface already provides:
+
+- **Interfaces** — give **every** member (method, property, event) a `` describing the operation. Document parameters/returns (``, ``) where it adds clarity.
+- **Contract / DTO properties** — every property on a contract (including `[Contract]` partial classes) gets a ``. (Standard `Id`/`ETag`/`ChangeLog` members that implement `IIdentifier`/`IETag`/`IChangeLog` may use ``.)
+- **Implementing / overriding members** — on the concrete class member that implements an interface member (or overrides a base member), use **``** rather than repeating the summary.
+- **Everything else** — a public type or member that is **not** an interface implementation/override gets its own `` (classes, standalone methods, properties, etc.).
+
+> **Do not invert this** (a common mistake): the `` goes on the **interface** member and on **contract properties**; the concrete class member that *implements* an interface gets **``**, not a fresh summary. Leaving interfaces or contract properties undocumented while summarising the implementing class is **backwards** — fix it the right way round.
+
+```csharp
+public interface IEmployeeService
+{
+ /// Gets the for the specified .
+ Task GetAsync(string id);
+
+ /// Creates a new .
+ Task> CreateAsync(Employee employee);
+}
+
+[ScopedService]
+public class EmployeeService(IUnitOfWork unitOfWork, IEmployeeRepository repository) : IEmployeeService
+{
+ private readonly IUnitOfWork _unitOfWork = unitOfWork.ThrowIfNull();
+ private readonly IEmployeeRepository _repository = repository.ThrowIfNull();
+
+ ///
+ public Task GetAsync(string id, CancellationToken cancellationToken = default) => _repository.GetAsync(id, cancellationToken);
+
+ ///
+ public Task> CreateAsync(Employee employee) => /* ... */;
+}
+```
+
+## Private Field Naming
+
+Private instance fields are always prefixed with `_`. No exceptions.
+
+```csharp
+private readonly IProductRepository _repository;
+private readonly IUnitOfWork _unitOfWork;
+private readonly ILogger _logger;
+```
+
+## Do Not
+
+- Do not emit `#nullable enable` or `#nullable restore` pragma directives in hand-authored files — nullable is enabled project-wide via `enable` in `Directory.Build.props`. These pragmas are reserved for auto-generated `.g.cs` files produced by code generators.
+- Do not add `using` statements to individual `.cs` files — declare all imports in `GlobalUsing.cs`.
+- Do not leave `GlobalUsing.cs` unsorted after editing — re-sort the whole file alphabetically (all namespaces equally, no `System.*` grouping) rather than appending new entries at the end.
+- Do not use block-scoped namespace declarations.
+- Do not add braces to single-line `if` bodies.
+- Do not suppress nullable warnings with `!` without a comment explaining why.
+- Do not name private fields without the `_` prefix.
+- Do not split method/constructor declarations (one parameter per line) or otherwise wrap statements across lines unless the line would exceed 250 characters.
+- Do not use `DateTime.UtcNow` or `DateTimeOffset.UtcNow` — use `Runtime.UtcNow` (or `Runtime.UtcNow.UtcDateTime` for a `DateTime`).
+- Do not use `Guid.NewGuid()` — use `Runtime.NewGuid()`.
+- Do not omit or drop `CancellationToken` — every `async`/`Task`-returning method takes one (`CancellationToken cancellationToken = default`, last parameter) and passes it to every downstream awaitable call.
+- Do not replace a private backing field with an auto-property simply because it could be one — backing fields are a valid developer choice.
+- Do not leave interface members or contract properties undocumented — each gets a ``.
+- Do not invert the doc convention — summaries go on **interfaces and contract properties**; the **implementing** class member gets `` (not a fresh summary). Summarising the concrete class while leaving the interface/contract undocumented is backwards.
diff --git a/.github/instructions/coreex-domain.instructions.md b/.github/instructions/coreex-domain.instructions.md
new file mode 100644
index 00000000..b4a742c2
--- /dev/null
+++ b/.github/instructions/coreex-domain.instructions.md
@@ -0,0 +1,208 @@
+---
+applyTo: "**/Domain/**/*.cs"
+description: "Domain layer conventions: aggregates, entities, value objects, PersistenceState tracking, and mutation methods"
+tags: ["domain", "ddd", "aggregates", "entities", "value-objects", "result"]
+---
+
+# Domain Layer Conventions
+
+The Domain layer is **optional**. It is introduced only when a domain contains aggregates with meaningful business rules and invariants that must be enforced at the model level — not in orchestration code. For example, a checkout/basket domain with state-machine transitions and nested item rules benefits from this layer; a simple CRUD-oriented domain (like a product catalog) typically does not.
+
+## NuGet / Project References
+
+| Package | Key types provided |
+|---|---|
+| `CoreEx.DomainDriven` | `Aggregate`, `Entity`, `PersistenceState`, `.AsNew()`, `.AsNotModified()`, `.SetPersistenceState()` |
+| `CoreEx` | `Result`, `Result`, `Result.GoAsync()`, `.ThenAs()`, `.ThenAsAsync()`, `Result.BusinessError()`, `Result.NotFoundError()`, `Result.ValidationError()`, `Runtime.NewId()`, `.ThrowIfNull()`, `.ThrowIfNullOrEmpty()`, `.ThrowIfInactive()`, `.ThrowIfLessThanZero()`, `ValidationException` |
+
+## Aggregates
+
+Aggregates are clusters of related entities treated as a single consistency boundary. Extend `Aggregate`:
+
+```csharp
+public sealed class Basket : Aggregate
+{
+ private List _items = [];
+
+ // Factory methods are the only public construction paths.
+ public static Basket CreateNew(string customerId) => new Basket(Runtime.NewId())
+ {
+ CustomerId = customerId,
+ Status = BasketStatus.Empty
+ }.AsNew();
+
+ public static Basket CreateFrom(string id, string customerId, BasketStatus status,
+ IEnumerable? items, ChangeLog? changeLog, string? etag) => new Basket(id)
+ {
+ CustomerId = customerId,
+ Status = status,
+ _items = items is null ? [] : [.. items.Select(i => i.Clone(PersistenceState.NotModified))],
+ ChangeLog = changeLog,
+ ETag = etag
+ }.AsNotModified();
+
+ private Basket(string id) : base(id) { }
+
+ public string CustomerId { get; private set => field = value.ThrowIfNullOrEmpty(); } = null!;
+ public BasketStatus Status { get; private set => field = value.ThrowIfNull().ThrowIfInactive(); } = null!;
+ public IReadOnlyList Items => _items;
+ public decimal Total => _items.Where(i => i.PersistenceState.IsNotRemoved).Sum(i => i.Pricing.Total);
+}
+```
+
+### Factory Methods
+
+Provide two factory methods per aggregate:
+
+- `CreateNew(...)` — constructs a new aggregate with a generated ID, initial state, and calls `.AsNew()` to mark it as `PersistenceState.New`.
+- `CreateFrom(...)` — reconstructs from persisted data and calls `.AsNotModified()`.
+
+Both are the **only** public construction paths. The constructor is `private` to prevent partially-constructed instances.
+
+### Mutation Guards — `OnCheckCanMutate`
+
+Override `OnCheckCanMutate()` to enforce the conditions under which the aggregate may accept mutations. Return `Result.BusinessError(...)` (not an exception) when the condition is not met:
+
+```csharp
+protected override Result OnCheckCanMutate() => Status.CanBeMutated
+ ? Result.Success
+ : Result.BusinessError($"Basket has a status of '{Status}' and cannot be modified.",
+ c => c.WithKey(Id).WithErrorCode("invalid-status"));
+```
+
+### Post-Mutation Recalculation — `OnMutate`
+
+Override `OnMutate()` to re-derive any dependent state after a mutation is applied. This is called automatically by `Modify(...)` after the mutation succeeds:
+
+```csharp
+protected override void OnMutate()
+{
+ if (Status.CanBeMutated)
+ Status = _items.Any(i => i.PersistenceState.IsNotRemoved) ? BasketStatus.Active : BasketStatus.Empty;
+}
+```
+
+### Public Mutation Methods
+
+Public mutation methods should return `Result` or `Result` — this is the preferred style because it makes failures explicit and composable in `Result` pipelines in the Application layer. `BusinessException` can be thrown where that feels more natural, but the `Result` return style is recommended for consistency with the aggregate's `OnCheckCanMutate()` pattern. Use `Modify(...)` to apply the mutation, which enforces the `OnCheckCanMutate()` guard:
+
+```csharp
+public Result ItemAdd(BasketItem item) => Modify(() =>
+{
+ item.ThrowIfNull();
+ if (_items.FirstOrDefault(i => i.ProductId == item.ProductId && i.PersistenceState.IsNotRemoved) is BasketItem existing)
+ existing.IncreaseQuantity(item.Pricing.Quantity);
+ else
+ _items.Add(item.Clone(PersistenceState.New));
+
+ return Result.Success;
+});
+
+public Result ItemUpdate(string basketItemId, decimal quantity, string? etag)
+{
+ var item = _items.FirstOrDefault(i => i.Id == basketItemId.ThrowIfNullOrEmpty() && i.PersistenceState.IsNotRemoved);
+ if (item is null)
+ return Result.NotFoundError();
+
+ if (quantity != item.Pricing.Quantity)
+ Modify(() =>
+ {
+ item.OverrideQuantity(quantity);
+ item.SetETag(etag);
+ });
+
+ return Result.Success;
+}
+```
+
+## Entities
+
+Child entities within an aggregate extend `Entity`. Apply the same factory-method and private-constructor pattern:
+
+```csharp
+public sealed class BasketItem : Entity
+{
+ public static BasketItem CreateNew(string productId, string sku, string text, ItemPricing pricing)
+ => new BasketItem(Runtime.NewId()) { ProductId = productId, Sku = sku, Text = text, Pricing = pricing }.AsNew();
+
+ public static BasketItem CreateFrom(string id, string productId, string sku, string text, ItemPricing pricing, string? etag)
+ => new BasketItem(id) { ProductId = productId, Sku = sku, Text = text, Pricing = pricing, ETag = etag }.AsNotModified();
+
+ private BasketItem(string id) : base(id) { }
+
+ public string ProductId { get; private set => field = value.ThrowIfNullOrEmpty(); } = null!;
+ public string Sku { get; private set => field = value.ThrowIfNullOrEmpty(); } = null!;
+ public ItemPricing Pricing { get; private set => field = value.ThrowIfNull().EnsureIsValid(); } = null!;
+
+ // Internal mutation helpers — only callable by the owning aggregate.
+ internal void OverrideQuantity(decimal quantity) => Modify(() => Pricing = Pricing with { Quantity = quantity });
+ internal void Delete() => Remove();
+}
+```
+
+Keep mutation methods on child entities `internal` so they can only be invoked by the owning aggregate — never directly from the Application layer.
+
+Guard methods (`.ThrowIfNull()`, `.ThrowIfNullOrEmpty()`, `.ThrowIfInactive()`, `.ThrowIfLessThanZero()`) **return the guarded value** when the check passes, making them natural for inline use in property setters, `init` expressions, and method calls — as shown throughout the examples above. They also chain: `value.ThrowIfNull().ThrowIfInactive()` checks both conditions and returns the value if both pass.
+
+## PersistenceState
+
+`PersistenceState` tracks the lifecycle of each aggregate and entity so the Infrastructure layer knows exactly what to persist without being told explicitly:
+
+| State | Meaning |
+|---|---|
+| `New` | Newly created; insert on next commit |
+| `NotModified` | Loaded from store; no action required |
+| `Modified` | Changed since load; update on next commit |
+| `Removed` | Marked for deletion; delete on next commit |
+
+Use the helpers on `PersistenceState` for filtering:
+
+```csharp
+_items.Where(i => i.PersistenceState.IsNotRemoved) // active items
+_items.Any(i => i.PersistenceState.IsNewOrModified) // HasChanges check
+```
+
+## Value Objects
+
+Value objects represent concepts with no independent identity — defined entirely by their values. Implement as `sealed record` to get structural equality and `with`-expression mutation for free. Enforce invariants in property initialisers:
+
+```csharp
+public sealed record class ItemPricing
+{
+ public required Contracts.UnitOfMeasure UnitOfMeasure { get; init => field = value.ThrowIfInactive(); }
+ public decimal UnitPrice { get; init => field = value.ThrowIfLessThanZero(); }
+ public decimal Quantity { get; init => field = value.ThrowIfLessThanZero(); }
+ public decimal Total => UnitPrice * Quantity;
+
+ public ItemPricing EnsureIsValid() => DecimalRuleHelper.CheckScale(Quantity, UnitOfMeasure.Scale) ? this
+ : throw new ValidationException($"Quantity decimal places exceed the unit-of-measure scale of {UnitOfMeasure.Scale}.");
+}
+```
+
+Place value objects in a `ValueObjects/` sub-folder within the Domain project.
+
+## When to Introduce the Domain Layer
+
+Only introduce a Domain layer when the domain genuinely has:
+
+- Aggregates with invariants that must be enforced at the model level (e.g., state-machine transitions, child-collection rules).
+- Business rules that depend on the current aggregate state, not on external I/O.
+- The need to protect consistency boundaries across multiple child entities.
+
+For CRUD-oriented domains, skip the Domain layer entirely and let the Application service orchestrate directly against repository interfaces.
+
+## Do Not
+
+- Do not perform async I/O (repository calls, HTTP requests) inside domain classes — async work belongs in Application services or Policies.
+- Do not expose child entity mutation methods as `public` — use `internal` so only the owning aggregate can drive mutations.
+- Prefer returning `Result.BusinessError(...)` or `Result.NotFoundError()` over throwing exceptions for expected business failures in domain methods — this keeps failures explicit and composable. Throwing `BusinessException` is acceptable where it feels more natural, but the `Result` style is recommended for consistency.
+- Do not reference Infrastructure, Application, or host assemblies from the Domain layer — it depends only on Contracts and CoreEx.
+- Do not model value objects as classes with mutable properties — use `sealed record` with `init` setters and invariant enforcement at construction.
+
+## Further Reading
+
+- [Domain Layer Guide](https://github.com/Avanade/CoreEx/blob/main/samples/docs/domain-layer.md) — aggregates, entities, value objects, and `PersistenceState` walkthrough.
+- [Pattern Catalog](https://github.com/Avanade/CoreEx/blob/main/samples/docs/patterns.md) — Aggregate, Entity, and Value Object pattern entries with cross-links.
+- [Layer Dependencies](https://github.com/Avanade/CoreEx/blob/main/samples/docs/layers.md) — when to introduce the Domain layer and its position in the dependency graph.
+- [CoreEx.DomainDriven README](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.DomainDriven/README.md) — `Aggregate`, `Entity`, and `PersistenceState`.
+- [CoreEx Results README](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx/Results/README.md) — `Result` type, pipeline operators (`.GoAsync`, `.ThenAs`, `.ThenAsAsync`), and error propagation semantics.
diff --git a/.github/instructions/coreex-event-subscribers.instructions.md b/.github/instructions/coreex-event-subscribers.instructions.md
new file mode 100644
index 00000000..63637f9a
--- /dev/null
+++ b/.github/instructions/coreex-event-subscribers.instructions.md
@@ -0,0 +1,255 @@
+---
+applyTo: "**/Subscribe/**/*.cs"
+description: "Event subscriber conventions: SubscribedBase, SubscribedBase, ValueValidator, ErrorHandler, subject naming, and Subscribe host Program.cs composition"
+tags: ["subscribers", "messaging", "service-bus", "event-handling", "integration", "subscribe-host"]
+---
+
+# Event Subscriber Conventions
+
+## NuGet / Project References
+
+| Package | Key types provided |
+|---|---|
+| `CoreEx.Events` | `SubscribedBase`, `SubscribedBase`, `[Subscribe(...)]`, `EventSubscriberArgs`, `ErrorHandler`, `ErrorHandling`, `EventData`, `.Key` |
+| `CoreEx.Azure.Messaging.ServiceBus` | `ServiceBusSessionReceiverOptions`, `.AzureServiceBusReceiving()`, `.WithSessionReceiver()`, `.WithSubscribedSubscriber()`, `.WithHostedService()` |
+| `CoreEx` | `[ScopedService]`, `.ThrowIfNull()`, `.Required()`, `Result`, `Result.Success`, `IValidator` |
+
+## Subscriber Structure
+
+Each subscriber is a small, focused class that:
+
+1. Opts in to one or more message subjects via `[Subscribe("subject")]` attributes.
+2. Extends `SubscribedBase` (untyped) or `SubscribedBase` (typed payload with optional validation).
+3. Delegates immediately to an Application-layer service or adapter — no business logic in the subscriber.
+4. Returns `Result` or `Result` so that error handling and dead-lettering decisions can be expressed declaratively.
+
+All subscribers are decorated with `[ScopedService]` for automatic DI discovery. Dependencies are injected via primary constructor and guarded with `.ThrowIfNull()`.
+
+### Untyped subscriber — `SubscribedBase`
+
+Use when the relevant data is carried in the message key rather than a typed payload. Extract the key with `.Required()`, which throws a `ValidationException` if the key is absent:
+
+```csharp
+[ScopedService, Subscribe("contoso.products.product.deleted")]
+public class ProductDeleteSubscriber(IProductSyncAdapter adapter) : SubscribedBase
+{
+ private readonly IProductSyncAdapter _adapter = adapter.ThrowIfNull();
+
+ protected override Task OnReceiveAsync(
+ EventData @event, EventSubscriberArgs args, CancellationToken cancellationToken = default)
+ => _adapter.DeleteAsync(@event.Key.Required(), cancellationToken);
+}
+```
+
+For custom exception handling (e.g. converting a specific `NotFoundException` to a silent completion rather than dead-lettering), set `ErrorHandler` in the constructor — see [Error Handling](#error-handling) below.
+
+### Typed subscriber — `SubscribedBase`
+
+Use when the message carries a typed payload that should be deserialized and optionally validated before `OnReceiveAsync` is called. Wire a `ValueValidator` to validate the deserialized value:
+
+```csharp
+[ScopedService]
+[Subscribe("contoso.products.product.created.v1")]
+[Subscribe("contoso.products.product.updated.v1")]
+public class ProductModifySubscriber(IProductSyncAdapter adapter) : SubscribedBase
+{
+ private readonly IProductSyncAdapter _adapter = adapter.ThrowIfNull();
+
+ public override IValidator? ValueValidator => ProductValidator.Default;
+
+ protected override Task OnReceiveAsync(
+ Product value, EventData @event, EventSubscriberArgs args, CancellationToken cancellationToken = default)
+ => _adapter.ModifyAsync(value, cancellationToken);
+}
+```
+
+Multiple `[Subscribe]` attributes on a single class handle multiple subjects with the same logic — no duplication required.
+
+## Subject Naming
+
+Use dot-separated lowercase subject strings:
+
+```
+{solution}.{domain}.{entity}.{action}[.v{n}]
+```
+
+The version suffix `[.v{n}]` is driven by whether the message carries a **payload** (a CloudEvent data element), not by whether it is an integration event or a command message:
+
+- **With payload** → include a version suffix. The payload has a schema that can evolve; consumers need to know which version to deserialise: `contoso.products.product.created.v1`
+- **Without payload (key-only)** → no version suffix. There is no data schema to version: `contoso.products.reservation.confirm`
+
+Command messages follow the same rule — a key-only command carries no version; a command that includes a payload does.
+
+Examples:
+- `contoso.products.product.created.v1` — has payload → versioned
+- `contoso.products.product.updated.v1` — has payload → versioned
+- `contoso.products.product.deleted` — key-only (no payload) → no version
+- `contoso.products.reservation.confirm` — key-only (no payload) → no version
+- `contoso.products.reservation.cancel` — key-only (no payload) → no version
+
+> **Note:** CoreEx supports integration events only — domain events (aggregate-internal) are not provided out of the box.
+
+### Publishing
+
+This same subject convention governs publishing. The `EventFormatter` in `CoreEx.Events` derives the subject automatically from the entity type and action when `EventData.CreateEventWith(value, action)` is called. Publishing is an **application service concern** — the service adds events to the unit of work inside `_unitOfWork.TransactionAsync(...)`, and they are committed atomically with the database write:
+
+```csharp
+_unitOfWork.Events.Add(EventData.CreateEventWith(product, EventAction.Created));
+```
+
+The **outbox** is purely a transactional relay mechanism — it durably captures the events inside the same database transaction, then an Outbox Relay host forwards them to the broker asynchronously. The application service is unaware of the broker; it only adds events to the unit of work.
+
+## Error Handling
+
+Define a static `ErrorHandler` to control how specific exceptions are treated — for example, converting a known `NotFoundException` to an informational completion rather than dead-lettering:
+
+```csharp
+internal static readonly ErrorHandler DefaultErrorHandler = new ErrorHandler()
+ .Add(ex => ex.ErrorCode == "pending-reservation-not-found"
+ ? ErrorHandling.CompleteAsInformation // consume silently; log as informational
+ : null); // null = fall through to default handling (retry / dead-letter)
+```
+
+Assign it in the constructor: `ErrorHandler = DefaultErrorHandler;`
+
+Share the same `ErrorHandler` instance across related subscribers (e.g., both Confirm and Cancel subscribers can reference the same static instance).
+
+## Accessing Event Data
+
+```csharp
+var key = @event.Key.Required(); // Returns the key, or throws ValidationException if null/default
+```
+
+In typed subscribers (`SubscribedBase`), the deserialized value is passed directly as the first parameter to `OnReceiveAsync` — no manual deserialization needed. That is the preferred approach whenever a payload is expected.
+
+For the rare case where an untyped subscriber (`SubscribedBase`) needs to deserialize the payload, use the protected `DeserializeValue` helper inherited from the base class:
+
+```csharp
+var result = DeserializeValue(@event, args, valueIsRequired: true);
+if (result.IsFailure) return result.AsResult();
+var value = result.Value;
+```
+
+## Program.cs Composition
+
+The Subscribe host `Program.cs` follows a predictable CoreEx shape. Key sections in order:
+
+```csharp
+// 1. Execution context and dynamic service discovery
+builder.Services
+ .AddExecutionContext()
+ .AddReferenceDataOrchestrator() // non-generic — binds the IReferenceDataProvider from DI at runtime
+ .AddMvcWebApi()
+ .AddHttpWebApi()
+ .AddHostedServiceManager();
+
+builder.Services.AddDynamicServicesUsing(typeof(Program).Assembly, typeof(MyApp.Application.AssemblyMarker).Assembly, typeof(MyApp.Infrastructure.AssemblyMarker).Assembly);
+
+// 2. Caching — L1 memory cache + L2 Redis + FusionCache hybrid + idempotency provider
+builder.Services.AddMemoryCache();
+builder.AddRedisDistributedCache("redis");
+builder.Services.AddFusionCache()
+ .WithRegisteredMemoryCache()
+ .WithRegisteredDistributedCache()
+ .WithBackplane(sp => new RedisBackplane(new RedisBackplaneOptions { Configuration = ... }))
+ .WithSystemTextJsonSerializer(JsonDefaults.SerializerOptions);
+builder.Services
+ .AddFusionHybridCache()
+ .AddDefaultCacheKeyProvider()
+ .AddHybridCacheIdempotencyProvider();
+
+// 3. Infrastructure — database, EF, outbox publisher (for transactional writes inside subscribers)
+// SQL Server variant:
+builder.AddSqlServerClient("SqlServer");
+builder.Services
+ .AddSqlServerDatabase()
+ .AddSqlServerUnitOfWork()
+ .AddSqlServerOutboxPublisher() // outbox publisher becomes the default IEventPublisher
+ .AddDbContext()
+ .AddEfDb();
+
+// PostgreSQL variant (use instead):
+// builder.AddAzureNpgsqlDataSource("Postgres");
+// builder.Services
+// .AddPostgresDatabase()
+// .AddPostgresUnitOfWork()
+// .AddEventFormatter()
+// .AddPostgresOutboxPublisher()
+// .AddDbContext()
+// .AddEfDb();
+
+// 4. Azure Service Bus publisher — direct publish capability (not the default IEventPublisher)
+builder.AddAzureServiceBusClient("ServiceBus");
+builder.Services.AddAzureServiceBusPublisher((_, c) =>
+{
+ c.SessionIdStrategy = ServiceBusSessionStrategy.UsePartitionKeyConvertedToAnId;
+}, addAsDefaultIEventPublisher: false); // false because outbox publisher is already the default
+
+// 5. Event formatter + subscriber manager
+builder.Services
+ .AddEventFormatter()
+ .AddSubscribedManager((_, c) => c.AddSubscribersUsing());
+
+// 6. Azure Service Bus receiver wiring
+builder.Services.AzureServiceBusReceiving()
+ .WithSessionReceiver(_ =>
+ {
+ var o = ServiceBusSessionReceiverOptions.CreateForTopicSubscription();
+ o.SessionProcessorOptions.MaxConcurrentSessions = 4;
+ return o;
+ })
+ .WithSubscribedSubscriber() // routes received messages through the SubscribedManager
+ .WithHostedService() // runs the receiver as a BackgroundService
+ .Build();
+
+// 7. External API clients (if needed — for domains with inter-domain HTTP calls)
+builder.AddTypedHttpClient("ProductsApi");
+
+// 8. Health checks, OpenTelemetry
+builder.Services.PostConfigureAllHealthChecks();
+builder.Services.AddControllers();
+builder.Services.AddOpenApiDocument(s =>
+{
+ s.Title = builder.Environment.ApplicationName;
+ s.AddCoreExConfiguration();
+});
+
+builder.WithCoreExTelemetry()
+ .WithCoreExServiceBusTelemetry()
+ .WithCoreExSqlServerTelemetry() // or .WithCoreExPostgresTelemetry() for PostgreSQL
+ .UseOtlpExporter();
+
+// 9. Build and middleware pipeline
+var app = builder.Build();
+
+app.UseCoreExExceptionHandler();
+app.UseHttpsRedirection();
+app.UseAuthorization();
+app.UseExecutionContext();
+app.MapControllers();
+
+app.UseOpenApi();
+app.UseSwaggerUi();
+app.MapHealthChecks();
+app.MapHostedServices(); // exposes pause/resume management endpoints per partition
+
+app.Run();
+```
+
+`AddSubscribersUsing()` scans the assembly containing `T` and auto-registers every `[Subscribe]`-decorated class — adding a new subscriber requires only creating the class, no `Program.cs` edits needed.
+
+`MapHostedServices()` exposes runtime management endpoints to **pause and resume** the receiver per partition without restarting the process.
+
+## Do Not
+
+- Do not embed business logic in subscriber classes — delegate immediately to an Application-layer service or adapter.
+- Do not use MediatR or in-process event dispatchers — subscribers react to integration events from the broker only.
+- Do not manually register subscriber classes in DI — `AddSubscribersUsing()` discovers them automatically via `[ScopedService]`.
+- Do not omit `AddEventFormatter()` from `Program.cs` — it is required for message parsing and deserialization.
+- Do not set `addAsDefaultIEventPublisher: true` for the Service Bus publisher when the outbox publisher is the intended default `IEventPublisher`.
+
+## Further Reading
+
+- [Hosts Layer Guide — Subscribe Host](https://github.com/Avanade/CoreEx/blob/main/samples/docs/hosts-layer.md) — Subscribe host architecture, Program.cs shape, and subscriber patterns.
+- [Pattern Catalog](https://github.com/Avanade/CoreEx/blob/main/samples/docs/patterns.md) — Subscribe, Publish, Transactional Outbox, and Event-Driven Replication pattern entries.
+- [CoreEx.Azure.Messaging.ServiceBus README](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.Azure.Messaging.ServiceBus/README.md) — `SubscribedBase`, `ErrorHandler`, and Service Bus receiver configuration.
diff --git a/.github/instructions/coreex-host-setup.instructions.md b/.github/instructions/coreex-host-setup.instructions.md
new file mode 100644
index 00000000..898181d0
--- /dev/null
+++ b/.github/instructions/coreex-host-setup.instructions.md
@@ -0,0 +1,424 @@
+---
+applyTo: "**/Program.cs"
+description: "Host setup conventions for Program.cs: API host, Subscribe host, Outbox Relay host, middleware, service registration, and distributed caching"
+tags: ["program-cs", "host-setup", "middleware", "dependency-registration", "caching"]
+---
+
+# Host Setup Conventions (Program.cs)
+
+The host is a **composition root only** — no business logic. There are three host types in a CoreEx solution depending on the capabilities required. Each follows the same opening skeleton, then diverges based on its responsibilities.
+
+> **Further Reading**: [Hosts Layer Guide](https://github.com/Avanade/CoreEx/blob/main/samples/docs/hosts-layer.md) · [Layer Dependencies](https://github.com/Avanade/CoreEx/blob/main/samples/docs/layers.md) · [Pattern Catalog](https://github.com/Avanade/CoreEx/blob/main/samples/docs/patterns.md)
+
+---
+
+## Scaffolding an API host
+
+An API host is **not** part of the base `coreex` solution — it is added on demand when the user asks to expose functionality over HTTP (e.g. *"create a CRUD API for Employee"*, or *"create the Employee service **and** an API"*). Creating it is an **explicit-ask action** — confirm before scaffolding (per the always-on "Do Not Create Projects" rule); never auto-create it to satisfy a feature request.
+
+> **Agent instruction:** When an API is requested and no Api host exists:
+> 1. **Detect** the host: look for `**/*.Api/*.Api.csproj`. The file system is authoritative for project existence (unlike database state) — no further checking is needed.
+> 2. **If present**, skip to authoring controllers (see [coreex-api-controllers](./coreex-api-controllers.instructions.md)).
+> 3. **If absent, confirm creation** with the user — default the name to `{Solution}.Api` and the physical location to `src/` (the template default). Do not create it without confirmation.
+> 4. **Recover the original selections** from the solution-root `AGENTS.md` "Feature Configuration" (cross-check `dbex.yaml`). These default the `coreex-api` template — which takes **`data-provider`, `refdata-enabled`, `outbox-enabled`** (a subset of the solution options). Re-state the resolved values for confirmation rather than re-prompting; if `AGENTS.md` and `dbex.yaml` disagree, **stop and flag** rather than guessing.
+> 5. **Scaffold** with the recovered values, naming consistently with the solution so the derived `domain-name`/`solution-name` tokens align with the existing projects:
+> ```
+> dotnet new coreex-api -n {Solution}.Api --data-provider --refdata-enabled --outbox-enabled
+> ```
+> **Run it from the solution root** — the directory that already contains `src/` and `tests/`. The template is rooted at `src/...` and `tests/...` (and uses `preferNameDirectory: false`, so it does **not** create a name-based subfolder); it merges its `src/`/`tests/` into the existing ones. Running it from inside `src/` produces nested `src/src/...` paths. If that happens, **delete the misplaced output and re-run from the solution root** — do **not** hand-move the generated files to "fix" the layout. Summarise the output on success; relay it **verbatim** on failure.
+> 6. **Author the controller(s)** for the requested entity/operations per [coreex-api-controllers](./coreex-api-controllers.instructions.md), exposing only the confirmed operations.
+> 7. **Verify by building the project directly — not the solution.** The host is not yet in the `.slnx` (that is step 9), so a *solution-wide* build would silently **skip** it and give false confidence. Build and test the new project(s) **by path** — this compiles them and their referenced projects regardless of solution membership:
+> ```
+> dotnet build .csproj
+> dotnet test .csproj
+> ```
+> Fix any errors here, in-session, before handing off — compilation does **not** require the project to be in the solution.
+> 8. **Update the recording:** amend the solution-root `AGENTS.md` — add the Api host to the *Project Structure* block and note it under hosts. (The feature selections themselves do not change.)
+> 9. **Add the new project(s) to the solution — final step, in-session.** Once the changes are verified (step 7), wire the projects into the `.slnx` from the solution root, batched by target folder, using the **actual generated** paths:
+> ```
+> dotnet sln .slnx add .csproj --solution-folder hosts
+> dotnet sln .slnx add .csproj --solution-folder tests
+> ```
+> Always via `dotnet sln add` — **never hand-edit the `.slnx` XML** (manual edits are error-prone and have wiped solution files). This is the **last** action, after the code is verified and `AGENTS.md` is updated, so it cannot interrupt pending work. A final `dotnet build .slnx` confirms the wiring.
+> **Exception — Visual Studio with the solution open:** writing the `.slnx` triggers an IDE reload that can interrupt and discard pending changes. *Only* in that case, defer these commands to an end-of-task **Manual steps** list for the user to run instead. The default environment (Claude Code / Copilot / CLI) runs them in-session.
+
+The scaffolded `Program.cs` wiring is described below; it is generated **complete and ready-to-compile** via the option-driven `#if` blocks — there is **no post-CodeGen uncomment / "phase 2" step**.
+
+> **Reference-data wiring is automatic — nothing to uncomment.** The host wires reference data and dynamic services **without referencing any CodeGen-generated type**, so it compiles at scaffold time *and* is correct once CodeGen runs:
+> ```csharp
+> // refdata-enabled hosts only (template #if):
+> builder.Services.AddReferenceDataOrchestrator(); // non-generic — resolves the IReferenceDataProvider from DI at runtime
+> // all hosts — scans the assemblies (via the stable AssemblyMarker types) for [ScopedService] types, registering them all (incl. CodeGen-generated):
+> builder.Services.AddDynamicServicesUsing(typeof({Solution}.Application.AssemblyMarker).Assembly, typeof({Solution}.Infrastructure.AssemblyMarker).Assembly);
+> ```
+> The generated `ReferenceDataService` is `[ScopedService]`-decorated, so the assembly scan picks it up automatically and the orchestrator binds to it via `IReferenceDataProvider` — no manual step. Before CodeGen there are simply no ref-data entities to serve (requests return empty), which is correct. **Do not** add `AddReferenceDataOrchestrator()` or `AddDynamicServicesUsing<…generated types…>()` — that reintroduces the compile-time dependency the marker approach removes.
+
+---
+
+## Scaffolding a Subscribe host
+
+A Subscribe host is **not** part of the base `coreex` solution — it is added on demand when the user **explicitly** asks to *"add/create the subscribe host"*, *"consume events"*, or similar. Creating it is an **explicit-ask action** — confirm before scaffolding (per the always-on "Do Not Create Projects" rule); never auto-create it.
+
+Like the Relay host, the Subscribe host is **fully template-generated**: the `coreex-subscribe` template emits a complete, compilable `Program.cs` and test project with no Phase-2 / uncomment step. Unlike the Relay, the Subscribe host **does** have follow-on authoring work — subscribers are added to it over time as new event types are consumed.
+
+> **Critical naming rule — always pass `-n` with the full host-suffixed name:**
+> The `coreex-subscribe` template's `sourceName` is `app-name.Subscribe` — the `.Subscribe` suffix is **part of the template source token**, not appended automatically. The `-n` value replaces that entire token, so you **must** include the suffix:
+> ```
+> dotnet new coreex-subscribe -n {Solution}.Subscribe ... ✓ correct
+> dotnet new coreex-subscribe -n {Solution} ... ✗ wrong — project loses the .Subscribe suffix
+> and derived solution-name / domain-name tokens
+> resolve incorrectly, breaking cross-project references
+> ```
+> Omitting `-n` entirely is equally wrong — `dotnet new` falls back to the directory name (typically the solution root, e.g. `Foo.Bar`), which has the same effect as passing the bare solution name.
+
+> **Agent instruction:** When the user asks to add/create the Subscribe host:
+> 1. **Detect** it: look for `**/*.Subscribe/*.Subscribe.csproj`. The file system is authoritative (unlike database state) — no further checking is needed.
+> 2. **If present**, skip to authoring subscribers (see [coreex-event-subscribers](./coreex-event-subscribers.instructions.md)).
+> 3. **If absent, confirm creation** with the user — default the name to `{Solution}.Subscribe` and the physical location to `src/` (the template default). Do not create it without confirmation.
+> 4. **Recover the original selections** from the solution-root `AGENTS.md` "Feature Configuration" (cross-check `dbex.yaml`). The `coreex-subscribe` template takes **`data-provider`**, **`messaging-provider`**, and **`refdata-enabled`** — default all three from the recorded `coreex` selections. Re-state the resolved values for confirmation rather than re-prompting; if `AGENTS.md` and `dbex.yaml` disagree, **stop and flag** rather than guessing.
+> 5. **Scaffold** with the recovered values, naming consistently with the solution so the derived `domain-name`/`solution-name` tokens align with the existing projects:
+> ```
+> dotnet new coreex-subscribe -n {Solution}.Subscribe --data-provider --messaging-provider --refdata-enabled
+> ```
+> **Run it from the solution root** — the directory that already contains `src/` and `tests/`. The template is rooted at `src/...`/`tests/...` (and uses `preferNameDirectory: false`, so it merges into the existing folders). Running it from inside `src/` produces nested `src/src/...` paths; if that happens, **delete the misplaced output and re-run from the solution root** — do **not** hand-move the files. Summarise the output on success; relay it **verbatim** on failure.
+> 6. **Author the subscriber(s)** for any event types the user has asked to consume, per [coreex-event-subscribers](./coreex-event-subscribers.instructions.md).
+> 7. **Verify by building the project directly — not the solution.** The host is not yet in the `.slnx` (it is added in step 9), so a *solution-wide* build would silently skip it:
+> ```
+> dotnet build