Skip to content

Fix FirstOrDefaultAsync(x => x.Id == value) always returning null on DocumentCollection<string, T>#24

Merged
mrdevrobot merged 2 commits intomainfrom
copilot/fix-firstordefaultasync-issue
Mar 30, 2026
Merged

Fix FirstOrDefaultAsync(x => x.Id == value) always returning null on DocumentCollection<string, T>#24
mrdevrobot merged 2 commits intomainfrom
copilot/fix-firstordefaultasync-issue

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 30, 2026

FirstOrDefaultAsync(x => x.Id == value) on a DocumentCollection<string, T> always returned null even when the document existed. The ToLowerInvariant() workaround succeeded because it bypassed both the IndexOptimizer and BsonExpressionEvaluator, falling through to FindAllAsync() + in-memory LINQ filter.

Root cause

Two gaps in the query expression pipeline:

  • IndexOptimizer.Matches compared secondary index PropertyPaths[0] against the LINQ-extracted C# property name using plain OrdinalIgnoreCase. "_id" (BSON wire name) and "Id" (C# property name) are not equal under that comparison, so any index stored under the BSON alias would never be selected.

  • BsonExpressionEvaluator.TryCompileBody only normalised "id""_id" when building the BSON scan predicate, leaving the "_id" spelling itself unhandled.

Changes

  • IndexOptimizer.Matches — normalises both sides through "_id""Id" before comparing, so a secondary index is found regardless of which spelling was used when it was registered:
static string Normalize(string s) =>
    s.Equals("_id", StringComparison.OrdinalIgnoreCase) ? "Id" : s;

return Normalize(indexPath).Equals(Normalize(propertyName), StringComparison.OrdinalIgnoreCase);
  • BsonExpressionEvaluator.TryCompileBody — maps both "id" and "_id" to "_id" so the BSON scan predicate always targets the correct wire field name.

  • StringMapperIdLookupTests — 7 regression tests covering constant equality, closure-captured variable equality, with and without a secondary unique index on Id, the ToLowerInvariant() workaround, multi-document filtering, and no-match returning null.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • av-build-tel-api-v1.avaloniaui.net
    • Triggering command: /usr/share/dotnet/dotnet dotnet exec --runtimeconfig /home/REDACTED/.nuget/packages/avalonia.buildservices/11.3.2/tools/netstandard2.0/runtimeconfig.json /home/REDACTED/.nuget/packages/avalonia.buildservices/11.3.2/tools/netstandard2.0/Avalonia.BuildServices.Collector.dll (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

…ringMapper PK lookup

Agent-Logs-Url: https://github.com/EntglDb/BLite/sessions/105d5ab1-67c6-4efe-b447-20cebca7b0b6

Co-authored-by: mrdevrobot <12503462+mrdevrobot@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix StringMapperBase PK lookup returning null Fix FirstOrDefaultAsync(x => x.Id == value) always returning null on DocumentCollection<string, T> Mar 30, 2026
Copilot AI requested a review from mrdevrobot March 30, 2026 15:11
@mrdevrobot mrdevrobot marked this pull request as ready for review March 30, 2026 19:42
Copilot AI review requested due to automatic review settings March 30, 2026 19:42
@mrdevrobot mrdevrobot merged commit 4bee54a into main Mar 30, 2026
1 check passed
@mrdevrobot mrdevrobot deleted the copilot/fix-firstordefaultasync-issue branch March 30, 2026 19:43
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a LINQ query regression where FirstOrDefaultAsync(x => x.Id == value) could return null for DocumentCollection<string, T> when a secondary index on the Id field exists, by aligning primary-key field naming between the query optimizer and BSON predicate compilation.

Changes:

  • Normalize Id vs _id when IndexOptimizer matches a predicate to an available secondary index.
  • Normalize id/_id to _id in BsonExpressionEvaluator when compiling simple binary predicates.
  • Add regression tests covering constant/closure equality, with/without a secondary unique Id index, workaround behavior, multi-document filtering, and no-match cases.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
tests/BLite.Tests/StringMapperIdLookupTests.cs Adds regression coverage for string primary-key equality queries across optimized/non-optimized paths.
src/BLite.Core/Query/IndexOptimizer.cs Makes index selection robust to Id vs _id naming differences.
src/BLite.Core/Query/BsonExpressionEvaluator.cs Ensures primary-key field normalization in compiled BSON scan predicates for binary comparisons.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +98 to 103
// "Id" and "_id" both refer to the BSON primary-key field.
// The serializer writes the root-entity primary key as "_id" regardless of the
// C# property name when the property is "Id". Nested-entity mappers write "id".
// Normalise both here so that predicates on x.Id always scan for "_id".
if (bsonName == "id" || bsonName == "_id") bsonName = "_id";

Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

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

In TryCompileBody’s simple-binary branch, the Id/_id normalization logic is now duplicated and slightly diverges from the earlier .Equals()-method-call branch (which still only special-cases "id"). Consider extracting a small helper (e.g., NormalizePrimaryKeyField) and using it in both branches; also, the added comment mentions nested-entity behavior even though this evaluator returns null for nested paths (see TryCompile_NestedPath_ReturnsNull), which can be misleading here.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

StringMapperBase<T> PK lookup via FirstOrDefaultAsync(x => x.Id == value) always returns null for DocumentCollection<string, T>

3 participants