Fix case-insensitive null/empty filter operators in compound filters

Builds on #2587. The previous guard only checked the primary operator,
leaving two gaps in two-operator filters under case-insensitive mode:

- A content primary with a null/empty secondary (e.g. "Contains x OR
  IsNull") dropped the secondary check entirely.
- A null/empty primary with a content secondary (e.g. "IsEmpty OR
  Contains x") silently made the secondary comparison case-sensitive.

Use a raw (unwrapped) property for all four null/empty arms in both the
primary and secondary switches, and always lowercase the property for
content comparisons. Also build the secondary expression for null/empty
operators, which previously produced no expression in any mode because
those operators carry no filter value.

Add tests covering the case-insensitive null/empty operators and both
compound-filter scenarios.
@
This commit is contained in:
Vladimir Enchev
2026-06-26 11:51:02 +03:00
parent 31a6d6e2b4
commit d9f2ae4cec
2 changed files with 144 additions and 24 deletions

View File

@@ -340,6 +340,123 @@ namespace Radzen.Blazor.Tests
Assert.Equal("Test", result[0].Name);
}
private static IQueryable<TestItem> NullEmptyTestData() => new List<TestItem>
{
new TestItem { Id = 1, Name = null },
new TestItem { Id = 2, Name = "" },
new TestItem { Id = 3, Name = "Test" }
}.AsQueryable();
[Fact]
public void Where_FiltersWithIsNull_CaseInsensitive()
{
var filters = new List<FilterDescriptor>
{
new FilterDescriptor { Property = "Name", FilterOperator = FilterOperator.IsNull }
};
var result = NullEmptyTestData().Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.CaseInsensitive).ToList();
Assert.Single(result);
Assert.Equal(1, result[0].Id);
}
[Fact]
public void Where_FiltersWithIsNotNull_CaseInsensitive()
{
var filters = new List<FilterDescriptor>
{
new FilterDescriptor { Property = "Name", FilterOperator = FilterOperator.IsNotNull }
};
var result = NullEmptyTestData().Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.CaseInsensitive).ToList();
Assert.Equal(new[] { 2, 3 }, result.Select(r => r.Id).OrderBy(i => i).ToArray());
}
[Fact]
public void Where_FiltersWithIsEmpty_CaseInsensitive()
{
var filters = new List<FilterDescriptor>
{
new FilterDescriptor { Property = "Name", FilterOperator = FilterOperator.IsEmpty }
};
var result = NullEmptyTestData().Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.CaseInsensitive).ToList();
Assert.Single(result);
Assert.Equal(2, result[0].Id);
}
[Fact]
public void Where_FiltersWithIsNotEmpty_CaseInsensitive()
{
var filters = new List<FilterDescriptor>
{
new FilterDescriptor { Property = "Name", FilterOperator = FilterOperator.IsNotEmpty }
};
var result = NullEmptyTestData().Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.CaseInsensitive).ToList();
Assert.Equal(new[] { 1, 3 }, result.Select(r => r.Id).OrderBy(i => i).ToArray());
}
[Fact]
public void Where_ContainsPrimary_WithIsNullSecondary_CaseInsensitive()
{
var data = new List<TestItem>
{
new TestItem { Id = 1, Name = null },
new TestItem { Id = 2, Name = "ABC" },
new TestItem { Id = 3, Name = "xyz" }
}.AsQueryable();
var filters = new List<FilterDescriptor>
{
new FilterDescriptor
{
Property = "Name",
FilterValue = "abc",
FilterOperator = FilterOperator.Contains,
LogicalFilterOperator = LogicalFilterOperator.Or,
SecondFilterValue = null,
SecondFilterOperator = FilterOperator.IsNull
}
};
var result = data.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.CaseInsensitive).ToList();
Assert.Equal(new[] { 1, 2 }, result.Select(r => r.Id).OrderBy(i => i).ToArray());
}
[Fact]
public void Where_IsEmptyPrimary_WithContainsSecondary_CaseInsensitive()
{
var data = new List<TestItem>
{
new TestItem { Id = 1, Name = null },
new TestItem { Id = 2, Name = "" },
new TestItem { Id = 3, Name = "ABC" },
new TestItem { Id = 4, Name = "xyz" }
}.AsQueryable();
var filters = new List<FilterDescriptor>
{
new FilterDescriptor
{
Property = "Name",
FilterOperator = FilterOperator.IsEmpty,
LogicalFilterOperator = LogicalFilterOperator.Or,
SecondFilterValue = "abc",
SecondFilterOperator = FilterOperator.Contains
}
};
var result = data.Where(filters, LogicalFilterOperator.And, FilterCaseSensitivity.CaseInsensitive).ToList();
Assert.Equal(new[] { 2, 3 }, result.Select(r => r.Id).OrderBy(i => i).ToArray());
}
[Fact]
public void Where_CombinesFiltersWithAnd()
{

View File

@@ -752,11 +752,9 @@ namespace Radzen
var constant = Expression.Constant(constantValue, constantType);
if (caseInsensitive && !isEnumerable
&& filter.FilterOperator != FilterOperator.IsNull
&& filter.FilterOperator != FilterOperator.IsNotNull
&& filter.FilterOperator != FilterOperator.IsEmpty
&& filter.FilterOperator != FilterOperator.IsNotEmpty)
var rawProperty = property;
if (caseInsensitive && !isEnumerable)
{
property = Expression.Call(notNullCheck(property), typeof(string).GetMethod("ToLower", Type.EmptyTypes)!);
}
@@ -816,10 +814,10 @@ namespace Radzen
Expression.Call(typeof(Enumerable), nameof(Enumerable.Except), new Type[] { collectionItemType! }, constant, notNullCheck(property))) : Expression.Constant(true),
FilterOperator.StartsWith => Expression.Call(notNullCheck(property), typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, constant),
FilterOperator.EndsWith => Expression.Call(notNullCheck(property), typeof(string).GetMethod("EndsWith", new[] { typeof(string) })!, constant),
FilterOperator.IsNull => Expression.Equal(property, Expression.Constant(null, property.Type)),
FilterOperator.IsNotNull => Expression.NotEqual(property, Expression.Constant(null, property.Type)),
FilterOperator.IsEmpty => Expression.Equal(property, Expression.Constant(String.Empty)),
FilterOperator.IsNotEmpty => Expression.NotEqual(property, Expression.Constant(String.Empty)),
FilterOperator.IsNull => Expression.Equal(rawProperty, Expression.Constant(null, rawProperty.Type)),
FilterOperator.IsNotNull => Expression.NotEqual(rawProperty, Expression.Constant(null, rawProperty.Type)),
FilterOperator.IsEmpty => Expression.Equal(rawProperty, Expression.Constant(String.Empty)),
FilterOperator.IsNotEmpty => Expression.NotEqual(rawProperty, Expression.Constant(String.Empty)),
_ => null
};
@@ -834,24 +832,29 @@ namespace Radzen
}
Expression? secondExpression = null;
if (secondConstant != null)
var secondIsNullOrEmptyOperator = filter.SecondFilterOperator == FilterOperator.IsNull
|| filter.SecondFilterOperator == FilterOperator.IsNotNull
|| filter.SecondFilterOperator == FilterOperator.IsEmpty
|| filter.SecondFilterOperator == FilterOperator.IsNotEmpty;
if (secondConstant != null || secondIsNullOrEmptyOperator)
{
secondExpression = filter.SecondFilterOperator switch
{
FilterOperator.Equals => Expression.Equal(notNullCheck(property), secondConstant),
FilterOperator.NotEquals => Expression.NotEqual(notNullCheck(property), secondConstant),
FilterOperator.LessThan => Expression.LessThan(notNullCheck(property), secondConstant),
FilterOperator.LessThanOrEquals => Expression.LessThanOrEqual(notNullCheck(property), secondConstant),
FilterOperator.GreaterThan => Expression.GreaterThan(notNullCheck(property), secondConstant),
FilterOperator.GreaterThanOrEquals => Expression.GreaterThanOrEqual(notNullCheck(property), secondConstant),
FilterOperator.Contains => Expression.Call(notNullCheck(property), typeof(string).GetMethod("Contains", new[] { typeof(string) })!, secondConstant),
FilterOperator.DoesNotContain => Expression.Not(Expression.Call(notNullCheck(property), property.Type.GetMethod("Contains", new[] { typeof(string) })!, secondConstant)),
FilterOperator.StartsWith => Expression.Call(notNullCheck(property), typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, secondConstant),
FilterOperator.EndsWith => Expression.Call(notNullCheck(property), typeof(string).GetMethod("EndsWith", new[] { typeof(string) })!, secondConstant),
FilterOperator.IsNull => Expression.Equal(property, Expression.Constant(null, property.Type)),
FilterOperator.IsNotNull => Expression.NotEqual(property, Expression.Constant(null, property.Type)),
FilterOperator.IsEmpty => Expression.Equal(property, Expression.Constant(String.Empty)),
FilterOperator.IsNotEmpty => Expression.NotEqual(property, Expression.Constant(String.Empty)),
FilterOperator.Equals => Expression.Equal(notNullCheck(property), secondConstant!),
FilterOperator.NotEquals => Expression.NotEqual(notNullCheck(property), secondConstant!),
FilterOperator.LessThan => Expression.LessThan(notNullCheck(property), secondConstant!),
FilterOperator.LessThanOrEquals => Expression.LessThanOrEqual(notNullCheck(property), secondConstant!),
FilterOperator.GreaterThan => Expression.GreaterThan(notNullCheck(property), secondConstant!),
FilterOperator.GreaterThanOrEquals => Expression.GreaterThanOrEqual(notNullCheck(property), secondConstant!),
FilterOperator.Contains => Expression.Call(notNullCheck(property), typeof(string).GetMethod("Contains", new[] { typeof(string) })!, secondConstant!),
FilterOperator.DoesNotContain => Expression.Not(Expression.Call(notNullCheck(property), property.Type.GetMethod("Contains", new[] { typeof(string) })!, secondConstant!)),
FilterOperator.StartsWith => Expression.Call(notNullCheck(property), typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, secondConstant!),
FilterOperator.EndsWith => Expression.Call(notNullCheck(property), typeof(string).GetMethod("EndsWith", new[] { typeof(string) })!, secondConstant!),
FilterOperator.IsNull => Expression.Equal(rawProperty, Expression.Constant(null, rawProperty.Type)),
FilterOperator.IsNotNull => Expression.NotEqual(rawProperty, Expression.Constant(null, rawProperty.Type)),
FilterOperator.IsEmpty => Expression.Equal(rawProperty, Expression.Constant(String.Empty)),
FilterOperator.IsNotEmpty => Expression.NotEqual(rawProperty, Expression.Constant(String.Empty)),
_ => null
};
}