From 290b1d2141dd86b5fe45ac675620378286440522 Mon Sep 17 00:00:00 2001 From: Paul DeVito Date: Sat, 10 Oct 2026 00:14:20 +0300 Subject: [PATCH] fix(operators)!: drop constant lists in case-insensitive in and not-in The case-insensitive string path of InOperator(true) and NotInOperator(true) now reads the list only from a NewArrayExpression, the shape that the parser makes. The case-sensitive path still accepts a ConstantExpression list. Filter strings do not change. Examples: ComparisonOperator.InOperator(true).GetExpression(title.Body, Expression.Constant(new List { "LAMB" }), null) before: x => ((x.Title != null) AndAlso .Contains(x.Title.ToLower())), with the list ["lamb"] after: NullReferenceException Title ^^* ["LAMB"] before and after: matches the title "lamb" BREAKING CHANGE: a direct call to InOperator(true) or NotInOperator(true) with a ConstantExpression list throws NullReferenceException. Pass Expression.NewArrayInit(typeof(string), ...) instead, or use a filter string. --- .../FilterParsingRegressionTests.cs | 20 ------------------- QueryKit/Operators/ComparisonOperator.cs | 6 ++---- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/QueryKit.UnitTests/FilterParsingRegressionTests.cs b/QueryKit.UnitTests/FilterParsingRegressionTests.cs index d6c67b3..ffd4f0b 100644 --- a/QueryKit.UnitTests/FilterParsingRegressionTests.cs +++ b/QueryKit.UnitTests/FilterParsingRegressionTests.cs @@ -261,26 +261,6 @@ public void comparison_operator_factory_has_one_test_case_per_operator_type() ComparisonOperatorFactories().Should().HaveCount(24); } - [Theory] - [InlineData(false, new[] { "lamb" })] - [InlineData(true, new[] { "null", "other" })] - public void case_insensitive_in_operator_factory_reads_a_constant_list(bool notIn, string[] expectedFirstNames) - { - var people = new[] - { - new TestingPerson { Title = null, FirstName = "null" }, - new TestingPerson { Title = "Lamb", FirstName = "lamb" }, - new TestingPerson { Title = "other", FirstName = "other" }, - }; - Expression> title = x => x.Title; - var comparisonOperator = notIn ? ComparisonOperator.NotInOperator(true) : ComparisonOperator.InOperator(true); - - var body = comparisonOperator.GetExpression(title.Body, Expression.Constant(new List { "LAMB" }), null); - var filterExpression = Expression.Lambda>(body, title.Parameters); - - people.AsQueryable().Where(filterExpression).Select(x => x.FirstName).Should().Equal(expectedFirstNames); - } - [Fact] public void comparison_operator_factory_with_uses_all_builds_all_expression() { diff --git a/QueryKit/Operators/ComparisonOperator.cs b/QueryKit/Operators/ComparisonOperator.cs index ad8e760..2e01b40 100644 --- a/QueryKit/Operators/ComparisonOperator.cs +++ b/QueryKit/Operators/ComparisonOperator.cs @@ -618,8 +618,7 @@ public override Expression GetExpression(Expression left, Expression right, T var listType = typeof(List); var caseList = Activator.CreateInstance(listType); - // A caller can pass the list as a constant, like v1.14.2. - var originalList = (values ?? ((ConstantExpression)right).Value) as IEnumerable; + var originalList = values as IEnumerable; foreach (var value in originalList!) { listType.GetMethod("Add")!.Invoke(caseList, new[] { caseMode == CaseInsensitiveMode.Upper ? value.ToUpper() : value.ToLower() }); @@ -864,8 +863,7 @@ public override Expression GetExpression(Expression left, Expression right, T var listType = typeof(List); var caseList = Activator.CreateInstance(listType); - // A caller can pass the list as a constant, like v1.14.2. - var originalList = (values ?? ((ConstantExpression)right).Value) as IEnumerable; + var originalList = values as IEnumerable; foreach (var value in originalList!) { listType.GetMethod("Add")!.Invoke(caseList, new[] { caseMode == CaseInsensitiveMode.Upper ? value.ToUpper() : value.ToLower() });