diff --git a/.claude/commands/coreex-adapter.md b/.claude/commands/coreex-adapter.md new file mode 100644 index 00000000..de6c540b --- /dev/null +++ b/.claude/commands/coreex-adapter.md @@ -0,0 +1,14 @@ +--- +description: "Create or modify a CoreEx Infrastructure-layer adapter (anti-corruption layer). USE FOR: new adapter interface in Application/Adapters/{ExternalDomain}/, new adapter implementation in Infrastructure/Adapters/{ExternalDomain}/, new typed HTTP client in Infrastructure/Clients/{ExternalDomain}/, event-driven sync/replication adapter (IXxxSyncAdapter), unit tests for HTTP clients with MockHttpClientFactory. DO NOT USE FOR: repositories within the same domain (use coreex-repository), application services that call adapters (use coreex-app-service), event subscriber hosts that drive sync adapters (see coreex-event-subscribers.instructions.md)." +allowed-tools: [Read, Glob, Grep, Edit, Write, Bash] +--- + + + +Read `.github/skills/coreex-adapter/SKILL.md` and follow the instructions in that file. diff --git a/.claude/commands/coreex-aggregate.md b/.claude/commands/coreex-aggregate.md new file mode 100644 index 00000000..a6113687 --- /dev/null +++ b/.claude/commands/coreex-aggregate.md @@ -0,0 +1,14 @@ +--- +description: "Add or modify a DDD domain object in a CoreEx Domain layer. USE FOR: new aggregate root (Aggregate), new child entity (Entity) owned by an aggregate, new value object (sealed record), adding mutation methods, PersistenceState-aware factory methods, mutation guards (OnCheckCanMutate/OnMutate). Interviews the developer to determine which of the three (aggregate root / entity / value object) applies, then follows the appropriate pattern. DO NOT USE FOR: CRUD-oriented domains with no Domain layer (use coreex-app-service directly against repositories), Application-layer mappers between aggregate and contract (see coreex-application-services.instructions.md), Infrastructure persistence of aggregates (use coreex-repository)." +allowed-tools: [Read, Glob, Grep, Edit, Write, Bash] +--- + + + +Read `.github/skills/coreex-aggregate/SKILL.md` and follow the instructions in that file. diff --git a/.claude/commands/coreex-api-e2e.md b/.claude/commands/coreex-api-e2e.md new file mode 100644 index 00000000..e73956b2 --- /dev/null +++ b/.claude/commands/coreex-api-e2e.md @@ -0,0 +1,14 @@ +--- +description: "Create a complete new entity with CRUD API endpoints end-to-end in a single guided workflow: DTO contract, database migration, EF Core repository, validator (+ unit tests), optional policy guard (+ unit tests), application service, API endpoint, and integration tests. USE FOR: adding a brand-new entity to an existing CoreEx solution where the full stack — from database table to HTTP endpoint — is needed. DO NOT USE FOR: modifying an existing entity or endpoint (use the targeted L1 skill directly), read-only façade entities backed by an external adapter with no local table (use individual L1 skills), or any partial-stack additions." +allowed-tools: [Read, Glob, Grep, Edit, Write, Bash] +--- + + + +Read `.github/skills/coreex-api-e2e/SKILL.md` and follow the instructions in that file. diff --git a/.claude/commands/coreex-api.md b/.claude/commands/coreex-api.md new file mode 100644 index 00000000..87349a49 --- /dev/null +++ b/.claude/commands/coreex-api.md @@ -0,0 +1,14 @@ +--- +description: "Add or modify a CoreEx API controller (or Minimal API endpoint) in an *.Api host. USE FOR: scaffolding the MVC controller pair (XxxController + XxxReadController), GET/query/schema endpoints, POST create, PUT + PATCH full-entity update, DELETE, and custom business-action endpoints. Covers both exception-based and Result service styles, and Minimal API as an alternative to MVC. DO NOT USE FOR: Api host setup / Program.cs (use coreex-scaffold), application services (use coreex-app-service), API integration tests (use coreex-test-api)." +allowed-tools: [Read, Glob, Grep, Edit, Write, Bash] +--- + + + +Read `.github/skills/coreex-api/SKILL.md` and follow the instructions in that file. diff --git a/.claude/commands/coreex-app-service.md b/.claude/commands/coreex-app-service.md new file mode 100644 index 00000000..b8b0d942 --- /dev/null +++ b/.claude/commands/coreex-app-service.md @@ -0,0 +1,14 @@ +--- +description: "Create or modify a CoreEx Application-layer service. USE FOR: new service class (exception-based or Result), adding CRUD/business operations, CQRS read service (XxxReadService), adapter interface in Application/Adapters/, policy class in Application/Policies/, application-level mapper (Domain → Contract). DO NOT USE FOR: Infrastructure repositories (use coreex-repository), validators (use coreex-validator), controller endpoints (use coreex-api)." +allowed-tools: [Read, Glob, Grep, Edit, Write, Bash] +--- + + + +Read `.github/skills/coreex-app-service/SKILL.md` and follow the instructions in that file. diff --git a/.claude/commands/coreex-contract.md b/.claude/commands/coreex-contract.md new file mode 100644 index 00000000..47867d68 --- /dev/null +++ b/.claude/commands/coreex-contract.md @@ -0,0 +1,14 @@ +--- +description: "Create or modify a hand-authored contract (DTO/entity) in a CoreEx domain. USE FOR: new root entity contract, new subordinate/request contract, modifying an existing contract (add property, add interface, wire ref-data), extracting a shared base class. DO NOT USE FOR: reference-data contracts (generated via coreex-refdata / *.CodeGen), Infrastructure persistence models (generated by *.Database CodeGen)." +allowed-tools: [Read, Glob, Grep, Edit, Write, Bash] +--- + + + +Read `.github/skills/coreex-contract/SKILL.md` and follow the instructions in that file. diff --git a/.claude/commands/coreex-db-migration.md b/.claude/commands/coreex-db-migration.md new file mode 100644 index 00000000..4069e10b --- /dev/null +++ b/.claude/commands/coreex-db-migration.md @@ -0,0 +1,14 @@ +--- +description: "Add or change a database table for a CoreEx domain. USE FOR: new transactional table, new reference-data table, altering an existing table (columns, indexes, constraints), or any other schema change (indexes, functions, stored procs). Scaffolds the correct migration script, updates dbex.yaml, applies the migration, and regenerates Infrastructure persistence models. DO NOT USE FOR: outbox provisioning (use dotnet run -- script outbox directly), seed data only changes (dotnet run -- Data), or CoreEx contract/service generation (that is *.CodeGen, not *.Database)." +allowed-tools: [Read, Glob, Grep, Edit, Write, Bash] +--- + + + +Read `.github/skills/coreex-db-migration/SKILL.md` and follow the instructions in that file. diff --git a/.claude/commands/coreex-graphql.md b/.claude/commands/coreex-graphql.md new file mode 100644 index 00000000..b9aed7ab --- /dev/null +++ b/.claude/commands/coreex-graphql.md @@ -0,0 +1,14 @@ +--- +description: "Add or extend GraphQL-lite query support (CoreEx.Data.GraphQL) on a CoreEx *.Api host. USE FOR: first-time AddCoreExGraphQLLite/MapCoreExGraphQLLite wiring in Program.cs, registering AddQuery/AddGet roots over an entity's existing QueryArgsConfig, bulk-exposing reference data via AddReferenceDataQueries, adding a matching GraphQL root for an endpoint coreex-api just scaffolded, recording GraphQL enablement in the host's AGENTS.md. DO NOT USE FOR: REST controller endpoints (use coreex-api), defining or changing a QueryArgsConfig itself (use coreex-repository or coreex-refdata), general Program.cs setup unrelated to GraphQL (use coreex-scaffold)." +allowed-tools: [Read, Glob, Grep, Edit, Write, Bash] +--- + + + +Read `.github/skills/coreex-graphql/SKILL.md` and follow the instructions in that file. diff --git a/.claude/commands/coreex-policy.md b/.claude/commands/coreex-policy.md new file mode 100644 index 00000000..c11e92d5 --- /dev/null +++ b/.claude/commands/coreex-policy.md @@ -0,0 +1,14 @@ +--- +description: "Create or modify a CoreEx Application-layer policy class. USE FOR: new policy class in Application/Policies/, EnsureExists guard (referenced entity must exist), business rule guards requiring I/O, multi-method policy, composing policies in Result pipelines. DO NOT USE FOR: synchronous validation rules (use coreex-validator), Infrastructure repositories (use coreex-repository), application service scaffolding (use coreex-app-service)." +allowed-tools: [Read, Glob, Grep, Edit, Write, Bash] +--- + + + +Read `.github/skills/coreex-policy/SKILL.md` and follow the instructions in that file. diff --git a/.claude/commands/coreex-refdata.md b/.claude/commands/coreex-refdata.md new file mode 100644 index 00000000..22c73c6d --- /dev/null +++ b/.claude/commands/coreex-refdata.md @@ -0,0 +1,14 @@ +--- +description: "Add or modify a reference data type in a CoreEx domain. USE FOR: new ref-data entity (new table + seed rows + CodeGen), adding extra properties to an existing type, adding seed rows for an existing type, wiring an existing ref-data type into a contract. DO NOT USE FOR: non-reference-data entity tables (use coreex-db-migration), hand-authoring generated .g.cs contracts (always use CodeGen)." +allowed-tools: [Read, Glob, Grep, Edit, Write, Bash] +--- + + + +Read `.github/skills/coreex-refdata/SKILL.md` and follow the instructions in that file. diff --git a/.claude/commands/coreex-repository.md b/.claude/commands/coreex-repository.md new file mode 100644 index 00000000..02b544b5 --- /dev/null +++ b/.claude/commands/coreex-repository.md @@ -0,0 +1,14 @@ +--- +description: "Create or modify a CoreEx Infrastructure-layer repository. USE FOR: new repository class, adding CRUD operations, adding a custom query (QueryArgsConfig), bidirectional mapper (BiDirectionMapper), EfDb model accessor, Result pipeline variants. DO NOT USE FOR: Application-layer service logic, domain invariants, typed HTTP clients/adapters (those follow adapter conventions in the infrastructure instructions, not this skill)." +allowed-tools: [Read, Glob, Grep, Edit, Write, Bash] +--- + + + +Read `.github/skills/coreex-repository/SKILL.md` and follow the instructions in that file. diff --git a/.claude/commands/coreex-scaffold.md b/.claude/commands/coreex-scaffold.md index 15da1144..b07b9d68 100644 --- a/.claude/commands/coreex-scaffold.md +++ b/.claude/commands/coreex-scaffold.md @@ -1,5 +1,5 @@ --- -description: "CoreEx Solution Scaffolder — guides solution shaping after bootstrap (hosts, database, messaging, refdata/outbox/DDD/ROP options, and an optional Aspire AppHost for local multi-host orchestration/dashboard) and turns the answers into dotnet new template commands." +description: "Guide a developer through CoreEx solution shaping after bootstrap, using a short plain-English interview that turns user answers into safe dotnet new template inputs. USE FOR: bootstrap-only repos, deciding API-only vs API plus relay vs API plus subscriber, choosing SQL Server vs Postgres vs no database, choosing refdata/outbox/DDD/ROP options, installing CoreEx.Template, checking current solution shape, adding missing Api/Relay/Subscribe hosts to an existing repo, adding a .NET Aspire AppHost for local multi-host orchestration and dashboard visibility, and optionally preparing a first local runnable state with local dependency assets plus database/code-generation steps. DO NOT USE FOR: unrelated runtime debugging, bootstrap creation, or forcing root re-scaffolding over an existing solution. INVOKES: workspace inspection, ask-questions style interviews, dotnet new install/list, dry-run validation, solution wiring, optional local dependency asset creation, focused build/test validation, and either template generation or manual retrofit work depending on repo shape." allowed-tools: [Read, Glob, Grep, Edit, Write, Bash] --- @@ -11,4 +11,4 @@ allowed-tools: [Read, Glob, Grep, Edit, Write, Bash] propose the change upstream in Avanade/CoreEx instead, then refresh once it is released. --> -Read `.github/skills/coreex-solution-scaffolder/SKILL.md` and follow the instructions in that file. +Read `.github/skills/coreex-scaffold/SKILL.md` and follow the instructions in that file. diff --git a/.claude/commands/coreex-subscriber-e2e.md b/.claude/commands/coreex-subscriber-e2e.md new file mode 100644 index 00000000..8a78b83b --- /dev/null +++ b/.claude/commands/coreex-subscriber-e2e.md @@ -0,0 +1,14 @@ +--- +description: "Create a complete new event or command subscriber end-to-end in a single guided workflow: event/command DTO contract (if new), subscriber handler, optional application service and repository, and integration tests. USE FOR: adding a brand-new subscriber to an existing CoreEx Subscribe host for any scenario — command handling, event-data-sync replication, or event-driven business-process choreography. DO NOT USE FOR: modifying an existing subscriber (use coreex-subscriber directly), API endpoint work (use coreex-api-e2e), or setting up the Subscribe host itself (use coreex-scaffold)." +allowed-tools: [Read, Glob, Grep, Edit, Write, Bash] +--- + + + +Read `.github/skills/coreex-subscriber-e2e/SKILL.md` and follow the instructions in that file. diff --git a/.claude/commands/coreex-subscriber.md b/.claude/commands/coreex-subscriber.md new file mode 100644 index 00000000..30220083 --- /dev/null +++ b/.claude/commands/coreex-subscriber.md @@ -0,0 +1,14 @@ +--- +description: "Add or modify an event/command subscriber in a CoreEx Subscribe host. USE FOR: command subscriber (owns the contract, delegates to app service), event-data-sync subscriber (delegates to IXxxSyncAdapter), event-business-process subscriber (choreography step, delegates to app service). Covers SubscribedBase, SubscribedBase, ValueValidator, ErrorHandler, and subject naming. DO NOT USE FOR: API controllers (use coreex-api), application services (use coreex-app-service), replication adapter implementations (use coreex-adapter), Subscribe host Program.cs setup (see coreex-host-setup.instructions.md), Subscribe-test integration tests (use coreex-test-subscribe)." +allowed-tools: [Read, Glob, Grep, Edit, Write, Bash] +--- + + + +Read `.github/skills/coreex-subscriber/SKILL.md` and follow the instructions in that file. diff --git a/.claude/commands/coreex-test-api.md b/.claude/commands/coreex-test-api.md new file mode 100644 index 00000000..9aad85c4 --- /dev/null +++ b/.claude/commands/coreex-test-api.md @@ -0,0 +1,14 @@ +--- +description: "Write or update an integration test in a CoreEx *.Test.Api project. USE FOR: new XxxReadTests/XxxMutateTests partial classes, per-operation test files (Get/Query/Create/Update/Patch/Delete), OneTimeSetUp seeding + cache + outbox wiring, seed data (read-data.seed.yaml/mutate-data.seed.yaml), .res.json/.req.json resources, ETag/concurrency and soft-delete scenarios, outbox event assertions, inter-domain HTTP mocking. DO NOT USE FOR: Subscribe host tests (use coreex-test-subscribe), Outbox Relay host tests (use coreex-test-relay), pure unit tests with no infrastructure (validators/aggregates/adapters already cover their own Test.Unit guidance), the controller/endpoint implementation itself (use coreex-api)." +allowed-tools: [Read, Glob, Grep, Edit, Write, Bash] +--- + + + +Read `.github/skills/coreex-test-api/SKILL.md` and follow the instructions in that file. diff --git a/.claude/commands/coreex-test-relay.md b/.claude/commands/coreex-test-relay.md new file mode 100644 index 00000000..53f90c33 --- /dev/null +++ b/.claude/commands/coreex-test-relay.md @@ -0,0 +1,14 @@ +--- +description: "Understand, verify, or (rarely) extend an Outbox Relay host integration test in a CoreEx *.Test.Relay project. USE FOR: understanding the templated RelayTests.cs shape, verifying the relay forwards outbox events to the broker, hosted-service pause/resume endpoint checks, diagnosing Service Bus emulator entity-not-found failures. DO NOT USE FOR: API host tests (use coreex-test-api), Subscribe host tests (use coreex-test-subscribe), the relay host's Program.cs/hosted-service setup (see coreex-host-setup.instructions.md)." +allowed-tools: [Read, Glob, Grep, Edit, Write, Bash] +--- + + + +Read `.github/skills/coreex-test-relay/SKILL.md` and follow the instructions in that file. diff --git a/.claude/commands/coreex-test-subscribe.md b/.claude/commands/coreex-test-subscribe.md new file mode 100644 index 00000000..deb6c34f --- /dev/null +++ b/.claude/commands/coreex-test-subscribe.md @@ -0,0 +1,14 @@ +--- +description: "Write or update an integration test in a CoreEx *.Test.Subscribe project. USE FOR: SubscriberTests partial classes, simulating broker message receipt via ServiceBusSubscribedSubscriber, command/event-data-sync/event-business-process test scenarios, ErrorHandler outcome assertions, unsubscribed-subject tests. Shares DB/cache/outbox OneTimeSetUp foundations with coreex-test-api — see that skill for the shared setup mechanics. DO NOT USE FOR: API host tests (use coreex-test-api), Outbox Relay host tests (use coreex-test-relay), the subscriber implementation itself (use coreex-subscriber)." +allowed-tools: [Read, Glob, Grep, Edit, Write, Bash] +--- + + + +Read `.github/skills/coreex-test-subscribe/SKILL.md` and follow the instructions in that file. diff --git a/.claude/commands/coreex-validator.md b/.claude/commands/coreex-validator.md new file mode 100644 index 00000000..4ef2a261 --- /dev/null +++ b/.claude/commands/coreex-validator.md @@ -0,0 +1,14 @@ +--- +description: "Create or modify a CoreEx validator in the Application layer. USE FOR: new Validator (no injection), new Validator with constructor injection, AbstractValidator (FluentValidation-style), adding rules to an existing validator, nested entity/collection/dictionary validators. DO NOT USE FOR: domain invariants in aggregates, FluentValidation NuGet package, Infrastructure-layer checks." +allowed-tools: [Read, Glob, Grep, Edit, Write, Bash] +--- + + + +Read `.github/skills/coreex-validator/SKILL.md` and follow the instructions in that file. diff --git a/.github/SKILL_AUTHORING.md b/.github/SKILL_AUTHORING.md index 543a90e1..7afb85dc 100644 --- a/.github/SKILL_AUTHORING.md +++ b/.github/SKILL_AUTHORING.md @@ -90,6 +90,24 @@ Rules: concrete sample names only in prose, clearly framed as examples ("e.g. …"). - Verify every link before committing; prefer the always-present targets (1, 2) as the backbone. +## Shipping a New `coreex-*` Skill + +Every `coreex-*` skill that ships to consumer repos needs three more pieces kept in sync by name (`coreex-`), +none of which live under `skills/`: + +1. **Claude command wrapper** — `.claude/commands/coreex-.md`: frontmatter `description` copied verbatim from + the skill's `description`, plus `allowed-tools: [Read, Glob, Grep, Edit, Write, Bash]`, then a one-line body + ("Read `.github/skills/coreex-/SKILL.md` and follow the instructions in that file."). This is required + because Claude Code only exposes `/` commands from `.claude/commands/`, not `.github/skills/`. +2. **Copilot prompt** — `.github/prompts/coreex-.prompt.md` (see existing prompts for the thin-delegate shape). +3. **`CoreEx.Template.csproj` Copy block** — add the skill's files (and its `.claude/commands/coreex-.md` + wrapper) to the `CopyTemplateAiContext` target's `SourceFiles`/`DestinationFiles` lists so `dotnet new coreex-ai` + actually ships it. Add matching `FilesPresent` assertions to `tools/validate-template-pack.ps1`. + +A skill missing any of these three is invocable in this repo but silently absent, or Claude-Code-invisible, in every +consumer repo — a gap this checklist exists to prevent recurring (see `coreex-scaffold` and the 17 L1/L2 skills that +originally shipped without a `.claude/commands` wrapper). + ## Frontmatter Requirements All SKILL.md files must include: diff --git a/.github/agents/README.md b/.github/agents/README.md index 6148237a..ee495880 100644 --- a/.github/agents/README.md +++ b/.github/agents/README.md @@ -43,7 +43,7 @@ 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 + └── .github/docs/coreex/agents/*.md ← 18 per-package AI guides │ read manifest referenced-packages to distinguish: @@ -74,7 +74,7 @@ The `referenced-packages` field in the manifest lets the agent distinguish betwe | `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/agents/`** — 18 per-package AI usage guides: the base `CoreEx` package plus one per `src/CoreEx.*` package (including `CoreEx.Cosmos`, which is newly published as a preview-quality package — its API surface may still change without following strict semver until it stabilizes). All 18 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`. @@ -88,12 +88,12 @@ The `referenced-packages` field in the manifest lets the agent distinguish betwe --- -## Why sync all 17 package guides unconditionally +## Why sync all 18 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 17 guides are small markdown files — the total download is negligible. +- All 18 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. @@ -120,7 +120,7 @@ dotnet new coreex-ai --app-folder - `.github/instructions/` — the scoped, auto-injected instruction files - `.github/prompts/` — the `coreex-scaffold` prompt plus one prompt per per-capability (L1) skill -- `.github/skills/` — the full skill suite (`coreex-docs-sync`, `acquire-codebase-knowledge`, `coreex-solution-scaffolder`, `aspire`, and the 14 L1 skills) +- `.github/skills/` — the full skill suite (`coreex-docs-sync`, `acquire-codebase-knowledge`, `coreex-scaffold`, `aspire`, and the 15 L1 skills) - `.github/agents/coreex-expert.agent.md` — this agent - `.claude/commands/` — the Claude Code equivalents - `.github/docs/coreex/` — the local docs cache (architecture docs + per-package guides) the expert reads first, already populated at the pinned version — no separate sync step needed on first install diff --git a/.github/agents/coreex-expert.agent.md b/.github/agents/coreex-expert.agent.md index 1795e177..0f865b88 100644 --- a/.github/agents/coreex-expert.agent.md +++ b/.github/agents/coreex-expert.agent.md @@ -146,7 +146,7 @@ These skills are part of the CoreEx AI workflow set and live in `.github/skills/ **Broader routing:** -- Greenfield solution or host scaffolding, or adding a .NET Aspire AppHost for local multi-host orchestration/dashboard → `/coreex-scaffold` (`coreex-solution-scaffolder` skill), which runs the matching [CoreEx.Template](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.Template/README.md) `dotnet new coreex*` commands. +- Greenfield solution or host scaffolding, or adding a .NET Aspire AppHost for local multi-host orchestration/dashboard → `/coreex-scaffold` (`coreex-scaffold` skill), which runs the matching [CoreEx.Template](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.Template/README.md) `dotnet new coreex*` commands. - Repo mapping or onboarding documentation → `/acquire-codebase-knowledge`. - Retrofit that no single skill covers → inspect the current code and recommend the smallest manual changes aligned to the samples and instructions. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 29e74647..5b0527a3 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -58,6 +58,7 @@ podman compose -f docker-compose.yml up -d # Podman preferred; `docker compose | `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` | +| `cosmos-emulator` | 8081, 10251-10254 | Azure Cosmos DB emulator; backs the Customers domain database and `CoreEx.Cosmos.Test.Unit`; under rootless Podman prefer `--privileged` or host networking if it doesn't come up cleanly (see `docker-compose.yml` comment) | | `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. @@ -68,8 +69,8 @@ Connection strings for each service in development are in each host's `appsettin - **Host layers** (composition roots, no business logic): `*.Api`, `*.Relay`, `*.Subscribe`. - **Design-time tooling** (no runtime presence): `*.CodeGen` (generates reference-data layer from `ref-data.yaml`) and `*.Database` (schema, seeding, outbox infrastructure via DbEx). - **Sample flow**: Controllers → `WebApi` helpers → Application services (validate + `IUnitOfWork`) → Infrastructure repositories (EF + explicit mappers) → transactional outbox → relay publishes to Service Bus → subscribers consume. -- **Polyglot data**: Products uses PostgreSQL (`CoreEx.Database.Postgres` + `CoreEx.EntityFrameworkCore`); Shopping uses SQL Server (`CoreEx.Database.SqlServer` + `CoreEx.EntityFrameworkCore`). Layers above Infrastructure are database-agnostic. -- **Primary domains**: Products and Shopping complete; Orders WIP. See `samples\README.md` for topology. +- **Polyglot data**: Products uses PostgreSQL (`CoreEx.Database.Postgres` + `CoreEx.EntityFrameworkCore`); Shopping uses SQL Server (`CoreEx.Database.SqlServer` + `CoreEx.EntityFrameworkCore`); Customers uses Azure Cosmos DB (`CoreEx.Cosmos` — preview, API surface may still change without following strict semver until it stabilizes; direct SDK access, no EF Core, no `*.Database` project; containers are code-first via `ReplaceOrCreateContainerAsync`). Layers above Infrastructure are database-agnostic. +- **Primary domains**: Products and Shopping complete; Customers (Cosmos DB) demonstrates typed CRUD + transactional outbox but has no Relay/Subscribe host yet; Orders WIP. See `samples\README.md` for topology. - **Aspire**: orchestrates all sample hosts in `samples\aspire\Contoso.Aspire\AppHost.cs` for local distributed development and E2E testing. ## Key Conventions That Matter in This Repo diff --git a/.github/coreex-ai-workflows.md b/.github/coreex-ai-workflows.md index 25d31401..ba81970b 100644 --- a/.github/coreex-ai-workflows.md +++ b/.github/coreex-ai-workflows.md @@ -18,7 +18,7 @@ This folder contains the AI artefacts that give GitHub Copilot and Claude Code a | 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. | +| Skills | `skills/*/SKILL.md` | Reasoning-based commands for open-ended tasks. GitHub Copilot auto-discovers `.github/skills/*/SKILL.md` directly. Claude Code does not — it only exposes `/` commands from `.claude/commands/*.md` — so every `coreex-*` skill ships a matching thin-delegate wrapper there (`.claude/commands/coreex-.md`, "Read `.github/skills/coreex-/SKILL.md` and follow it") for true `/coreex-` invocation in Claude Code. | | Authoring guides | `INSTRUCTION_AUTHORING.md`, `SKILL_AUTHORING.md` — present in this repo only; not copied into consumer repos by `dotnet new coreex-ai`. | Standards for writing new instruction files and skills. | ## Agent @@ -28,7 +28,7 @@ This folder contains the AI artefacts that give GitHub Copilot and Claude Code a - 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 17 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. +The agent uses a local doc cache (populated by `/coreex-docs-sync`) to avoid live GitHub fetches on every question. It covers all 18 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 @@ -55,14 +55,15 @@ Instructions are passive — no action is needed to activate them. The global fi Three artefact types cooperate, each with a distinct job: - **Instruction** (`instructions/*.instructions.md`) — invariant rules that are *auto-injected* whenever an edited file matches the instruction's `applyTo` glob. You never invoke them; they passively shape every edit to a matching file. -- **Skill** (`skills/coreex-*/`) — a reasoning *workflow* you invoke explicitly (`/coreex-` in Claude Code) to create or modify something. Each skill owns a `SKILL.md` and a `references/workflow.md` that drive the interaction. -- **Prompt** (`prompts/coreex-*.prompt.md`) — the GitHub Copilot entry point that *delegates to the matching skill's workflow*. Skills and prompts map 1:1 by name, so `/coreex-contract` (Claude Code skill) and `coreex-contract.prompt.md` (Copilot prompt) run the same workflow. +- **Skill** (`skills/coreex-*/`) — a reasoning *workflow* you invoke explicitly to create or modify something. Each skill owns a `SKILL.md` and a `references/workflow.md` that drive the interaction. GitHub Copilot invokes a skill directly by reading `SKILL.md`; Claude Code requires the matching `.claude/commands/coreex-.md` thin wrapper (see below) for `/coreex-` to work. +- **Prompt** (`prompts/coreex-*.prompt.md`) — the GitHub Copilot entry point that *delegates to the matching skill's workflow*. Skills and prompts map 1:1 by name, so `/coreex-contract` and `coreex-contract.prompt.md` run the same workflow. +- **Claude command** (`.claude/commands/coreex-*.md`) — the Claude Code entry point, mirroring the prompt's role: a thin wrapper ("Read `.github/skills/coreex-/SKILL.md` and follow it") that makes `/coreex-` work in Claude Code. Every `coreex-*` skill ships one, 1:1 by name — this is what gives Claude Code and Copilot equivalent slash-command coverage over the same skill catalog. **Feature Configuration.** A CoreEx solution's project-wide choices — `data-provider`, `refdata-enabled`, `rop-enabled`, `outbox-enabled`, `messaging-provider` — are persisted in the solution-root `AGENTS.md` **Feature Configuration** block. Skills read that block before asking anything, so recorded decisions are not re-prompted from one skill to the next. Whether a Domain layer is present is inferred from the existence of the `src/*.Domain/` project rather than a flag, since the Domain layer is now an independent addon template (`coreex-domain`). ### Version-pin discipline -`CoreEx.Template` and the core `CoreEx` package are released from the same repo at the same version number — pinning one always means pinning the other. Every `dotnet new install CoreEx.Template` invocation — in `AGENTS.md`'s Cold Start walkthrough, `coreex-solution-scaffolder`, `coreex-docs-sync`, or anywhere else — must carry an explicit `::`, resolved as follows, and must never fall through to a bare/latest install: +`CoreEx.Template` and the core `CoreEx` package are released from the same repo at the same version number — pinning one always means pinning the other. Every `dotnet new install CoreEx.Template` invocation — in `AGENTS.md`'s Cold Start walkthrough, `coreex-scaffold`, `coreex-docs-sync`, or anywhere else — must carry an explicit `::`, resolved as follows, and must never fall through to a bare/latest install: - **Project already references a `CoreEx` NuGet package** (check `Directory.Packages.props`, `*.csproj`, `Directory.Build.props`): pin to that exact version. This is the common case for a refresh (`/coreex-docs-sync`) or adding a host to an existing solution. - **No `CoreEx` reference yet** (true first-time adoption): resolve the latest stable release explicitly (e.g. `dotnet package search CoreEx.Template --exact-match`, or the [NuGet.org listing](https://www.nuget.org/packages/CoreEx.Template)) and pin to that specific version rather than letting install resolve silently to "whatever is latest right now." @@ -74,14 +75,14 @@ A version mismatch between the installed AI-asset bundle (`.github/docs/coreex/m | Command | Type | What it does | |---------|------|-------------| | [`CoreEx.Template`](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.Template/README.md) | Template pack | Deterministic `dotnet new` scaffolding for a CoreEx solution plus API, relay, and subscriber hosts. Always install with an explicit pinned version — `dotnet new install CoreEx.Template::` (see [Version-pin discipline](#version-pin-discipline)) — then run the `coreex*` templates in a terminal. `dotnet new coreex-ai` installs the full AI workflow set (instructions, prompts, skills, the `coreex-expert` agent, `.claude/commands/`, and the `.github/docs/coreex/` docs cache, self-describing via `manifest.txt`) into a consuming project as one version-pinned bundle. | -| [`/acquire-codebase-knowledge`](./skills/acquire-codebase-knowledge/README.md) | Skill | Maps an unfamiliar codebase and produces seven structured onboarding documents. | -| [`/coreex-scaffold`](./skills/coreex-solution-scaffolder/README.md) · [prompt](./prompts/coreex-scaffold.prompt.md) | Skill + prompt | Guides greenfield solution scaffolding, chooses the smallest safe CoreEx.Template shape, and runs the matching `dotnet new coreex*` commands. | +| [`/acquire-codebase-knowledge`](./skills/acquire-codebase-knowledge/README.md) | Skill | Maps an unfamiliar codebase and produces seven structured onboarding documents. Generic (no CoreEx dependency) and framework-repo-only — not copied into consumer repos by `dotnet new coreex-ai`. | +| [`/coreex-scaffold`](./skills/coreex-scaffold/README.md) · [prompt](./prompts/coreex-scaffold.prompt.md) | Skill + prompt | Guides greenfield solution scaffolding, chooses the smallest safe CoreEx.Template shape, and runs the matching `dotnet new coreex*` commands. | | [`/coreex-docs-sync`](./skills/coreex-docs-sync/README.md) | Skill | Refreshes the whole AI asset bundle (instructions, skills, prompts, agent, `.claude/commands/`, and the `.github/docs/coreex/` docs cache) as one version-pinned `dotnet new coreex-ai --force` reinstall, matching the project's referenced `CoreEx` NuGet version. No live GitHub `main` fetch. | -| [`/aspire`](./skills/aspire/README.md) | Skill | Orchestrates Aspire distributed apps locally: start, stop, logs, debug. | +| [`/aspire`](./skills/aspire/README.md) | Skill | Orchestrates Aspire distributed apps locally: start, stop, logs, debug. Generic (wraps the Aspire CLI, no CoreEx dependency) and framework-repo-only — not copied into consumer repos by `dotnet new coreex-ai`. | #### Per-capability skills (L1) -Fifteen skills add or modify a single CoreEx capability on an existing solution. Each is invoked as `/coreex-` in Claude Code, or via the matching [`prompts/coreex-.prompt.md`](./prompts/) in Copilot (1:1 by name). Every skill reads the solution-root `AGENTS.md` **Feature Configuration** first to avoid redundant questioning. +Fifteen skills add or modify a single CoreEx capability on an existing solution. Each is invoked as `/coreex-` in Claude Code (via its `.claude/commands/coreex-.md` thin wrapper) or via the matching [`prompts/coreex-.prompt.md`](./prompts/) in Copilot — 1:1 by name across all three (skill, prompt, Claude command). Every skill reads the solution-root `AGENTS.md` **Feature Configuration** first to avoid redundant questioning. | Skill / prompt | Capability | |----------------|-----------| diff --git a/.github/instructions/coreex-host-setup.instructions.md b/.github/instructions/coreex-host-setup.instructions.md index 3145b4c6..6d2309f3 100644 --- a/.github/instructions/coreex-host-setup.instructions.md +++ b/.github/instructions/coreex-host-setup.instructions.md @@ -18,7 +18,7 @@ The host is a **composition root only** — no business logic. There are three h > **Split vs. consolidate:** Api/Relay/Subscribe as separate processes is a workload-isolation convention, not a technical requirement — see [Hosts Layer Guide § Choosing a Host Topology](/.github/docs/coreex/hosts-layer.md#choosing-a-host-topology-split-vs-consolidate) before assuming a small/low-traffic solution needs all three. -> **Related skill:** to scaffold a solution or an additional host (Api / Subscribe / Relay), invoke the [`coreex-solution-scaffolder`](/.github/skills/coreex-solution-scaffolder/SKILL.md) skill. +> **Related skill:** to scaffold a solution or an additional host (Api / Subscribe / Relay), invoke the [`coreex-scaffold`](/.github/skills/coreex-scaffold/SKILL.md) skill. > This file holds the invariants that must hold on **any** edit to a host `Program.cs`; the skill drives the > step-by-step **creation** procedure. (The per-host "Scaffolding an … host" blocks below stay here — they carry the > Feature Configuration recover/record guardrails that must be honoured whenever a host is added.) diff --git a/.github/prompts/coreex-scaffold.prompt.md b/.github/prompts/coreex-scaffold.prompt.md index f7e3d18b..bc66a7a0 100644 --- a/.github/prompts/coreex-scaffold.prompt.md +++ b/.github/prompts/coreex-scaffold.prompt.md @@ -12,10 +12,10 @@ description: Guide me through choosing and running the right CoreEx.Template dot Guide this workspace through CoreEx scaffolding. -Use `.github/skills/coreex-solution-scaffolder/SKILL.md` and its referenced workflow as the authoritative workflow contract when they exist. +Use `.github/skills/coreex-scaffold/SKILL.md` and its referenced workflow as the authoritative workflow contract when they exist. Operational contract: -- Use `.github/skills/coreex-solution-scaffolder/SKILL.md` and its referenced workflow as the sole source of truth for interview order, defaults, and guardrails. +- Use `.github/skills/coreex-scaffold/SKILL.md` and its referenced workflow as the sole source of truth for interview order, defaults, and guardrails. - Keep the interview deterministic: one scaffold question per turn, exactly one editable field per confirmation card, and wait for confirmation before moving on. - Preserve the skill's multiple-choice format: use a `text` field only for the base solution name and `select` fields for all other interview questions when confirmation cards are available. - Inspect the workspace before scaffolding, run the safest dry-run path before real template commands, and scaffold only the smallest safe CoreEx shape. diff --git a/.github/skills/coreex-api/SKILL.md b/.github/skills/coreex-api/SKILL.md index aae95f02..0ee150a7 100644 --- a/.github/skills/coreex-api/SKILL.md +++ b/.github/skills/coreex-api/SKILL.md @@ -1,6 +1,6 @@ --- name: coreex-api -description: "Add or modify a CoreEx API controller (or Minimal API endpoint) in an *.Api host. USE FOR: scaffolding the MVC controller pair (XxxController + XxxReadController), GET/query/schema endpoints, POST create, PUT + PATCH full-entity update, DELETE, and custom business-action endpoints. Covers both exception-based and Result service styles, and Minimal API as an alternative to MVC. DO NOT USE FOR: Api host setup / Program.cs (use coreex-solution-scaffolder), application services (use coreex-app-service), API integration tests (use coreex-test-api)." +description: "Add or modify a CoreEx API controller (or Minimal API endpoint) in an *.Api host. USE FOR: scaffolding the MVC controller pair (XxxController + XxxReadController), GET/query/schema endpoints, POST create, PUT + PATCH full-entity update, DELETE, and custom business-action endpoints. Covers both exception-based and Result service styles, and Minimal API as an alternative to MVC. DO NOT USE FOR: Api host setup / Program.cs (use coreex-scaffold), application services (use coreex-app-service), API integration tests (use coreex-test-api)." argument-hint: "Optional: entity name, operations needed (get/query/create/update/delete/custom), exception-based or Result service style, MVC or Minimal API" tags: ["api", "controller", "mvc", "minimal-api", "webapi", "routing", "cqrs", "coreex"] --- @@ -29,7 +29,7 @@ Guides you through adding or modifying HTTP API endpoints in an `*.Api` host. Co ## When Not to Use -- Api host setup and `Program.cs` composition — use `coreex-solution-scaffolder` (see [`/.github/instructions/coreex-host-setup.instructions.md`](/.github/instructions/coreex-host-setup.instructions.md)) +- Api host setup and `Program.cs` composition — use `coreex-scaffold` (see [`/.github/instructions/coreex-host-setup.instructions.md`](/.github/instructions/coreex-host-setup.instructions.md)) - Application service creation — use `coreex-app-service` - API integration tests — use `coreex-test-api` (hand off once the endpoint is implemented) - Subscriber or relay hosts — controllers do not belong there @@ -67,8 +67,8 @@ For full workflow and code examples see [`references/workflow.md`](references/wo ## Key References - [`/.github/instructions/coreex-api-controllers.instructions.md`](/.github/instructions/coreex-api-controllers.instructions.md) — authoritative conventions: MVC vs Minimal API, WebApi helpers, attributes, route parameter rules, CQRS split -- [`/.github/instructions/coreex-host-setup.instructions.md`](/.github/instructions/coreex-host-setup.instructions.md) — Api host / `Program.cs` composition (scaffolded by `coreex-solution-scaffolder`) -- Related skills: [`coreex-app-service`](../coreex-app-service/SKILL.md) (controllers delegate to it), [`coreex-test-api`](../coreex-test-api/SKILL.md) (integration tests for these endpoints), [`coreex-subscriber`](../coreex-subscriber/SKILL.md) (sibling host entry point), [`coreex-solution-scaffolder`](../coreex-solution-scaffolder/SKILL.md) (host setup), [`coreex-graphql`](../coreex-graphql/SKILL.md) (optional GraphQL-lite root alongside a query endpoint) +- [`/.github/instructions/coreex-host-setup.instructions.md`](/.github/instructions/coreex-host-setup.instructions.md) — Api host / `Program.cs` composition (scaffolded by `coreex-scaffold`) +- Related skills: [`coreex-app-service`](../coreex-app-service/SKILL.md) (controllers delegate to it), [`coreex-test-api`](../coreex-test-api/SKILL.md) (integration tests for these endpoints), [`coreex-subscriber`](../coreex-subscriber/SKILL.md) (sibling host entry point), [`coreex-scaffold`](../coreex-scaffold/SKILL.md) (host setup), [`coreex-graphql`](../coreex-graphql/SKILL.md) (optional GraphQL-lite root alongside a query endpoint) - Illustrative examples (CoreEx sample — not present in your project): - [`ProductController` + `ProductReadController`](https://github.com/Avanade/CoreEx/tree/main/samples/src/Contoso.Products.Api/Controllers) — exception-based, full CRUD + query + $query - [`BasketController` + `BasketReadController`](https://github.com/Avanade/CoreEx/tree/main/samples/src/Contoso.Shopping.Api/Controllers) — Result<T> style, custom business actions, cross-tagged nested route diff --git a/.github/skills/coreex-docs-sync/README.md b/.github/skills/coreex-docs-sync/README.md index c86ce890..874d2054 100644 --- a/.github/skills/coreex-docs-sync/README.md +++ b/.github/skills/coreex-docs-sync/README.md @@ -51,7 +51,7 @@ No arguments required. ``` .github/instructions/*.instructions.md -.github/skills/** ← coreex-docs-sync, coreex-solution-scaffolder, all L1 skills, and all L2 skills +.github/skills/** ← coreex-docs-sync, coreex-scaffold, all L1 skills, and all L2 skills .github/prompts/*.prompt.md .github/agents/coreex-expert.agent.md .claude/commands/** @@ -76,6 +76,7 @@ No arguments required. CoreEx.Azure.Messaging.ServiceBus.md CoreEx.Caching.FusionCache.md CoreEx.CodeGen.md + CoreEx.Cosmos.md CoreEx.Data.md CoreEx.Data.GraphQL.md CoreEx.Database.md @@ -90,7 +91,7 @@ No arguments required. ``` All of the above are part of the one `dotnet new coreex-ai --force` bundle — none are fetched or -written independently. All 17 package guides are always present regardless of which packages the +written independently. All 18 package guides are always present regardless of which packages the project currently references, so the expert can recommend adopting a new one with full knowledge of what it offers; it reads the project's actual package references live each session (no longer cached) to tell "already in use" from "you'd need to add this." diff --git a/.github/skills/coreex-docs-sync/SKILL.md b/.github/skills/coreex-docs-sync/SKILL.md index bc8fb7cb..1cc1bb69 100644 --- a/.github/skills/coreex-docs-sync/SKILL.md +++ b/.github/skills/coreex-docs-sync/SKILL.md @@ -76,6 +76,7 @@ Keeps `.github/instructions/`, `.github/skills/`, `.github/prompts/`, `.github/a CoreEx.Azure.Messaging.ServiceBus.md CoreEx.Caching.FusionCache.md CoreEx.CodeGen.md + CoreEx.Cosmos.md CoreEx.Data.md CoreEx.Data.GraphQL.md CoreEx.Database.md diff --git a/.github/skills/coreex-graphql/SKILL.md b/.github/skills/coreex-graphql/SKILL.md index 6cf0c08f..4feced95 100644 --- a/.github/skills/coreex-graphql/SKILL.md +++ b/.github/skills/coreex-graphql/SKILL.md @@ -1,6 +1,6 @@ --- name: coreex-graphql -description: "Add or extend GraphQL-lite query support (CoreEx.Data.GraphQL) on a CoreEx *.Api host. USE FOR: first-time AddCoreExGraphQLLite/MapCoreExGraphQLLite wiring in Program.cs, registering AddQuery/AddGet roots over an entity's existing QueryArgsConfig, bulk-exposing reference data via AddReferenceDataQueries, adding a matching GraphQL root for an endpoint coreex-api just scaffolded, recording GraphQL enablement in the host's AGENTS.md. DO NOT USE FOR: REST controller endpoints (use coreex-api), defining or changing a QueryArgsConfig itself (use coreex-repository or coreex-refdata), general Program.cs setup unrelated to GraphQL (use coreex-solution-scaffolder)." +description: "Add or extend GraphQL-lite query support (CoreEx.Data.GraphQL) on a CoreEx *.Api host. USE FOR: first-time AddCoreExGraphQLLite/MapCoreExGraphQLLite wiring in Program.cs, registering AddQuery/AddGet roots over an entity's existing QueryArgsConfig, bulk-exposing reference data via AddReferenceDataQueries, adding a matching GraphQL root for an endpoint coreex-api just scaffolded, recording GraphQL enablement in the host's AGENTS.md. DO NOT USE FOR: REST controller endpoints (use coreex-api), defining or changing a QueryArgsConfig itself (use coreex-repository or coreex-refdata), general Program.cs setup unrelated to GraphQL (use coreex-scaffold)." argument-hint: "Optional: host name, entity/entities to expose, whether to bulk-expose reference data" tags: ["graphql", "query", "api", "webapi", "coreex"] --- @@ -28,7 +28,7 @@ Guides you through adding `CoreEx.Data.GraphQL` (GraphQL-lite) to an `*.Api` hos - REST controller/endpoint work — use `coreex-api` - Defining or changing a `QueryArgsConfig` itself — use `coreex-repository` (entity queries) or `coreex-refdata` (reference data queries). GraphQL-lite only bridges to the existing config; it never adds new filter/sort capability of its own -- General host `Program.cs` setup unrelated to GraphQL — use `coreex-solution-scaffolder` +- General host `Program.cs` setup unrelated to GraphQL — use `coreex-scaffold` - Subscribe/Relay hosts — GraphQL-lite is a REST-adjacent query surface for `*.Api` hosts only ## Quick Reference @@ -61,6 +61,6 @@ For full workflow and code examples see [`references/workflow.md`](references/wo - [`/.github/instructions/coreex-host-setup.instructions.md`](/.github/instructions/coreex-host-setup.instructions.md) — `Program.cs` composition, GraphQL registration pattern - [`/.github/instructions/coreex-api-controllers.instructions.md`](/.github/instructions/coreex-api-controllers.instructions.md) — `QueryArgsConfig`/`QueryAsync` conventions that GraphQL-lite bridges to -- Related skills: [`coreex-api`](../coreex-api/SKILL.md) (REST alternative/companion — hand off here after adding a REST query endpoint), [`coreex-repository`](../coreex-repository/SKILL.md) and [`coreex-refdata`](../coreex-refdata/SKILL.md) (own the `QueryArgsConfig` that GraphQL-lite bridges to), [`coreex-solution-scaffolder`](../coreex-solution-scaffolder/SKILL.md) (host setup) +- Related skills: [`coreex-api`](../coreex-api/SKILL.md) (REST alternative/companion — hand off here after adding a REST query endpoint), [`coreex-repository`](../coreex-repository/SKILL.md) and [`coreex-refdata`](../coreex-refdata/SKILL.md) (own the `QueryArgsConfig` that GraphQL-lite bridges to), [`coreex-scaffold`](../coreex-scaffold/SKILL.md) (host setup) - [`CoreEx.Data.GraphQL` AGENTS.md (CoreEx sample — illustrative, not in your project)](https://github.com/Avanade/CoreEx/blob/main/src/CoreEx.Data.GraphQL/AGENTS.md) — full registration API, query syntax, and non-goals - [Products sample `Program.cs` (CoreEx sample — illustrative)](https://github.com/Avanade/CoreEx/blob/main/samples/src/Contoso.Products.Api/Program.cs) — working `AddCoreExGraphQLLite`/`MapCoreExGraphQLLite` example diff --git a/.github/skills/coreex-solution-scaffolder/README.md b/.github/skills/coreex-scaffold/README.md similarity index 95% rename from .github/skills/coreex-solution-scaffolder/README.md rename to .github/skills/coreex-scaffold/README.md index a87eedb8..d0ba7557 100644 --- a/.github/skills/coreex-solution-scaffolder/README.md +++ b/.github/skills/coreex-scaffold/README.md @@ -31,7 +31,7 @@ Guides a developer through selecting the right `CoreEx.Template` scaffolding sha If the prompt file is not present, attach the skill file directly in Copilot Chat: ``` -#file:.github/skills/coreex-solution-scaffolder/SKILL.md scaffold a new CoreEx solution for my requirements +#file:.github/skills/coreex-scaffold/SKILL.md scaffold a new CoreEx solution for my requirements ``` ## What it will do diff --git a/.github/skills/coreex-solution-scaffolder/SKILL.md b/.github/skills/coreex-scaffold/SKILL.md similarity index 99% rename from .github/skills/coreex-solution-scaffolder/SKILL.md rename to .github/skills/coreex-scaffold/SKILL.md index 4685d6d7..0fb7a243 100644 --- a/.github/skills/coreex-solution-scaffolder/SKILL.md +++ b/.github/skills/coreex-scaffold/SKILL.md @@ -1,5 +1,5 @@ --- -name: coreex-solution-scaffolder +name: coreex-scaffold description: "Guide a developer through CoreEx solution shaping after bootstrap, using a short plain-English interview that turns user answers into safe dotnet new template inputs. USE FOR: bootstrap-only repos, deciding API-only vs API plus relay vs API plus subscriber, choosing SQL Server vs Postgres vs no database, choosing refdata/outbox/DDD/ROP options, installing CoreEx.Template, checking current solution shape, adding missing Api/Relay/Subscribe hosts to an existing repo, adding a .NET Aspire AppHost for local multi-host orchestration and dashboard visibility, and optionally preparing a first local runnable state with local dependency assets plus database/code-generation steps. DO NOT USE FOR: unrelated runtime debugging, bootstrap creation, or forcing root re-scaffolding over an existing solution. INVOKES: workspace inspection, ask-questions style interviews, dotnet new install/list, dry-run validation, solution wiring, optional local dependency asset creation, focused build/test validation, and either template generation or manual retrofit work depending on repo shape." argument-hint: "Optional: base solution name, whether this is new or retrofit, required hosts, database choice, messaging needs, and whether to add an Aspire AppHost for local orchestration." tags: ["coreex", "scaffolding", "retrofit", "template", "hosts", "aspire", "apphost", "orchestration"] diff --git a/.github/skills/coreex-solution-scaffolder/assets/README.md b/.github/skills/coreex-scaffold/assets/README.md similarity index 98% rename from .github/skills/coreex-solution-scaffolder/assets/README.md rename to .github/skills/coreex-scaffold/assets/README.md index 446e9320..88b55ebe 100644 --- a/.github/skills/coreex-solution-scaffolder/assets/README.md +++ b/.github/skills/coreex-scaffold/assets/README.md @@ -6,7 +6,7 @@ directly — propose the change upstream in Avanade/CoreEx instead, then refresh once it is released. --> -# coreex-solution-scaffolder assets +# coreex-scaffold assets Static local-dev infrastructure fallbacks bundled with this skill: `docker-compose.local.yml` and `servicebus-config.template.json`. diff --git a/.github/skills/coreex-solution-scaffolder/assets/docker-compose.local.yml b/.github/skills/coreex-scaffold/assets/docker-compose.local.yml similarity index 97% rename from .github/skills/coreex-solution-scaffolder/assets/docker-compose.local.yml rename to .github/skills/coreex-scaffold/assets/docker-compose.local.yml index 8ce4ab29..8aaebfd0 100644 --- a/.github/skills/coreex-solution-scaffolder/assets/docker-compose.local.yml +++ b/.github/skills/coreex-scaffold/assets/docker-compose.local.yml @@ -1,4 +1,4 @@ -# Static, full-featured local-dev fallback bundled with the coreex-solution-scaffolder skill — see +# Static, full-featured local-dev fallback bundled with the coreex-scaffold skill — see # README.md (this directory) for what this is, where it came from, and when to reconcile it. name: CoreEx diff --git a/.github/skills/coreex-solution-scaffolder/assets/servicebus-config.template.json b/.github/skills/coreex-scaffold/assets/servicebus-config.template.json similarity index 100% rename from .github/skills/coreex-solution-scaffolder/assets/servicebus-config.template.json rename to .github/skills/coreex-scaffold/assets/servicebus-config.template.json diff --git a/.github/skills/coreex-solution-scaffolder/references/workflow.md b/.github/skills/coreex-scaffold/references/workflow.md similarity index 97% rename from .github/skills/coreex-solution-scaffolder/references/workflow.md rename to .github/skills/coreex-scaffold/references/workflow.md index 4e6e5ded..cf05fb7f 100644 --- a/.github/skills/coreex-solution-scaffolder/references/workflow.md +++ b/.github/skills/coreex-scaffold/references/workflow.md @@ -365,7 +365,7 @@ For local development there are two validation modes: - `dotnet test` for `tests/[solution].Test.Unit` should run by default when the project exists. - API, relay, and subscriber tests may depend on local SQL Server or Postgres, Redis, Service Bus, or other host prerequisites. If those are not configured, skip those tests and say why. - A missing local dependency is a deferred setup item, not necessarily a scaffolding failure. -- If the user wants runnable local validation and local dependency files are missing, materialize them first from bundled templates or repo-standard equivalents, for example `.github/skills/coreex-solution-scaffolder/assets/docker-compose.local.yml` and `.github/skills/coreex-solution-scaffolder/assets/servicebus-config.template.json`. These are a static, options-agnostic fallback for use when it is unsafe to re-run the root scaffold — see `.github/skills/coreex-solution-scaffolder/assets/README.md` for why they exist and how they relate to the scaffold's own generated `docker-compose.yml`/`servicebus/Config.json`. +- If the user wants runnable local validation and local dependency files are missing, materialize them first from bundled templates or repo-standard equivalents, for example `.github/skills/coreex-scaffold/assets/docker-compose.local.yml` and `.github/skills/coreex-scaffold/assets/servicebus-config.template.json`. These are a static, options-agnostic fallback for use when it is unsafe to re-run the root scaffold — see `.github/skills/coreex-scaffold/assets/README.md` for why they exist and how they relate to the scaffold's own generated `docker-compose.yml`/`servicebus/Config.json`. - If the generated shape includes SQL Server, Redis, or Service Bus and the repo lacks a root `docker-compose.yml`, create one before trying to run broader tests. - If Service Bus emulator wiring is needed and the repo lacks `servicebus/Config.json`, create it before starting the dependency stack. - For `tools/[solution].Database`, run from that directory so `dbex.yaml` is discovered correctly. Prefer `dotnet run -- All` for first-run local setup and `dotnet run -- CodeGen` or `dotnet run -- Database` for narrower follow-up work. diff --git a/.github/skills/coreex-subscriber-e2e/SKILL.md b/.github/skills/coreex-subscriber-e2e/SKILL.md index 17984898..5ffee9ff 100644 --- a/.github/skills/coreex-subscriber-e2e/SKILL.md +++ b/.github/skills/coreex-subscriber-e2e/SKILL.md @@ -1,6 +1,6 @@ --- name: coreex-subscriber-e2e -description: "Create a complete new event or command subscriber end-to-end in a single guided workflow: event/command DTO contract (if new), subscriber handler, optional application service and repository, and integration tests. USE FOR: adding a brand-new subscriber to an existing CoreEx Subscribe host for any scenario — command handling, event-data-sync replication, or event-driven business-process choreography. DO NOT USE FOR: modifying an existing subscriber (use coreex-subscriber directly), API endpoint work (use coreex-api-e2e), or setting up the Subscribe host itself (use coreex-solution-scaffolder)." +description: "Create a complete new event or command subscriber end-to-end in a single guided workflow: event/command DTO contract (if new), subscriber handler, optional application service and repository, and integration tests. USE FOR: adding a brand-new subscriber to an existing CoreEx Subscribe host for any scenario — command handling, event-data-sync replication, or event-driven business-process choreography. DO NOT USE FOR: modifying an existing subscriber (use coreex-subscriber directly), API endpoint work (use coreex-api-e2e), or setting up the Subscribe host itself (use coreex-scaffold)." argument-hint: "Optional: event or command subject, subscriber scenario (command / event-data-sync / business-process)" tags: ["coreex", "subscriber", "event", "command", "end-to-end", "vertical-slice", "messaging"] --- @@ -26,7 +26,7 @@ Guides you through adding a complete new event or command subscriber in one sitt ## When Not to Use - Modifying an existing subscriber handler — invoke `coreex-subscriber` directly -- Setting up the Subscribe host itself (it does not yet exist) — use `coreex-solution-scaffolder` +- Setting up the Subscribe host itself (it does not yet exist) — use `coreex-scaffold` - Building an API endpoint — use `coreex-api-e2e` ## Workflow Overview diff --git a/AGENTS.md b/AGENTS.md index 5a5f7446..2a392e63 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,6 +33,7 @@ reference data, validation, and data access — into a consistent, composable ba | `CoreEx.EntityFrameworkCore` | EF Core integration, typed CRUD, `ValueConverter` bridges | | `CoreEx.RefData` | Typed reference data with hybrid-cache-backed orchestrator | | `CoreEx.Caching.FusionCache` | `IHybridCache` backed by ZiggyCreatures FusionCache (L1/L2 + Redis backplane) | +| `CoreEx.Cosmos` | **Preview — newly added; API surface may still change without following strict semver until it stabilizes.** Typed Azure Cosmos DB access: `CosmosDbContainer`/`CosmosDbMappedContainer` for CRUD + query with ETag/multi-tenancy/logical-delete support, a `TransactionalBatch`-based transactional outbox, and a Change Feed Processor-based outbox relay | | `CoreEx.Data` | OData-esque dynamic querying (`QueryArgs`/`PagingArgs`/`QueryArgsConfig`), `ItemsResult` | | `CoreEx.Data.GraphQL` | Transport-agnostic GraphQL-lite bridge (`IGraphQLEngine`) over `CoreEx.Data` querying + `JsonFilter` field projection; hosted via `CoreEx.AspNetCore`'s `MapCoreExGraphQLLite` | | `CoreEx.UnitTesting` | Fluent test toolkit: event assertions, outbox assertions, JSON seed data | @@ -126,7 +127,7 @@ For a **brand-new blank repository** (no `src/`, `tests/`, or `tools/` yet), run This installs: - `.github/instructions/` — scoped instruction files auto-injected by Copilot for each file type - `.github/prompts/` — the scaffolding prompt plus one `coreex-.prompt.md` per L1/L2 skill -- `.github/skills/` — the CoreEx skill suite: `coreex-bootstrap`, `coreex-docs-sync`, `coreex-solution-scaffolder`, the L1 skills +- `.github/skills/` — the CoreEx skill suite: `coreex-bootstrap`, `coreex-docs-sync`, `coreex-scaffold`, the L1 skills (`coreex-contract`, `coreex-refdata`, `coreex-db-migration`, `coreex-repository`, `coreex-adapter`, `coreex-app-service`, `coreex-validator`, `coreex-policy`, `coreex-aggregate`, `coreex-api`, `coreex-subscriber`, `coreex-test-api`, `coreex-test-subscribe`, `coreex-test-relay`), and the L2 end-to-end skills diff --git a/CHANGELOG.md b/CHANGELOG.md index b7d400bb..58e8ef18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ Represents the **NuGet** versions. - This release contains **significant breaking changes** - there is **no** upgrade path from the previous `v3.x` versions; however, the core capabilities and patterns remain largely consistent. - A number of capabilities have been removed as they were not widely used, considered legacy/obsolete, or there are better alternatives available. - Not all existing capabilities have been re-implemented in this release; the intention is to (re-)add further capabilities in future releases as required. -- Special call out to key contributors: [israels](https://github.com/israels) and [spruit-avanade](https://github.com/spruit-avanade). +- Special call out to key contributors: [israels](https://github.com/israels), [spruit-avanade](https://github.com/spruit-avanade) and [chullybun](https://github.com/chullybun). ## v3.* and earlier - These versions are now considered **legacy** and are no longer maintained; there are no further updates planned for these versions, including bug fixes or security patches. diff --git a/Directory.Packages.props b/Directory.Packages.props index e2dd24c5..16f6c58e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,82 +6,81 @@ - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - + + + + + + + + - + - - - + + + - - - + + + - + - - - - - + + + + + - - + + - - - - - - - - - + + + + + + + + + - - + + - + - - + + - + @@ -89,26 +88,26 @@ - - - - - + + + + + - - - - - - + + + + + + - - - + + + diff --git a/README.md b/README.md index c5e57f69..29ce8fee 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,7 @@ The remainder of this README is organized into the following sections. - [Design Principles](#design-principles) - [Key Capabilities](#key-capabilities) - [Patterns in Practice](#patterns-in-practice) -- [Version 4 (preview)](#version-4-preview) -- [Status](#status) +- [Build and Package Status](#build-and-package-status) - [Getting Started](#getting-started) - [Samples](#samples) - [AI](#ai) @@ -123,18 +122,7 @@ The [Pattern Catalog](./samples/docs/patterns.md) is the best entry point: it in → **[View the full pattern catalog](./samples/docs/patterns.md)** -## Version 4 (preview) - -This is a **major** version release; a re-imagine / re-invention of the existing capabilities to enable a more modern, flexible and maintainable codebase. -- This release contains **significant breaking changes** - there is **no** documented upgrade path from the previous `v3.x` versions; however, the core capabilities and patterns remain largely consistent. -- A number of capabilities have been removed as they were not widely used, considered legacy/obsolete, or there are better alternatives available. -- Not all existing capabilities have been re-implemented in this release; the intention is to (re-)add further capabilities in future releases as required. - -Version 4 is currently in **preview**; the packages are published with a `-preview` suffix and may contain future breaking changes. The packages in their current state can be used for Production-based solutions. Feedback is very welcome to help shape the final release. - -The Copilot and Claude [AI](#ai) integrations should be considered experimental and subject to change/improvements. - -## Status +## Build and Package Status The build status is [![CI](https://github.com/Avanade/CoreEx/actions/workflows/CI.yml/badge.svg)](https://github.com/Avanade/CoreEx/actions/workflows/CI.yml) with the NuGet package status as follows, including links to the underlying source code and documentation: @@ -145,6 +133,7 @@ Project/Package | Description | Source `CoreEx.AspNetCore.NSwag`
 
[![NuGet version](https://img.shields.io/nuget/vpre/CoreEx.AspNetCore.NSwag)](https://www.nuget.org/packages/CoreEx.AspNetCore.NSwag/absoluteLatest) | Provides the NSwag `IOperationProcessor` integration that reads CoreEx MVC attributes (`[Paging]`, `[Query]`, `[Accepts]`, `[IdempotencyKey]`, `[ProducesNotFoundProblem]`) and injects the corresponding parameters, request bodies, and response entries into the generated OpenAPI specification. | [Link](./src/CoreEx.AspNetCore.NSwag) `CoreEx.Azure.Messaging.ServiceBus`
 
[![NuGet version](https://img.shields.io/nuget/vpre/CoreEx.Azure.Messaging.ServiceBus)](https://www.nuget.org/packages/CoreEx.Azure.Messaging.ServiceBus/absoluteLatest) | Provides Azure Service Bus integration for CoreEx: a `ServiceBusPublisher` implementing `IEventPublisher`, subscriber bases wired to `EventSubscriberBase`, and receiver hosts with built-in resiliency, metrics, and session support. | [Link](./src/CoreEx.Azure.Messaging.ServiceBus) `CoreEx.Caching.FusionCache`
 
[![NuGet version](https://img.shields.io/nuget/vpre/CoreEx.Caching.FusionCache)](https://www.nuget.org/packages/CoreEx.Caching.FusionCache/absoluteLatest) | Provides a `FusionHybridCache` implementation of `IHybridCache` backed by the ZiggyCreatures FusionCache library, bridging CoreEx caching contracts to FusionCache's L1/L2 hybrid and backplane capabilities. | [Link](./src/CoreEx.Caching.FusionCache) +`CoreEx.Cosmos`
 
[![NuGet version](https://img.shields.io/nuget/vpre/CoreEx.Cosmos)](https://www.nuget.org/packages/CoreEx.Cosmos/absoluteLatest) | **Preview — newly added; API surface may still change without following strict semver until it stabilizes.** Provides the core Azure Cosmos DB access layer: `ICosmosDb`/`CosmosDb` as the CoreEx-Cosmos bridge, `CosmosDbContainer` and `CosmosDbMappedContainer` for typed CRUD + query operations with ETag/concurrency, multi-tenancy, logical-delete, and type-discriminator support, a `TransactionalBatch`-based transactional outbox (`CosmosDbUnitOfWork`), and a Change Feed Processor-based outbox relay. | [Link](./src/CoreEx.Cosmos) `CoreEx.Data`
 
[![NuGet version](https://img.shields.io/nuget/vpre/CoreEx.Data)](https://www.nuget.org/packages/CoreEx.Data/absoluteLatest) | Provides the `IUnitOfWork` transactional orchestration contract, `DataResult` mutation outcome types, data model base classes, and the `QueryArgsConfig` / `QueryFilterParser` / `QueryOrderByParser` pipeline for safe, explicitly-configured OData-style `$filter` and `$orderby` LINQ query translation. | [Link](./src/CoreEx.Data) `CoreEx.Data.GraphQL`
 
[![NuGet version](https://img.shields.io/nuget/vpre/CoreEx.Data.GraphQL)](https://www.nuget.org/packages/CoreEx.Data.GraphQL/absoluteLatest) | Provides a transport-agnostic "GraphQL-lite" bridge (`IGraphQLEngine`) over the existing `CoreEx.Data` querying pipeline: a single registered root field maps a GraphQL selection set to `QueryArgs`/`PagingArgs` and `JsonFilter` field projection, reusing each entity's existing `QueryArgsConfig` — no new per-entity resolver code required. | [Link](./src/CoreEx.Data.GraphQL) `CoreEx.Database`
 
[![NuGet version](https://img.shields.io/nuget/vpre/CoreEx.Database)](https://www.nuget.org/packages/CoreEx.Database/absoluteLatest) | Provides the `IDatabase` / `Database` ADO.NET abstraction, `DatabaseCommand` fluent query builder, `DatabaseRecord` row reader, multi-result-set support, convention-based column mapping, database wildcard translation, typed mapper contracts, and the transactional outbox relay infrastructure for publishing events from a relational database. | [Link](./src/CoreEx.Database) @@ -221,7 +210,7 @@ The repository includes an AI workflow set in [`.github/`](./.github/) that give See the [full skill catalog](./.github/coreex-ai-workflows.md#prompts-skills-and-templates) for full detail on each. -**Also available** — repo-maintenance and local-orchestration skills: [`/coreex-scaffold`](./.github/skills/coreex-solution-scaffolder/README.md) (greenfield solution scaffolding), [`/coreex-docs-sync`](./.github/skills/coreex-docs-sync/README.md) (refresh cached CoreEx docs), [`/acquire-codebase-knowledge`](./.github/skills/acquire-codebase-knowledge/README.md) (map an existing codebase), and [`/aspire`](./.github/skills/aspire/README.md) (orchestrate Aspire apps locally). +**Also available** — repo-maintenance and local-orchestration skills: [`/coreex-scaffold`](./.github/skills/coreex-scaffold/README.md) (greenfield solution scaffolding), [`/coreex-docs-sync`](./.github/skills/coreex-docs-sync/README.md) (refresh cached CoreEx docs), [`/acquire-codebase-knowledge`](./.github/skills/acquire-codebase-knowledge/README.md) (map an existing codebase), and [`/aspire`](./.github/skills/aspire/README.md) (orchestrate Aspire apps locally). **Instructions** — 11 scoped instruction files are injected automatically when editing matching file types (contracts, services, repositories, controllers, tests, etc.). No action required. diff --git a/Version.props b/Version.props index aea689d7..3fe9b80e 100644 --- a/Version.props +++ b/Version.props @@ -1,5 +1,5 @@ - 4.0.0-preview-5 + 4.0.0 diff --git a/consumer-instructions/README.md b/consumer-instructions/README.md index 4fe13003..3132e729 100644 --- a/consumer-instructions/README.md +++ b/consumer-instructions/README.md @@ -16,7 +16,7 @@ The per-capability instruction files live in the repo's root [`.github/instructi 1. Copy `consumer-instructions/.github/copilot-instructions.md` to your project's `.github/copilot-instructions.md`. 2. Copy the instruction files from the repo's `.github/instructions/` that match what you're building into your project's `.github/instructions/` folder. -3. If you want the guided greenfield scaffolding workflow in a non-template repository, copy the canonical files from the repo's [`.github/prompts/`](../.github/prompts/) and [`.github/skills/coreex-solution-scaffolder/`](../.github/skills/coreex-solution-scaffolder/) folders. +3. If you want the guided greenfield scaffolding workflow in a non-template repository, copy the canonical files from the repo's [`.github/prompts/`](../.github/prompts/) and [`.github/skills/coreex-scaffold/`](../.github/skills/coreex-scaffold/) folders. Copilot applies the global instructions to every chat interaction and injects the file-scoped instructions automatically based on which file is open. If you copied the greenfield scaffold prompt and skill, run `/coreex-scaffold` to choose the right `CoreEx.Template` commands before generating the solution. diff --git a/gen/CoreEx.Generator/Utility/HandlebarsCodeGenerator.cs b/gen/CoreEx.Generator/Utility/HandlebarsCodeGenerator.cs index 6f2a829a..5e66be4c 100644 --- a/gen/CoreEx.Generator/Utility/HandlebarsCodeGenerator.cs +++ b/gen/CoreEx.Generator/Utility/HandlebarsCodeGenerator.cs @@ -9,7 +9,7 @@ namespace CoreEx.Generator.Utility; /// public class HandlebarsCodeGenerator { - private readonly HandlebarsTemplate _template; + private readonly HandlebarsTemplate _template; /// /// Static constructor. @@ -61,5 +61,7 @@ public HandlebarsCodeGenerator(StreamReader sr) /// The primary context value referenced within the template. /// The optional secondary data. /// The resulting generated output. - public string Generate(CodeGenContext context, object? data = null) => _template(context, data); + /// The underlying delegate declares TData as non-nullable, but Handlebars.Net accepts and correctly handles a + /// secondary value at runtime (e.g. when none is supplied); the null-forgiving operator below is therefore intentional and safe. + public string Generate(CodeGenContext context, object? data = null) => _template(context, data!); } \ No newline at end of file diff --git a/gen/CoreEx.Generator/Utility/HandlebarsHelpers.cs b/gen/CoreEx.Generator/Utility/HandlebarsHelpers.cs index 6de6d692..32d7a6d6 100644 --- a/gen/CoreEx.Generator/Utility/HandlebarsHelpers.cs +++ b/gen/CoreEx.Generator/Utility/HandlebarsHelpers.cs @@ -26,25 +26,13 @@ public static void RegisterHelpers() _areRegistered = true; // Increments indent only! - Handlebars.RegisterHelper("indent++", (in w, in options, in context, in args) => - { - var hc = (CodeGenContext)options.Data["Root"]; - hc.IncrementIndent(); - }); + Handlebars.RegisterHelper("indent++", (in w, in options, in context, in args) => GetRootContext(options).IncrementIndent()); // Decrements indent only! - Handlebars.RegisterHelper("indent--", (in w, in options, in context, in args) => - { - var hc = (CodeGenContext)options.Data["Root"]; - hc.DecrementIndent(); - }); + Handlebars.RegisterHelper("indent--", (in w, in options, in context, in args) => GetRootContext(options).DecrementIndent()); // Writes the current indent string. - Handlebars.RegisterHelper("indent", (in w, in options, in context, in args) => - { - var hc = (CodeGenContext)options.Data["Root"]; - w.WriteSafeString(hc.GetIndentString()); - }); + Handlebars.RegisterHelper("indent", (in w, in options, in context, in args) => w.WriteSafeString(GetRootContext(options).GetIndentString())); Handlebars.RegisterHelper("bo", (w, _, __) => w.WriteSafeString("{")); Handlebars.RegisterHelper("bc", (w, _, __) => w.WriteSafeString("}")); @@ -69,6 +57,12 @@ public static void RegisterHelpers() } } + /// + /// Gets the root from the Handlebars helper , validating that it is present and correctly typed rather than blindly trusting it. + /// + private static CodeGenContext GetRootContext(in HelperOptions options) + => options.Data["Root"] as CodeGenContext ?? throw new InvalidOperationException("The Handlebars 'Root' data value must be a non-null CodeGenContext; this indicates the template was invoked outside of the expected CodeGenContext-based execution pipeline."); + /// /// Perform the actual IfEq equality check. /// diff --git a/nuget-publish.ps1 b/nuget-publish.ps1 index 5c370d7d..711dc5f7 100644 --- a/nuget-publish.ps1 +++ b/nuget-publish.ps1 @@ -54,6 +54,7 @@ param( "src\CoreEx.Azure.Messaging.ServiceBus", "src\CoreEx.Caching.FusionCache", "src\CoreEx.CodeGen", + "src\CoreEx.Cosmos", "src\CoreEx.Data", "src\CoreEx.Data.GraphQL", "src\CoreEx.Database", diff --git a/samples/README.md b/samples/README.md index b4316a86..b6ffa511 100644 --- a/samples/README.md +++ b/samples/README.md @@ -1,10 +1,10 @@ # Contoso Samples -The `samples` folder contains reference implementations of two domain microservices built with CoreEx: **Products** and **Shopping**. A third, **Orders**, is a work in progress that will eventually demonstrate asynchronous workflow processing. +The `samples` folder contains reference implementations of domain microservices built with CoreEx: **Products** (PostgreSQL) and **Shopping** (SQL Server) are complete; **Customers** (Azure Cosmos DB) demonstrates typed CRUD, reference data, and a transactional outbox against a schemaless store, but has no Outbox Relay/Subscribe host yet. A fourth, **Orders**, is a work in progress that will eventually demonstrate asynchronous workflow processing. ![Sample architecture interactions](../images/SampleArchitectureInteractions.png "Architecture") -Each domain is an independently deployable unit with an API host, an Outbox Relay host, and an Event Subscriber host, backed by an applicable data repository, and connected to other domains via synchronous HTTP and asynchronous messaging over Azure Service Bus. +Each of Products and Shopping is an independently deployable unit with an API host, an Outbox Relay host, and an Event Subscriber host, backed by an applicable data repository, and connected to other domains via synchronous HTTP and asynchronous messaging over Azure Service Bus. Customers is API-only for now — no Relay or Subscribe host — and is not wired into the inter-domain messaging shown below. > **Documentation** — detailed guides for layers, patterns, tooling, and testing are in [`samples/docs`](docs/). > @@ -97,6 +97,7 @@ See [Patterns](docs/patterns.md) for the full catalog of architectural patterns |---|---| | `src/Contoso.Products.*` | Products domain — Contracts, Application, Infrastructure, API, Relay, Subscribe, CodeGen, Database | | `src/Contoso.Shopping.*` | Shopping domain — same layer split plus Domain aggregate | +| `src/Contoso.Customers.*` | Customers domain (Cosmos DB) — Contracts, Application, Infrastructure, API, CodeGen; no Relay/Subscribe/Database project (schemaless, code-first containers) | | `src/Contoso.Orders.*` | Orders domain (work in progress) | | `aspire/Contoso.Aspire` | Aspire AppHost — orchestrates all hosts for local development and E2E validation | | `tests/Contoso.*.Test.*` | Unit, API, Relay, and Subscribe test projects per domain | @@ -140,6 +141,8 @@ dotnet run --project samples/src/Contoso.Shopping.Database -- All dotnet run --project samples/src/Contoso.Orders.Database -- All ``` +> Customers (Cosmos DB) has no `*.Database` project — it is schemaless, and its containers are created/reset code-first at test/run time via `ReplaceOrCreateContainerAsync` (see [`Contoso.Customers.Test.Api/DatabaseSetUp.cs`](tests/Contoso.Customers.Test.Api/DatabaseSetUp.cs)); no separate migration step is required. + > The E2E runner's **Database Migration and Base Data Refresh** option can also apply pending migrations across all domains without restarting hosts. See [Aspire & E2E](docs/aspire.md) for details. See [Tooling](docs/tooling.md) for the full list of database commands and what each does. @@ -160,9 +163,11 @@ dotnet test samples/tests/Contoso.Products.Test.Api dotnet test samples/tests/Contoso.Products.Test.Relay dotnet test samples/tests/Contoso.Products.Test.Subscribe dotnet test samples/tests/Contoso.Shopping.Test.Api +dotnet test samples/tests/Contoso.Customers.Test.Unit +dotnet test samples/tests/Contoso.Customers.Test.Api ``` -The required infrastructure (data store, Redis, Service Bus emulator) must be running for API, Relay, and Subscribe tests. +The required infrastructure (data store, Redis, Service Bus emulator) must be running for API, Relay, and Subscribe tests. Customers' API tests require the `cosmos-emulator` container instead of a SQL Server/Postgres store. See [Testing](docs/testing.md) for an explanation of test taxonomy, intra-domain vs inter-domain boundaries, data seeding, mock patterns, and the fluent assertion model. diff --git a/samples/docs/infrastructure-layer.md b/samples/docs/infrastructure-layer.md index 821c0b76..061a51c4 100644 --- a/samples/docs/infrastructure-layer.md +++ b/samples/docs/infrastructure-layer.md @@ -5,6 +5,7 @@ The Infrastructure layer provides the **concrete implementations** of all abstra **Example projects** - [`samples/src/Contoso.Products.Infrastructure`](../src/Contoso.Products.Infrastructure) - [`samples/src/Contoso.Shopping.Infrastructure`](../src/Contoso.Shopping.Infrastructure) +- [`samples/src/Contoso.Customers.Infrastructure`](../src/Contoso.Customers.Infrastructure) (Cosmos DB — see [Cosmos DB repositories](#cosmos-db-repositories) below) --- @@ -61,6 +62,46 @@ public sealed class ProductsEfDb(ProductsDbContext dbContext) : EfDb ⚠️ **Preview**: [`CoreEx.Cosmos`](../../src/CoreEx.Cosmos) is newly added in this release — treat its API surface as subject to change without following strict semver until it stabilizes. + +Customers is schemaless and code-first — there is no `*.Database`/DbEx migration project and no EF Core `DbContext`. `ref-data.yaml`-driven CodeGen still produces the generated persistence models (`Infrastructure/Persistence/*.g.cs`), same as the relational domains. Instead, `CosmosDb` (from `CoreEx.Cosmos`) is sub-classed once per domain to declare its containers, each exposed as a typed `CosmosDbContainer` (same contract/model type) or `CosmosDbMappedContainer` (contract mapped to a distinct persistence model), analogous in role to an `EfDb` unit-of-work facade: + +```csharp +// samples/src/Contoso.Customers.Infrastructure/Repositories/CustomersCosmosDb.cs +public class CustomersCosmosDb(CosmosClient client, string databaseId) : CosmosDb(client, databaseId, _options) +{ + private static readonly CosmosDbOptions _options = new(); + + public CosmosDbContainer ContactMethods => Container("ref-data", o => o.WithTypeDiscriminator()); + + public CosmosDbMappedContainer Customers + => Container("customers").ToMappedModel(new CustomerMapper()); +} +``` + +The repository then delegates to the container's typed CRUD (`GetAsync`, `CreateAsync`, `UpdateAsync`, `DeleteAsync`) and LINQ-style `Query(...)` for listing, returning the same CoreEx `DataResult`/`ItemsResult` shapes used by the EF Core repositories above: + +```csharp +// samples/src/Contoso.Customers.Infrastructure/Repositories/CustomerRepository.cs +[ScopedService] +public class CustomerRepository(CustomersCosmosDb cosmos) : ICustomerRepository +{ + private readonly CustomersCosmosDb _cosmos = cosmos.ThrowIfNull(); + + public Task GetAsync(string id, CancellationToken ct = default) => _cosmos.Customers.GetAsync(CompositeKey.Create(id), ct); + public Task> CreateAsync(Contracts.Customer customer, CancellationToken ct = default) => _cosmos.Customers.CreateAsync(customer, ct); + // ... +} +``` + +Multi-document writes (e.g. an entity plus its outbox event) go through `CosmosDbUnitOfWork`, which uses `TransactionalBatch` to commit atomically within a partition — the Cosmos analogue of the EF Core `IUnitOfWork`/outbox pattern used by Products and Shopping. Because Customers has no Relay host yet, published events accumulate in the outbox container but are not currently forwarded to Service Bus. + +> **See also**: [`CosmosDb`](../../src/CoreEx.Cosmos/CosmosDb.cs) · [`CosmosDbContainer`](../../src/CoreEx.Cosmos/CosmosDbContainer.cs) · [`CosmosDbMappedContainer`](../../src/CoreEx.Cosmos/CosmosDbMappedContainer.cs) · [`CosmosDbUnitOfWork`](../../src/CoreEx.Cosmos/CosmosDbUnitOfWork.cs) + +--- + ## Mapping The `Mapping/` sub-folder contains **bidirectional mappers** that translate between Contract types and Persistence model types. They extend `BiDirectionMapper` and are source-generated for reference-data types; hand-authored for entity types that require custom field mapping. diff --git a/samples/docs/layers.md b/samples/docs/layers.md index bc67974b..86ea839d 100644 --- a/samples/docs/layers.md +++ b/samples/docs/layers.md @@ -1,8 +1,8 @@ # Layers -CoreEx promotes a clean, layered architecture for building enterprise APIs and distributed services. The sample domains — **Products** (Postgres) and **Shopping** (SQL Server) — demonstrate these patterns in a polyglot setting, showing that the same architectural approach scales across different persistence technologies. Each domain is decomposed into business layers (**Contracts**, **Domain** (optional), **Application**, **Infrastructure**) and one or more host layers (**API**, **Outbox Relay**, **Subscribe**). The business layers enforce a strict inward dependency rule — `Domain` references only `Contracts`; `Application` references `Contracts` and `Domain`; `Infrastructure` references `Application` (and transitively `Domain`) — never the reverse; the host layers act as composition roots that wire everything together and delegate to Application logic. +CoreEx promotes a clean, layered architecture for building enterprise APIs and distributed services. The sample domains — **Products** (Postgres), **Shopping** (SQL Server), and **Customers** (Azure Cosmos DB) — demonstrate these patterns in a polyglot setting, showing that the same architectural approach scales across different persistence technologies. Each domain is decomposed into business layers (**Contracts**, **Domain** (optional), **Application**, **Infrastructure**) and one or more host layers (**API**, **Outbox Relay**, **Subscribe**). The business layers enforce a strict inward dependency rule — `Domain` references only `Contracts`; `Application` references `Contracts` and `Domain`; `Infrastructure` references `Application` (and transitively `Domain`) — never the reverse; the host layers act as composition roots that wire everything together and delegate to Application logic. Customers currently has only an API host — no Relay or Subscribe — since it is a work-in-progress sample. -> **Polyglot data**: Products is backed by PostgreSQL via [`CoreEx.Database.Postgres`](../../src/CoreEx.Database.Postgres) and [`CoreEx.EntityFrameworkCore`](../../src/CoreEx.EntityFrameworkCore). Shopping uses SQL Server via [`CoreEx.Database.SqlServer`](../../src/CoreEx.Database.SqlServer) and the same EF Core integration. The layers above the Infrastructure boundary are completely unaware of the underlying database technology. +> **Polyglot data**: Products is backed by PostgreSQL via [`CoreEx.Database.Postgres`](../../src/CoreEx.Database.Postgres) and [`CoreEx.EntityFrameworkCore`](../../src/CoreEx.EntityFrameworkCore). Shopping uses SQL Server via [`CoreEx.Database.SqlServer`](../../src/CoreEx.Database.SqlServer) and the same EF Core integration. Customers is backed by Azure Cosmos DB via [`CoreEx.Cosmos`](../../src/CoreEx.Cosmos) (preview — newly added, API surface may still change) — a schemaless store accessed directly through the Cosmos SDK, with no EF Core and no `*.Database` migration project (containers are created code-first). The layers above the Infrastructure boundary are completely unaware of the underlying database technology. ``` caller / message broker diff --git a/samples/docs/testing.md b/samples/docs/testing.md index 80c17def..167ad808 100644 --- a/samples/docs/testing.md +++ b/samples/docs/testing.md @@ -25,6 +25,8 @@ Understanding this distinction is the key to understanding every test setup deci | Products API (`POST /api/inventory/reserve`) | Shopping | **Inter** | Mocked — `MockHttpClientFactory` intercepts the outbound call | | Azure Service Bus (direct publish) | Shopping | **Inter** | Captured via `UseExpectedAzureServiceBusPublisher()` | | Azure Service Bus (relay) | Products Outbox Relay | **Inter** | Real — drained and asserted via `GetAndClearAzureServiceBusAsync` | +| Customers Cosmos DB (containers) | Customers | **Intra** | Real — created/reset code-first via `ReplaceOrCreateContainerAsync` in `[OneTimeSetUp]` | +| Customers outbox (Cosmos DB) | Customers | **Intra** | Real — captured via `UseExpectedCosmosDbOutboxPublisher()`; asserted via `ExpectCosmosDbOutboxEvents(...)` / `ExpectNoCosmosDbOutboxEvents()`. No Relay host yet, so captured events are never actually forwarded to Service Bus. | The Shopping `Basket_Checkout_Save_Failure` test is the sharpest illustration of the boundary: when the outbox write fails mid-checkout, Shopping falls back to publishing a `reservation.cancel` command *directly* to Service Bus (bypassing the outbox, since the DB transaction has already rolled back). The test asserts that: - No outbox events are published (intra-domain write failed, as injected). diff --git a/samples/docs/tooling.md b/samples/docs/tooling.md index 12e8f408..2399e743 100644 --- a/samples/docs/tooling.md +++ b/samples/docs/tooling.md @@ -23,11 +23,13 @@ Each domain has a `*.CodeGen` console project (e.g. `Contoso.Products.CodeGen`, | `*.g.cs` controller route | API host | HTTP endpoint exposing the entity collection. | | `*.g.cs` service method | Application | Service-layer method delegating to the repository. | | `*.g.cs` repository interface | Application | Interface declaration for the entity repository. | -| `*.g.cs` repository | Infrastructure | EF Core repository implementation. | -| `*.g.cs` mapper | Infrastructure | Bidirectional EF Core mapper for the entity. | +| `*.g.cs` repository | Infrastructure | Repository implementation (EF Core, or Cosmos DB for a Cosmos-backed domain such as Customers — see `CosmosPersistenceModelGenerator`). | +| `*.g.cs` mapper | Infrastructure | Bidirectional mapper for the entity (EF Core `BiDirectionMapper`, or the equivalent Cosmos mapper). | All generated files carry the `.g.cs` suffix, clearly distinguishing them from hand-authored code and excluding them from manual maintenance. +> Customers (Cosmos DB) uses the same `*.CodeGen` mechanism, but its templates target Cosmos containers instead of an EF Core `DbContext` — persistence models are still generated (via `CosmosPersistenceModelGenerator`), there is just no `DbContext`/database-migration generation step, because Customers has no `*.Database` project (see [Database Management](#database-management-database) below). + ### `ref-data.yaml` structure ```yaml @@ -60,6 +62,8 @@ Each domain has a `*.Database` console project (e.g. `Contoso.Products.Database` Products uses `DbEx.Postgres` (PostgreSQL); Shopping uses `DbEx.SqlServer` (SQL Server), demonstrating that the same tooling approach is database-agnostic. A MySQL variant (`DbEx.MySql`) is also available. +> Customers has no `*.Database` project. Cosmos DB is schemaless, so there is no schema to migrate — its containers are created (or reset) code-first at test/run time via `ReplaceOrCreateContainerAsync` (see [`Contoso.Customers.Test.Api/DatabaseSetUp.cs`](../tests/Contoso.Customers.Test.Api/DatabaseSetUp.cs)), and reference data is seeded directly into its containers rather than via DbEx's `Data` command. + ### DbEx commands DbEx exposes a rich command set. A single run can execute one or more commands in order: diff --git a/samples/src/Contoso.Products.Relay/Program.cs b/samples/src/Contoso.Products.Relay/Program.cs index 81110b59..b830c229 100644 --- a/samples/src/Contoso.Products.Relay/Program.cs +++ b/samples/src/Contoso.Products.Relay/Program.cs @@ -43,6 +43,10 @@ private static void Main(string[] args) builder.Services.PostConfigureAllHealthChecks(); // Add OpenTelemetry tracing. +#if NET8_0 + // Workaround: net8.0's regex automata cap is too low for the combined ActivitySource patterns below; remove once net8.0 support is dropped. + AppContext.SetData("REGEX_NONBACKTRACKING_MAX_AUTOMATA_SIZE", 10_000); +#endif builder.WithCoreExTelemetry() .WithCoreExPostgresTelemetry() .WithCoreExServiceBusTelemetry() diff --git a/samples/src/Contoso.Products.Subscribe/Program.cs b/samples/src/Contoso.Products.Subscribe/Program.cs index dfce35cf..93e74b56 100644 --- a/samples/src/Contoso.Products.Subscribe/Program.cs +++ b/samples/src/Contoso.Products.Subscribe/Program.cs @@ -92,6 +92,10 @@ private static void Main(string[] args) }); // Add OpenTelemetry tracing. +#if NET8_0 + // Workaround: net8.0's regex automata cap is too low for the combined ActivitySource patterns below; remove once net8.0 support is dropped. + AppContext.SetData("REGEX_NONBACKTRACKING_MAX_AUTOMATA_SIZE", 10_000); +#endif builder.WithCoreExTelemetry() .WithCoreExServiceBusTelemetry() .WithCoreExPostgresTelemetry() diff --git a/samples/src/Contoso.Shopping.Relay/Program.cs b/samples/src/Contoso.Shopping.Relay/Program.cs index 01fbeb7d..c96ae134 100644 --- a/samples/src/Contoso.Shopping.Relay/Program.cs +++ b/samples/src/Contoso.Shopping.Relay/Program.cs @@ -43,6 +43,10 @@ private static void Main(string[] args) builder.Services.PostConfigureAllHealthChecks(); // Add OpenTelemetry tracing. +#if NET8_0 + // Workaround: net8.0's regex automata cap is too low for the combined ActivitySource patterns below; remove once net8.0 support is dropped. + AppContext.SetData("REGEX_NONBACKTRACKING_MAX_AUTOMATA_SIZE", 10_000); +#endif builder.WithCoreExTelemetry() .WithCoreExSqlServerTelemetry() .WithCoreExServiceBusTelemetry() diff --git a/samples/src/Contoso.Shopping.Subscribe/Program.cs b/samples/src/Contoso.Shopping.Subscribe/Program.cs index 52cf9efe..717dfc16 100644 --- a/samples/src/Contoso.Shopping.Subscribe/Program.cs +++ b/samples/src/Contoso.Shopping.Subscribe/Program.cs @@ -101,6 +101,10 @@ private static void Main(string[] args) }); // Add OpenTelemetry tracing. +#if NET8_0 + // Workaround: net8.0's regex automata cap is too low for the combined ActivitySource patterns below; remove once net8.0 support is dropped. + AppContext.SetData("REGEX_NONBACKTRACKING_MAX_AUTOMATA_SIZE", 10_000); +#endif builder.WithCoreExTelemetry() .WithCoreExServiceBusTelemetry() .WithCoreExSqlServerTelemetry() diff --git a/src/CoreEx.Cosmos/AGENTS.md b/src/CoreEx.Cosmos/AGENTS.md index f71e92d6..2dca8514 100644 --- a/src/CoreEx.Cosmos/AGENTS.md +++ b/src/CoreEx.Cosmos/AGENTS.md @@ -1,5 +1,7 @@ # CoreEx.Cosmos — AI Usage Guide +> 🚧 **Preview**: newly added in this release. The API surface may still change in a future release without following strict semver until it stabilizes. + Azure Cosmos DB implementation of the CoreEx core CRUD + query access layer pattern (model-direct and contract-to-model), structurally mirroring `CoreEx.EntityFrameworkCore`'s `EfDb`/`EfDbModel`/`EfDbMappedModel` shape, plus a `TransactionalBatch`-based transactional outbox and a Change Feed Processor-based outbox relay. ## Registration diff --git a/src/CoreEx.Cosmos/README.md b/src/CoreEx.Cosmos/README.md index c424f88c..f94ec1b0 100644 --- a/src/CoreEx.Cosmos/README.md +++ b/src/CoreEx.Cosmos/README.md @@ -1,5 +1,7 @@ # CoreEx.Cosmos +> 🚧 **Preview**: newly added in this release. The API surface may still change in a future release without following strict semver until it stabilizes. + > Provides the core [Azure Cosmos DB](https://learn.microsoft.com/en-us/azure/cosmos-db/) access layer: `ICosmosDb`/`CosmosDb` as the CoreEx-Cosmos bridge, `CosmosDbContainer` and `CosmosDbMappedContainer` for typed CRUD + query operations, `CosmosDbQuery` as the composable, invoker-wrapped query/materialization type, `CosmosDbInvoker` for structured operation logging and exception mapping, a `CosmosDbUnitOfWork` transactional outbox (`TransactionalBatch`-based), and a Change Feed Processor-based outbox relay. ## Overview diff --git a/src/CoreEx.Template/CoreEx.Template.csproj b/src/CoreEx.Template/CoreEx.Template.csproj index b2f29b29..41dbce15 100644 --- a/src/CoreEx.Template/CoreEx.Template.csproj +++ b/src/CoreEx.Template/CoreEx.Template.csproj @@ -101,7 +101,7 @@ - + - - - - - - - - - - - - - - - - - - + - + - + - + - - + + - - + + - + - + @@ -76,14 +76,14 @@ - + - + - - + + - + diff --git a/src/CoreEx.Template/content/CoreEx.Core/tests/app-name.Test.Unit/GlobalUsing.cs b/src/CoreEx.Template/content/CoreEx.Core/tests/app-name.Test.Unit/GlobalUsing.cs index 532e14b6..6be75ebc 100644 --- a/src/CoreEx.Template/content/CoreEx.Core/tests/app-name.Test.Unit/GlobalUsing.cs +++ b/src/CoreEx.Template/content/CoreEx.Core/tests/app-name.Test.Unit/GlobalUsing.cs @@ -1,5 +1,6 @@ global using AwesomeAssertions; global using CoreEx; +global using CoreEx.Data.Json; // #if refdata-enabled global using CoreEx.RefData; global using CoreEx.RefData.Abstractions; @@ -8,7 +9,6 @@ global using CoreEx.Results; // #endif global using CoreEx.UnitTesting; -global using CoreEx.Data.Json; global using CoreEx.Validation; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Hosting; diff --git a/src/CoreEx.UnitTesting/UnitTestExExtensions.Cosmos.cs b/src/CoreEx.UnitTesting/UnitTestExExtensions.Cosmos.cs index 7e8ed1ce..a411875d 100644 --- a/src/CoreEx.UnitTesting/UnitTestExExtensions.Cosmos.cs +++ b/src/CoreEx.UnitTesting/UnitTestExExtensions.Cosmos.cs @@ -39,7 +39,10 @@ public static AspNetCore.ApiTester UseExpectedCosmosDbOutboxPublish /// Deliberately resolves from (the actual running host's DI container) rather than constructing a new, test-owned /// from configuration - unlike a relational connection string, a Cosmos DB database identifier is typically a host-owned literal (e.g. services.AddCosmosDb<TCosmosDb>("contoso")), not /// something re-derivable from configuration alone; resolving the host's own guarantees test seeding always targets the exact same database the host itself reads/writes, - /// structurally ruling out a test/host database-name mismatch rather than merely avoiding it by convention. + /// structurally ruling out a test/host database-name mismatch rather than merely avoiding it by convention. + /// This is the first call made against the local Cosmos DB emulator for each running test host, and the emulator is known to intermittently drop the TLS handshake ( + /// wrapping a connection-reset) under sustained load (e.g. repeated cross-TFM test passes in CI) - a small bounded retry-with-backoff is applied here, scoped purely to this test-setup call, rather than + /// altering any production / configuration. public static async Task GetCosmosDatabaseAsync(this TesterBase tester, CancellationToken cancellationToken = default) { // ICosmosDb is registered scoped (see CoreExCosmosExtensions.AddCosmosDb), so it cannot be resolved directly from the host's root IServiceProvider - a short-lived scope is created purely to @@ -47,7 +50,25 @@ public static async Task GetCosmosDatabaseAsync(this TesterBase tester // it, remain perfectly usable after this scope is disposed. using var scope = tester.ThrowIfNull().Services.CreateScope(); var cosmosDb = scope.ServiceProvider.GetRequiredService(); - await cosmosDb.Client.CreateDatabaseIfNotExistsAsync(cosmosDb.Database.Id, cancellationToken: cancellationToken).ConfigureAwait(false); - return cosmosDb.Database; + + const int maxAttempts = 4; + for (var attempt = 1; ; attempt++) + { + try + { + await cosmosDb.Client.CreateDatabaseIfNotExistsAsync(cosmosDb.Database.Id, cancellationToken: cancellationToken).ConfigureAwait(false); + return cosmosDb.Database; + } + catch (Exception ex) when (attempt < maxAttempts && IsTransientCosmosConnectionFailure(ex)) + { + await Task.Delay(TimeSpan.FromSeconds(attempt), cancellationToken).ConfigureAwait(false); + } + } } + + /// + /// Determines whether the represents a transient connection failure (e.g. a dropped TLS handshake) rather than a genuine configuration or data error. + /// + private static bool IsTransientCosmosConnectionFailure(Exception exception) => exception is System.Net.Http.HttpRequestException or IOException or System.Net.Sockets.SocketException + || exception.InnerException is not null && IsTransientCosmosConnectionFailure(exception.InnerException); } diff --git a/src/CoreEx/CoreEx.csproj b/src/CoreEx/CoreEx.csproj index df2a95c4..f7e9b5b4 100644 --- a/src/CoreEx/CoreEx.csproj +++ b/src/CoreEx/CoreEx.csproj @@ -43,7 +43,6 @@ - diff --git a/src/CoreEx/CoreExExtensions.OpenTelemetry.cs b/src/CoreEx/CoreExExtensions.OpenTelemetry.cs index 3bb59960..96b7984a 100644 --- a/src/CoreEx/CoreExExtensions.OpenTelemetry.cs +++ b/src/CoreEx/CoreExExtensions.OpenTelemetry.cs @@ -13,9 +13,13 @@ public static class CoreExExtensions /// The . /// The to support fluent-style method-chaining. public static OpenTelemetryBuilder WithCoreExTelemetry(this OpenTelemetryBuilder builder) - => builder.ThrowIfNull().ThrowIfNull() + { + builder.ThrowIfNull(); + + return builder .WithTracing(t => t.AddHttpClientInstrumentation().WithCoreExSources()) - .WithMetrics(m => m.AddHttpClientInstrumentation().AddRuntimeInstrumentation().AddProcessInstrumentation().AddMeter("Polly")); + .WithMetrics(m => m.AddHttpClientInstrumentation().AddRuntimeInstrumentation().AddMeter("Polly")); + } /// /// Enables (adds) the CoreEx-specified OpenTelemetry tracing sources. diff --git a/tools/validate-template-pack.ps1 b/tools/validate-template-pack.ps1 index 06a927e8..bc5f27bd 100644 --- a/tools/validate-template-pack.ps1 +++ b/tools/validate-template-pack.ps1 @@ -72,16 +72,34 @@ $testScenarios = @( ".github/agents/coreex-expert.agent.md" ".github/skills/coreex-bootstrap/SKILL.md" ".github/skills/coreex-docs-sync/SKILL.md" - ".github/skills/coreex-solution-scaffolder/SKILL.md" + ".github/skills/coreex-scaffold/SKILL.md" ".github/docs/coreex/manifest.txt" ".github/coreex-ai-workflows.md" ".claude/commands/coreex-bootstrap.md" ".claude/commands/coreex-expert.md" ".claude/commands/coreex-docs-sync.md" + ".claude/commands/coreex-scaffold.md" + ".claude/commands/coreex-adapter.md" + ".claude/commands/coreex-aggregate.md" + ".claude/commands/coreex-api.md" + ".claude/commands/coreex-api-e2e.md" + ".claude/commands/coreex-app-service.md" + ".claude/commands/coreex-contract.md" + ".claude/commands/coreex-db-migration.md" + ".claude/commands/coreex-graphql.md" + ".claude/commands/coreex-policy.md" + ".claude/commands/coreex-refdata.md" + ".claude/commands/coreex-repository.md" + ".claude/commands/coreex-subscriber.md" + ".claude/commands/coreex-subscriber-e2e.md" + ".claude/commands/coreex-test-api.md" + ".claude/commands/coreex-test-relay.md" + ".claude/commands/coreex-test-subscribe.md" + ".claude/commands/coreex-validator.md" ) FilesAbsent = @( ".github/copilot-instructions.md" - ".github/skills/solution-scaffolder" + ".github/skills/coreex-solution-scaffolder" ) FileContains = @{ ".github/instructions/coreex.instructions.md" = 'applyTo: "**"' @@ -102,14 +120,14 @@ $testScenarios = @( ".github/instructions/coreex.instructions.md" ".github/instructions/coreex-validators.instructions.md" ".github/skills/coreex-docs-sync/SKILL.md" - ".github/skills/coreex-solution-scaffolder/SKILL.md" + ".github/skills/coreex-scaffold/SKILL.md" ".github/docs/coreex/manifest.txt" ".github/coreex-ai-workflows.md" ".claude/commands/coreex-docs-sync.md" ) FilesAbsent = @( ".github/copilot-instructions.md" - ".github/skills/solution-scaffolder" + ".github/skills/coreex-solution-scaffolder" ) FileContains = @{ ".github/instructions/coreex.instructions.md" = 'applyTo: "backend/'