diff --git a/Sieve/Services/SieveProcessor.cs b/Sieve/Services/SieveProcessor.cs index 5b36ea1..1bfe1b0 100644 --- a/Sieve/Services/SieveProcessor.cs +++ b/Sieve/Services/SieveProcessor.cs @@ -202,13 +202,8 @@ protected virtual IQueryable ApplyFiltering(TSieveModel model, if (filterTerm.OperatorIsCaseInsensitive && !isFilterTermValueNull) { - propertyValue = Expression.Call(propertyValue, - typeof(string).GetMethods() - .First(m => m.Name == "ToUpper" && m.GetParameters().Length == 0)); - - filterValue = Expression.Call(filterValue, - typeof(string).GetMethods() - .First(m => m.Name == "ToUpper" && m.GetParameters().Length == 0)); + propertyValue = GetStringMethodExpression(propertyValue, "ToUpper"); + filterValue = GetStringMethodExpression(filterValue, "ToUpper"); } var expression = GetExpression(filterTerm, filterValue, propertyValue); @@ -333,19 +328,30 @@ private static Expression GetExpression(TFilterTerm filterTerm, dynamic filterVa FilterOperator.LessThan => Expression.LessThan(propertyValue, filterValue), FilterOperator.GreaterThanOrEqualTo => Expression.GreaterThanOrEqual(propertyValue, filterValue), FilterOperator.LessThanOrEqualTo => Expression.LessThanOrEqual(propertyValue, filterValue), - FilterOperator.Contains => Expression.Call(propertyValue, - typeof(string).GetMethods().First(m => m.Name == "Contains" && m.GetParameters().Length == 1), - filterValue), - FilterOperator.StartsWith => Expression.Call(propertyValue, - typeof(string).GetMethods().First(m => m.Name == "StartsWith" && m.GetParameters().Length == 1), - filterValue), - FilterOperator.EndsWith => Expression.Call(propertyValue, - typeof(string).GetMethods().First(m => m.Name == "EndsWith" && m.GetParameters().Length == 1), - filterValue), + FilterOperator.Contains => GetStringMethodExpression(filterValue, propertyValue, "Contains"), + FilterOperator.StartsWith => GetStringMethodExpression(filterValue, propertyValue, "StartsWith"), + FilterOperator.EndsWith => GetStringMethodExpression(filterValue, propertyValue, "EndsWith"), _ => Expression.Equal(propertyValue, filterValue) }; } + private static Expression GetStringMethodExpression(dynamic filterValue, dynamic propertyValue, string methodName) => + propertyValue.Type == typeof(string) && filterValue.Type == typeof(string) + ? Expression.Call(propertyValue, GetStringMethod(methodName, 1), filterValue) + : Expression.Call(CallToString(propertyValue), GetStringMethod(methodName, 1), CallToString(filterValue)); + + private static Expression GetStringMethodExpression(dynamic propertyValue, string methodName) => + propertyValue.Type == typeof(string) + ? Expression.Call(propertyValue, GetStringMethod(methodName, 0)) + : Expression.Call(CallToString(propertyValue), GetStringMethod(methodName, 0)); + + private static Expression CallToString(dynamic value) => + Expression.Call(value, typeof(object).GetMethod("ToString")); + + private static MethodInfo GetStringMethod(string methodName, int paramCount) => + typeof(string).GetMethods() + .First(m => m.Name == methodName && m.GetParameters().Length == paramCount); + // Workaround to ensure that the filter value gets passed as a parameter in generated SQL from EF Core private static Expression GetClosureOverConstant(T constant, Type targetType) { diff --git a/SieveUnitTests/General.cs b/SieveUnitTests/General.cs index 886ce50..d36b7f8 100644 --- a/SieveUnitTests/General.cs +++ b/SieveUnitTests/General.cs @@ -39,7 +39,7 @@ public General(ITestOutputHelper testOutputHelper) { Id = 0, Title = "A", - LikeCount = 100, + LikeCount = 500, IsDraft = true, CategoryId = null, TopComment = new Comment { Id = 0, Text = "A1" }, @@ -254,6 +254,108 @@ public void CanFilterNullableIntsWithNotEqual() Assert.True(result.Count() == 2); Assert.True(nullableResult.Count() == 3); } + + [Theory] + [InlineData(5)] + [InlineData(0)] + public void CanFilterIntsUsingContainsOperator(int likeCount) + { + var model = new SieveModel + { + Filters = $"LikeCount@={likeCount}" + }; + + var result = _processor.Apply(model, _posts); + + Assert.True(result.Count() == 3); + } + + [Theory] + [InlineData(5)] + [InlineData(0)] + public void CanFilterIntsUsingCaseInsensitiveContainsOperator(int likeCount) + { + var model = new SieveModel + { + Filters = $"LikeCount@=*{likeCount}" + }; + + var result = _processor.Apply(model, _posts); + + Assert.True(result.Count() == 3); + } + + [Theory] + [InlineData(5)] + [InlineData(0)] + public void CanFilterIntsUsingDoesNotContainsOperator(int likeCount) + { + var model = new SieveModel + { + Filters = $"LikeCount!@={likeCount}" + }; + + var result = _processor.Apply(model, _posts); + + Assert.True(result.Count() == 2); + } + + [Theory] + [InlineData(5)] + [InlineData(0)] + public void CanFilterIntsUsingCaseInsensitiveDoesNotContainsOperator(int likeCount) + { + var model = new SieveModel + { + Filters = $"LikeCount!@=*{likeCount}" + }; + + var result = _processor.Apply(model, _posts); + + Assert.True(result.Count() == 2); + } + + [Theory] + [InlineData(5)] + public void CanFilterIntsUsingCaseInsensitiveDoesNotStartsWithOperator(int likeCount) + { + var model = new SieveModel + { + Filters = $"LikeCount!_=*{likeCount}" + }; + + var result = _processor.Apply(model, _posts); + + Assert.True(result.Count() == 2); + } + + [Theory] + [InlineData(5)] + public void CanFilterIntsUsingStartsWithOperator(int likeCount) + { + var model = new SieveModel + { + Filters = $"LikeCount_={likeCount}" + }; + + var result = _processor.Apply(model, _posts); + + Assert.True(result.Count() == 3); + } + + [Theory] + [InlineData(0)] + public void CanFilterIntsUsingEndsWithOperator(int likeCount) + { + var model = new SieveModel + { + Filters = $"LikeCount_-={likeCount}" + }; + + var result = _processor.Apply(model, _posts); + + Assert.True(result.Count() == 3); + } [Theory] [InlineData(@"Text@=*\,")]