Skip to content

Introduce CoreEx.Cosmos: typed Cosmos DB data access, outbox, and Contoso.Customers sample - #186

Merged
chullybun merged 35 commits into
mainfrom
introduce-cosmos
Sep 20, 2026
Merged

chullybun merged 35 commits into
mainfrom
introduce-cosmos

Conversation

@chullybun

Copy link
Copy Markdown
Collaborator

Primary: CoreEx.Cosmos (new package)

New src/CoreEx.Cosmos package providing typed CRUD, query, and multi-tenant/filter data access over Azure Cosmos DB, mirroring the existing CoreEx.Database/CoreEx.EntityFrameworkCore shape:

  • CosmosDb/ICosmosDb, CosmosDbContainer<T> / CosmosDbMappedContainer<T> (Create/Get/Update/Upsert/Delete/Query).
  • CosmosDbModelBase, CosmosDbModelOptions<TModel> (partition-key strategy, fixed partition key, type discriminator), CosmosDbReferenceDataModelBase, CosmosDbArgs/CosmosDbOptions.
  • CosmosDbQuery<T> query-wrapper type (avoids IQueryable<T> extension-method collisions with EF Core).
  • Multi-set query support (SelectMultiSetAsync / SelectMultiSetWithResultAsync) under Extended/.
  • CosmosDbUnitOfWork transactional outbox, matching the SQL Server/Postgres invoker + metrics pattern.
  • Full outbox relay implementation under Outbox/: CosmosDbOutboxRelay, CosmosDbOutboxRelayHostedService, CosmosDbOutboxRelayProcessor, CosmosDbEventPublisher, resiliency and DI extensions.
  • CosmosMetrics, OpenTelemetry wiring, health check (CosmosDbHealthCheck), type-discriminator guard.
  • CoreEx.CodeGen: new CosmosPersistenceModelGenerator + Handlebars template so ref-data code generation can target Cosmos persistence models.
  • CoreEx.UnitTesting: Cosmos-specific expectations/extensions (UnitTestExExpectations.Cosmos, UnitTestExExtensions.Cosmos) and outbox test helpers.
  • Full unit test suite: tests/CoreEx.Cosmos.Test.Unit (CRUD, concurrency, filtering, tenant, TTL, type-discriminator, multi-set, outbox relay, seeding).
  • New src/CoreEx.Cosmos/README.md and AGENTS.md documenting the package.

New Contoso.Customers sample domain (samples/src/Contoso.Customers.*, samples/tests/Contoso.Customers.*) — a Cosmos DB-backed reference implementation (Api/Application/CodeGen/Contracts/Infrastructure + unit/integration tests), demonstrating the new package end-to-end alongside the existing SQL Server/PostgreSQL samples.

Solution/build plumbing to include the above: CoreEx.sln, CoreEx.slnx, *.slnf filters, Directory.Packages.props (Cosmos SDK + emulator package references), docker-compose.yml (Cosmos DB emulator service), samples/docs/hosts-layer.md.

Secondary: changes to other CoreEx projects (incidental to the above)

CoreEx (core)

  • Mapping/Mapper.cs: MapStandardFrom/MapStandardInto no longer auto-copy PartitionKey — a partition key's meaning is layer/purpose-specific (e.g. a Cosmos shard key vs. an event ordering key), so silently copying it conflated unrelated concerns. Documented on IReadOnlyPartitionKey and Mapper's XML docs; set it deliberately per destination instead (e.g. CosmosDbModelOptions<T>.WithPartitionKey).
  • Data/Model.cs: fixed Schema.TryGetMetadata<TModel>'s defaulted Name being ignored when its bool return was false — the out-param always carries a sensible default, so the type-discriminator default was wrongly skipped for types without a [Schema] attribute.
  • New Data/ITimeToLive.cs / IReadOnlyTimeToLive.cs — generic TTL abstraction (consumed by Cosmos models).
  • Hosting/: new CircuitBreakerResiliency, RetryResiliency, ResilienceOwner — circuit-breaker/retry resiliency helpers promoted out of Service Bus into generic, reusable CoreEx.Hosting primitives, now shared by Service Bus, Cosmos, and database outbox relays; TimerHostedServiceBase updated to use them.
  • Invokers/InvokerTracer.cs: tracing harmonization to support the above.

CoreEx.AspNetCore

  • WebApi.cs / WebApi.MergePatch.cs: fixed ETag comparisons (304 Not Modified and merge-patch concurrency checks) to normalize both sides via ETag.ParseETag before comparing — previously only the incoming header value was quote-stripped, so a provider whose native ETag is itself already quote-wrapped (e.g. Cosmos DB's raw _etag system property) could never match, even when genuinely unchanged.

CoreEx.Azure.Messaging.ServiceBus

  • New ServiceBusPublisherResiliency — configurable transient-retry resiliency for ServiceBusPublisher.
  • ServiceBusReceiverResiliency.cs and OpenTelemetry extensions updated to use the promoted CoreEx.Hosting circuit-breaker/retry helpers instead of local implementations.

CoreEx.Database / CoreEx.Database.SqlServer / CoreEx.Database.Postgres

  • New DatabaseOutboxRelayResiliencyExecutor and related DatabaseOutboxRelay* changes harmonizing outbox relay metrics, tracing, and resiliency across SQL Server/Postgres/Cosmos so all three providers share one pattern.
  • Fixed incompatible outbox relay PartitionSize/PerWorkerPartitionCount defaults.
  • Outbox relay lag metrics now recorded on failure too (not only success), with a documented alerting pattern.

CoreEx.Data

  • New IMultiSetArgsCore, IUnitOfWork.SynchronizeETag abstractions shared across database providers.
  • Relocated the JSON seed-data reader types (JsonDataReader, JsonDataReaderArgs, JsonDataReaderOptions, JsonPropertyNamingConvention) from CoreEx.UnitTesting into CoreEx.Data/Json/ so both production Cosmos seeding and test seeding can share them without a test-only dependency.

CoreEx.Events

  • New CloudEventTracingExtensions; EventPublisherBase/IEventPublisher and OpenTelemetry extensions updated for distributed-tracing parity across publishers (including the new Cosmos outbox event publisher).

CoreEx.EntityFrameworkCore

  • EfDbArgs / EfDbModelOptions adjustments for parity with the new Cosmos model-options shape.

Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com

chullybun and others added 21 commits August 18, 2026 16:01
…ata-access for Azure Cosmos DB

Adds CosmosDb/CosmosDbContainer<TModel>/CosmosDbMappedContainer with ETag-based optimistic
concurrency, logical delete, multi-tenancy, type-discriminator multi-type containers, fixed/
per-model partition keys, time-to-live, and a CosmosDbQuery<TModel> wrapper (avoiding bare
IQueryable<T> extension collisions with other CoreEx/EF Core packages). Physical deletes now
pre-check tenant ownership and WithFilter rules the same way Get/Update do, with a fast-path
skip when none are configured. Promotes ITimeToLive/IReadOnlyTimeToLive to CoreEx.Data for reuse
by future non-Cosmos NoSQL packages, and fixes a Model.PrepareTypeDiscriminator fallback bug
along the way. Includes a Cosmos emulator service in docker-compose.yml and a full unit test
project run against it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
The merge of main into introduce-cosmos left CoreEx.Data.csproj and
CoreEx.Data.Test.Unit.csproj each listed twice, likely from the merge
resolution keeping both the pre-merge entry and the incoming one instead
of deduping. Removed the redundant entries; confirmed no duplicate paths
remain and all project references still resolve to real files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
…/metrics pattern

Implements the transactional unit-of-work + outbox-write pieces of the Cosmos backlog
(1a/1b): CosmosDbUnitOfWork enlists Create/Update/Delete operations into a single
TransactionalBatch instead of executing immediately, enforcing Cosmos's single-container/
single-partition-key atomicity constraint client-side; CosmosDbEventPublisher writes
paired outbox-event documents into the same batch, kept invisible to ordinary business
queries via an automatic id-prefix exclusion filter (no per-container opt-in needed).
IUnitOfWork gains SynchronizeETag to resolve a mutated value's true ETag after a
deferred-execution commit (a no-op for the relational providers, which already have it
immediately). Delete inside a unit-of-work now forces a pre-read so a delete of an
already-gone item can't silently fail the whole batch, keeping WasMutated accurate for
the standard WhereMutated-driven event-publishing pattern. Adds CosmosDbUnitOfWorkInvoker/
CosmosDbInvoker.OrchestrateUnitOfWorkTransactionAsync/CosmosMetrics under a new
CoreEx.Cosmos.Extended namespace, mirroring SqlServerUnitOfWorkInvoker/SqlServerMetrics.

Also raises the local Cosmos emulator's container-count ceiling (AZURE_COSMOS_EMULATOR_
PARTITION_COUNT), the actual root cause of this session's intermittent test flakiness
that looked like transient "high demand" 503s.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
…ing helpers

Needed a self-pausing circuit breaker for the upcoming Cosmos change feed
processor host; rather than duplicate ServiceBusReceiverResiliency's Polly
pipelines, generalized them into CircuitBreakerResiliency<TOwner> and
RetryResiliency<TOwner> (with shared owner-flow via ResilienceOwner<TOwner>),
so ASB and Cosmos share one implementation. ASB's own resiliency class is now
a thin wrapper supplying its pause-reason/dead-letter-exclusion/retry-classification
specifics.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
…acing across providers

Adds the Change Feed Processor-based Cosmos outbox relay (CoreEx.Cosmos.Outbox):
decode/publish/cleanup-delete batch processing, circuit-breaker-protected
start/pause/resume via the CFP, and DI registration - mirroring the Azure
Service Bus receiver shape rather than SQL/Postgres's poll-loop, since CFP is
push-driven. CosmosDbOutboxEvent/CosmosDbEventPublisher moved into the same
namespace alongside it.

Fixes a real bug in already-shipped code: CosmosDb/CosmosDbOptions cached
CosmosDbContainer<TModel>/CosmosDbModelOptions<TModel> by containerId alone,
so any container hosting more than one type-discriminated model would throw
InvalidCastException the moment both types were requested from the same
scoped ICosmosDb - now keyed by (containerId, TModel).

Harmonizes outbox relay observability across SQL Server, Postgres, and
Cosmos: promotes the W3C trace-linking logic (and CircuitBreakerResiliency
earlier this branch) into shared, provider-agnostic helpers, and unifies
metric names to Outbox{Enqueued|RelayPublished|RelayPublishFailed|
RelayOldestLagDuration|RelayNewestLagDuration}. Along the way, drops the
promoted tracing helper's baggage propagation entirely (was silently
cross-contaminating causally-unrelated events' baggage across a batched
relay's shared outgoing call) and fixes ActivityContext.TryParse's IsRemote
defaulting to false.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
…zation, and Hosting resiliency promotion

Docs had drifted from code across several packages: Cosmos README/AGENTS still described the relay as unimplemented, SQL/Postgres metric names were stale post-harmonization, and neither CoreEx.Hosting nor CoreEx.Azure.Messaging.ServiceBus documented the new generic CircuitBreakerResiliency<TOwner>/RetryResiliency<TOwner> promotion.
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
Absorbs transient send failures (throttling, momentary timeouts) silently within SendBatchAsync via a shared, overridable Polly retry pipeline, reusing the existing IsTransient classifier and CoreEx.Hosting's generic RetryResiliency<TOwner> - built once and cached to avoid rebuilding it on every scoped instance.
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
…lting alert pattern

Oldest/newest relay lag was only recorded on a successful publish, so a batch stuck behind a permanently-failing item made the histogram go silent instead of climbing - fixed identically across Cosmos, SQL Server, and Postgres. Documented the enqueue-vs-publish divergence and lag-histogram alert pattern in each package's AGENTS.md, since all three relays share the same strict-ordering starvation risk (Cosmos checkpoint ordering, SQL/Postgres contiguous-claim-from-oldest-pending).
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
…Ex.Hosting helpers

Bring self-pausing/self-resuming circuit-breaker resilience to the SQL Server/Postgres outbox relays, matching Cosmos DB and Azure Service Bus. Along the way, fixed a real gap: one partition's failure previously aborted every other assigned partition in the same tick, then paused the whole hosted service forever with no self-recovery mechanism. Promoted the capability further into TimerHostedServiceBase itself as an opt-in Resiliency property, so any future poll-based hosted service can adopt the same self-healing behavior for free.
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
…efaults

PerWorkerPartitionCount defaulted to an unconditional literal 6 while PartitionSize defaulted to 4, and PartitionPicker throws when perWorkerPartitionCount exceeds partitionSize - so AddSqlServerOutboxRelayHostedService()/AddPostgresOutboxRelayHostedService() threw at startup with zero configuration overrides, exactly the pattern shown in AGENTS.md and the CoreEx.Template scaffolding. Default now derives from whatever PartitionSize resolves to.
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
DbEx (used for SQL Server/Postgres test seeding) has no Cosmos DB equivalent, so
CoreEx.Cosmos.Test.Unit's tests created containers and seeded data ad hoc, inline,
per test. Adds a CoreEx.UnitTesting.Cosmos namespace with container lifecycle
helpers (DeleteContainerIfExistsAsync/ReplaceOrCreateContainerAsync) and raw-JSON
batch-import helpers (ImportBatchAsync) that hand JsonDataReader-sourced JsonArray
data straight to the Cosmos SDK - no TModel typing needed, since a fixture author
already controls the exact document shape (partition key property, type
discriminator) directly, and Container.CreateItemAsync auto-extracts the partition
key from the item's own shape.

Moves JsonDataReader (+ JsonDataReaderArgs/Options/JsonPropertyNamingConvention)
from CoreEx.UnitTesting.Data to CoreEx.Data.Json, since it has no test-framework
coupling and CoreEx.Data already groups query/data concerns this way - it's now
usable directly by the new Cosmos batch-import helpers without a circular
dependency back into CoreEx.UnitTesting. Expands JsonDataReaderTests to cover
previously-untested paths (ParseYaml, numeric/embedded dynamic parameters,
tenant/user tokens, naming conventions, RootNodePreProcessor).

Proves the new primitives end-to-end in CosmosDbSeedingTests: reset a container,
seed it from an embedded YAML fixture, then read/assert via the typed
CosmosDbContainer<TModel> API.

Bumps UnitTestEx to 5.11.1, which hardens TestSetUp's static constructor against
non-.NET binaries in the test output directory - needed because Microsoft.Azure.Cosmos's
native ServiceInterop/CRTCompat binaries, pulled in transitively by every project
referencing CoreEx.UnitTesting, otherwise crash any UnitTestEx-based test suite
(confirmed against all ten Contoso sample projects) via an unguarded Assembly.LoadFrom.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
Unlike Id/ETag/TenantId/IsDeleted/TypeDiscriminator/ChangeLog, a partition key's
meaning is layer/purpose-specific rather than invariant - a Cosmos DB physical
partition/shard key (chosen for storage distribution and RU throughput) and an
event's ordering/session key (chosen so related events are processed in order)
are routinely different values for the same logical entity. Auto-copying it
across a Contract->Cosmos-model mapping conflated the two, and could trip
CosmosDbModelOptions.WithPartitionKey's own mismatch guard as an unintended side
effect of the copy rather than a genuine configuration error. Set it deliberately
at each destination instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
Add Cosmos-backed reference-data code generation to CoreEx.CodeGen (repository:
Cosmos), alongside the existing None/EntityFramework modes: a new
CosmosPersistenceModelGenerator + CosmosPersistenceModel_cs.hbs template
generate a Cosmos persistence model per ref-data entity, and
Repository_cs.hbs's generated repository queries it via a hand-authored
CosmosDb-derived wrapper (mirroring the EfDb wrapper convention). Renames
CosmosDbItemBase to CosmosDbModelBase and adds CosmosDbReferenceDataModelBase
(Code/Text/Description/SortOrder/IsActive/StartsOn/EndsOn) as its ref-data-model
counterpart. Renames CosmosDbModelOptions<TModel>.WithTypeDiscriminatorFilter to
WithTypeDiscriminator - the "Filter" suffix was superfluous.

Introduces the first real consumer of this: a new Contoso.Customers domain
(Contracts/Application/Infrastructure/CodeGen skeleton, wired into
CoreEx.sln/.slnx/CoreEx.Samples.Build.slnf), with CustomerType/ContactMethod
reference data generated end-to-end through the new Cosmos pipeline. Contracts,
Application, and hand-authored Infrastructure pieces (CustomersCosmosDb,
ReferenceDataRepository) are in place; the remaining domain content (Customer
entity, business services, Api/Relay/Subscribe hosts, and the Shopping
integration) is tracked as follow-on work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
…None default

Introduces the Customers domain as the first real consumer of CoreEx.Cosmos and
its CoreEx.UnitTesting.Cosmos test-seeding toolkit: Contracts/Application/
Infrastructure layers, an Api host, and Test.Unit/Test.Common/Test.Api projects,
scaffolded via the CoreEx.CodeGen "repository: Cosmos" mode (CustomerType/
ContactMethod reference data, generated end-to-end into Contracts/Application/
Infrastructure). CustomerService enforces a delete guard (HasShopped) and the
Cosmos partition key is kept deliberately simple - equal to Id, set once by
CustomerMapper, with no WithPartitionKey/WithFixedPartitionKey configuration.

That simplification exposed a real gap in CoreEx.Cosmos itself: CosmosDbModelOptions
required an explicit partition-key configuration or a model implementing
IReadOnlyPartitionKey, throwing InvalidOperationException otherwise. Reworked
GetPartitionKeyValue/GetPartitionKey to fall back to PartitionKey.None uniformly
whenever no override is configured and the model has no (or an empty) partition
key value - the simplest possible container shape needs zero partition-key
configuration or interface implementation at all. CosmosDbMappedContainer's
Delete overloads gained the same optional/defaulted PartitionKey? shape Get
already had.

CosmosDbBatch (CoreEx.UnitTesting.Cosmos) gains ImportDiscriminatedBatchAsync,
reusing the existing "$^TypeName"-grouped fixture convention from the relational
ref-data seed YAML files so a single fixture shape works unchanged for both SQL/
Postgres and Cosmos-backed reference data - expansion (id/isActive/sortOrder)
is delegated entirely to JsonDataReaderOptions.CreateForReferenceData, with only
the type-discriminator stamping added on top.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
…nKey.None fixes

- Add AddCosmosDbEventPublisher/AddCosmosDbUnitOfWork/AddCosmosDbHealthCheck/WithCoreExCosmosDbTelemetry,
  mirroring the Postgres/SqlServer DI/telemetry/health-check conventions (Aspire's Cosmos client
  integration registers no health check of its own, unlike Npgsql/SqlClient).
- Add UseExpectedCosmosDbOutboxPublisher/ExpectCosmosDbOutboxEvents to CoreEx.UnitTesting, plus a
  GetCosmosDatabaseAsync TesterBase helper that resolves the host's own DI-registered ICosmosDb so test
  seeding always targets the exact database/containers the API host itself reads from.
- Add full CustomerMutateTests (Create/Update/Delete) API test suite for Contoso.Customers.
- Split CosmosDbContainer/CosmosDbMappedContainer's GetAsync/DeleteAsync into a required-partitionKey
  (raw string) overload and a no-partition-key overload (falls back to WithFixedPartitionKey/None),
  replacing the previous single nullable-optional-parameter shape; threads the raw partition key value
  through to a paired outbox-event write for delete-only unit-of-work transactions.
- Split IEventPublisher.Rollback(int) into Dequeue(int) (pre-publish, unchanged behavior) and a new
  RollbackAsync() hook (post-publish undo, no-op by default) to fix the event-publisher test spy
  optimistically capturing "published" events before a deferred Cosmos batch actually commits; wired
  into both CosmosDbInvoker and DatabaseInvoker's failure paths.
- Fix CosmosDbOutboxEvent.PartitionKey and CosmosDbModelBase.PartitionKey to omit the JSON property when
  null (matching TimeToLive's existing treatment) instead of writing an explicit "partitionKey": null -
  the latter resolves to the SDK's PartitionKey.Null, a different partition than PartitionKey.None,
  causing an undiagnosable TransactionalBatch BadRequest for any model with no partition key configured.
- Fix CosmosDbEventPublisher to treat a null resolved partition key as PartitionKey.None (a valid,
  real single logical partition) instead of throwing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
…ate Cosmos seeding helpers

- Add CustomerMutateTests.Patch.cs and CustomerReadTests (Get/Query) for the Cosmos-backed
  Contoso.Customers sample, with supporting seed data and composite indexes for multi-field ordering.
- Fix ETag quote-mismatch bugs in WebApi.MergePatch.cs (PATCH concurrency check) and WebApi.cs
  (304 Not Modified) that made a Cosmos-style pre-quoted ETag never match.
- Fix CosmosDbQuery to default unset paging to PagingArgs.DefaultTake (via the shared WithPaging
  extension) instead of applying no limit at all, matching EF-backed domains.
- Add a prominent BypassFilters remark clarifying it never affects the built-in tenant/logical-delete
  checks, and harden CosmosDbTransaction.Enlist's container-identity check to compare Container.Id/
  Database.Id instead of relying on reference equality.
- Remove EfDbModelOptions' misleading WithTenantFilter(allowFilterBypass) parameter - the point-op
  tenant check never honoured it, so tenant isolation is now always unconditional, matching Cosmos.
- Move CosmosDbBatch/CosmosDbContainerExtensions from CoreEx.UnitTesting into CoreEx.Cosmos.Extended:
  both are pure Microsoft.Azure.Cosmos SDK helpers with no test-framework dependency, and the move
  drops CoreEx.UnitTesting's direct Microsoft.Azure.Cosmos package dependency entirely (confirmed via
  the built .nuspec - it now only depends on CoreEx.Cosmos, which carries it transitively).
- Update README/AGENTS docs for CoreEx.Cosmos and CoreEx.UnitTesting to reflect the move and fix
  unrelated pre-existing staleness (JsonDataReader's earlier relocation to CoreEx.Data.Json).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
Port release/3's Cosmos multi-set query mechanism to the current
CoreEx.Cosmos project, adapted for discriminator-keyed (not positional)
demuxing:

- Extract a shared, minimal IMultiSetArgsCore base interface into
  CoreEx.Data (MinimumRows/MaximumRows/StopOnNull/InvokeResult) for
  cross-provider reuse; CoreEx.Database.Extended.IMultiSetArgs now
  extends it.
- Add CoreEx.Cosmos.Extended.IMultiSetArgs/IMultiSetArgs<TModel>,
  MultiSetSingleArgs<TModel>, MultiSetCollArgs<TColl, TModel>, and
  MultiSetOptions (PartitionKey/Args/MultiSetArgs).
- Add CosmosDbMultiSetExtensions.SelectMultiSetAsync: resolves the
  type-discriminator JSON property from the ambient naming policy,
  executes a single raw-stream query across all requested
  discriminators, always excludes co-located outbox documents, applies
  a defensive IS_DEFINED-guarded tenant/logical-delete SQL predicate
  as a server-side (RU/bandwidth) optimization on top of the existing
  per-item CheckModel check, demuxes/deserializes/accumulates per
  document, then validates MinimumRows/MaximumRows and invokes
  InvokeResult() per IMultiSetArgs in supplied order (honoring
  StopOnNull).
- CosmosDbArgs.QueryRequestOptions, where supplied, takes precedence
  over one built from MultiSetOptions.PartitionKey; a genuine
  partition-key mismatch between the two throws ArgumentException.
- Add CosmosDbMultiSetTests (13 tests) and SoftDeleteAnimalItem test
  model; update CoreEx.Cosmos/CoreEx.Data README/AGENTS docs.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
…mos multi-set queries

Closes the one gap found comparing the ported multi-set feature against release/3
and the current CoreEx.Cosmos package convention: every other CosmosDbContainer<TModel>
operation has a WithResultAsync pairing, but SelectMultiSetAsync was exception-only.

- IMultiSetArgs.AddItem now returns Result (propagating a genuine CheckModel failure
  rather than only swallowing the silently-excluded null case).
- CosmosDbMultiSetExtensions engine rewritten to return Task<Result> internally;
  SelectMultiSetAsync is now a thin ThrowOnError() wrapper over the new
  SelectMultiSetWithResultAsync.
- Added success/failure-path tests for the new method; all 74 CoreEx.Cosmos.Test.Unit
  tests pass.
- Updated README.md/AGENTS.md to document the new ROP counterpart.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
Per V3->V4 convention, ArgumentException/InvalidOperationException/NotSupportedException
guard-clause and invariant violations must be thrown directly, even from a Result-returning
method - Result.Fail is reserved for genuine business/domain-level outcomes.

- Reverted MinimumRows/MaximumRows/malformed-response/deserialization-failure checks in
  CosmosDbMultiSetExtensions from Result.Fail(...) back to plain throws (mirrors
  CosmosDbContainer.DeleteWithResultAsync's own InvalidOperationException guard clause).
- IMultiSetArgs.AddItem still returns Result, since it must propagate a genuine WithFilter
  authorization-style Result.IsFailure from CheckModel/CheckFilters - that remains correct.
- Updated tests: the two min/max-rows tests now assert throwing (even via
  SelectMultiSetWithResultAsync); added a new genuine-failure test using WithFilter's
  nonQueryResult to verify a real business Result.IsFailure (AuthenticationException) is
  still correctly surfaced.
- Corrected README.md/AGENTS.md wording accordingly.

All 75 CoreEx.Cosmos.Test.Unit tests pass; full CoreEx.sln builds with 0 warnings/errors.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
- Add per-event relay marker spans (outbox.relay.publish) parented on the original producer's trace context, so the relay hop is visible end-to-end from the originating PUT trace, alongside a batch-level link back to all originating traces for the relay's own operational trace.
- Add a Sampler to filter out ServiceBus background polling/lock-renewal noise (Receive, RenewMessageLockAsync) from traces.
- Fix relay markers being emitted before the publish call completed, which could show a false-positive 'relayed' marker for an event whose publish subsequently failed and was retried; markers now only emit after a confirmed successful publish (DatabaseOutboxRelayBase and CosmosDbOutboxRelayProcessor).
- Remove a redundant duplicate Activity.Stop() call in InvokerTracer.TraceComplete.
- Add unit test coverage for the new Sampler and relay-marker/link extensions.
- Update samples/docs/hosts-layer.md accordingly.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
Copilot AI lite review requested due to automatic review settings September 19, 2026 17:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

One or more issues must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 6 High severity · 1 Medium severity

Open (7)
What changed in this PR

Introduces CoreEx.Cosmos, a typed Cosmos DB access/outbox package, and a Cosmos-backed Contoso.Customers sample, alongside shared framework improvements for ETags, resiliency, TTL, mapping, and seed data.

Changes:

  • Adds Cosmos CRUD, querying, transactional outbox, relay, code generation, tests, and documentation.
  • Adds the Customers sample domain, API, persistence, code generation, and integration tests.
  • Harmonizes shared CoreEx infrastructure and relocates JSON seed-data support into CoreEx.Data.
File Description
tests/​Directory.Build.props Updated as part of this pull request.
tests/​CoreEx.Test.Unit/​Mapping/​MapTests.cs Updated as part of this pull request.
tests/​CoreEx.Database.SqlServer.Test.Unit/​Repository/​TestEfDb.cs Updated as part of this pull request.
tests/​CoreEx.Database.SqlServer.Test.Unit/​EntityFrameworkBehaviorTests.cs Updated as part of this pull request.
tests/​CoreEx.Database.Postgres.Test.Unit/​Repository/​TestEfDb.cs Updated as part of this pull request.
tests/​CoreEx.Database.Postgres.Test.Unit/​EntityFrameworkBehaviorTests.cs Updated as part of this pull request.
tests/​CoreEx.Cosmos.Test.Unit/​GlobalUsing.cs Updated as part of this pull request.
tests/​CoreEx.Cosmos.Test.Unit/​Data/​read-data.seed.yaml Updated as part of this pull request.
tests/​CoreEx.Cosmos.Test.Unit/​appsettings.unittest.json Updated as part of this pull request.
src/​CoreEx/​Invokers/​InvokerTracer.cs Updated as part of this pull request.
src/​CoreEx/​Hosting/​ResilienceOwner.cs Updated as part of this pull request.
src/​CoreEx/​GlobalUsing.cs Updated as part of this pull request.
src/​CoreEx/​Data/​Model.cs Updated as part of this pull request.
src/​CoreEx/​Data/​ITimeToLive.cs Updated as part of this pull request.
src/​CoreEx/​Data/​IReadOnlyTimeToLive.cs Updated as part of this pull request.
src/​CoreEx/​Data/​IReadOnlyPartitionKey.cs Updated as part of this pull request.
src/​CoreEx.UnitTesting/​GlobalUsing.cs Updated as part of this pull request.
src/​CoreEx.UnitTesting/​Events/​EventPublisherDecorator.cs Updated as part of this pull request.
src/​CoreEx.UnitTesting/​CoreEx.UnitTesting.csproj Updated as part of this pull request.
src/​CoreEx.UnitTesting/​AGENTS.md Updated as part of this pull request.
src/​CoreEx.Template/​content/​CoreEx.Core/​tests/​app-name.Test.Unit/​GlobalUsing.cs Updated as part of this pull request.
src/​CoreEx.Template/​content/​CoreEx.Core/​_Directory.Packages.props Updated as part of this pull request.
src/​CoreEx.Events/​Publishing/​EventPublisherBase.cs Updated as part of this pull request.
src/​CoreEx.Events/​CoreExEventsExtensions.OpenTelemetry.cs Updated as part of this pull request.
src/​CoreEx.EntityFrameworkCore/​EfDbArgs.cs Updated as part of this pull request.
src/​CoreEx.Database/​Outbox/​DatabaseOutboxRelayResiliencyExecutor.cs Updated as part of this pull request.
src/​CoreEx.Database/​Outbox/​DatabaseOutboxRelayArgs.cs Updated as part of this pull request.
src/​CoreEx.Database/​GlobalUsing.cs Updated as part of this pull request.
src/​CoreEx.Database/​Extended/​IMultiSetArgs.cs Updated as part of this pull request.
src/​CoreEx.Database/​Abstractions/​DatabaseInvoker.cs Updated as part of this pull request.
src/​CoreEx.Database.SqlServer/​SqlServerUnitOfWork.cs Updated as part of this pull request.
src/​CoreEx.Database.SqlServer/​README.md Updated as part of this pull request.
src/​CoreEx.Database.SqlServer/​GlobalUsing.cs Updated as part of this pull request.
src/​CoreEx.Database.SqlServer/​Extended/​SqlServerUnitOfWorkInvoker.cs Updated as part of this pull request.
src/​CoreEx.Database.SqlServer/​AGENTS.md Updated as part of this pull request.
src/​CoreEx.Database.Postgres/​README.md Updated as part of this pull request.
src/​CoreEx.Database.Postgres/​PostgresUnitOfWork.cs Updated as part of this pull request.
src/​CoreEx.Database.Postgres/​GlobalUsing.cs Updated as part of this pull request.
src/​CoreEx.Database.Postgres/​Extended/​PostgresUnitOfWorkInvoker.cs Updated as part of this pull request.
src/​CoreEx.Database.Postgres/​AGENTS.md Updated as part of this pull request.
src/​CoreEx.Data/​Json/​JsonPropertyNamingConvention.cs Updated as part of this pull request.
src/​CoreEx.Data/​Json/​JsonDataReaderArgs.cs Updated as part of this pull request.
src/​CoreEx.Data/​Json/​JsonDataReader.cs Updated as part of this pull request.
src/​CoreEx.Data/​IUnitOfWork.SynchronizeETag.cs Updated as part of this pull request.
src/​CoreEx.Data/​GlobalUsing.cs Updated as part of this pull request.
src/​CoreEx.Data/​CoreEx.Data.csproj Updated as part of this pull request.
src/​CoreEx.Cosmos/​Outbox/​CosmosDbOutboxRelayInvoker.cs Updated as part of this pull request.
src/​CoreEx.Cosmos/​GlobalUsing.cs Updated as part of this pull request.
src/​CoreEx.Cosmos/​Extended/​TypeDiscriminatorGuard.cs Updated as part of this pull request.
src/​CoreEx.Cosmos/​CosmosDbContainer.Query.cs Updated as part of this pull request.
src/​CoreEx.Cosmos/​CoreExCosmosExtensions.OpenTelemetry.cs Updated as part of this pull request.
src/​CoreEx.Cosmos/​CoreEx.Cosmos.csproj Updated as part of this pull request.
src/​CoreEx.CodeGen/​Scripts/​ref-data-script.yaml Updated as part of this pull request.
src/​CoreEx.CodeGen/​RefData/​Templates/​Repository_cs.hbs Updated as part of this pull request.
src/​CoreEx.CodeGen/​RefData/​Templates/​CosmosPersistenceModel_cs.hbs Updated as part of this pull request.
src/​CoreEx.CodeGen/​RefData/​Generators/​CosmosPersistenceModelGenerator.cs Updated as part of this pull request.
src/​CoreEx.Azure.Messaging.ServiceBus/​ServiceBusPublisherResiliency.cs Updated as part of this pull request.
src/​CoreEx.Azure.Messaging.ServiceBus/​GlobalUsing.cs Updated as part of this pull request.
src/​CoreEx.AspNetCore/​Abstractions/​WebApi.MergePatch.cs Updated as part of this pull request.
src/​CoreEx.AspNetCore/​Abstractions/​WebApi.cs Updated as part of this pull request.
samples/​tests/​Contoso.Shopping.Test.Unit/​GlobalUsing.cs Updated as part of this pull request.
samples/​tests/​Contoso.Products.Test.Unit/​GlobalUsing.cs Updated as part of this pull request.
samples/​tests/​Contoso.Orders.Test.Unit/​GlobalUsing.cs Updated as part of this pull request.
samples/​tests/​Contoso.Customers.Test.Unit/​Validators/​AddressValidatorTests.cs Updated as part of this pull request.
samples/​tests/​Contoso.Customers.Test.Unit/​GlobalUsing.cs Updated as part of this pull request.
samples/​tests/​Contoso.Customers.Test.Unit/​EntryPoint.cs Updated as part of this pull request.
samples/​tests/​Contoso.Customers.Test.Unit/​Contoso.Customers.Test.Unit.csproj Updated as part of this pull request.
samples/​tests/​Contoso.Customers.Test.Common/​TestData.cs Updated as part of this pull request.
samples/​tests/​Contoso.Customers.Test.Common/​Data/​ref-data.seed.yaml Updated as part of this pull request.
samples/​tests/​Contoso.Customers.Test.Common/​Data/​read-data.seed.yaml Updated as part of this pull request.
samples/​tests/​Contoso.Customers.Test.Common/​Data/​mutate-data.seed.yaml Updated as part of this pull request.
samples/​tests/​Contoso.Customers.Test.Common/​Contoso.Customers.Test.Common.csproj Updated as part of this pull request.
samples/​tests/​Contoso.Customers.Test.Api/​GlobalUsing.cs Updated as part of this pull request.
samples/​tests/​Contoso.Customers.Test.Api/​CustomerReadTests.cs Updated as part of this pull request.
samples/​tests/​Contoso.Customers.Test.Api/​CustomerMutateTests.cs Updated as part of this pull request.
samples/​tests/​Contoso.Customers.Test.Api/​appsettings.unittest.json Updated as part of this pull request.
samples/​src/​Contoso.Customers.Infrastructure/​Repositories/​ReferenceDataRepository.g.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Infrastructure/​Repositories/​ReferenceDataRepository.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Infrastructure/​Repositories/​CustomersCosmosDb.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Infrastructure/​Repositories/​CustomerQueryArgsConfig.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Infrastructure/​Persistence/​CustomerType.g.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Infrastructure/​Persistence/​Customer.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Infrastructure/​Persistence/​ContactMethod.g.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Infrastructure/​Persistence/​Address.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Infrastructure/​Mapping/​CustomerTypeMapper.g.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Infrastructure/​Mapping/​CustomerMapper.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Infrastructure/​Mapping/​ContactMethodMapper.g.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Infrastructure/​Mapping/​AddressMapper.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Infrastructure/​GlobalUsing.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Infrastructure/​Contoso.Customers.Infrastructure.csproj Updated as part of this pull request.
samples/​src/​Contoso.Customers.Contracts/​GlobalUsing.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Contracts/​CustomerType.g.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Contracts/​CustomerLite.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Contracts/​CustomerBase.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Contracts/​Customer.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Contracts/​Contoso.Customers.Contracts.csproj Updated as part of this pull request.
samples/​src/​Contoso.Customers.Contracts/​ContactMethod.g.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Contracts/​Address.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.CodeGen/​ref-data.yaml Updated as part of this pull request.
samples/​src/​Contoso.Customers.CodeGen/​Program.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.CodeGen/​Contoso.Customers.CodeGen.csproj Updated as part of this pull request.
samples/​src/​Contoso.Customers.Application/​Validators/​CustomerValidator.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Application/​Validators/​AddressValidator.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Application/​Repositories/​IReferenceDataRepository.g.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Application/​Repositories/​ICustomerRepository.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Application/​ReferenceDataService.g.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Application/​Interfaces/​ICustomerService.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Application/​Interfaces/​ICustomerReadService.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Application/​GlobalUsing.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Application/​CustomerReadService.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Application/​Contoso.Customers.Application.csproj Updated as part of this pull request.
samples/​src/​Contoso.Customers.Api/​Properties/​launchSettings.json Updated as part of this pull request.
samples/​src/​Contoso.Customers.Api/​GlobalUsing.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Api/​Controllers/​CustomerReadController.cs Updated as part of this pull request.
samples/​src/​Contoso.Customers.Api/​Contoso.Customers.Api.csproj Updated as part of this pull request.
samples/​src/​Contoso.Customers.Api/​appsettings.json Updated as part of this pull request.
samples/​src/​Contoso.Customers.Api/​appsettings.Development.json Updated as part of this pull request.
CoreEx.Samples.Test.slnf Updated as part of this pull request.
CoreEx.Samples.Build.slnf Updated as part of this pull request.
CoreEx.Core.Test.Sequential.slnf Updated as part of this pull request.
CoreEx.Core.slnf Updated as part of this pull request.
AGENTS.md Updated as part of this pull request.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread samples/src/Contoso.Customers.Application/CustomerService.cs Outdated
Comment thread src/CoreEx.CodeGen/RefData/Templates/Repository_cs.hbs
Comment thread src/CoreEx.Cosmos/CosmosDbContainer.Upsert.cs
Comment thread src/CoreEx.Cosmos/CosmosDbQuery.cs Outdated
Comment thread src/CoreEx.Cosmos/Extended/CosmosDbInvoker.cs Outdated
Comment thread src/CoreEx.Cosmos/Extended/CosmosDbInvoker.cs
Comment thread src/CoreEx.Cosmos/Outbox/CosmosDbOutboxRelayProcessor.cs Outdated
Fixes for all 7 Copilot review comments on the CoreEx.Cosmos introduction:
- CustomerService.DeleteAsync: close a TOCTOU race by carrying the read ETag into a
  conditional delete inside the unit-of-work transaction, catching ConcurrencyException
  and re-surfacing the 'already shopped' business rule if it changed underneath us.
- CosmosDbQuery.ApplyPagingIfSet: stop defaulting to PagingArgs.DefaultTake when paging
  was never explicitly requested, so ToListAsync/ToCollectionAsync/ToMappedItemsAsync (and
  the generated Cosmos reference-data repository, which relies on this) return the full
  result set unless the caller opts into paging.
- CosmosDbContainer.Upsert: add UpsertWithinTransactionAsync, used whenever a
  CosmosDbUnitOfWork transaction is active. Forces a pre-read to determine create-vs-update
  before enlisting in the TransactionalBatch (which can't tolerate a 404 discovered
  post-hoc), instead of failing the whole batch when upserting a new key.
- CosmosDbInvoker.OrchestrateUnitOfWorkTransactionAsync: track eventStartCount and
  discard/rollback queued events for the current nesting level on any failure (not just
  after HasBeenPublished), and add CosmosDbTransaction.IsAborted/Abort() so a nested
  failure the outer work delegate ignores still aborts the whole batch at commit time.
- CosmosDbOutboxRelayProcessor.DeleteOneAsync: branch on doc.PartitionKey being null
  (PartitionKey.None, a supported/used model shape) instead of null-forcing it, which
  threw and left published no-partition-key outbox documents stuck until TTL expiry.

Also fixes the CI build failure: ServiceBusReceiverTests.GetAndClearAzureServiceBusAsync_
ReturnsAllPublishedMessages flaked on net8.0 only (16 messages found instead of 10).
NUnit does not guarantee test execution order, and reflection-based method enumeration
differs between .NET runtime versions; other tests in the fixture that publish-then-
abandon/circuit-break messages ran before this one on net8.0 and leaked into its count
assertion. Pinned it to Order(-1) so it always runs first, regardless of TFM/runtime.

Added regression tests: CosmosDbContainerQueryTests (unbounded query with no paging),
CosmosDbUnitOfWorkTests (upsert-new-key create-in-transaction, upsert-existing-key update,
nested-failure-aborts-whole-batch, reused-unit-of-work-does-not-leak-abandoned-event),
CosmosDbOutboxRelayTests (PartitionKey.None publish+delete).

Verified: CoreEx.Cosmos.Test.Unit 82/82 passing; Contoso.Customers.Test.Api 45/45 passing;
ServiceBusReceiverTests 27/27 passing across 3 repeated runs x 3 TFMs.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
Copilot AI review requested due to automatic review settings September 19, 2026 18:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

One or more issues must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 5 High severity

Open (5)
Resolved since last review (7)
Previously missed (1)

In code that hasn't changed since last review

Low severity Use Runtime.NewGuid for outbox identifiers

src/​CoreEx.Cosmos/​Outbox/​CosmosDbEventPublisher.cs:67

Framework-generated identifiers should come from CoreEx's ambient identifier provider so callers and tests can substitute them consistently. Guid.NewGuid() bypasses that provider for every outbox document; use Runtime.NewGuid() instead.

Comment thread src/CoreEx.Cosmos/CosmosDbArgs.cs
Comment thread src/CoreEx.Cosmos/Extended/CosmosDbBatch.cs
Comment thread src/CoreEx.Data/IUnitOfWork.SynchronizeETag.cs
Comment thread src/CoreEx.Database/Outbox/DatabaseOutboxRelayArgs.cs
Comment thread src/CoreEx.Events/Publishing/EventPublisherBase.cs
…ters consistency with EF)

CosmosDbQuery<TModel>.AsQueryable() previously short-circuited entirely when
CosmosDbArgs.BypassFilters was true, skipping the call to
CosmosDbModelOptions<TModel>.ApplyFilters altogether. This silently bypassed
the mandatory tenant, logical-delete, type-discriminator, and outbox-document
exclusion predicates - not just the additive, explicitly opt-in-bypassable
WithFilter registrations - contradicting the documented CosmosDbArgs.BypassFilters
contract and diverging from CoreEx.EntityFrameworkCore's EfDbModel<TModel>.Query(),
which always calls EfDbModelOptions<TModel>.ApplyFilters unconditionally and lets
ApplyFilters itself make the per-registration bypass decision.

Fix: AsQueryable now always calls Container.Options.ApplyFilters(...), matching
EF's call-site pattern exactly. ApplyFilters already correctly gated only the
additive _filters collection on (args.BypassFilters && allowFilterBypass); the
mandatory tenant/logical-delete/type-discriminator/outbox-exclusion predicates
were already applied unconditionally inside ApplyFilters, they just weren't
being reached when BypassFilters was true.

Test changes (tests/CoreEx.Cosmos.Test.Unit/CosmosDbContainerFilterTests.cs):
- Replaced AsQueryable_WithBypassFilters_SurfacesFilteredItems (which asserted
  the old, buggy broad-bypass behavior) with
  AsQueryable_WithBypassFilters_OnlyBypassesFiltersRegisteredAsBypassable,
  confirming BypassFilters has no effect on a filter registered without
  allowFilterBypass: true.
- Added AsQueryable_WithBypassFilters_BypassesFilterRegisteredAsBypassable,
  confirming the per-registration opt-in bypass still works as documented.
- Added AsQueryable_WithBypassFilters_TenantFilterStillApplies and
  AsQueryable_WithBypassFilters_LogicalDeleteFilterStillApplies, the direct
  regression coverage for the review comment: both mandatory filters remain
  applied via AsQueryable even when BypassFilters is true.

Verified: CoreEx.Cosmos.Test.Unit 85/85 passing across net8.0/net9.0/net10.0.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
Copilot AI review requested due to automatic review settings September 19, 2026 19:07
…ainer provisioning; misc PR review fixes

CoreEx.Cosmos:
- Add CosmosDbModelOptions.HasQueryOnlyFilters and make IMultiSetArgsT.BuildFilterClause throw
  NotSupportedException when a model has query-only WithFilter registrations, since such filters were
  silently ignored by multi-set queries (only enforced by normal CosmosDbQuery), letting documents through
  that a single-set query would have excluded. Callers must register a nonQueryResult instead, which multi-set
  already enforces correctly per-item.
- Auto-provision the outbox relay lease container (partitioned on /id) in CosmosDbOutboxRelay.StartAsync via
  Database.CreateContainerIfNotExistsAsync, since it was previously only resolved via GetContainer (a proxy
  reference, never created) and a fresh Cosmos database would fail ChangeFeedProcessor.StartAsync. Add
  CosmosDbOutboxRelayOptions.LeaseContainerThroughput for non-serverless accounts.
- Fix stale CosmosDbItemBase references (-> CosmosDbModelBase) in AGENTS.md, README.md, and a test comment.
- Correct docker-compose.yml Cosmos emulator container-count comment (24, not 25).
- Add regression tests for both fixes, validated bidirectionally against pre-fix behaviour.

CoreEx.Events:
- SubscribedManager: also clear ActivityTraceFlags.Recorded on the current activity (not just its parent) when
  suppressing tracing for unsubscribed events, so a child activity created before the current one stops is
  correctly excluded by the ambient ParentBasedSampler regardless of which activity is its immediate parent.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
Copilot AI review requested due to automatic review settings September 20, 2026 00:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

An unresolved critical Cosmos outbox-ID mapping issue remains, and the broad cross-project change warrants human review.

Review effort: Lite
Findings: 1 High severity

Open (1)
Resolved since last review (2)

Comment thread src/CoreEx.Cosmos/CosmosDbModelOptions.cs Outdated
…nal Id properties

CosmosDbModelOptions.ResolveOutboxIdExclusion previously only matched a string property explicitly
decorated with [JsonPropertyName("id")]. A model that does not implement IIdentifier<string>/
IReadOnlyIdentifier<string> and exposes a plain, unannotated string Id property - relying on the
configured JSON serializer's naming policy (e.g. camelCase) to map it to Cosmos DB's reserved "id" -
was invisible to this fallback, so _outboxIdExclusion resolved to null and outbox event documents
co-located in the same container could leak into ordinary business queries for that model.

Extend the fallback to also match a public string property named "Id" (case-insensitive) that has
neither an explicit [JsonPropertyName] (which would mean it is deliberately mapped elsewhere) nor a
[JsonIgnore] attribute. Add a regression test (ConventionIdKeyedItem +
Query_WithOutboxDocumentsPresent_AutomaticallyExcludesThem_ModelWithConventionalUnannotatedIdProperty),
validated bidirectionally (fails pre-fix, passes post-fix). Full CoreEx.Cosmos.Test.Unit suite:
106/106 passing on net8.0/net9.0/net10.0.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
Copilot AI review requested due to automatic review settings September 20, 2026 00:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved critical type-discriminator handling and moderate correctness and dependency issues block approval.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
Resolved since last review (1)

Comment thread src/CoreEx.Cosmos/CosmosDbModelOptions.cs Outdated
…eate/Update/Upsert/logical-delete

CosmosDbModelOptions<TModel>.WithTypeDiscriminator(typeDiscriminator) stores an explicit override value
purely for filtering (IsTypeDiscriminatorMismatch), but Model.PrepareCreate/PrepareUpdate/PrepareTypeDiscriminator
always stamp their own default (SchemaAttribute.Name or the type name) - the configured override was never
applied to the model actually being persisted. A model created/updated with an explicit override would
therefore be written with the default discriminator instead, immediately fail its own configured-discriminator
check, and become invisible to the container's own queries/point reads.

Add CosmosDbModelOptions<TModel>.ApplyTypeDiscriminator(model), which re-stamps the configured override (where
set) after Model.PrepareCreate/PrepareUpdate/PrepareTypeDiscriminator have already run. Wire it into
CosmosDbContainer<TModel>'s Create, Update, Upsert (both the transactional pre-read stamp and the delegated
Create/Update paths), and the logical-delete read-modify-write path, so every model-mutation path that
stamps a type discriminator consistently applies the explicit override.

Confirmed EF Core (CoreEx.EntityFrameworkCore) has no equivalent bug: EfDbModelOptions<TModel> only exposes
TypeDiscriminatorSupport for feature detection and has no WithTypeDiscriminator override mechanism to
desynchronize from Model.PrepareCreate/PrepareUpdate's default stamping.

Add regression tests (CreateAsync_StampsExplicitTypeDiscriminatorOverride_WhenConfigured,
UpdateAsync_StampsExplicitTypeDiscriminatorOverride_WhenConfigured), validated bidirectionally (fails
pre-fix, passes post-fix). Full CoreEx.Cosmos.Test.Unit suite: 108/108 passing on net8.0/net9.0/net10.0.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
Copilot AI review requested due to automatic review settings September 20, 2026 14:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

A critical code-generation conflict and other unresolved review findings must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
Resolved since last review (1)
Previously missed (2)

In code that hasn't changed since last review

Medium severity Add integration tests for Customer reference-data endpoints

samples/​src/​Contoso.Customers.Api/​Controllers/​ReferenceDataController.cs:15

The new reference-data controller has no matching API integration tests. Existing sample API coverage includes a dedicated HostTests.ReferenceData.cs covering each reference-data route and the named endpoint; without equivalent Customer tests, query/paging/filter parsing and named-data behavior can regress without detection. Add read tests for these three endpoints.

Low severity Register Cosmos database health check in setup guidance

src/​CoreEx.Cosmos/​AGENTS.md:13

This setup guidance is incorrect: AddAzureCosmosClient does not register the package's CosmosDbHealthCheck, and AddCosmosDb also leaves health checks unregistered. A host following these instructions will omit the Cosmos database readiness check; the implementation and sample explicitly require AddCosmosDbHealthCheck() (see CoreExCosmosExtensions.DependencyInjection.cs:80-85 and Contoso.Customers.Api/Program.cs:57).

Comment thread samples/src/Contoso.Customers.Api/Controllers/ReferenceDataController.cs Outdated
Extend CoreEx.CodeGen's reference-data schema/generation with a 'Cosmos' repository option
(alongside the existing None/EntityFramework), including the cosmosRepositoryName and
cosmosPersistenceModel configuration properties (root and per-entity), and modelPlural for the
persistence model name. Regenerate Contoso.Customers.Api's ReferenceDataController.g.cs from this
new Cosmos-aware code generation, replacing the previous hand-written stand-in (which existed only
because repository: Cosmos did not yet generate API-layer code).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
Copilot AI review requested due to automatic review settings September 20, 2026 15:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Critical CodeGen and multi-set discriminator issues, plus outbox TTL and resiliency concerns, remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 High severity

Open (2)
Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Enable or validate Cosmos TTL for outbox containers

src/​CoreEx.Cosmos/​Outbox/​CosmosDbEventPublisher.cs:42

Setting a per-document ttl does not enable expiry unless the Cosmos container has a non-null DefaultTimeToLive. This default is used as the relay's cleanup-failure safety net, but neither this registration nor the sample container setup enables or validates container TTL, so a cleanup failure can leave outbox documents indefinitely and allow storage/RU growth. Provision outbox-hosting containers with TTL enabled or fail fast/document that prerequisite.

Comment thread src/CoreEx.CodeGen/RefData/Templates/CosmosPersistenceModel_cs.hbs
Comment thread src/CoreEx.Cosmos/Extended/IMultiSetArgsT.cs Outdated
…eries; fix(codegen): support non-string idType for Cosmos-backed reference data

- IMultiSetArgs.TypeDiscriminator replaced with ResolveTypeDiscriminator(cosmosDb, containerId), resolving CosmosDbModelOptions<TModel>.EffectiveTypeDiscriminator (the explicit WithTypeDiscriminator override where configured) instead of always the schema/CLR-name default - a multi-set query would otherwise search for the wrong discriminator value and never find documents persisted with an override.
- CosmosDbMultiSetExtensions resolves each IMultiSetArgs discriminator once up front and reuses it consistently in SQL parameters, demux lookups, and error messages.
- Added regression test SelectMultiSetAsync_FindsModelConfiguredWithExplicitTypeDiscriminatorOverride; validated bidirectionally.
- CodeGen: added EntityConfig.MapperIdExpression (parses the Cosmos persistence model's string Id into the configured idType) and CollectionInherits (uses the two-type-param ReferenceDataCollection<TId, TRef> for non-String idType) so a Cosmos-backed reference-data entity with idType Guid/Int32/Int64 now generates compiling code instead of a type-mismatch/CS0311 failure. Validated by running the generator against a temporary Guid idType entity.
- Updated CoreEx.Cosmos AGENTS.md/README.md to describe the new resolution mechanism.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
Copilot AI review requested due to automatic review settings September 20, 2026 15:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The critical outbox rollback issue and unresolved TTL configuration issues block approval.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
Resolved since last review (2)
Previously missed (2)

In code that hasn't changed since last review

Medium severity Enable TTL on outbox-capable Cosmos containers

src/​CoreEx.Cosmos/​Outbox/​CosmosDbEventPublisher.cs:42

Setting OutboxTimeToLiveSeconds only writes the per-item ttl; Cosmos ignores that field unless the container has TTL enabled. AddCosmosDb and the Customers setup create containers without DefaultTimeToLive, so the advertised seven-day bound is inert and outbox documents can accumulate indefinitely during relay outages. Enable per-item TTL (defaultTtl: -1) when provisioning each outbox-capable container, or fail fast/document this prerequisite.

Low severity Update Cosmos health check registration guidance

src/​CoreEx.Cosmos/​AGENTS.md:13

This registration guidance contradicts the package implementation: AddAzureCosmosClient does not add a Cosmos health check, while AddCosmosDbHealthCheck is required and is explicitly called by the sample. A consumer following this example will omit the cosmos-database readiness check; update the example and the following explanation to register the custom check.

Comment thread src/CoreEx.Database/Abstractions/DatabaseInvoker.cs Outdated
…t the publisher-global HasBeenPublished

DatabaseInvoker.OrchestrateUnitOfWorkTransactionAsync and its Cosmos DB mirror,
CosmosDbInvoker.OrchestrateUnitOfWorkTransactionAsync, decided whether to call
Outbox.RollbackAsync() (undo an already-published batch) or Outbox.Dequeue()
(discard not-yet-published events) by checking the publisher-global
IEventPublisher.HasBeenPublished flag. That flag is scoped to the Outbox
instance's lifetime, not to a single transaction invocation, so on a
request-scoped IUnitOfWork reused across multiple sequential TransactionAsync
calls, a later, entirely unrelated transaction that fails before publishing
anything of its own (even with zero events) would still see HasBeenPublished
== true from an earlier successful publish and wrongly call RollbackAsync(),
undoing that earlier transaction's genuine, already-committed publish.

Fixed by introducing a local publishedByThisInvocation flag, set only when
THIS invocation's own root branch actually calls PublishAsync, and checked
instead of the global flag.

While writing the regression test, found a second, related bug: Dequeue(count)
unconditionally throws once HasBeenPublished is (globally) true, regardless of
count - so even Dequeue(0) for the same "later unrelated failed transaction"
scenario would still crash. Fixed by only calling Dequeue when this invocation
actually enqueued its own events to remove (addedByThisInvocation > 0);
otherwise the Outbox is left untouched entirely.

Added CosmosDbUnitOfWorkTests.TransactionAsync_ReusedAfterSuccessfulPublish_SubsequentUnrelatedFailure_DoesNotRollBackEarlierPublish
with a tracking fake IEventPublisher, validated bidirectionally (reverted the
fix, confirmed the test reproduces the original bug, restored, confirmed
pass). Full CoreEx.Cosmos.Test.Unit suite: 110/110 passing on
net8.0/net9.0/net10.0. Full solution build: 0 warnings/errors.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
Copilot AI review requested due to automatic review settings September 20, 2026 16:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The final review includes five moderate findings and one documentation nit requiring follow-up before approval.

Review effort: Lite
Findings: None

Resolved since last review (1)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Enable or validate Cosmos TTL for outbox cleanup

src/​CoreEx.Cosmos/​Outbox/​CosmosDbEventPublisher.cs:42

OutboxTimeToLiveSeconds only writes the per-document ttl property; Cosmos DB ignores that property unless the containing container has TTL enabled (DefaultTimeToLive set). The package neither provisions nor validates that setting, so the documented cleanup safety net is ineffective on a normally created container and failed cleanup can leave outbox documents indefinitely. Enable/validate container TTL during provisioning and document the prerequisite (including for the sample).

@chullybun
chullybun merged commit f43f556 into main Sep 20, 2026
4 checks passed
@chullybun
chullybun deleted the introduce-cosmos branch September 20, 2026 16:59
@chullybun chullybun added the enhancement New feature or request label Sep 20, 2026
@chullybun chullybun added this to the v4.0.0 milestone Sep 20, 2026
@chullybun chullybun mentioned this pull request Sep 20, 2026
4 tasks
chullybun added a commit that referenced this pull request Sep 21, 2026
* Rename coreex-solution-scaffolder to coreex-scaffold, add full Claude/Copilot command parity, bump to stable v4.0.0, and sync docs for CoreEx.Cosmos (PR #186)

- Rename coreex-solution-scaffolder skill to coreex-scaffold for naming consistency
- Add missing .claude/commands/*.md wrappers for full Claude/Copilot skill parity
- Bump Version.props to stable 4.0.0
- Remove prerelease OpenTelemetry.Instrumentation.Process dependency blocking stable packaging
- Sync AGENTS.md, copilot-instructions.md, samples docs, and agents/README.md with CoreEx.Cosmos and Contoso.Customers introduced in PR #186

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>

* Fix stale README table of contents entries

CHANGELOG.md: add chullybun to key-contributor call-out (manual tweak).
README.md: remove dead 'Version 4 (preview)' TOC link (no matching heading) and fix 'Status' to point at the actual 'Build and Package Status' heading.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>

* Publish CoreEx.Cosmos as preview package in v4.0.0

- Add src\CoreEx.Cosmos to nuget-publish.ps1's ProjectsToPublish list
- Replace 'not packaged' language with a preview/subject-to-change
  disclaimer across README.md, AGENTS.md, CoreEx.Cosmos's own
  README.md/AGENTS.md, and samples docs, since it now ships alongside
  the rest of v4.0.0 instead of being held back

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>

* Address PR #187 review comments

- Add missing CoreEx.Cosmos AGENTS.md to CoreEx.Template's AI doc bundle
  and bump the per-package guide count from 17 to 18 throughout
  .github/agents/README.md
- Fix validate-template-pack.ps1's FilesAbsent checks to assert against
  the actual old skill path (.github/skills/coreex-solution-scaffolder)
  instead of a path that was never used
- Sync .claude/commands/coreex-scaffold.md's frontmatter description
  with the renamed coreex-scaffold skill's actual description
- Correct samples/docs/infrastructure-layer.md and tooling.md: Cosmos
  CodeGen does generate persistence models (CosmosPersistenceModelGenerator)
  - only DbContext/database-migration generation is absent
- Fix a non-compiling CustomerRepository doc snippet (missing _cosmos
  field declaration)
- Remove CoreExExtensions.WithCoreExTelemetry's automatic process-wide
  AppContext.SetData mutation of the regex NonBacktracking automata cap
  (a library should not silently raise a global resource limit for
  every regex in a consuming app); documented the net8.0 opt-in via
  RuntimeHostConfigurationOption for hosts that need it

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>

* Restore net8.0 regex-automata cap opt-in as host-explicit call, not library-side mutation

WithCoreExTelemetry() no longer silently calls AppContext.SetData for the net8.0
non-backtracking regex automata-size cap (per PR #187 review feedback). However,
Contoso.Shopping/Products Subscribe+Relay hosts combine WithCoreExTelemetry() with
WithCoreExServiceBusTelemetry()'s wildcard ActivitySource pattern, which reliably
exceeds net8.0's default 1,000-node cap and throws NotSupportedException at host
startup (reproduced and confirmed via live dotnet run).

Adds CoreExExtensions.IncreaseNet8RegexNonBacktrackingAutomataLimit() as an explicit,
documented, host-invoked opt-in (still AppContext.SetData under the hood - it is the
only mechanism that actually works). The MSBuild RuntimeHostConfigurationOption/
runtimeconfig.json route does not work for this switch: it surfaces the value to
AppContext.GetData as a boxed string, but dotnet/runtime's
SymbolicRegexThresholds.GetSymbolicRegexSafeSizeThreshold() requires a boxed int
(is int pattern match), so the runtimeconfig-supplied value is silently ignored
and the default of 1,000 still applies - verified empirically with an isolated
net8.0 console app and against a live Contoso.Shopping.Relay run.

The 4 affected sample hosts now call the new method, wrapped in #if NET8_0 so it
compiles out entirely on net9.0/net10.0 and can be deleted outright once net8.0
support is dropped. The CoreEx.Template-generated hosts are unaffected: the
scaffolded solution's Directory.Build.props pins TargetFramework to net10.0 only.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>

* Move net8.0 regex-automata workaround inline into affected samples only

CoreEx's public WithCoreExTelemetry() no longer mentions this edge case at all -
it only affects 4 sample hosts that combine it with WithCoreExServiceBusTelemetry's
wildcard ActivitySource pattern on net8.0, so it doesn't belong in the library's
public API docs. Each of the 4 affected Contoso Subscribe/Relay samples now sets
AppContext.SetData("REGEX_NONBACKTRACKING_MAX_AUTOMATA_SIZE", 10_000) directly,
inline, wrapped in #if NET8_0 for easy removal once net8.0 support is dropped.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>

* Address PR #187 medium/low review findings

- HandlebarsHelpers: replace null-forgiving Root casts with a validating GetRootContext helper that throws deterministically on a malformed invocation.
- HandlebarsCodeGenerator.Generate: document why the null-forgiving operator on the Handlebars data parameter is safe.
- .github/agents/README.md: correct L1 skill count (14 -> 15) to include coreex-graphql.
- .github/copilot-instructions.md: add preview-quality disclaimer to the CoreEx.Cosmos polyglot-data reference.
- samples/docs/infrastructure-layer.md: declare the CosmosDbOptions field used by the CustomersCosmosDb example so it compiles.
- CoreEx.Template GlobalUsing.cs: alphabetize CoreEx.Data.Json using.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>

* fix: retry transient Cosmos emulator connection failures in test setup

CreateDatabaseIfNotExistsAsync (the first Cosmos call per test-host TFM pass) intermittently fails against the local Cosmos emulator with an SSL/connection-reset error under sustained CI load. Add a bounded retry-with-backoff scoped to this test-setup call only; no production CosmosClient/CosmosDbOptions changes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>

* docs: sync 17->18 package-guide count and add missing CoreEx.Cosmos.md to docs-sync lists

Addresses PR review follow-up: .github/coreex-ai-workflows.md and .github/skills/coreex-docs-sync/{SKILL,README}.md still listed 17 per-package guides and omitted CoreEx.Cosmos.md from the cache-layout/refresh file lists, inconsistent with the 18-guide bundle CoreEx.Template.csproj now produces.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Eric Sibly <eric.sibly@avanade.com>

---------

Signed-off-by: Eric Sibly <eric.sibly@avanade.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants