diff --git a/src/BLite.Core/Query/BsonExpressionEvaluator.cs b/src/BLite.Core/Query/BsonExpressionEvaluator.cs index 30a3e1f..60741e0 100644 --- a/src/BLite.Core/Query/BsonExpressionEvaluator.cs +++ b/src/BLite.Core/Query/BsonExpressionEvaluator.cs @@ -123,7 +123,8 @@ private static HashSet BuildKnownBsonPrimitives() // ── Bare bool member: e => e.IsActive → IsActive == true ────────────── if (body is MemberExpression bareM && bareM.Expression == parameter && - bareM.Type == typeof(bool)) + bareM.Type == typeof(bool) && + IsPersistedMember(bareM.Member)) { var bsonName = bareM.Member.Name.ToLowerInvariant(); if (bsonName == "id") bsonName = "_id"; @@ -134,7 +135,8 @@ private static HashSet BuildKnownBsonPrimitives() if (body is MemberExpression { Member.Name: "HasValue" } hasValueExpr && hasValueExpr.Expression is MemberExpression innerHasValueMember && innerHasValueMember.Expression == parameter && - Nullable.GetUnderlyingType(innerHasValueMember.Type) != null) + Nullable.GetUnderlyingType(innerHasValueMember.Type) != null && + IsPersistedMember(innerHasValueMember.Member)) { var bsonName = innerHasValueMember.Member.Name.ToLowerInvariant(); if (bsonName == "id") bsonName = "_id"; @@ -147,7 +149,8 @@ hasValueExpr.Expression is MemberExpression innerHasValueMember && // Fast path: !e.BoolProp → BoolProp == false if (notExpr.Operand is MemberExpression notM && notM.Expression == parameter && - notM.Type == typeof(bool)) + notM.Type == typeof(bool) && + IsPersistedMember(notM.Member)) { var bsonName = notM.Member.Name.ToLowerInvariant(); if (bsonName == "id") bsonName = "_id"; @@ -167,7 +170,8 @@ hasValueExpr.Expression is MemberExpression innerHasValueMember && if (mc.Method.Name == "Equals" && mc.Arguments.Count == 1 && mc.Object is MemberExpression equalsOnMember && - equalsOnMember.Expression == parameter) + equalsOnMember.Expression == parameter && + IsPersistedMember(equalsOnMember.Member)) { var fieldName = equalsOnMember.Member.Name; var bsonName = fieldName.ToLowerInvariant(); @@ -191,7 +195,8 @@ mc.Object is MemberExpression equalsOnMember && strMember.Expression == parameter && strMember.Type == typeof(string) && mc.Arguments.Count == 1 && - mc.Method.Name is "Contains" or "StartsWith" or "EndsWith") + mc.Method.Name is "Contains" or "StartsWith" or "EndsWith" && + IsPersistedMember(strMember.Member)) { var bsonName = strMember.Member.Name.ToLowerInvariant(); if (bsonName == "id") bsonName = "_id"; @@ -210,7 +215,8 @@ mc.Method.Name is "IsNullOrEmpty" or "IsNullOrWhiteSpace" && mc.Arguments.Count == 1 && mc.Arguments[0] is MemberExpression staticStrMember && staticStrMember.Expression == parameter && - staticStrMember.Type == typeof(string)) + staticStrMember.Type == typeof(string) && + IsPersistedMember(staticStrMember.Member)) { var bsonName = staticStrMember.Member.Name.ToLowerInvariant(); if (bsonName == "id") bsonName = "_id"; @@ -227,7 +233,8 @@ mc.Arguments[0] is MemberExpression staticStrMember && { var argUnwrapped = UnwrapConvert(mc.Arguments[0]); if (argUnwrapped is MemberExpression inMember && - inMember.Expression == parameter) + inMember.Expression == parameter && + IsPersistedMember(inMember.Member)) { var (ok, collection) = TryEvaluate(mc.Object); if (ok && collection != null) @@ -242,7 +249,8 @@ mc.Arguments[0] is MemberExpression staticStrMember && { var argUnwrapped = UnwrapConvert(mc.Arguments[1]); if (argUnwrapped is MemberExpression enumInMember && - enumInMember.Expression == parameter) + enumInMember.Expression == parameter && + IsPersistedMember(enumInMember.Member)) { var (ok, collection) = TryEvaluateCollection(mc.Arguments[0]); if (ok && collection != null) @@ -288,7 +296,7 @@ ExpressionType.GreaterThan or ExpressionType.GreaterThanOrEqual or nodeType = Flip(nodeType); } - if (leftInner is MemberExpression member && member.Expression == parameter) + if (leftInner is MemberExpression member && member.Expression == parameter && IsPersistedMember(member.Member)) { var fieldName = member.Member.Name; var bsonName = fieldName.ToLowerInvariant(); @@ -341,6 +349,8 @@ ExpressionType.GreaterThan or ExpressionType.GreaterThanOrEqual or var instanceExpr = UnwrapNullableValue(UnwrapConvert(ctMc.Object!)); if (instanceExpr is not MemberExpression ctMember || ctMember.Expression != parameter) return null; + if (!IsPersistedMember(ctMember.Member)) + return null; var fieldName = ctMember.Member.Name; var bsonName = fieldName.ToLowerInvariant(); @@ -690,6 +700,16 @@ private static (bool Ok, object? Value) TryEvaluateCollection(Expression express private static bool IsDirectParameterAccess(Expression expr, ParameterExpression p) => expr is MemberExpression m && m.Expression == p; + /// + /// True for a field, or a property with a setter - the shapes BLite's document mapper actually + /// persists as a BSON field. A get-only property (public bool IsOpen => State != Closed) has + /// no backing BSON field at all, so pushing it down into would scan + /// every document for a field name that can never exist and silently return false for + /// everyone - wrong, instead of falling back to a real in-memory evaluation of the getter. + /// + private static bool IsPersistedMember(MemberInfo member) + => member is not PropertyInfo { CanWrite: false }; + /// /// Unwraps a single Convert / ConvertChecked node if present. /// Enum comparisons are compiled to Equal(Convert(x.Role,Int32), Convert(3,Int32))