Fix FirstOrDefaultAsync(x => x.Id == value) always returning null on DocumentCollection<string, T>#24
Conversation
…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>
There was a problem hiding this comment.
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
Idvs_idwhenIndexOptimizermatches a predicate to an available secondary index. - Normalize
id/_idto_idinBsonExpressionEvaluatorwhen 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.
| // "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"; | ||
|
|
There was a problem hiding this comment.
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.
FirstOrDefaultAsync(x => x.Id == value)on aDocumentCollection<string, T>always returnednulleven when the document existed. TheToLowerInvariant()workaround succeeded because it bypassed both theIndexOptimizerandBsonExpressionEvaluator, falling through toFindAllAsync()+ in-memory LINQ filter.Root cause
Two gaps in the query expression pipeline:
IndexOptimizer.Matchescompared secondary indexPropertyPaths[0]against the LINQ-extracted C# property name using plainOrdinalIgnoreCase."_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.TryCompileBodyonly 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: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 onId, theToLowerInvariant()workaround, multi-document filtering, and no-match returningnull.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/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: