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/.editorconfig b/.editorconfig
index 6ed50a36..23a4e765 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,13 +1,196 @@
+root = true
+
+# ── Universal ────────────────────────────────────────────────────────────────
+
[*]
-indent_style = space
-indent_size = 4
+charset = utf-8
+indent_style = space
+indent_size = 4
trim_trailing_whitespace = true
-insert_final_newline = false
+insert_final_newline = false
+
+# ── C# source files ──────────────────────────────────────────────────────────
[*.cs]
indent_style = space
-indent_size = 4
+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
-[*.{json,jsn,xml,yaml,yml,props,csproj,sln,sql}]
+# 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
\ No newline at end of file
+indent_size = 2
+
+# ── Markdown ──────────────────────────────────────────────────────────────────
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.github/INSTRUCTION_AUTHORING.md b/.github/INSTRUCTION_AUTHORING.md
index bfb1a8c9..187eafe4 100644
--- a/.github/INSTRUCTION_AUTHORING.md
+++ b/.github/INSTRUCTION_AUTHORING.md
@@ -1,133 +1,246 @@
---
-description: "Standards for creating and maintaining .instructions.md files"
applyTo: "**/*.instructions.md"
+description: "Standards for authoring and maintaining Copilot instruction files in this repository"
tags: ["authoring", "standards", "instructions", "documentation"]
---
# Instruction File Authoring Standards
-When creating or updating any Copilot instruction Markdown file (`.instructions.md`), follow these rules to keep guidance durable, easy to review, and maintainable.
+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:
-## Purpose
+- **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.
-Instruction files define scoped AI guidance for specific file types or code areas. They must be predictable and machine-readable.
+Write instruction files as you would a concise internal coding guide for a capable new team member, not as a policy document.
-## General Authoring Rules
+---
-- Prefer precise, testable directives over vague guidance.
-- Avoid overlapping or conflicting instructions across files.
-- Keep content reusable and not tied to one temporary task.
-- Use imperative language ("Use", "Prefer", "Do not", "Validate").
-- If a rule is scoped to a subset of files, use a path-specific `.instructions.md` file rather than adding it to the global file.
-- Do not restate general rules in multiple files unless required for clarity.
-- When unsure, produce fewer, clearer rules.
+## Principles
-## Required Format
+- **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.
+
+---
-All `.instructions.md` files must begin with YAML frontmatter and follow this section order:
+## Required Format
-```yaml
+```markdown
---
-description: "Short, concrete summary of what the file governs"
-applyTo: "src/**/*.cs"
+applyTo: ""
+description: ""
tags: ["tag1", "tag2"]
---
-# Purpose
+# Conventions
-[One paragraph explaining why this guidance exists]
+## NuGet / Project References
-## Scope
+| Package | Key types provided |
+|---|---|
+| `CoreEx.X` | `TypeA`, `TypeB`, `TypeC` |
-[What files and scenarios this applies to]
+##
-## Required Rules
+
-[MUST / MUST NOT directives, phrased imperatively]
+```csharp
+// real example matching what the codebase actually looks like
+```
-## Preferred Patterns
+##
-[SHOULD directives, repeated patterns, best practices]
+...
-## Validation
+## Do Not
-[Concrete checks: build, test, lint, docs validation]
+- Do not use `SomeWrongType` — use `CorrectType` instead.
+- Do not call `SomeAntiPattern()` directly; delegate to the application service.
-## Examples
+## Further Reading
-[Optional: "Preferred" and "Avoid" patterns]
+- [`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
-- `description` must be one sentence and concrete.
-- `applyTo` must use explicit glob patterns with the narrowest safe scope. Examples:
- - `src/**/*.cs` — all C# files in source
- - `tests/**/*.cs` — all test files
- - `**/Program.cs` — program entry points
-- `applyTo` must **not** be `**` unless the file is intentionally repository-wide.
-- If multiple globs are needed, keep them explicit and readable, separated by semicolons or as separate lines.
-- `tags` should reflect the instruction's domain (e.g., `["validation", "dependency-injection", "testing"]`).
+- 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.
-## Content Rules
+```markdown
+## Further Reading
-- **Required rules** must be phrased as MUST / MUST NOT / SHOULD where possible.
-- **Validation section** must include concrete checks (build, test, lint, docs validation) when applicable.
-- **Examples** must show "preferred" and "avoid" patterns when useful.
-- Do not include secrets, tokens, or environment-specific sensitive values.
-- Keep sections short and scannable; each section should fit on one screen without scrolling.
+- [`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
-When multiple instruction files might apply to the same file:
+- `.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 |
-- Repository-wide instructions define defaults.
-- Path-specific instruction files define narrower, stronger rules for matching files.
-- If a new file would conflict with an existing instruction file, revise the narrow file instead of creating duplicate policy.
-- Always document the relationship between overlapping instruction files in cross-references.
+### CoreEx.CodeGen (`*.CodeGen` project)
-## Cross-Referencing
+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.
-Link between instruction files using relative paths or workspace absolute paths:
+| 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` |
-- Relative: `../other-instruction.md`
-- Absolute: `/.github/instructions/host-setup.instructions.md`
-- Always verify links work before committing.
+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.
-## Example Structure
+### DbEx (`*.Database` project)
-A well-formed instruction file:
+A development-time migration and generation tool. Reads YAML configuration and SQL migration scripts to produce database schema, outbox infrastructure, and EF Core scaffolding.
-```yaml
+| 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
---
-description: "Controller conventions for CoreEx API hosts"
applyTo: "**/Controllers/**/*.cs"
-tags: ["controllers", "api", "dependency-injection"]
+description: "API controller conventions for CoreEx: inheritance, routing, WebApi helper usage, and CQRS separation"
+tags: ["controllers", "api", "routing", "dependency-injection"]
---
-# Purpose
+# API Controller Conventions
+
+## NuGet / Project References
+
+| Package | Key types provided |
+|---|---|
+| `CoreEx.AspNetCore` | `WebApi`, `[IdempotencyKey]`, `[ProducesNotFoundProblem]`, `[Query]`, `[Paging]` |
+| `CoreEx.AspNetCore.NSwag` | `[OpenApiTag]` |
-Controllers define HTTP endpoints for CoreEx API hosts. This guidance ensures consistent routing, dependency injection, and use of CoreEx WebApi helpers.
+## Structure
-## Scope
+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.
-Applies to all `Controllers/` directories in API projects (`.Api` projects).
+```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
-## Required Rules
+Return `Task` via the `WebApi` helper. Do not return typed `ActionResult` directly.
-- MUST inherit from `WebApiControllerBase` or `WebApiControllerBase`.
-- MUST use `[Route("api/v1/...")]` and follow REST conventions.
-- MUST NOT inject `IUnitOfWork` directly; receive it only through application service dependency.
+```csharp
+[HttpPost, IdempotencyKey]
+public Task CreateAsync([FromBody] Product product) =>
+ _webApi.PostAsync(Request, () => _service.CreateAsync(product), statusCode: HttpStatusCode.Created);
+```
-## Preferred Patterns
+## Do Not
-- Prefer `PostAsync()`, `PutAsync()`, `PatchAsync()` from `WebApi` helpers over manual response building.
-- Prefer explicit dependency injection over service locator patterns.
-- Prefer PATCH with `application/merge-patch+json` for partial updates.
+- 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.
-## Validation
+## Further Reading
-- Build the project: `dotnet build`
-- Run tests: `dotnet test`
-- Check inheritance with: `grep "class.*Controller" Controllers/*.cs`
+- [`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/README.md b/.github/README.md
new file mode 100644
index 00000000..440ae17f
--- /dev/null
+++ b/.github/README.md
@@ -0,0 +1,59 @@
+# 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. |
+| Domain templates | `templates/domain/` | 77 ready-made source-file templates covering all domain layers and both database engines. See the [templates README](./templates/domain/README.md). |
+| 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 and Skills
+
+| Command | Type | What it does |
+|---------|------|-------------|
+| [`/scaffold-domain-from-templates`](./templates/domain/README.md) | Prompt | Fast, deterministic domain scaffolding — clones the canonical templates with placeholder substitution. Use when your entity fits the standard shape and you want exact output. |
+| [`/generate-domain`](./skills/generate-domain/README.md) | Skill | Guided, reasoning-based domain scaffolding — use when your entity has custom fields, business rules, or you want the agent to apply conventions for you. |
+| [`/add-capability`](./skills/add-capability/README.md) | Skill | Retrofits an existing domain with messaging/integration capabilities (Outbox Relay, Subscribe, Service Bus wiring). |
+| [`/acquire-codebase-knowledge`](./skills/acquire-codebase-knowledge/README.md) | Skill | Maps an unfamiliar codebase and produces seven structured onboarding documents. |
+| [`/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. |
+| `/init` | Prompt | Initializes a new CoreEx solution or workspace. |
+| `/setup` | Prompt | Configures an existing CoreEx solution with standard tooling and settings. |
+
+## Domain templates
+
+See [templates/domain/README.md](./templates/domain/README.md) for the full option set, directory layout, and invocation instructions for both Claude Code and GitHub Copilot.
diff --git a/.github/SKILL_AUTHORING.md b/.github/SKILL_AUTHORING.md
index 9a30c34d..27fd041c 100644
--- a/.github/SKILL_AUTHORING.md
+++ b/.github/SKILL_AUTHORING.md
@@ -68,7 +68,7 @@ Reusable templates and examples:
When skills reference each other, instructions, or samples:
- **Relative paths**: `../other-skill/references/...` (for other skills)
-- **Absolute workspace paths**: `/.github/instructions/host-setup.instructions.md`, `/samples/src/Contoso.Products.Api/Program.cs`
+- **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
@@ -132,9 +132,9 @@ For detailed step-by-step guidance, see [`references/workflow.md`](references/wo
## Key References
-- [Application Services Instructions](/.github/instructions/application-services.instructions.md)
-- [Contracts Instructions](/.github/instructions/contracts.instructions.md)
-- [Host Setup Instructions](/.github/instructions/host-setup.instructions.md)
+- [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)
- [Sample Domains](./samples/src/Contoso.Products/)
- [Roslyn Source Generation](./docs/capabilities.md)
```
diff --git a/.github/agents/README.md b/.github/agents/README.md
new file mode 100644
index 00000000..b8bd1d1c
--- /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
+ generate-domain/ # optional — new domain scaffolding
+ add-capability/ # optional — retrofit messaging/integration
+ 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.
diff --git a/.github/agents/coreex-expert.agent.md b/.github/agents/coreex-expert.agent.md
index ab7c7d82..7dbdeeae 100644
--- a/.github/agents/coreex-expert.agent.md
+++ b/.github/agents/coreex-expert.agent.md
@@ -1,47 +1,120 @@
---
name: CoreEx Expert
-description: "Use when you need to explain, understand, or decide how CoreEx works. 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: [vscode/getProjectSetupInfo, vscode/installExtension, vscode/memory, vscode/newWorkspace, vscode/resolveMemoryFileUri, vscode/runCommand, vscode/vscodeAPI, vscode/extensions, vscode/askQuestions, execute/runNotebookCell, execute/getTerminalOutput, execute/killTerminal, execute/sendToTerminal, execute/createAndRunTask, execute/runInTerminal, execute/runTests, read/getNotebookSummary, read/problems, read/readFile, read/viewImage, read/terminalSelection, read/terminalLastCommand, agent/runSubagent, edit/createDirectory, edit/createFile, edit/createJupyterNotebook, edit/editFiles, edit/editNotebook, edit/rename, search/changes, search/codebase, search/fileSearch, search/listDirectory, search/textSearch, search/usages, web/fetch, web/githubRepo, web/githubTextSearch, browser/openBrowserPage, browser/readPage, browser/screenshotPage, browser/navigatePage, browser/clickElement, browser/dragElement, browser/hoverElement, browser/typeInPage, browser/runPlaywrightCode, browser/handleDialog, todo]
+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 for this repository.
+You are the CoreEx Expert.
Your mission:
-- Provide authoritative, repo-grounded guidance on CoreEx architecture, patterns, and practices.
+- Provide authoritative guidance on CoreEx architecture, patterns, and practices.
- Prefer CoreEx-native primitives and conventions over generic .NET advice.
-- Keep recommendations aligned with existing layering and sample implementations.
-
-Primary sources of truth:
-- .github/copilot-instructions.md
-- docs/agent-interaction-guide.md
-- docs/agent-prompt-recipes.md
-- .github/instructions/api-controllers.instructions.md
-- .github/instructions/application-services.instructions.md
-- .github/instructions/contracts.instructions.md
-- .github/instructions/repositories.instructions.md
-- .github/instructions/event-subscribers.instructions.md
-- .github/instructions/host-setup.instructions.md
-- .github/instructions/tests.instructions.md
-- .github/instructions/validators.instructions.md
-
-Operating rules:
+- 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 Outbox.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`.
+ - `coreex-version`: 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.
+- 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 relay, API plus subscribe, and orchestration shapes.
-
-Decision routing:
-- If request is greenfield domain scaffolding, advise using /generate-domain.
-- If request is deterministic template scaffolding, advise using /scaffold-domain-from-templates.
-- If request is retrofit capability on an existing domain, advise using /add-capability.
-- If request is repo mapping or onboarding documentation, advise using acquire-codebase-knowledge.
-
-Response format:
-1. Recommendation.
-2. Why this fits CoreEx.
-3. Evidence from repo files.
-4. Risks and tradeoffs.
-5. Minimal next steps.
+- 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 scaffolding → advise using `/generate-domain`.
+- Retrofit capability on an existing domain → advise using `/add-capability`.
+- 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
index 0cd0f65d..191a78d1 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -1,4 +1,5 @@
---
+# 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"]
---
@@ -25,12 +26,34 @@ CoreEx is a modular .NET framework for enterprise APIs and distributed services.
- **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\`).
-- **Domain layers**: `*.Contracts` → `*.Application` → `*.Infrastructure` → `*.Api`, plus `*.Database`, `*.Outbox.Relay`, `*.Subscribe` (messaging).
-- **Sample flow**: Controllers → `WebApi` helpers → Application services (validate + `IUnitOfWork`) → Infrastructure repositories (EF + explicit mappers) → outbox events → relay publishes to Service Bus → subscribers consume.
+- **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`, `*.Outbox.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 sample hosts in `samples\aspire\Contoso.Aspire\AppHost.cs`.
+- **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
@@ -50,19 +73,34 @@ CoreEx is a modular .NET framework for enterprise APIs and distributed services.
- 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\` or `Application\Repositories\`;
+ - 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. run mutations inside `_unitOfWork.ExecuteAsync(...)`;
- 5. add `EventData` within the same unit-of-work scope.
+ 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.
-- When working in application or infrastructure code, follow `.github\instructions\application-services.instructions.md`, `.github\instructions\repositories.instructions.md`, and related scoped instruction files.
+
+### 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:
@@ -83,31 +121,48 @@ CoreEx is a modular .NET framework for enterprise APIs and distributed services.
- OpenAPI/health endpoints standard in hosts.
### Data and Messaging
-- SQL Server + outbox + Azure Service Bus are first-class patterns.
-- Shopping: synchronous HTTP reservation + transactional outbox + async event publishing. Preserve this split.
+- 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: NUnit + FluentAssertions.
+- Framework: UnitTestEx + NUnit + AwesomeAssertions (the `AwesomeAssertions` NuGet package — not FluentAssertions).
- Sample: `WithGenericTester` (unit) or `WithApiTester` (API/Subscribe/Relay).
-- Integration tests: `Data\data.yaml` (Test.Common) + `Resources\` JSON expectations + `ExpectSqlServerOutboxEvents(...)`.
+- Integration tests: `Data\data.yaml` (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.
-- Use `GlobalUsing.cs` per project; do not scatter `using` directives.
- 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
-- Do not edit generated code directly. If changes are needed, update the source templates or generation logic.
-- Generated code files are typically marked with a comment at the top indicating they are auto-generated and should not be edited manually.
-- Generated code also has the file name pattern `*.g.cs`, `*.g.sql`, `*.g.pgsql` or similar to indicate its nature.
-- Copilot should not suggest edits to generated code files. If it does, the suggestion should be rejected or redirected to the source templates.
+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](.github/INSTRUCTION_AUTHORING.md#generated-code) for full generator ownership detail.
## Key Docs to Read Before Large Changes
-- `README.md` for repo-level positioning and top-level commands.
-- `samples\README.md` for the runnable Contoso architecture and local setup.
-- `docs\capabilities.md` for the deeper CoreEx capability/pattern explanations.
-- `.github\instructions\*.instructions.md` for area-specific rules when editing `Program.cs`, contracts, application services, repositories, validators, subscribers, or tests.
+- `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 and Skills)
diff --git a/.github/instructions/api-controllers.instructions.md b/.github/instructions/api-controllers.instructions.md
deleted file mode 100644
index 57d09026..00000000
--- a/.github/instructions/api-controllers.instructions.md
+++ /dev/null
@@ -1,122 +0,0 @@
----
-applyTo: "**/Controllers/**/*.cs"
-description: "API controller conventions for CoreEx: inheritance, routing, dependency injection, CQRS separation, and WebApi integration"
-tags: ["controllers", "api", "routing", "cqrs", "dependency-injection"]
----
-
-# API Controller Conventions
-
-## NuGet / Project References
-
-| Package | Key types provided |
-|---|---|
-| `CoreEx.AspNetCore` | `WebApi`, `[IdempotencyKey]`, `[Accepts]`, `[ProducesNotFoundProblem]`, `[Query]`, `[Paging]`, `HttpNames`, `app.UseCoreExExceptionHandler()`, `app.UseExecutionContext()`, `app.UseIdempotencyKey()`, `app.MapHealthChecks()` |
-| `CoreEx.AspNetCore.NSwag` | `[OpenApiTag]`, `app.UseOpenApi()`, `app.UseSwaggerUi()`, `s.AddCoreExConfiguration()` |
-| `CoreEx` | `WebApplicationBuilderExtensions.AddHostSettings()`, `AddExecutionContext()` |
-
-## Structure
-
-- Inherit from `ControllerBase`. Never inherit from `Controller` (that brings View support).
-- Decorate with `[ApiController]` and `[Route("...")]` on the class.
-- Add `[OpenApiTag("TagName")]` to group endpoints in the generated OpenAPI document.
-- Inject `WebApi` and the relevant service interface via primary constructor. Guard with `.ThrowIfNull()`.
-- Split read operations and write operations into separate controller classes (`ProductController` for mutations, `ProductReadController` for queries) 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();
-}
-```
-
-## Method Signatures
-
-All action methods return `Task` using the `WebApi` helper. Do not return typed `ActionResult` directly.
-
-| HTTP Verb | WebApi helper | Notes |
-|---|---|---|
-| `GET` / `HEAD` | `_webApi.GetAsync(...)` | Use both attributes together |
-| `POST` | `_webApi.PostAsync(...)` or `PostWithResultAsync` | 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 |
-
-## Route Parameters
-
-Validate route parameters inline using `.Required()`:
-
-```csharp
-[HttpGet("{id}"), HttpHead("{id}")]
-public Task GetAsync(string id) =>
- _webApi.GetAsync(Request, (_, _) => _service.GetAsync(id.Required()));
-```
-
-## POST — Create with Location Header
-
-Use `ro.WithLocationUri(...)` to set the `Location` response header:
-
-```csharp
-[HttpPost]
-[Accepts]
-[ProducesResponseType(201)]
-[IdempotencyKey]
-public Task PostAsync() => _webApi.PostAsync(Request, (ro, _) =>
-{
- ro.WithLocationUri(p => new Uri($"/api/products/{p.Id}", UriKind.Relative));
- return _service.CreateAsync(ro.Value);
-});
-```
-
-## PATCH — Merge-Patch
-
-Always supply both `get:` and `put:` delegates. PATCH merges the incoming patch document over the fetched entity and calls `put`:
-
-```csharp
-[HttpPatch("{id}")]
-[Accepts(HttpNames.MergePatchJsonMediaTypeName)]
-public Task PatchAsync(string id) => _webApi.PatchAsync(Request,
- get: (ro, _) => _service.GetAsync(id.Required()),
- put: (ro, _) => _service.UpdateAsync(ro.Value.Adjust(p => p.Id = id)));
-```
-
-## 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() =>
- _webApi.GetAsync(Request, (ro, _) => _service.QueryAsync(ro.QueryArgs, ro.PagingArgs));
-```
-
-## 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)
- => _webApi.GetAsync(Request, (ro, ct) => ReferenceDataOrchestrator.Current.GetWithFilterAsync(codes, text, ro.IsIncludeInactive, ct));
-```
-
-## Response Metadata Attributes
-
-Decorate actions with standard response metadata attributes:
-
-- `[ProducesResponseType(StatusCodes.Status201Created)]`
-- `[ProducesNotFoundProblem()]` on GET/PUT/PATCH/DELETE where not-found is expected.
-- `[Accepts]` to document the consumed media type.
-
-## Result-Based Services
-
-When the service returns `Result` (Shopping-style domain services), use the `PostWithResultAsync` / `GetWithResultAsync` variants:
-
-```csharp
-[HttpPost("{basketId}/checkout")]
-public Task CheckoutAsync(string basketId) =>
- _webApi.PostWithResultAsync(Request, (_, _) =>
- _service.CheckoutAsync(basketId.Required()), HttpStatusCode.OK);
-```
diff --git a/.github/instructions/application-services.instructions.md b/.github/instructions/application-services.instructions.md
deleted file mode 100644
index 85e064b2..00000000
--- a/.github/instructions/application-services.instructions.md
+++ /dev/null
@@ -1,172 +0,0 @@
----
-applyTo: "**/Application/**/*.cs"
-description: "Application service conventions: ScopedService registration, dependency injection, validation, unit of work patterns, and business logic structure"
-tags: ["services", "application-layer", "dependency-injection", "validation", "unit-of-work"]
----
-
-# Application Service Conventions
-
-## NuGet / Project References
-
-| Package | Key types provided |
-|---|---|
-| `CoreEx` | `[ScopedService]`, `IUnitOfWork`, `Runtime`, `NotFoundException`, `BusinessException`, `ValidationException`, `.ThrowIfNull()`, `.ThrowIfNullOrEmpty()` |
-| `CoreEx.Data` | `DataResult`, `ItemsResult`, `QueryArgs`, `PagingArgs` |
-| `CoreEx.Events` | `EventData`, `EventAction` |
-| `CoreEx.Validation` | `Validator`, `.ValidateAndThrowAsync()`, `.ValidateWithResultAsync()` |
-| `CoreEx.Results` | `Result`, `Result.GoAsync()`, `.ThenAs()`, `.ThenAsAsync()` |
-| `CoreEx.RefData` | `ReferenceDataOrchestrator` |
-
-## Structure
-
-- Define a public interface (e.g., `IProductService`) in the Application project.
-- 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();
-}
-```
-
-## Guard Clauses
-
-Use CoreEx null/empty guards at the top of each method before any logic:
-
-```csharp
-public async Task UpdateAsync(Product product)
-{
- product.ThrowIfNull();
- product.Id.ThrowIfNullOrEmpty();
- // ...
-}
-```
-
-## Validation
-
-Call the validator before any persistence operations. Throw on first error set:
-
-```csharp
-await ProductValidator.Default.ValidateAndThrowAsync(product);
-```
-
-For `Result` style, use `ValidateWithResultAsync` and propagate with `ThenAs`:
-
-```csharp
-var result = await Result.GoAsync(() => MyValidator.Default.ValidateWithResultAsync(value));
-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).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.");
-```
-
-## Unit of Work and Events
-
-Wrap all side-effectful database operations in `_unitOfWork.ExecuteAsync(...)`. Add integration events inside that scope so event and data writes are atomic:
-
-```csharp
-return await _unitOfWork.ExecuteAsync(async () =>
-{
- var dr = await _repository.CreateAsync(product).ConfigureAwait(false);
- return dr.WhereMutated(v =>
- _unitOfWork.Events.Add(EventData.CreateEventWith(v, EventAction.Created)));
-}).ConfigureAwait(false);
-```
-
-- `WhereMutated(action)` — executes `action` only when the data result has a mutation; add the event inside this callback.
-- `EventData.CreateEventWith(value, action)` — creates a typed event from the entity.
-- `EventAction.Created`, `EventAction.Updated`, `EventAction.Deleted` — use the standard constants.
-
-For delete where the entity value is gone, carry the ID via `.WithKey(id)`:
-
-```csharp
-_unitOfWork.Events.Add(
- EventData.CreateEventWith(default, EventAction.Deleted).WithKey(id));
-```
-
-## Result Style (Domain-Aggregate Services)
-
-For services operating on DDD aggregates (e.g., Shopping Basket), use `Result` chains instead of exceptions for expected failures. Compose with `Result.GoAsync`, `.ThenAs`, `.ThenAsAsync`:
-
-```csharp
-public Task> CreateAsync(string customerId)
-{
- var aggregate = Domain.Basket.CreateNew(customerId.ThrowIfNullOrEmpty());
-
- return _unitOfWork.ExecuteAsync(async () =>
- {
- var br = await _repository.CreateAsync(aggregate).ConfigureAwait(false);
- return br.ThenAs(b =>
- {
- var contract = BasketMapper.Map(b);
- _unitOfWork.Events.Add(EventData.CreateEventWith(contract, EventAction.Created));
- return contract;
- });
- });
-}
-```
-
-For multi-step orchestration with early exit:
-
-```csharp
-var pr = await Result.GoAsync(() => SomeValidator.Default.ValidateWithResultAsync(input))
- .ThenAsAsync(v => _someAdapter.EnsureExistsAsync(v.Id!));
-
-if (pr.IsFailure)
- return pr.AsResult();
-```
-
-## Read Services
-
-Split read operations into a separate service with an `IXxxReadService` interface when the project follows CQRS. Read services do not use UnitOfWork and do not publish events:
-
-```csharp
-[ScopedService]
-public class ProductReadService(IProductRepository repository) : IProductReadService
-{
- private readonly IProductRepository _repository = repository.ThrowIfNull();
-
- public Task GetAsync(string id) => _repository.GetAsync(id);
- public Task> QueryAsync(QueryArgs? query, PagingArgs? paging)
- => _repository.QueryAsync(query, paging);
-}
-```
-
-## 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 `ProductsHttpClient`:
-
-```csharp
-// Application layer — interface only
-public interface IProductAdapter
-{
- Task GetAsync(string id);
- Task ReserveInventoryAsync(MovementRequest request);
-}
-
-// Infrastructure layer — implementation
-[ScopedService]
-public class ProductAdapter(ProductsHttpClient httpClient) : IProductAdapter { ... }
-```
-
-## ConfigureAwait
-
-Always call `.ConfigureAwait(false)` on every `await` inside service and repository methods.
diff --git a/.github/instructions/contracts.instructions.md b/.github/instructions/contracts.instructions.md
deleted file mode 100644
index fd1d256c..00000000
--- a/.github/instructions/contracts.instructions.md
+++ /dev/null
@@ -1,156 +0,0 @@
----
-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
-
-## NuGet / Project References
-
-| Package | Key types provided |
-|---|---|
-| `CoreEx` | `[Contract]`, `IIdentifier`, `ICompositeKey`, `IETag`, `IChangeLog`, `ChangeLog`, `[ReadOnly]`, `[Localization]` |
-| `CoreEx.RefData` | `ReferenceData`, `ReferenceDataCollection`, `[ReferenceData]`, `[ReferenceData]`, `ReferenceDataSortOrder` |
-| `CoreEx.Generator` | Roslyn source generator — add as `OutputItemType="Analyzer" ReferenceOutputAssembly="false"` |
-
-```xml
-
-
-
-
-
-```
-
-## Source Generation
-
-Mark contract classes with the `[Contract]` attribute and declare them `partial`. Roslyn source generation fills in serialization, equality, and change-tracking code. Never manually implement the generated members.
-
-```csharp
-[Contract]
-public partial class Product : ProductBase, IETag, IChangeLog { }
-```
-
-## 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, IIdentifier, IETag, IChangeLog
-{
- [ReadOnly(true)]
- public string? Id { 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).
-
-## Reference Data Properties
-
-Use `[ReferenceData]` on code properties that back a reference data relationship. Declare the property `partial` so source generation can emit the navigation accessor:
-
-```csharp
-[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 code.
-
-## Localization Labels
-
-Decorate properties with `[Localization("Human label")]` when the default property name would produce a poor validation error message:
-
-```csharp
-[Localization("Sub-category")]
-public partial string? SubCategoryCode { get; set; }
-// Validation error: "Sub-category is required." (not "SubCategoryCode is required.")
-```
-
-## 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:
-
-```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 */ }
-
-[Contract]
-public partial class ProductLite : ProductBase { /* subset for list queries */ }
-```
-
-## Reference Data Contracts
-
-Reference data types inherit from `ReferenceData` and use `[ReferenceData]` attribute. Pair each type with a typed collection class:
-
-```csharp
-[ReferenceData]
-public partial class Category : ReferenceData { }
-
-public class CategoryCollection() : ReferenceDataCollection(ReferenceDataSortOrder.Code) { }
-```
-
-For reference data that carries additional fields (e.g., `UnitOfMeasure.Scale`), add those as plain properties and mark computed ones with `[JsonIgnore]`:
-
-```csharp
-[ReferenceData]
-public partial class UnitOfMeasure : ReferenceData
-{
- public int Scale { get; init; }
-
- [JsonIgnore]
- public int Precision => 16 - Scale;
-}
-```
-
-## 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. Computed helpers (like the `IsQuantityValidForKind` example above) are acceptable read-only shorthands but must not mutate state.
diff --git a/.github/instructions/coreex-api-controllers.instructions.md b/.github/instructions/coreex-api-controllers.instructions.md
new file mode 100644
index 00000000..68c5ff77
--- /dev/null
+++ b/.github/instructions/coreex-api-controllers.instructions.md
@@ -0,0 +1,273 @@
+---
+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
+
+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.
+- Add `[OpenApiTag("TagName")]` to group endpoints in the generated OpenAPI document. Can also be placed on an individual action method to cross-tag it into a different OpenAPI group.
+- Inject `WebApi` and the relevant service interface via primary constructor. Guard with `.ThrowIfNull()`.
+- Split read operations and write operations into separate controller classes (e.g., `ProductController` for mutations, `ProductReadController` for queries) 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();
+}
+```
+
+### Method Signatures
+
+All action methods return `Task` using the `WebApi` helper. Do not return typed `ActionResult` directly.
+
+#### 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) =>
+ _webApi.GetAsync(Request, (_, _) => _service.GetAsync(id.Required()));
+```
+
+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() => _webApi.PostAsync(Request, (ro, _) =>
+{
+ ro.WithLocationUri(p => new Uri($"/api/products/{p.Id}", UriKind.Relative));
+ return _service.CreateAsync(ro.Value);
+});
+```
+
+### PATCH — Merge-Patch
+
+Always supply both `get:` and `put:` delegates. PATCH merges the incoming patch document over the fetched entity and calls `put`:
+
+```csharp
+[HttpPatch("{id}")]
+[Accepts(HttpNames.MergePatchJsonMediaTypeName)]
+public Task PatchAsync(string id) => _webApi.PatchAsync(Request,
+ get: (ro, _) => _service.GetAsync(id.Required()),
+ put: (ro, _) => _service.UpdateAsync(ro.Value.Adjust(p => p.Id = id)));
+```
+
+### 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() =>
+ _webApi.GetAsync(Request, (ro, _) => _service.QueryAsync(ro.QueryArgs, ro.PagingArgs));
+```
+
+### 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)
+ => _webApi.GetAsync(Request, (ro, ct) => ReferenceDataOrchestrator.Current.GetWithFilterAsync(codes, text, ro.IsIncludeInactive, ct));
+```
+
+### 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() =>
+ _webApi.GetAsync(Request, (ro, _) => _service.QuerySchemaAsync());
+```
+
+### 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) =>
+ _webApi.PostWithResultAsync(Request, (_, _) =>
+ _service.CheckoutAsync(basketId.Required()), HttpStatusCode.OK);
+
+[HttpPost("{basketId}/items")]
+[IdempotencyKey]
+[Accepts]
+[ProducesResponseType(typeof(Basket), StatusCodes.Status200OK)]
+public Task ItemAddAsync(string basketId) =>
+ _webApi.PostWithResultAsync(Request, (ro, _) =>
+ _service.ItemAddAsync(basketId.Required(), ro.Value), HttpStatusCode.OK);
+```
+
+---
+
+## 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)
+ => webApi.GetWithResultAsync(request, (_, _) => service.GetAsync(id.Required())))
+ .Produces().ProducesNotFoundProblem();
+```
+
+**POST — create with Location header:**
+```csharp
+app.MapPost("api/products",
+ (HttpRequest request, WebApi webApi, IProductService service)
+ => webApi.PostWithResultAsync(request, async (ro, _) =>
+ {
+ ro.WithLocationUri(p => new Uri($"api/products/{p.Id}", UriKind.Relative));
+ return await service.CreateAsync(ro.Value).ConfigureAwait(false);
+ }))
+ .Accepts().ProducesCreated().WithIdempotencyKey();
+```
+
+**PUT:**
+```csharp
+app.MapPut("api/products/{id}",
+ (HttpRequest request, WebApi webApi, IProductService service, string id)
+ => webApi.PutWithResultAsync(request, (ro, _) =>
+ service.UpdateAsync(ro.Value.Adjust(p => p.Id = id))))
+ .Accepts().Produces().ProducesNotFoundProblem();
+```
+
+**PATCH — JSON Merge-Patch:**
+```csharp
+app.MapPatch("api/products/{id}",
+ (HttpRequest request, WebApi webApi, IProductService service, string id)
+ => webApi.PatchWithResultAsync(request,
+ get: (_, _) => service.GetAsync(id.Required()),
+ put: (ro, _) => service.UpdateAsync(ro.Value.Adjust(p => p.Id = id))))
+ .Accepts(HttpNames.MergePatchJsonMediaTypeName).Produces().ProducesNotFoundProblem();
+```
+
+**DELETE:**
+```csharp
+app.MapDelete("api/products/{id}",
+ (HttpRequest request, WebApi webApi, IProductService service, string id)
+ => webApi.DeleteWithResultAsync(request, (_, _) => service.DeleteAsync(id.Required())))
+ .ProducesNoContent();
+```
+
+**Query with filtering and paging:**
+```csharp
+app.MapGet("api/products",
+ (HttpRequest request, WebApi webApi, IProductReadService service)
+ => webApi.GetWithResultAsync(request, (ro, _) => service.QueryAsync(ro.QueryArgs, ro.PagingArgs)))
+ .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.
+
+---
+
+## 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.
+
+## 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..b30f65b6
--- /dev/null
+++ b/.github/instructions/coreex-application-services.instructions.md
@@ -0,0 +1,349 @@
+---
+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();
+}
+```
+
+## 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()).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)
+{
+ product.ThrowIfNull(); // checked here; not passed anywhere yet
+ await ProductValidator.Default.ValidateAndThrowAsync(product).ConfigureAwait(false);
+ var current = await _repository.GetAsync(product.Id.ThrowIfNullOrEmpty()).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.SubCategoryCode).Mandatory().IsValid();
+ 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);
+```
+
+**`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).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);
+```
+
+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));
+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).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.");
+```
+
+## 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 () =>
+{
+ var dr = await _repository.CreateAsync(product).ConfigureAwait(false);
+ return dr.WhereMutated(v =>
+ _unitOfWork.Events.Add(EventData.CreateEventWith(v, EventAction.Created)));
+}).ConfigureAwait(false);
+```
+
+- `WhereMutated(action)` — executes `action` only when the data result records a mutation; add the event inside this callback.
+- `EventData.CreateEventWith(value, action)` — creates a typed event from the entity.
+- `EventAction.Created`, `EventAction.Updated`, `EventAction.Deleted` — use the standard constants.
+
+For delete where the entity value is no longer available, carry the ID via `.WithKey(id)`:
+
+```csharp
+_unitOfWork.Events.Add(
+ EventData.CreateEventWith(default, EventAction.Deleted).WithKey(id));
+```
+
+## 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)
+{
+ var aggregate = Domain.Basket.CreateNew(customerId.ThrowIfNullOrEmpty());
+
+ return _unitOfWork.TransactionAsync(async () =>
+ {
+ var br = await _repository.CreateAsync(aggregate).ConfigureAwait(false);
+ return br.ThenAs(b =>
+ {
+ var contract = BasketMapper.Map(b);
+ _unitOfWork.Events.Add(EventData.CreateEventWith(contract, EventAction.Created));
+ return contract;
+ });
+ });
+}
+```
+
+For multi-step orchestration with early exit on the first failure:
+
+```csharp
+var pr = await Result.GoAsync(() => SomeValidator.Default.ValidateWithResultAsync(input))
+ .ThenAsAsync(v => _someAdapter.EnsureExistsAsync(v.Id!));
+
+if (pr.IsFailure)
+ return pr.AsResult();
+```
+
+## CQRS — Read Services
+
+Split read operations into a separate service with an `IXxxReadService` interface. 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.
+
+The interface lives in `Interfaces/` alongside the write service interface (e.g., `IProductReadService.cs` next to `IProductService.cs`). The implementation lives in the same folder as the write service implementation.
+
+```csharp
+[ScopedService]
+public class ProductReadService(IProductRepository repository) : IProductReadService
+{
+ private readonly IProductRepository _repository = repository.ThrowIfNull();
+
+ public Task GetAsync(string id) => _repository.GetAsync(id);
+ public Task> QueryAsync(QueryArgs? query, PagingArgs? paging)
+ => _repository.QueryAsync(query, paging);
+}
+```
+
+## 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);
+ Task ReserveInventoryAsync(Domain.Basket basket);
+ Task CancelReservationAsync(Domain.Basket basket);
+}
+
+// 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) => Result
+ .GoAsync(() => _productAdapter.GetAsync(productId))
+ .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);
+```
+
+## 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 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`.
+
+## 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..fd098d51
--- /dev/null
+++ b/.github/instructions/coreex-contracts.instructions.md
@@ -0,0 +1,225 @@
+---
+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
+
+## 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
+
+
+
+
+```
+
+## 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.
+
+```csharp
+[Contract]
+public partial class Product : ProductBase, IETag, IChangeLog { }
+```
+
+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; }
+}
+```
+
+## 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. Three conditions must all 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.
+
+## Localization Labels
+
+Decorate properties with `[Localization("Human label")]` when the default property name would produce a poor validation error message:
+
+```csharp
+[Localization("Sub-category")]
+public partial string? SubCategoryCode { get; set; }
+// Validation error: "Sub-category is required." (not "SubCategoryCode is required.")
+```
+
+## 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; }
+}
+```
+
+## Typed Collection Classes
+
+Pair entity contracts with a typed collection class when the contract represents a resource commonly returned as a list:
+
+```csharp
+public partial class MovementCollection : List { }
+```
+
+## Reference Data Contracts
+
+Reference data types inherit from `ReferenceData` and use `[ReferenceData]` attribute. Pair each type with a typed collection class:
+
+```csharp
+[ReferenceData]
+public partial class Category : ReferenceData { }
+
+public class CategoryCollection() : ReferenceDataCollection(ReferenceDataSortOrder.Code) { }
+```
+
+Reference data contract `*.g.cs` files are generated by the domain's `*.CodeGen` project from `ref-data.yaml`. A hand-authored partial class in the same namespace can extend the generated type with additional computed members or constants:
+
+```csharp
+// MovementKind.g.cs — generated, do not edit.
+// MovementKind.cs — hand-authored extension.
+public partial class MovementKind
+{
+ public const string Adjust = "A";
+ public const string Issue = "I";
+ public const string Receive = "R";
+}
+```
+
+For reference data that carries additional stored fields (e.g., `UnitOfMeasure.Scale`), add those as plain properties on the hand-authored partial and mark computed ones with `[JsonIgnore]`:
+
+```csharp
+public partial class UnitOfMeasure
+{
+ [JsonIgnore]
+ public int Precision => 16 - Scale; // Scale is a generated stored field
+}
+```
+
+## 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) | `*.CodeGen` project (`ref-data.yaml` + Handlebars templates) | `ref-data.yaml` or the templates |
+| `*.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 edit `*.g.cs` files directly — regenerate via the appropriate tooling.
+
+## 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..807cb015
--- /dev/null
+++ b/.github/instructions/coreex-conventions.instructions.md
@@ -0,0 +1,122 @@
+---
+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
+global using System;
+global using System.Collections.Generic;
+global using System.Threading;
+global using System.Threading.Tasks;
+global using Microsoft.Extensions.Logging;
+global using CoreEx;
+global using Contoso.Products.Application.Interfaces;
+```
+
+**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.
+
+## 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) => _repository.GetAsync(id);
+
+// Multi-line single expression — use =>
+public Task> QueryAsync(QueryArgs? query, PagingArgs? paging)
+ => _repository.QueryAsync(query, paging);
+
+// 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)
+{
+ product.ThrowIfNull();
+ await ProductValidator.Default.ValidateAndThrowAsync(product).ConfigureAwait(false);
+ return await _repository.UpdateAsync(product).ConfigureAwait(false);
+}
+```
+
+## 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 add `using` statements to individual `.cs` files — declare all imports in `GlobalUsing.cs`.
+- 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 replace a private backing field with an auto-property simply because it could be one — backing fields are a valid developer choice.
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..2d3c412b
--- /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());
+}
+```
+
+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);
+}
+```
+
+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()
+ .AddMvcWebApi()
+ .AddHttpWebApi()
+ .AddHostedServiceManager();
+
+builder.Services.AddDynamicServicesUsing();
+
+// 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..e66d9d32
--- /dev/null
+++ b/.github/instructions/coreex-host-setup.instructions.md
@@ -0,0 +1,297 @@
+---
+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)
+
+---
+
+## Key Registrations by Host Type
+
+### API Host
+
+| Package | Key registrations |
+|---|---|
+| `CoreEx.AspNetCore` | `AddMvcWebApi()`, `AddHttpWebApi()`, `AddExecutionContext()`, `UseCoreExExceptionHandler()`, `UseExecutionContext()`, `UseIdempotencyKey()`, `MapHealthChecks()` |
+| `CoreEx.AspNetCore.NSwag` | `AddOpenApiDocument()`, `AddCoreExConfiguration()`, `UseOpenApi()`, `UseSwaggerUi()` |
+| `CoreEx.Caching.FusionCache` | `AddFusionCache()`, `AddFusionHybridCache()`, `AddDefaultCacheKeyProvider()`, `AddHybridCacheIdempotencyProvider()` |
+| `CoreEx.Database.SqlServer` | `AddSqlServerDatabase()`, `AddSqlServerUnitOfWork()`, `AddSqlServerOutboxPublisher()`, `AddSqlServerClient("SqlServer")` |
+| `CoreEx.Database.Postgres` | `AddPostgresDatabase()`, `AddPostgresUnitOfWork()`, `AddPostgresOutboxPublisher()`, `AddAzureNpgsqlDataSource("Postgres")` |
+| `CoreEx.EntityFrameworkCore` | `AddDbContext()`, `AddEfDb()` |
+| `CoreEx.Events` | `AddEventFormatter()` |
+| `CoreEx.RefData` | `AddReferenceDataOrchestrator()` |
+| `Aspire.StackExchange.Redis.DistributedCaching` | `AddRedisDistributedCache("redis")` |
+| `FusionCache.Backplane.StackExchangeRedis` | `RedisBackplane`, `RedisBackplaneOptions` |
+| `OpenTelemetry.*` | `WithCoreExTelemetry()`, `WithCoreExSqlServerTelemetry()` / `WithCoreExPostgresTelemetry()`, `UseOtlpExporter()` |
+
+### Subscribe Host
+
+| Package | Key registrations |
+|---|---|
+| `CoreEx.AspNetCore` | `AddMvcWebApi()`, `AddHttpWebApi()`, `AddExecutionContext()`, `AddHostedServiceManager()`, `UseCoreExExceptionHandler()`, `UseExecutionContext()`, `MapHealthChecks()`, `MapHostedServices()` |
+| `CoreEx.Caching.FusionCache` | `AddFusionCache()`, `AddFusionHybridCache()`, `AddDefaultCacheKeyProvider()`, `AddHybridCacheIdempotencyProvider()` |
+| `CoreEx.Events` | `AddEventFormatter()`, `AddSubscribedManager()` |
+| `CoreEx.Database.SqlServer` | `AddSqlServerDatabase()`, `AddSqlServerUnitOfWork()`, `AddSqlServerOutboxPublisher()`, `AddSqlServerClient("SqlServer")` |
+| `CoreEx.Database.Postgres` | `AddPostgresDatabase()`, `AddPostgresUnitOfWork()`, `AddPostgresOutboxPublisher()`, `AddAzureNpgsqlDataSource("Postgres")` |
+| `CoreEx.EntityFrameworkCore` | `AddDbContext()`, `AddEfDb()` |
+| `CoreEx.RefData` | `AddReferenceDataOrchestrator()` |
+| `CoreEx.Azure.Messaging.ServiceBus` | `AddAzureServiceBusClient("ServiceBus")`, `AddAzureServiceBusPublisher(..., addAsDefaultIEventPublisher: false)`, `AzureServiceBusReceiving()`, `WithCoreExServiceBusTelemetry()` |
+| `Aspire.StackExchange.Redis.DistributedCaching` | `AddRedisDistributedCache("redis")` |
+| `FusionCache.Backplane.StackExchangeRedis` | `RedisBackplane`, `RedisBackplaneOptions` |
+| `OpenTelemetry.*` | `WithCoreExTelemetry()`, `WithCoreExServiceBusTelemetry()`, `WithCoreExSqlServerTelemetry()` / `WithCoreExPostgresTelemetry()`, `UseOtlpExporter()` |
+
+### Outbox Relay Host
+
+| Package | Key registrations |
+|---|---|
+| `CoreEx.AspNetCore` | `AddMvcWebApi()`, `AddHttpWebApi()`, `AddExecutionContext()`, `AddHostedServiceManager()`, `UseCoreExExceptionHandler()`, `UseExecutionContext()`, `MapHealthChecks()`, `MapHostedServices()` |
+| `CoreEx.Database.SqlServer` | `AddSqlServerDatabase()`, `AddSqlServerUnitOfWork()`, `AddSqlServerOutboxRelay()`, `AddSqlServerOutboxRelayHostedService()` |
+| `CoreEx.Database.Postgres` | `AddPostgresDatabase()`, `AddPostgresUnitOfWork()`, `AddPostgresOutboxRelay()`, `AddPostgresOutboxRelayHostedService()` |
+| `CoreEx.Azure.Messaging.ServiceBus` | `AddAzureServiceBusClient("ServiceBus")`, `AddAzureServiceBusPublisher(...)`, `ServiceBusSessionStrategy` |
+| `OpenTelemetry.*` | `WithCoreExTelemetry()`, `WithCoreExSqlServerTelemetry()` / `WithCoreExPostgresTelemetry()`, `WithCoreExServiceBusTelemetry()`, `UseOtlpExporter()` |
+
+---
+
+## API Host
+
+The API host is the primary HTTP composition root. It exposes controllers, OpenAPI docs, reference-data endpoints, and idempotency support.
+
+```csharp
+var builder = WebApplication.CreateBuilder(args);
+builder.AddHostSettings();
+
+builder.Services
+ .AddPrecisionTimeProvider()
+ .AddExecutionContext()
+ .AddReferenceDataOrchestrator()
+ .AddMvcWebApi()
+ .AddHttpWebApi();
+
+builder.Services.AddDynamicServicesUsing();
+
+// L1/L2 caching with FusionCache + Redis backplane.
+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();
+
+// Database, EF, outbox publisher.
+// SQL Server variant:
+builder.AddSqlServerClient("SqlServer");
+builder.Services
+ .AddSqlServerDatabase()
+ .AddSqlServerUnitOfWork()
+ .AddEventFormatter()
+ .AddSqlServerOutboxPublisher()
+ .AddDbContext()
+ .AddEfDb();
+
+// PostgreSQL variant (use instead of SQL Server):
+// builder.AddAzureNpgsqlDataSource("Postgres");
+// builder.Services
+// .AddPostgresDatabase()
+// .AddPostgresUnitOfWork()
+// .AddEventFormatter()
+// .AddPostgresOutboxPublisher()
+// .AddDbContext