diff --git a/src/Gridify/Builder/BaseQueryBuilder.cs b/src/Gridify/Builder/BaseQueryBuilder.cs index 3bb31807..592dbe88 100644 --- a/src/Gridify/Builder/BaseQueryBuilder.cs +++ b/src/Gridify/Builder/BaseQueryBuilder.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; using System.Linq.Expressions; +using Gridify.Reflection; using Gridify.Syntax; namespace Gridify.Builder; @@ -123,7 +124,7 @@ public TQuery Build(ExpressionSyntax expression) return (result, isNested); } - var query = BuildQuery(mapTarget.Body, mapTarget.Parameters[0], right, op, gMap.Convertor); + var query = BuildQuery(mapTarget.Body, mapTarget.Parameters[0], right, op, gMap.Convertor, false); if (query == null) return null; if (hasIndexer) @@ -157,7 +158,8 @@ private static object AddIndexerNullCheck(LambdaExpression mapTarget, object que ParameterExpression parameter, ValueExpressionSyntax valueExpression, ISyntaxNode op, - Func? convertor) + Func? convertor, + bool isNested) { // Remove the boxing for value types if (body.NodeType == ExpressionType.Convert) body = ((UnaryExpression)body).Operand; @@ -197,7 +199,7 @@ private static object AddIndexerNullCheck(LambdaExpression mapTarget, object que { return BuildAlwaysFalseQuery(parameter); } - + if (value is DateTime dateTime) { if (mapper.Configuration.DefaultDateTimeKind.HasValue) @@ -207,6 +209,18 @@ private static object AddIndexerNullCheck(LambdaExpression mapTarget, object que } } + // handle case-Insensitive search + if (value is not null && (valueExpression.IsCaseInsensitive + || (mapper.Configuration.CaseInsensitiveFiltering && !isNested && body.Type == typeof(string))) + && op.Kind is not SyntaxKind.GreaterThan + && op.Kind is not SyntaxKind.LessThan + && op.Kind is not SyntaxKind.GreaterOrEqualThan + && op.Kind is not SyntaxKind.LessOrEqualThan) + { + value = value.ToString()?.ToLower(); + body = Expression.Call(body, MethodInfoHelper.GetToLowerMethod()); + } + var query = BuildQueryAccordingToValueType(body, parameter, value, op, valueExpression); return query; } diff --git a/src/Gridify/Builder/LinqQueryBuilder.cs b/src/Gridify/Builder/LinqQueryBuilder.cs index 9c788c5f..3a4464e8 100644 --- a/src/Gridify/Builder/LinqQueryBuilder.cs +++ b/src/Gridify/Builder/LinqQueryBuilder.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Data; -using System.Globalization; using System.Linq; using System.Linq.Expressions; using Gridify.Reflection; @@ -25,7 +23,8 @@ public class LinqQueryBuilder(IGridifyMapper mapper) : BaseQueryBuilder> BuildAlwaysFalseQuery(ParameterExpr switch (op.Kind) { - case SyntaxKind.Equal when !valueExpression.IsNullOrDefault && areBothStrings && (valueExpression.IsCaseInsensitive || mapper.Configuration.CaseInsensitiveFiltering): - be = Expression.Call( - null, - MethodInfoHelper.GetCaseAwareEqualsMethod(), - body, - GetValueExpression(body.Type, value), - Expression.Constant(StringComparison.InvariantCultureIgnoreCase) - ); - break; case SyntaxKind.Equal when !valueExpression.IsNullOrDefault: be = Expression.Equal(body, GetValueExpression(body.Type, value)); break; @@ -142,15 +132,6 @@ protected override Expression> BuildAlwaysFalseQuery(ParameterExpr : Expression.Equal(body, Expression.Default(body.Type)); } - break; - case SyntaxKind.NotEqual when !valueExpression.IsNullOrDefault && areBothStrings && (valueExpression.IsCaseInsensitive || mapper.Configuration.CaseInsensitiveFiltering): - be = Expression.Not(Expression.Call( - null, - MethodInfoHelper.GetCaseAwareEqualsMethod(), - body, - GetValueExpression(body.Type, value), - Expression.Constant(StringComparison.InvariantCultureIgnoreCase)) - ); break; case SyntaxKind.NotEqual when !valueExpression.IsNullOrDefault: be = Expression.NotEqual(body, GetValueExpression(body.Type, value)); @@ -194,74 +175,60 @@ protected override Expression> BuildAlwaysFalseQuery(ParameterExpr case SyntaxKind.LessOrEqualThan when areBothStrings: be = GetLessThanOrEqualExpression(body, valueExpression, value); break; - case SyntaxKind.Like or SyntaxKind.NotLike: - if (areBothStrings && (valueExpression.IsCaseInsensitive || mapper.Configuration.CaseInsensitiveFiltering)) + case SyntaxKind.Like: + be = Expression.Call(body, MethodInfoHelper.GetStringContainsMethod(), GetValueExpression(body.Type, value)); + break; + case SyntaxKind.NotLike: + be = Expression.Not(Expression.Call(body, MethodInfoHelper.GetStringContainsMethod(), GetValueExpression(body.Type, value))); + break; + case SyntaxKind.StartsWith: + if (body.Type != typeof(string)) { - be = Expression.Call( - body, - MethodInfoHelper.GetCaseAwareStringContainsMethod(), - GetValueExpression(body.Type, value), - Expression.Constant(StringComparison.InvariantCultureIgnoreCase) - ); + body = Expression.Call(body, MethodInfoHelper.GetToStringMethod()); + be = Expression.Call(body, MethodInfoHelper.GetStartWithMethod(), GetValueExpression(body.Type, value?.ToString())); } else { - be = Expression.Call(body, MethodInfoHelper.GetStringContainsMethod(), GetValueExpression(body.Type, value)); + be = Expression.Call(body, MethodInfoHelper.GetStartWithMethod(), GetValueExpression(body.Type, value)); } - if (op.Kind == SyntaxKind.NotLike) - be = Expression.Not(be); - break; - case SyntaxKind.StartsWith or SyntaxKind.NotStartsWith: + case SyntaxKind.EndsWith: if (body.Type != typeof(string)) { body = Expression.Call(body, MethodInfoHelper.GetToStringMethod()); - be = Expression.Call(body, MethodInfoHelper.GetStartWithMethod(), GetValueExpression(body.Type, value?.ToString())); - } - else if (areBothStrings && (valueExpression.IsCaseInsensitive || mapper.Configuration.CaseInsensitiveFiltering)) - { - be = Expression.Call( - body, - MethodInfoHelper.GetCaseAwareStartsWithMethod(), - GetValueExpression(body.Type, value), - Expression.Constant(StringComparison.InvariantCultureIgnoreCase) - ); + be = Expression.Call(body, MethodInfoHelper.GetEndsWithMethod(), GetValueExpression(body.Type, value?.ToString())); } else { - be = Expression.Call(body, MethodInfoHelper.GetStartWithMethod(), GetValueExpression(body.Type, value)); + be = Expression.Call(body, MethodInfoHelper.GetEndsWithMethod(), GetValueExpression(body.Type, value)); } - if (op.Kind == SyntaxKind.NotStartsWith) - be = Expression.Not(be); - break; - case SyntaxKind.EndsWith or SyntaxKind.NotEndsWith: + case SyntaxKind.NotStartsWith: if (body.Type != typeof(string)) { body = Expression.Call(body, MethodInfoHelper.GetToStringMethod()); - be = Expression.Call(body, MethodInfoHelper.GetStartWithMethod(), GetValueExpression(body.Type, value?.ToString())); + be = Expression.Not(Expression.Call(body, MethodInfoHelper.GetStartWithMethod(), GetValueExpression(body.Type, value?.ToString()))); } - else if (areBothStrings && (valueExpression.IsCaseInsensitive || mapper.Configuration.CaseInsensitiveFiltering)) + else { - be = Expression.Call( - body, - MethodInfoHelper.GetCaseAwareEndsWithMethod(), - GetValueExpression(body.Type, value), - Expression.Constant(StringComparison.InvariantCultureIgnoreCase) - ); + be = Expression.Not(Expression.Call(body, MethodInfoHelper.GetStartWithMethod(), GetValueExpression(body.Type, value))); + } + + break; + case SyntaxKind.NotEndsWith: + if (body.Type != typeof(string)) + { + body = Expression.Call(body, MethodInfoHelper.GetToStringMethod()); + be = Expression.Not(Expression.Call(body, MethodInfoHelper.GetEndsWithMethod(), GetValueExpression(body.Type, value?.ToString()))); } else { - be = Expression.Call(body, MethodInfoHelper.GetEndsWithMethod(), GetValueExpression(body.Type, value)); + be = Expression.Not(Expression.Call(body, MethodInfoHelper.GetEndsWithMethod(), GetValueExpression(body.Type, value))); } - if (op.Kind == SyntaxKind.NotEndsWith) - be = Expression.Not(be); - break; - case SyntaxKind.CustomOperator: var token = (SyntaxToken)op; var customOperator = GridifyGlobalConfiguration.CustomOperators.Operators.First(q => q.GetOperator() == token!.Text); @@ -299,7 +266,7 @@ protected override Expression> CombineWithOrOperator(Expression a.NodeType == ExpressionType.Lambda) as LambdaExpression; - var conditionExp = BuildQuery(targetExp!.Body, targetExp.Parameters[0], value, op, gMap.Convertor); + var conditionExp = BuildQuery(targetExp!.Body, targetExp.Parameters[0], value, op, gMap.Convertor, true); if (conditionExp is not LambdaExpression lambdaExp) return null; @@ -336,14 +303,14 @@ private static LambdaExpression ParseMethodCallExpression(MethodCallExpression e { case MemberExpression member: { - if (op.Kind is not (SyntaxKind.Equal or SyntaxKind.NotEqual) || - !member.Type.IsSimpleTypeCollection(out _)) return GetAnyExpression(member, predicate); - return predicate.Body switch + if (op.Kind is SyntaxKind.Equal or SyntaxKind.NotEqual && + member.Type.IsSimpleTypeCollection(out _) && + predicate.Body is BinaryExpression binaryExpression) { - BinaryExpression binaryExpression => GetContainsExpression(member, binaryExpression, op), - MethodCallExpression { Method.Name: "Equals" } methodCallExpression => GetCaseSensitiveContainsExpression(member, methodCallExpression, op), - _ => GetAnyExpression(member, predicate) - }; + return GetContainsExpression(member, binaryExpression, op); + } + + return GetAnyExpression(member, predicate); } case MethodCallExpression { Method.Name: "SelectMany" } subExp when subExp.Arguments.Last() @@ -382,28 +349,6 @@ when subExp.Arguments.Last() is LambdaExpression wherePredicate && } } - private static LambdaExpression GetCaseSensitiveContainsExpression(MemberExpression member, MethodCallExpression methodCallExpression, ISyntaxNode op) - { - var param = GetParameterExpression(member); - var prop = GetPropertyOrField(member, param); - - var tp = prop.Type.IsGenericType - ? prop.Type.GenericTypeArguments.First() // list - : prop.Type.GetElementType(); // array - - if (tp == null) throw new GridifyFilteringException($"Can not detect the '{member.Member.Name}' property type."); - - var containsMethod = MethodInfoHelper.GetCaseAwareContainsMethod(tp); - var ignoreCaseComparerExpression = Expression.Constant(StringComparer.InvariantCultureIgnoreCase); - var value = methodCallExpression.Arguments[1]; - Expression containsExp = Expression.Call(containsMethod, prop, value, ignoreCaseComparerExpression); - if (op.Kind == SyntaxKind.NotEqual) - { - containsExp = Expression.Not(containsExp); - } - return GetExpressionWithNullCheck(prop, param, containsExp); - } - private static LambdaExpression GetContainsExpression(MemberExpression member, BinaryExpression binaryExpression, ISyntaxNode op) { var param = GetParameterExpression(member); @@ -530,10 +475,10 @@ private BinaryExpression GetGreaterThanExpression(Expression body, ValueExpressi GetStringComparisonCaseExpression(valueExpression.IsCaseInsensitive)), Expression.Constant(0)); } - private static ConstantExpression GetStringComparisonCaseExpression(bool isCaseInsensitive) + private ConstantExpression GetStringComparisonCaseExpression(bool isCaseInsensitive) { - return isCaseInsensitive - ? Expression.Constant(StringComparison.InvariantCultureIgnoreCase) + return isCaseInsensitive || mapper.Configuration.CaseInsensitiveFiltering + ? Expression.Constant(StringComparison.OrdinalIgnoreCase) : Expression.Constant(StringComparison.Ordinal); } diff --git a/src/Gridify/Reflection/MethodInfoHelper.cs b/src/Gridify/Reflection/MethodInfoHelper.cs index fe9d8c15..0a4a7e2f 100644 --- a/src/Gridify/Reflection/MethodInfoHelper.cs +++ b/src/Gridify/Reflection/MethodInfoHelper.cs @@ -6,6 +6,11 @@ namespace Gridify.Reflection; public static class MethodInfoHelper { + public static MethodInfo GetToLowerMethod() + { + return typeof(string).GetMethod("ToLower", [])!; + } + public static MethodInfo GetAnyMethod(Type type) { return typeof(Enumerable).GetMethods().Single(m => m.Name == "Any" && m.GetParameters().Length == 2).MakeGenericMethod(type); @@ -55,29 +60,4 @@ public static MethodInfo GetSelectMethod(this Type type) { return typeof(Enumerable).GetMethods().First(m => m.Name == "Select").MakeGenericMethod([type, type]); } - - public static MethodInfo GetCaseAwareContainsMethod(Type tp) - { - return typeof(Enumerable).GetMethods().Last(x => x.Name == "Contains").MakeGenericMethod(tp); - } - - public static MethodInfo GetCaseAwareStringContainsMethod() - { - return typeof(string).GetMethod("Contains", [typeof(string), typeof(StringComparison)])!; - } - - public static MethodInfo GetCaseAwareEqualsMethod() - { - return typeof(string).GetMethod("Equals", [typeof(string), typeof(string), typeof(StringComparison)])!; - } - - public static MethodInfo GetCaseAwareStartsWithMethod() - { - return typeof(string).GetMethod("StartsWith", [typeof(string), typeof(StringComparison)])!; - } - - public static MethodInfo GetCaseAwareEndsWithMethod() - { - return typeof(string).GetMethod("EndsWith", [typeof(string), typeof(StringComparison)])!; - } } diff --git a/test/EntityFrameworkPostgreSqlIntegrationTests/Issue193Tests.cs b/test/EntityFrameworkPostgreSqlIntegrationTests/Issue193Tests.cs deleted file mode 100644 index f3e77631..00000000 --- a/test/EntityFrameworkPostgreSqlIntegrationTests/Issue193Tests.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Gridify; -using Xunit; - -namespace EntityFrameworkPostgreSqlIntegrationTests; - -public class Issue193Tests -{ - - [Fact] - public void ApplyFiltering_WithCaseInsensitiveOperator_ShouldReturnExpectedResult() - { - // arrange - var dataSource = Test.GetTestDataSource(); - - var expected = dataSource.Where(q => q.FavouriteColorList.Contains("red", StringComparer.InvariantCultureIgnoreCase) | - q.FavouriteColorList.Contains("blue", StringComparer.InvariantCultureIgnoreCase)) - .ToList(); - - // act - var actual = dataSource.ApplyFiltering("FavouriteColorList=red/i|FavouriteColorList=blue/i").ToList(); - - // assert - Assert.NotEmpty(expected); - Assert.NotEmpty(actual); - Assert.Equal(expected.Count, actual.Count); - } - - [Fact] - public void ApplyFiltering_WithDefaultCaseInsensitiveFiltering_ShouldReturnExpectedResult() - { - // arrange - var dataSource = Test.GetTestDataSource(); - - var expected = dataSource.Where(q => q.FavouriteColorList.Contains("red", StringComparer.InvariantCultureIgnoreCase) | - q.FavouriteColorList.Contains("blue", StringComparer.InvariantCultureIgnoreCase)) - .ToList(); - - var mapper = new GridifyMapper(q => q.CaseInsensitiveFiltering = true).GenerateMappings(); - - // act - var actual = dataSource.ApplyFiltering("FavouriteColorList=red|FavouriteColorList=blue", mapper).ToList(); - - // assert - Assert.NotEmpty(expected); - Assert.NotEmpty(actual); - Assert.Equal(expected.Count, actual.Count); - } - - class Test - { - public string[] FavouriteColorList { get; set; } - - public static IQueryable GetTestDataSource() - { - return new List() - { - new() { FavouriteColorList = ["Green", "Blue"] }, - new() { FavouriteColorList = ["White", "Yellow"] }, - new() { FavouriteColorList = ["Red", "Orange"] }, - new() { FavouriteColorList = ["Purple", "Pink"] }, - new() { FavouriteColorList = ["Black", "Gray"] } - }.AsQueryable(); - } - } -} diff --git a/test/Gridify.Tests/GridifyExtensionsShould.cs b/test/Gridify.Tests/GridifyExtensionsShould.cs index 167de72e..f0266cb2 100644 --- a/test/Gridify.Tests/GridifyExtensionsShould.cs +++ b/test/Gridify.Tests/GridifyExtensionsShould.cs @@ -581,7 +581,7 @@ public void ApplyFiltering_GreaterThanOrEqualBetweenTwoStrings() public void ApplyFiltering_GreaterThanOrEqual_CaseInsensitive_BetweenTwoStrings() { var actual = _fakeRepository.AsQueryable().ApplyFiltering("name >= j/i").ToList(); - var expected = _fakeRepository.Where(q => string.Compare(q.Name, "j", StringComparison.InvariantCultureIgnoreCase) >= 0).ToList(); + var expected = _fakeRepository.Where(q => string.Compare(q.Name, "j", StringComparison.OrdinalIgnoreCase) >= 0).ToList(); Assert.Equal(expected.Count, actual.Count); Assert.Equal(expected, actual); diff --git a/test/Gridify.Tests/IssueTests/Issue193Tests.cs b/test/Gridify.Tests/IssueTests/Issue193Tests.cs new file mode 100644 index 00000000..5112e724 --- /dev/null +++ b/test/Gridify.Tests/IssueTests/Issue193Tests.cs @@ -0,0 +1,68 @@ +// using System; +// using System.Collections.Generic; +// using System.Linq; +// using Gridify; +// using Xunit; +// +// namespace EntityFrameworkPostgreSqlIntegrationTests; +// +// public class Issue193Tests +// { +// +// [Fact] +// public void ApplyFiltering_WithCaseInsensitiveOperator_ShouldReturnExpectedResult() +// { +// // arrange +// var dataSource = Test.GetTestDataSource(); +// +// var expected = dataSource.Where(q => q.FavouriteColorList.Contains("red", StringComparer.InvariantCultureIgnoreCase) | +// q.FavouriteColorList.Contains("blue", StringComparer.InvariantCultureIgnoreCase)) +// .ToList(); +// +// // act +// var actual = dataSource.ApplyFiltering("FavouriteColorList=red/i|FavouriteColorList=blue/i").ToList(); +// +// // assert +// Assert.NotEmpty(expected); +// Assert.NotEmpty(actual); +// Assert.Equal(expected.Count, actual.Count); +// } +// +// [Fact] +// public void ApplyFiltering_WithDefaultCaseInsensitiveFiltering_ShouldReturnExpectedResult() +// { +// // arrange +// var dataSource = Test.GetTestDataSource(); +// +// var expected = dataSource.Where(q => q.FavouriteColorList.Contains("red", StringComparer.InvariantCultureIgnoreCase) | +// q.FavouriteColorList.Contains("blue", StringComparer.InvariantCultureIgnoreCase)) +// .ToList(); +// +// var mapper = new GridifyMapper(q => q.CaseInsensitiveFiltering = true).GenerateMappings(); +// +// // act +// var actual = dataSource.ApplyFiltering("FavouriteColorList=red|FavouriteColorList=blue", mapper).ToList(); +// +// // assert +// Assert.NotEmpty(expected); +// Assert.NotEmpty(actual); +// Assert.Equal(expected.Count, actual.Count); +// } +// +// class Test +// { +// public string[] FavouriteColorList { get; set; } +// +// public static IQueryable GetTestDataSource() +// { +// return new List() +// { +// new() { FavouriteColorList = ["Green", "Blue"] }, +// new() { FavouriteColorList = ["White", "Yellow"] }, +// new() { FavouriteColorList = ["Red", "Orange"] }, +// new() { FavouriteColorList = ["Purple", "Pink"] }, +// new() { FavouriteColorList = ["Black", "Gray"] } +// }.AsQueryable(); +// } +// } +// }