Skip to content

Commit 18a6107

Browse files
pdevito3claude
andcommitted
fix: resolve type of conditional expressions in derived properties
Previously, derived properties with conditional expressions returning mixed nullable/non-nullable types would have their type resolved to Object, causing: "Unsupported value '0' for type 'Object'" errors when filtering. This occurred with expressions like: x.Date.HasValue ? (calculation).Days : (int?)null The fix properly resolves the type of conditional expressions by: 1. Detecting when a derived property expression has Object type 2. Unwrapping any Convert/Unary expressions to find the actual conditional 3. Analyzing the true/false branches to determine the correct type 4. Reconstructing the conditional expression with the proper type 5. Converting the left expression for comparison to use the resolved type Also improved CreateRightExpr to handle Object-typed conditional expressions by resolving their actual underlying type before creating comparison values. Test added to verify complex conditional expressions with nullable types work correctly for filtering operations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 8234adf commit 18a6107

File tree

2 files changed

+147
-23
lines changed

2 files changed

+147
-23
lines changed

QueryKit.IntegrationTests/Tests/DatabaseFilteringTests.cs

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3821,22 +3821,32 @@ public async Task can_filter_with_derived_property_containing_complex_conditiona
38213821

38223822
await testingServiceScope.InsertAsync(fakePersonOne, fakePersonTwo, fakePersonThree);
38233823

3824-
// This should not throw "Value cannot be null" anymore
3825-
Action act = () =>
3824+
// Create config with conditional derived property
3825+
var config = new QueryKitConfiguration(config =>
38263826
{
3827-
var config = new QueryKitConfiguration(config =>
3828-
{
3829-
config.DerivedProperty<TestingPerson>(x =>
3830-
x.Date.HasValue
3831-
? (DateOnly.FromDateTime(DateTime.UtcNow).ToDateTime(TimeOnly.MinValue) -
3832-
x.Date.Value.ToDateTime(TimeOnly.MinValue)).Days
3833-
: (int?)null
3834-
).HasQueryName("daysUntilDue");
3835-
});
3836-
};
3827+
config.DerivedProperty<TestingPerson>(x =>
3828+
x.Date.HasValue
3829+
? (x.Date.Value.ToDateTime(TimeOnly.MinValue) -
3830+
DateOnly.FromDateTime(DateTime.UtcNow).ToDateTime(TimeOnly.MinValue)).Days
3831+
: (int?)null
3832+
).HasQueryName("daysFromNow");
3833+
});
3834+
3835+
// Act & Assert - Should not throw "Unsupported value '0' for type 'Object'"
3836+
var futureQuery = $"""daysFromNow > 0""";
3837+
var queryablePeople = testingServiceScope.DbContext().People;
3838+
var futurePeople = await queryablePeople.ApplyQueryKitFilter(futureQuery, config).ToListAsync();
3839+
3840+
// Filter for past dates (negative days)
3841+
var pastQuery = $"""daysFromNow < 0""";
3842+
var pastPeople = await queryablePeople.ApplyQueryKitFilter(pastQuery, config).ToListAsync();
3843+
3844+
// Verify results - The main fix is that these queries should work without throwing exceptions
3845+
futurePeople.Should().NotBeNull();
3846+
futurePeople.Should().Contain(p => p.Id == fakePersonOne.Id, "future date should match > 0");
38373847

3838-
// Assert - Should not throw ArgumentNullException
3839-
act.Should().NotThrow<ArgumentNullException>("null values in expressions should be handled properly");
3848+
pastPeople.Should().NotBeNull();
3849+
pastPeople.Should().Contain(p => p.Id == fakePersonTwo.Id, "past date should match < 0");
38403850
}
38413851

38423852
}

QueryKit/FilterParser.cs

Lines changed: 123 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -222,11 +222,55 @@ from closingBracket in Parse.Char(']')
222222
{ typeof(sbyte), value => sbyte.Parse(value, CultureInfo.InvariantCulture) },
223223
};
224224

225-
private static Expression CreateRightExpr(Expression leftExpr, string right, ComparisonOperator op,
225+
private static Expression CreateRightExpr(Expression leftExpr, string right, ComparisonOperator op,
226226
IQueryKitConfiguration? config = null, string? propertyPath = null)
227227
{
228228
var targetType = leftExpr.Type;
229-
229+
230+
// Handle expressions with Object type - check for ConditionalExpression inside Convert/Unary
231+
if (targetType == typeof(object))
232+
{
233+
var innerExpr = leftExpr;
234+
235+
// Unwrap Convert/Unary expressions
236+
while (innerExpr is UnaryExpression unaryExpr && unaryExpr.NodeType == ExpressionType.Convert)
237+
{
238+
innerExpr = unaryExpr.Operand;
239+
}
240+
241+
if (innerExpr is ConditionalExpression conditionalExpr)
242+
{
243+
// For conditional expressions, use the type of the branches
244+
// Prefer nullable types if one branch is nullable
245+
var trueType = conditionalExpr.IfTrue.Type;
246+
var falseType = conditionalExpr.IfFalse.Type;
247+
248+
if (trueType == falseType)
249+
{
250+
targetType = trueType;
251+
}
252+
else if (Nullable.GetUnderlyingType(trueType) != null || Nullable.GetUnderlyingType(falseType) != null)
253+
{
254+
// One is nullable, use the nullable version
255+
var underlyingTrue = Nullable.GetUnderlyingType(trueType) ?? trueType;
256+
var underlyingFalse = Nullable.GetUnderlyingType(falseType) ?? falseType;
257+
258+
if (underlyingTrue == underlyingFalse)
259+
{
260+
targetType = typeof(Nullable<>).MakeGenericType(underlyingTrue);
261+
}
262+
}
263+
else if (trueType.IsAssignableFrom(falseType))
264+
{
265+
targetType = trueType;
266+
}
267+
else if (falseType.IsAssignableFrom(trueType))
268+
{
269+
targetType = falseType;
270+
}
271+
}
272+
}
273+
230274
// Check if this property uses HasConversion
231275
if (config?.PropertyMappings != null && !string.IsNullOrEmpty(propertyPath))
232276
{
@@ -616,17 +660,87 @@ private static Parser<Expression> ComparisonExprParser<T>(ParameterExpression pa
616660
{
617661
propertyPath = GetPropertyPath(memberExpr, parameter);
618662
}
619-
620-
var rightExpr = CreateRightExpr(temp.leftExpr, temp.right, temp.op, config, propertyPath);
621-
663+
664+
var leftExprForComparison = temp.leftExpr;
665+
666+
// If the left expression is a conditional with Object type, convert it to the proper type
667+
if (leftExprForComparison.Type == typeof(object))
668+
{
669+
var innerExpr = leftExprForComparison;
670+
671+
// Unwrap Convert/Unary expressions
672+
while (innerExpr is UnaryExpression unaryExpr && unaryExpr.NodeType == ExpressionType.Convert)
673+
{
674+
innerExpr = unaryExpr.Operand;
675+
}
676+
677+
if (innerExpr is ConditionalExpression conditionalExpr)
678+
{
679+
// Determine the actual type
680+
var trueType = conditionalExpr.IfTrue.Type;
681+
var falseType = conditionalExpr.IfFalse.Type;
682+
Type actualType = typeof(object);
683+
684+
if (trueType == falseType)
685+
{
686+
actualType = trueType;
687+
}
688+
else if (Nullable.GetUnderlyingType(trueType) != null || Nullable.GetUnderlyingType(falseType) != null)
689+
{
690+
var underlyingTrue = Nullable.GetUnderlyingType(trueType) ?? trueType;
691+
var underlyingFalse = Nullable.GetUnderlyingType(falseType) ?? falseType;
692+
693+
if (underlyingTrue == underlyingFalse)
694+
{
695+
actualType = typeof(Nullable<>).MakeGenericType(underlyingTrue);
696+
}
697+
}
698+
else if (trueType.IsAssignableFrom(falseType))
699+
{
700+
actualType = trueType;
701+
}
702+
else if (falseType.IsAssignableFrom(trueType))
703+
{
704+
actualType = falseType;
705+
}
706+
707+
// If we found a better type, recreate the conditional with the correct type
708+
if (actualType != typeof(object) && actualType != null)
709+
{
710+
// Create a new conditional expression with the correct return type
711+
// This ensures proper type handling without unnecessary conversions
712+
var newIfTrue = conditionalExpr.IfTrue;
713+
var newIfFalse = conditionalExpr.IfFalse;
714+
715+
// Convert branches to the target type if needed
716+
if (newIfTrue.Type != actualType)
717+
{
718+
newIfTrue = Expression.Convert(newIfTrue, actualType);
719+
}
720+
if (newIfFalse.Type != actualType)
721+
{
722+
newIfFalse = Expression.Convert(newIfFalse, actualType);
723+
}
724+
725+
leftExprForComparison = Expression.Condition(
726+
conditionalExpr.Test,
727+
newIfTrue,
728+
newIfFalse,
729+
actualType);
730+
}
731+
}
732+
}
733+
734+
var rightExpr = CreateRightExpr(leftExprForComparison, temp.right, temp.op, config, propertyPath);
735+
622736
// Handle nested collection filtering
623-
if (temp.leftExpr is MethodCallExpression methodCall && IsNestedCollectionExpression(methodCall))
737+
if (leftExprForComparison is MethodCallExpression methodCall && IsNestedCollectionExpression(methodCall))
624738
{
625739
return CreateNestedCollectionFilterExpression<T>(methodCall, rightExpr, temp.op);
626740
}
627-
628-
629-
return temp.op.GetExpression<T>(temp.leftExpr, rightExpr, config?.DbContextType);
741+
742+
743+
return temp.op.GetExpression<T>(leftExprForComparison, rightExpr, config?.DbContextType);
630744
});
631745

632746
return arithmeticComparison.Or(regularComparison);

0 commit comments

Comments
 (0)