Skip to content
Merged
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
81 changes: 70 additions & 11 deletions src/BLite.Core/Query/BsonExpressionEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ private static HashSet<Type> BuildKnownBsonPrimitives()
var lp = TryCompileBody(andAlso.Left, parameter, registry, keyMap);
var rp = TryCompileBody(andAlso.Right, parameter, registry, keyMap);
if (lp != null && rp != null) return reader => lp(reader) && rp(reader);
return lp ?? rp;
return null;
}

return null;
Expand All @@ -114,7 +114,7 @@ private static HashSet<Type> BuildKnownBsonPrimitives()
var lp = TryCompileBody(orElse.Left, parameter, registry, keyMap);
var rp = TryCompileBody(orElse.Right, parameter, registry, keyMap);
if (lp != null && rp != null) return reader => lp(reader) || rp(reader);
return lp ?? rp;
return null;
}

return null;
Expand All @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fixed in 76d8218. AndAlso/OrElse now return null when both sides touch the parameter but either side cannot be compiled, so Strategy 2 won't run with a partially compiled predicate.

{
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,55 @@ 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 when a member is expected to have a persisted BSON field:
/// fields, properties with setters, and getter-only properties with either
/// compiler-generated (<c>&lt;Name&gt;k__BackingField</c>) or conventional
/// (<c>_name</c>) backing fields.
/// </summary>
private static bool IsPersistedMember(MemberInfo member)
{
if (member is FieldInfo)
return true;

if (member is not PropertyInfo property)
return false;

if (property.CanWrite)
return true;

var declaringType = property.DeclaringType;
if (declaringType is null)
return false;

var autoPropertyBackingField = $"<{property.Name}>k__BackingField";
if (HasFieldInHierarchy(declaringType, autoPropertyBackingField))
return true;

if (property.Name.Length == 0)
return false;

var conventionalBackingField = $"_{char.ToLowerInvariant(property.Name[0])}{property.Name[1..]}";
return HasFieldInHierarchy(declaringType, conventionalBackingField);
}

private static bool HasFieldInHierarchy(Type type, string fieldName)
{
#pragma warning disable IL2070, IL2075
for (var current = type; current is not null; current = current.BaseType)
{
var field = current.GetField(
fieldName,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);

if (field is not null)
return true;
}
#pragma warning restore IL2070, IL2075

return false;
}

/// <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