Skip to content

Fail explicitly on multi-get item errors - #314

Open
niemyjski wants to merge 20 commits into
mainfrom
feature/strict-mget-item-errors
Open

niemyjski wants to merge 20 commits into
mainfrom
feature/strict-mget-item-errors

Conversation

@niemyjski

@niemyjski niemyjski commented Aug 12, 2026 •

Copy link
Copy Markdown
Member

Summary

Fixes #313. Fixes #328.

Adds opt-in ThrowOnMultiGetErrors() handling to GetByIdsAsync. Strict mode rejects incomplete or mismatched MGET response items before fallback or document-result caching. Valid item errors produce one aggregated DocumentException only if they remain unresolved after any applicable time-series/parent-child search fallback. Ordinary found: false results and default non-strict behavior remain unchanged.

Final-audit fixes

1. Deterministic delete-by-query tests

Replace the four timing-dependent conflict tests from #291 and both background-writer helpers with ten scripted-transport cases and two controlled real-Elasticsearch cases. Retain ordinary live-server deletion coverage.

Scripted cases verify cumulative deletion counts, exact Retry(n) budgets/defaults, stopping on success, exhausted-budget warnings, one-time/disabled notifications, and request-level failures. Their real client transport supports product verification and synchronous mapping lookup and rejects unexpected requests.

Live cases disable scheduled refresh and introduce known unrefreshed updates. A probe must report three version conflicts and zero deletions before the operation under test. Retry(0) then returns seven deletions with three remaining; Retry(1) returns ten with none remaining. Index settings are restored in finally with a separate bounded cleanup token.

Production delete-by-query logic and retry budgets are unchanged. No background writers, artificial scheduling delays, weakened assertions, skipped tests, or analyzer suppressions are used to obtain a passing run.

2. Strict MGET validation and cache isolation

Validate response item count and metadata IDs against the actual MGET operations in request order. Reject null items, mismatched IDs (including unexpected duplicates), found results without a source, missing results with a source, and malformed error payloads. Seventeen new regression cases cover malformed responses, projections, cache bypass/preservation, and strict/default behavior. Source projections may omit their ID property: correlation uses response metadata.

Preserve fallback consistency propagation from #282 by cloning caller options, then disable internal query-result cache reads/writes with Cache(false).ReadCache(false). The outer batch read owns document/not-found caching after validation. Add ReadCache(bool) while retaining the parameterless API and independent write settings. The inline cache-isolation review finding is resolved with implementation and test evidence.

3. Documentation

Align caching/configuration/CRUD/troubleshooting guides, XML comments, and agent references. Correct the caching configuration constructor example. Explain cache ownership, explicit read/write overrides, fallback behavior, and the limits of strict mode.

Additional blockers repaired

  • Documentation dependency security: patch only the locked transitive uuid entry from 11.1.0 to 11.1.1. No manifest, dependency-major, or unrelated lockfile changes. Verification 35783111978 passed clean installation, zero reported npm vulnerabilities, twelve UUID buffer-boundary assertions, and the documentation build. The final-head audit independently repeated installation, the zero-vulnerability audit, and site build.
  • Standalone/design-time build configuration: the old SolutionName inference enabled nonexistent sibling project references when the solution name was empty or *Undefined*. Restrict inference to named workspaces and preserve explicit per-dependency overrides. Add build/VerifySourceReferences.proj with twelve cases and a permanent read-only CI job. The old configuration demonstrably fails the standalone regression; all repaired cases pass. Standalone provider/test builds work, and the formatter now loads the complete workspace without missing-reference warnings.
  • Earlier dependency-upgrade repairs retain the direct System.IO.Hashing reference and serial xUnit test-assembly settings.

Contract and compatibility

var documents = await repository.GetByIdsAsync(ids,
    o => o.ThrowOnMultiGetErrors());

// Also bypass document/result cache reads and writes.
var uncachedDocuments = await repository.GetByIdsAsync(ids,
    o => o.ThrowOnMultiGetErrors().Cache(false).ReadCache(false));

Strict mode is not a freshness guarantee, an atomic snapshot, or proof of absence. Existing positive cache hits remain eligible unless bypassed. Search fallback preserves consistency and soft-delete settings but is not real-time MGET. Never convert DocumentException into an empty batch for an irreversible decision. The no-fallback, cache-bypassing adapter in exceptionless/Exceptionless#2267 is not a presumed drop-in replacement candidate.

No additional unmerged PR is required: #282 and #291 are already included through main. Standalone builds use packages when no solution name is available; existing named combined/external workspace defaults and explicit source-reference overrides are retained.

Delivered revision

All three audit fixes and both additional repairs are on this PR branch at 561ff3b26d76221e76891976434444518ace51fa, delivered by non-force fast-forwards. A fresh comparison against main (a61d541613528428cc00bc2236602db72948a924) reports 20 ahead, 0 behind. No force-push or PR merge was performed.

Final repair commits:

Commit Change
62ceb3e Deterministic delete-by-query transport tests; remove background writers
909a01c MGET response/cache-preservation regression tests
e5829a3 Strict response completeness and correspondence validation
7b74006 Strict-response validation documentation
27b9235 Controlled real-Elasticsearch conflict tests
ffbe718 Independent cache-read documentation
b92b5c8 Correct transport product headers and mapping lookup
3f422e9 Minimal UUID security patch
7febad7 Source-reference regression matrix and permanent CI coverage
561ff3b Standalone/design-time source-reference inference fix

Temporary audit-control workflows/scripts remain outside this PR. The only new permanent CI job verifies source-reference settings; the existing build/publishing job is unchanged.

Verified results — September 22, 2026

Exact final-source audit 35784354103 completed successfully. The artifact records source SHA 561ff3b26d76221e76891976434444518ace51fa, distinct from the audit-control workflow SHA.

Gate Result
Standalone provider and test-project builds Passed
Release solution build Passed; 0 warnings, 0 errors
Full solution suite 887 passed, 0 failed/skipped: 728 Elasticsearch-project cases and 159 core cases
Source-reference regression matrix All 12 passed; old configuration fails the intended standalone assertion
Changed-file format/analyzer verification Passed; 0 files changed, no missing-project/workspace-load warnings
Documentation dependency audit 0 reported npm vulnerabilities
Documentation site build Passed
Library packaging Both packages passed, with net8.0/net10.0 assemblies, XML documentation, and correct final-source metadata

Additional stress and mutation evidence from audit 35779319164, on the unchanged runtime/test implementation at b92b5c8:

  • Twenty runs of 27 scripted regression cases: 540 passed.
  • Five runs of both controlled live-conflict cases: 10 passed.
  • Deliberately broken count accumulation, retry budget, and MGET validation each triggered their intended failures (6, 7, and 6 failing cases). Original source was restored and rebuilt before the full suite.

The VitePress large-chunk notice is a documentation asset-size advisory, not a compiler or test failure; its warning threshold was not raised. The custom audits created packages without publishing them. No existing publishing behavior was removed or disabled in this PR.

Merge status

The normal exact-head PR Build run 35784695259 has passed the new source-reference-settings job. Its existing build / build job is still queued for a GitHub runner at this update. The exact-source audit above has fully passed, but it is not being represented as completion of that separate required check. Do not bypass the repository's merge checks. The PR remains unmerged.

CI commands:

dotnet msbuild build/VerifySourceReferences.proj -target:Verify -verbosity:minimal
dotnet build Foundatio.Repositories.slnx --configuration Release
dotnet test --solution Foundatio.Repositories.slnx --configuration Release --no-build --report-github

Collect MGET item errors eagerly via a new GetItemErrors extension
instead of threading a bool through ToFindHits. GetByIdsAsync now
runs the multi-index fallback find before deciding whether to throw,
so time-series/parent-child repositories that resolve a document via
the fallback no longer fail under ThrowOnMultiGetErrors(). Only ids
still unresolved after the fallback raise a single aggregated
DocumentException, thrown before any cache write.

Also documents the deferred-throw semantics in XML docs, the guide
pages, and the skill reference, and switches the closed-index test to
AsyncDisposableAction.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 12, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-22T21:12:53.381675Z 561ff3b New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7702b9d351

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

Copy link
Copy Markdown
Member Author

Final audit status — September 22, 2026

The earlier exact-head run for 30f6ec4249aff665fd33e8e6ba7602d10d128566 finished successfully: https://github.com/FoundatioFx/Foundatio.Repositories/actions/runs/35746245103. The earlier description's pending status for that run is superseded.

The final audit has prepared six isolated commits covering:

  • Replacement of all four timing-dependent delete-by-query conflict tests and removal of both background-writer helpers (Make delete-by-query conflict retry tests deterministic and guarantee writer cleanup #328). The production retry implementation/budget is unchanged.
  • Ten scripted-transport retry cases, two controlled real-Elasticsearch conflict cases, and seventeen strict-MGET response-validation cases.
  • Strict-mode rejection of omitted/mismatched response items before fallback or cache writes, while allowing source projections to omit the source ID.
  • Consistent cache-read/write and strict-mode documentation, including the remaining freshness and transaction limitations.

The current validation run is https://github.com/FoundatioFx/Foundatio.Repositories/actions/runs/35771764019. It is queued for a hosted runner, not a passing test result. Its gates include repeated focused tests, deliberate regression mutations, controlled real-server conflicts, the complete suite, changed-file formatting, documentation build, and package creation without publishing. Two analyzer errors in the newly added integration fixture were corrected in this candidate without suppressions.

These final-audit changes have not yet been promoted to the PR branch. main was fetched and merged during candidate preparation and remains a61d541613528428cc00bc2236602db72948a924, already included in the PR. No force-push or production publishing-policy change is part of this audit.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ niemyjski
❌ github-actions[bot]
You have signed the CLA already but the status is still pending? Let us recheck it.

@sunnyrp sunnyrp left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

PR #314 — Fail explicitly on multi-get item errors

Verdict: REQUEST CHANGES

The core strict-MGET design holds up. Validation runs before fallback and before cache writes, positions line up with operations, and unresolved errors are aggregated after fallback. The fallback cache isolation from Codex's earlier finding is fixed and tested. Remaining items are one merge-gate blocker plus contract and release-note gaps that should be settled before this ships in a package.

Blocker

  1. license/cla is still PENDING on head 561ff3b. 11 of 20 commits are authored by github-actions[bot], which cannot sign a CLA. This needs either a squash merge under a human author (with Co-authored-by if wanted) or an explicit maintainer CLA override. Bot authorship also weakens commit provenance for a data-safety change.

Warnings (details inline)

  1. Default (non-strict) behavior changes, but the PR says it doesn't. findOptions.Cache(false).ReadCache(false) is unconditional. Before this PR, a non-strict GetByIdsAsync(ids, o => o.Cache()) that reached the time-series or parent/child fallback threw ArgumentException("Cache key is required..."). With Cache("key"), it wrote a FindResults<T> under a key the outer read later treats as ICollection<FindHit<T>>. Both are good fixes, but they are observable behavior changes and need release notes.
  2. Strict mode doesn't cover fallback-only reads. Parent/child IDs without routing never go through MGET. Partial shard failures in the fallback search (HTTP 200 with _shards.failed > 0) are also not surfaced. In both cases strict mode silently returns "missing". That contradicts #313's stated contract: every item is either found or explicitly found:false.
  3. Time-series retention: index_not_found_exception never resolves. A daily index removed by retention makes strict reads of its IDs throw forever. Callers can't tell "aged out" from "unavailable", so cleanup jobs can get permanently stuck.
  4. DocumentException carries no structured failure data, and the message has no size limit. Callers would have to parse strings to find which IDs failed. With a 1,000-ID batch, the message can be very large, and it gets logged and propagated.
  5. Strict validation rejects found:true with no _source. That breaks strict mode for indexes with _source disabled, or for subclasses whose ConfigureMultiGetRequest uses stored_fields / _source: false.
  6. Scope is broader than the minimal fix. One PR bundles strict MGET, the new ReadCache(bool) API, the #328 test rewrite, common.props inference changes plus a new CI job, a docs lockfile security patch, the System.IO.Hashing direct reference, and an xUnit attribute migration. Consider splitting, or at least squash-merging with a structured message and per-area release notes.

Nits: a public GetItemErrors overload that skips correlation validation; a dead e.Id is null branch; common.props now uses packages for single-project builds in external workspaces; the docs rewrite changes the documented meaning of Cache(false).

Security | Build | Dependencies | Accessibility | Requirements | Code Review | Data-loss analysis

Security: PASS

No malicious scripts, credential access, or CI tampering. The new source-reference-settings job has contents: read and only runs dotnet msbuild on an in-repo project. Tag-pinned actions match the repo's existing workflows.

Build: CI PASS | Local: NOT RUN

CI is green at 561ff3b: build / build, source-reference-settings, and CodeQL (actions and csharp). The PR reports 887 tests passing. I could not build or test locally during this review, so the test results come only from CI.

Dependencies: AUDITED

  • System.IO.Hashing 10.0.12 (direct): justified, because ElasticConfiguration.cs imports System.IO.Hashing directly. It is still a new direct dependency for NuGet consumers and should be listed in release notes.
  • docs/package-lock.json uuid 11.1.0 → 11.1.1: lockfile only, docs site only, not shipped in packages.

Accessibility: N/A (no UI)

Requirements (#313, #328): MOSTLY ALIGNED

  • #313: opt-in strict mode ✅, found:false stays non-error ✅, no not-found markers after item errors ✅, closed-index integration test ✅, mixed success/error test ✅. Fallback-only paths are not covered ⚠️ (Warning 2).
  • #328: deterministic scripted transport ✅, controlled live conflicts ✅, cleanup in finally ✅, no weakened assertions ✅.

Data-loss / inconsistency scenarios

# Scenario Mode Impact Covered by PR?
D1 MGET item error (closed or unavailable index) is treated as missing, and the caller deletes "orphans" default Data loss (the original #313 bug) No: the default is unchanged; callers must opt in
D2 Parent/child ID without routing, fallback search hits partial shard failures, and the doc comes back missing strict Data loss if the caller deletes on a miss No (Warning 2)
D3 Doc is in a non-date-derived daily index, MGET on the date index returns found:false, and fallback misses it (refresh lag or shard failure) strict Returned as missing and cached as a not-found marker Documented only ("not proof of absence")
D4 Stale positive cache hit for a doc deleted elsewhere strict + cache Returns a deleted doc: stale reads and authorization risk. It is the safe direction for cleanup Documented; bypass with Cache(false).ReadCache(false)
D5 Read-then-delete race (TOCTOU) both Concurrent writes between validation and mutation Documented; needs caller-side versioning
D6 Daily index removed by retention, so strict reads of its IDs always throw strict Liveness, not loss: jobs stall No (Warning 3)
D7 Default mode with ReadCache(false) and Cache(): an item-errored ID's existing positive entry is overwritten by a not-found marker default Cache churn only. Null markers are filtered on read, so no false "missing" is served Pre-existing
D8 Caller catches DocumentException and continues with an empty batch strict Recreates D1 Documented warning only; structured errors would help (Warning 4)

Existing comments

  • Codex P2 (fallback cache isolation): verified fixed at 561ff3b L210, with tests UnresolvedItemErrors_DoNotReadOrWriteFallbackCache and RecoveredItem_PreservesCallerCachingAndConsistency. I replied in the thread with one new observation (Warning 1). I did not resolve the thread because it contains a human reply.

var findOptions = options.Clone();
findOptions.PageLimit(1000);
// Only the outer batch read may cache results, after all item errors have been resolved.
findOptions.Cache(false).ReadCache(false).PageLimit(1000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING — undocumented change to default behavior (confidence: high)

This runs for all callers, not just ThrowOnMultiGetErrors(). That contradicts the PR description's statement that default non-strict behavior is unchanged.

Before this PR, for time-series and parent/child repos reaching the fallback:

  • GetByIdsAsync(ids, o => o.Cache()) threw ArgumentException("Cache key is required when enabling cache.") from GetCachedQueryResultAsync, because the cloned options kept Cache() without a key.
  • GetByIdsAsync(ids, o => o.Cache("key")) read and wrote a query-result FindResults<T> under key. The outer AddDocumentsToCacheAsync then stored ICollection<FindHit<T>> under the same key (type confusion).

Both are real bug fixes, and I agree with the change. Please:

  1. Update the PR description and changelog to say the fallback no longer uses caller cache settings in either mode.
  2. Add one non-strict regression test for Cache() without a key on DailyLogEventRepository that reaches the fallback (it threw before this PR). That locks in the fix.

findOptions.PageLimit(1000);
// Only the outer batch read may cache results, after all item errors have been resolved.
findOptions.Cache(false).ReadCache(false).PageLimit(1000);
var response = await FindAsync(NewQuery().Id(itemsToFind.Select(id => id.Value)!), findOptions).AnyContext();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING — strict mode doesn't cover fallback-only reads (confidence: ~80%)

Strict mode only checks errors collected from MGET. Two paths can still return a false "missing" with no exception:

  1. Parent/child IDs without routing are excluded from itemsForMultiGet (L173) and are only resolved here. ThrowOnMultiGetErrors() has no effect on them.
  2. Partial search failures. _search returns HTTP 200 with _shards.failed > 0 (with allow_partial_search_results defaulting to true) or timed_out: true. I found no Shards/TimedOut checks anywhere in src/, so FindAsync treats partial results as complete. IDs that MGET reported as found:false, or that never went through MGET, are then returned as missing and cached as not-found markers.

For callers using strict mode to avoid deleting on a false miss, (1) is the same data-loss path #313 describes.

Suggestion: when ShouldThrowOnMultiGetErrors() is true, fail on shard failures or timeout in the fallback (for example, have ToFindResults expose shard failures or check the raw response). Alternatively, state explicitly in the XML docs and guides that strict mode does not apply to parent/child reads without routing. A scripted-transport test with a _shards.failed: 1 fallback response would cover it.

if (itemErrors.Count > 0)
{
var stillMissingIds = new HashSet<string>(itemsToFind.Select(id => id.Value));
var unresolvedErrors = itemErrors.Where(e => e.Id is null || stillMissingIds.Contains(e.Id.ToString())).ToList();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

NIT — dead branch (confidence: ~90%)

On this path GetItemErrors always receives docOperations, and it throws when error.Id doesn't match the expected operation ID, which includes a null ID. So e.Id is null can't be true here. Keeping it is harmless and fail-safe, but a short comment saying it's defensive would stop future readers from thinking null IDs are expected.

{
var stillMissingIds = new HashSet<string>(itemsToFind.Select(id => id.Value));
var unresolvedErrors = itemErrors.Where(e => e.Id is null || stillMissingIds.Contains(e.Id.ToString())).ToList();
if (unresolvedErrors.Count > 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING — time-series retention makes strict reads fail permanently (confidence: ~85%)

For daily or monthly repos, MGET targets ElasticIndex.GetIndex(id), which is derived from the ObjectId date. After retention deletes that physical index, every strict read of those IDs returns index_not_found_exception. The fallback correctly doesn't find the doc, so this throws on every call.

That's the right default for safety, but callers can't tell "index aged out (the doc is really gone)" from "index temporarily unavailable". A cleanup job that meets one retention-expired ID will stall forever. This is exactly what GetByIdsAsync_WithMixedTimeSeriesItemErrorAndThrowOnMultiGetErrors_ThrowsDocumentException asserts.

Options:

  • expose the per-item errors on the exception (see the next comment) so callers can apply their own retention policy, or
  • document this outcome explicitly in the configuration guide's strict section, with a recommended caller pattern.

if (unresolvedErrors.Count > 0)
{
string message = String.Join("; ", unresolvedErrors.Select(e => $"id={e.Id}, index={e.Index}, type={e.Error?.Type}, reason={e.Error?.Reason}"));
throw new DocumentException($"Error getting documents: {message}");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING — no structured error data, and the message has no size limit (confidence: high)

  1. Callers can only find the failed IDs by parsing the message string. For a safety API, the usual approach is a typed payload. For example, a MultiGetItemsException : DocumentException exposing IReadOnlyCollection<MultiGetError> or IReadOnlyCollection<string> FailedIds, or at minimum exception.Data["FailedIds"]. Deriving from DocumentException keeps it backward compatible.
  2. With a large batch (PageLimit(1000) fallback; callers commonly pass hundreds or thousands of IDs), a cluster-wide outage produces one very large message. That message is logged and may surface in APIs. Consider capping it (for example, the first N entries plus "and M more") and grouping by index and type.

The reason text also contains internal index names. That's fine for logs, but worth noting for consumers that surface exception messages to end users.

/// </summary>
/// <exception cref="ArgumentNullException">The response is null.</exception>
/// <exception cref="DocumentException">The response contains an invalid item.</exception>
public static IReadOnlyCollection<MultiGetError> GetItemErrors<T>(this MultiGetResponse<T> response, ILogger? logger = null) where T : class

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

NIT — public API surface (confidence: ~70%)

This public overload skips the count and ID correlation checks (operations == null), so it gives weaker guarantees than the path GetByIdsAsync actually uses. It is also a Get* method that throws DocumentException on malformed input. Unless there's a known external consumer (Exceptionless#2267?), consider making it internal and adding it later on request. Once shipped, removing it is a breaking change.

if (!String.Equals(result.Id, expectedId, StringComparison.Ordinal))
throw new DocumentException($"Elasticsearch returned an unexpected multi-get result ID; expected {expectedId}, received {result.Id}.");

if (result.Found && result.Source is null)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING — found:true with no _source is valid in Elasticsearch (confidence: ~75%)

Elasticsearch returns found items without _source when the mapping has _source.enabled: false, when the request uses _source: false, or when it uses stored_fields only. ConfigureMultiGetRequest is protected virtual, so subclasses can configure any of these. For those repositories strict mode would throw on every found document.

The default path can't return such documents anyway (FindHit with a null document is dropped later), so rejecting them is defensible. But the message should say why, and the guide should list it as an unsupported configuration. Otherwise it reads like a cluster fault. DoesNotRequireProjectedSourceIdAsync covers _source: {} but not an absent _source.

Comment thread build/common.props
</PropertyGroup>

<!-- Standalone and design-time builds may not provide a solution name. -->
<PropertyGroup Condition="'$(SolutionName)' != '' And '$(SolutionName)' != '*Undefined*' And ($(SolutionName.Contains('All')) Or !$(SolutionName.Contains('Foundatio')))">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

NIT — behavior change for single-project builds in external workspaces (confidence: ~70%)

Before, SolutionName empty made !''.Contains('Foundatio') true, so standalone builds enabled source references. Now they use packages. That's the stated fix, and the scenario matrix covers it. The side effect: a developer in the combined or Exceptionless workspace who runs dotnet build path/to/Project.csproj (no solution) now silently resolves Foundatio.* packages instead of sibling sources. That can mask local source edits during debugging. Worth one line in the contributor docs: pass -p:ReferenceFoundatioSource=true for single-project builds.

@sunnyrp

sunnyrp commented Sep 25, 2026

Copy link
Copy Markdown

PR Review Summary

  • Verdict: REQUEST CHANGES
  • Build: CI PASS at 561ff3b (build, source-reference-settings, CodeQL). Not re-run locally. Tests: 887/887 per CI
  • Requirements: GetByIdsAsync silently treats per-item multi-get errors as missing documents #313 and Make delete-by-query conflict retry tests deterministic and guarantee writer cleanup #328 mostly aligned. Gap: strict mode doesn't cover fallback-only reads (parent/child without routing) or partial shard failures in the fallback search
  • Blockers: 1. license/cla is pending; 11/20 commits are authored by github-actions[bot]
  • Warnings: 6. Undocumented default-mode cache change; fallback-only and shard-failure gaps; permanent strict failures after time-series retention; no structured failed-ID payload and no limit on message size; found:true without _source rejected; scope bundling
  • Dependencies audited: yes. System.IO.Hashing 10.0.12 (new direct reference, justified); docs-only uuid 11.1.1
  • Data-loss matrix (D1–D8): in the review body
  • Details: see inline comments.

Suggested pre-merge plan

  1. Resolve the CLA: squash-merge under a human author, or get a maintainer CLA override.
  2. Decide the fallback-coverage contract. Either fail strict reads on _shards.failed > 0 or timeout in the fallback, or explicitly document parent/child reads without routing as out of scope. Add a scripted-transport test either way.
  3. Add structured failed IDs on the exception (subclass of DocumentException), keeping it backward compatible, and cap the message size.
  4. Add a non-strict regression test for Cache() without a key on the fallback path. Update the PR description and changelog to say fallback cache isolation applies in both modes.
  5. Document the time-series retention outcome and the unsupported _source-disabled configuration.
  6. Consider internal for the uncorrelated public GetItemErrors overload before it ships.
  7. Squash-merge with a structured message. Release notes should cover the new APIs (ThrowOnMultiGetErrors, ReadCache(bool), GetItemErrors), the new direct package dependency, the build SolutionName inference change, and the default-mode fallback cache fix.
  8. After release: bump Exceptionless, run its closed-index preservation tests against the package, and only then evaluate retiring the adapter in fix: Optimize cleanup jobs, eliminate raw ES client usage, add test coverage exceptionless/Exceptionless#2267.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants