Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 29 additions & 9 deletions src/BLite.Core/Query/BsonExpressionEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ private static HashSet<Type> 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))
Comment on lines 123 to +127
{
var bsonName = bareM.Member.Name.ToLowerInvariant();
if (bsonName == "id") bsonName = "_id";
Expand All @@ -134,7 +135,8 @@ private static HashSet<Type> 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";
Expand All @@ -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";
Expand All @@ -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();
Expand All @@ -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";
Expand All @@ -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";
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;

/// <summary>
/// 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 (<c>public bool IsOpen => State != Closed</c>) has
/// no backing BSON field at all, so pushing it down into <see cref="CreatePredicate"/> would scan
/// every document for a field name that can never exist and silently return <c>false</c> for
/// everyone - wrong, instead of falling back to a real in-memory evaluation of the getter.
/// </summary>
private static bool IsPersistedMember(MemberInfo member)
=> member is not PropertyInfo { CanWrite: false };

Comment on lines +703 to +712
/// <summary>
/// Unwraps a single <c>Convert</c> / <c>ConvertChecked</c> node if present.
/// Enum comparisons are compiled to <c>Equal(Convert(x.Role,Int32), Convert(3,Int32))</c>
Expand Down
Loading