Bug
BsonExpressionEvaluator.TryCompileBody pushes down member-name-based predicates (bare bool member, !x.Prop, .Equals(), string methods, IN, binary comparisons, CompareTo) into a raw BSON field lookup by property name, without checking whether that property is actually persisted.
A get-only computed property - e.g.
public bool IsOpen => State != ReservationState.Closed;
has no backing BSON field (nothing to deserialize back into on write), so the generated predicate scans every field in the document, never finds one literally named "isopen", and falls through to return false - for every document, regardless of the real value.
Repro
// entity.State == ReservationState.Created, so entity.IsOpen == true
await collection.InsertAsync(entity);
var direct = await collection.FindByIdAsync(entity.Id); // correct, IsOpen == true on the returned object
var viaQuery = await collection.FindAsync(x => x.IsOpen).ToListAsync(); // empty - wrong
FindByIdAsync (no predicate) returns the document intact; any query with a predicate that touches IsOpen returns nothing, because the BSON-level pushdown path silently misfires instead of falling back.
Impact
Silent wrong results, not an exception - much worse than the "not the same in-memory data cannot be used in this context" style errors most providers throw for untranslatable expressions. Any .Where/.FindAsync predicate anywhere touching a get-only computed property is affected, not just IsOpen - this is a general pushdown correctness bug, not specific to one entity.
Root cause
BsonExpressionEvaluator's member-based branches (bare bool, NOT, .Equals(), string methods, IN operator, binary comparisons via x.Prop op value, CompareTo) extract member.Name and build a BSON-field predicate unconditionally - there is no check that the member is actually stored (i.e. has a setter) rather than computed.
Fix (see linked PR)
Added IsPersistedMember(MemberInfo) (member is not PropertyInfo { CanWrite: false }) and gated every direct-member extraction point on it. When a member fails the check, TryCompileBody returns null, so the caller (DocumentCollection.FetchAsync) falls through to the existing full-scan + in-memory-filter strategy (Strategy 3), which compiles the real expression and evaluates the actual getter correctly.
Found and reproduced live via Modulon.Bills.Domain.Aggregates.Reservations.Reservation.IsOpen in a downstream app: a waiter terminal's local read replica had the reservation data (confirmed via FindByIdAsync) but GetOpenReservations()/GetByTableId() (both filtering on r.IsOpen) always returned nothing, making an occupied table appear free in the UI.
Bug
BsonExpressionEvaluator.TryCompileBodypushes down member-name-based predicates (bare bool member,!x.Prop,.Equals(), string methods, IN, binary comparisons,CompareTo) into a raw BSON field lookup by property name, without checking whether that property is actually persisted.A get-only computed property - e.g.
has no backing BSON field (nothing to deserialize back into on write), so the generated predicate scans every field in the document, never finds one literally named
"isopen", and falls through toreturn false- for every document, regardless of the real value.Repro
FindByIdAsync(no predicate) returns the document intact; any query with a predicate that touchesIsOpenreturns nothing, because the BSON-level pushdown path silently misfires instead of falling back.Impact
Silent wrong results, not an exception - much worse than the "not the same in-memory data cannot be used in this context" style errors most providers throw for untranslatable expressions. Any
.Where/.FindAsyncpredicate anywhere touching a get-only computed property is affected, not justIsOpen- this is a general pushdown correctness bug, not specific to one entity.Root cause
BsonExpressionEvaluator's member-based branches (bare bool, NOT,.Equals(), string methods, IN operator, binary comparisons viax.Prop op value,CompareTo) extractmember.Nameand build a BSON-field predicate unconditionally - there is no check that the member is actually stored (i.e. has a setter) rather than computed.Fix (see linked PR)
Added
IsPersistedMember(MemberInfo)(member is not PropertyInfo { CanWrite: false }) and gated every direct-member extraction point on it. When a member fails the check,TryCompileBodyreturnsnull, so the caller (DocumentCollection.FetchAsync) falls through to the existing full-scan + in-memory-filter strategy (Strategy 3), which compiles the real expression and evaluates the actual getter correctly.Found and reproduced live via
Modulon.Bills.Domain.Aggregates.Reservations.Reservation.IsOpenin a downstream app: a waiter terminal's local read replica had the reservation data (confirmed viaFindByIdAsync) butGetOpenReservations()/GetByTableId()(both filtering onr.IsOpen) always returned nothing, making an occupied table appear free in the UI.