From 116795e30be2f8fd518a44b8fc02edddde95e728 Mon Sep 17 00:00:00 2001 From: AliReZa Sabouri <7004080+alirezanet@users.noreply.github.com> Date: Tue, 21 Sep 2021 13:25:52 +0430 Subject: [PATCH] v2.1.0 (#22) Add support Case-Insensitive search using `/i` operator. #21 --- .github/workflows/publush.yml | 31 ++++++ .../workflows/{dotnet.yml => pullRequest.yml} | 6 +- README.md | 22 +++- .../Gridify.EntityFramework.csproj | 4 +- src/Gridify/Gridify.csproj | 4 +- src/Gridify/Syntax/Lexer.cs | 6 +- src/Gridify/Syntax/Parser.cs | 10 +- src/Gridify/Syntax/SyntaxKind.cs | 3 +- .../Syntax/SyntaxTreeToQueryConvertor.cs | 79 ++++++++------ src/Gridify/Syntax/ValueExpressionSyntax.cs | 4 +- .../EntityFramework6Tests.cs | 2 +- test/Gridify.Tests/GridifyExtensionsShould.cs | 103 +++++++++++++----- 12 files changed, 192 insertions(+), 82 deletions(-) create mode 100644 .github/workflows/publush.yml rename .github/workflows/{dotnet.yml => pullRequest.yml} (82%) diff --git a/.github/workflows/publush.yml b/.github/workflows/publush.yml new file mode 100644 index 00000000..f8ffda1d --- /dev/null +++ b/.github/workflows/publush.yml @@ -0,0 +1,31 @@ +#https://lukelowrey.com/use-github-actions-to-publish-nuget-packages/ +name: Publish Packages + +on: + push: + branches: [ master ] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 5.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --configuration Release --no-restore + - name: Test + run: dotnet test --configuration Release --no-build --verbosity normal + - name: Publish Gridify + uses: brandedoutcast/publish-nuget@v2.5.2 + with: + PROJECT_FILE_PATH: src/Gridify/Gridify.csproj + NUGET_KEY: ${{secrets.NUGET_API_KEY}} + - name: Publish Gridify.EntityFramework + uses: brandedoutcast/publish-nuget@v2.5.2 + with: + PROJECT_FILE_PATH: src/Gridify.EntityFramework/Gridify.EntityFramework.csproj + NUGET_KEY: ${{secrets.NUGET_API_KEY}} \ No newline at end of file diff --git a/.github/workflows/dotnet.yml b/.github/workflows/pullRequest.yml similarity index 82% rename from .github/workflows/dotnet.yml rename to .github/workflows/pullRequest.yml index 47ebafb8..388760b7 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/pullRequest.yml @@ -1,10 +1,8 @@ -name: .NET +name: Build Pull Requests on: - push: - branches: [ master , version2] pull_request: - branches: [ master , version2] + branches: [ master ] jobs: build: diff --git a/README.md b/README.md index 80303145..3dc0d6ee 100644 --- a/README.md +++ b/README.md @@ -165,22 +165,32 @@ But for example, if you need to just filter your data without paging or sorting We can easily create complex queries using parenthesis`()` with AND (`,`) + OR (`|`) operators. -**Escape character hint**: +--- +## Case-Insensitive search + +The **'/i'** operator can be use after string values for case insensitive search: +```c# +var gq = new GridifyQuery() { Filter = "FirstName=John/i" }; +// this is matched by => JOHN - john - John - jOHn - ... +``` +--- + +## Escape character -Filtering has four special character `, | ( )` to handle complex queries. If you want to use these characters in your query values (after `=`), you should add a backslash \ before them. +Filtering has five special character `, | ( ) /i` to handle complex queries and case-insensitive search. If you want to use these characters in your query values (after operator), you should add a backslash \ before them. having bellow regex could be helpfull `([(),|]|\/i)`. JavaScript escape example: ```javascript -let esc = (v) => v.replace(/([(),|])/g, '\\$1') +let esc = (v) => v.replace(/([(),|]|\/i)/g, '\\$1') ``` Csharp escape example: ```csharp var value = "(test,test2)"; -var esc = Regex.Replace(value, "([(),|])", "\\$1" ); // esc = \(test\,test2\) +var esc = Regex.Replace(value, "([(),|]|\/i)", "\\$1" ); // esc = \(test\,test2\) ``` - + --- - + ## Multiple OrderBy OrderBy accepts comma-separated field names followed by `asc` or `desc` keyword. by default, if you don't add these keywords, diff --git a/src/Gridify.EntityFramework/Gridify.EntityFramework.csproj b/src/Gridify.EntityFramework/Gridify.EntityFramework.csproj index 9b0e3165..a533edaa 100644 --- a/src/Gridify.EntityFramework/Gridify.EntityFramework.csproj +++ b/src/Gridify.EntityFramework/Gridify.EntityFramework.csproj @@ -9,11 +9,11 @@ netstandard2.0 Gridify.EntityFramework - 2.0.0 + 2.1.0 Alireza Sabouri TuxTeam Gridify (EntityFramework), Easy and optimized way to apply Filtering, Sorting, and Pagination using text-based data. - https://github.com/alirezanet/Gridify/tree/version2 + https://github.com/alirezanet/Gridify MIT true diff --git a/src/Gridify/Gridify.csproj b/src/Gridify/Gridify.csproj index 762db467..00092876 100644 --- a/src/Gridify/Gridify.csproj +++ b/src/Gridify/Gridify.csproj @@ -3,11 +3,11 @@ netstandard2.0 Gridify - 2.0.0 + 2.1.0 Alireza Sabouri TuxTeam Gridify, Easy and optimized way to apply Filtering, Sorting, and Pagination using text-based data. - https://github.com/alirezanet/Gridify/tree/version2 + https://github.com/alirezanet/Gridify MIT latest true diff --git a/src/Gridify/Syntax/Lexer.cs b/src/Gridify/Syntax/Lexer.cs index 7b843cc0..a7259a6a 100644 --- a/src/Gridify/Syntax/Lexer.cs +++ b/src/Gridify/Syntax/Lexer.cs @@ -61,6 +61,8 @@ public SyntaxToken NextToken() return new SyntaxToken(SyntaxKind.NotEqual, _position += 2, "!="); case '!' when peek == '*': return new SyntaxToken(SyntaxKind.NotLike, _position += 2, "!*"); + case '/' when peek == 'i': + return new SyntaxToken(SyntaxKind.CaseInsensitive, _position += 2, "/i"); case '<': return peek == '=' ? new SyntaxToken(SyntaxKind.LessOrEqualThan, _position += 2, "<=") : new SyntaxToken(SyntaxKind.LessThan, _position++, "<"); @@ -103,7 +105,9 @@ public SyntaxToken NextToken() var exitCharacters = new[] {'(', ')', ',', '|'}; var lastChar = '\0'; - while ((!exitCharacters.Contains(Current) || exitCharacters.Contains(Current) && lastChar == '\\') && _position < _text.Length) + while ((!exitCharacters.Contains(Current) || exitCharacters.Contains(Current) && lastChar == '\\') && + _position < _text.Length && + (!(Current == '/' && Peek(1) == 'i') || (Current == '/' && Peek(1) == 'i') && lastChar == '\\')) // exit on case-insensitive operator { lastChar = Current; Next(); diff --git a/src/Gridify/Syntax/Parser.cs b/src/Gridify/Syntax/Parser.cs index 8980dc0b..a0451ac6 100644 --- a/src/Gridify/Syntax/Parser.cs +++ b/src/Gridify/Syntax/Parser.cs @@ -90,7 +90,8 @@ private ExpressionSyntax ParseFactor() private ExpressionSyntax ParseValueExpression() { var valueToken = Match(SyntaxKind.ValueToken); - return new ValueExpressionSyntax(valueToken); + var isCaseInsensitive = IsMatch(SyntaxKind.CaseInsensitive); + return new ValueExpressionSyntax(valueToken, isCaseInsensitive); } private SyntaxToken NextToken() @@ -100,6 +101,13 @@ private SyntaxToken NextToken() return current; } + private bool IsMatch(SyntaxKind kind) + { + if (Current.Kind != kind) return false; + NextToken(); + return true; + } + private SyntaxToken Match(SyntaxKind kind) { if (Current.Kind == kind) diff --git a/src/Gridify/Syntax/SyntaxKind.cs b/src/Gridify/Syntax/SyntaxKind.cs index 530cb9b8..ad3d09c5 100644 --- a/src/Gridify/Syntax/SyntaxKind.cs +++ b/src/Gridify/Syntax/SyntaxKind.cs @@ -30,6 +30,7 @@ public enum SyntaxKind ValueToken, ParenthesizedExpression, NotStartsWith, - NotEndsWith + NotEndsWith, + CaseInsensitive } } \ No newline at end of file diff --git a/src/Gridify/Syntax/SyntaxTreeToQueryConvertor.cs b/src/Gridify/Syntax/SyntaxTreeToQueryConvertor.cs index 6550d65e..978e7890 100644 --- a/src/Gridify/Syntax/SyntaxTreeToQueryConvertor.cs +++ b/src/Gridify/Syntax/SyntaxTreeToQueryConvertor.cs @@ -15,7 +15,7 @@ private static (Expression> Expression, bool IsNested)? ConvertBin BinaryExpressionSyntax binarySyntax, IGridifyMapper mapper) { var left = (binarySyntax.Left as FieldExpressionSyntax)?.FieldToken.Text.Trim(); - var right = (binarySyntax.Right as ValueExpressionSyntax)?.ValueToken.Text; + var right = (binarySyntax.Right as ValueExpressionSyntax); var op = binarySyntax.OperatorToken; if (left == null || right == null) return null; @@ -41,7 +41,7 @@ private static (Expression> Expression, bool IsNested)? ConvertBin private static Expression>? GenerateNestedExpression( IGridifyMapper mapper, IGMap gMap, - string stringValue, + ValueExpressionSyntax value, SyntaxNode op) { var body = gMap.To.Body; @@ -49,7 +49,7 @@ private static (Expression> Expression, bool IsNested)? ConvertBin if (body is MethodCallExpression selectExp && selectExp.Method.Name == "Select") { var targetExp = selectExp.Arguments.Single(a => a.NodeType == ExpressionType.Lambda) as LambdaExpression; - var conditionExp = GenerateExpression(targetExp!.Body, targetExp.Parameters[0], stringValue, op, mapper.Configuration.AllowNullSearch, + var conditionExp = GenerateExpression(targetExp!.Body, targetExp.Parameters[0], value, op, mapper.Configuration.AllowNullSearch, gMap.Convertor); if (conditionExp == null) return null; @@ -68,13 +68,15 @@ private static LambdaExpression ParseMethodCallExpression(MethodCallExpression e case MemberExpression member: return GetAnyExpression(member, predicate); case MethodCallExpression subExp when subExp.Method.Name == "SelectMany" && - subExp.Arguments.Last() is LambdaExpression {Body: MemberExpression lambdaMember}: + subExp.Arguments.Last() is LambdaExpression { Body: MemberExpression lambdaMember }: { var newPredicate = GetAnyExpression(lambdaMember, predicate); return ParseMethodCallExpression(subExp, newPredicate); } case MethodCallExpression subExp when subExp.Method.Name == "Select" && subExp.Arguments.Last() is LambdaExpression - {Body: MemberExpression lambdaMember} lambda: + { + Body: MemberExpression lambdaMember + } lambda: { var newExp = new PredicateBuilder.ReplaceExpressionVisitor(predicate.Parameters[0], lambdaMember).Visit(predicate.Body); var newPredicate = GetExpressionWithNullCheck(lambdaMember, lambda.Parameters[0], newExp!); @@ -112,39 +114,45 @@ private static LambdaExpression GetExpressionWithNullCheck(MemberExpression prop private static LambdaExpression? GenerateExpression( Expression body, ParameterExpression parameter, - string stringValue, + ValueExpressionSyntax valueExpression, SyntaxNode op, bool allowNullSearch, Func? convertor) { // Remove the boxing for value types - if (body.NodeType == ExpressionType.Convert) body = ((UnaryExpression) body).Operand; + if (body.NodeType == ExpressionType.Convert) body = ((UnaryExpression)body).Operand; - object? value = stringValue; + object? value = valueExpression.ValueToken.Text; // execute user custom Convertor if (convertor != null) - value = convertor.Invoke(stringValue); + value = convertor.Invoke(valueExpression.ValueToken.Text) ?? null; + + if (allowNullSearch && op.Kind is SyntaxKind.Equal or SyntaxKind.NotEqual && value?.ToString() == "null") + value = null; - if (value != null && body.Type != value.GetType()) + // type fixer + if (value is not null && body.Type != value.GetType()) + { try { - if (allowNullSearch && op.Kind is SyntaxKind.Equal or SyntaxKind.NotEqual && value.ToString() == "null") - value = null; - else - { - // handle broken guids, github issue #2 - if (body.Type == typeof(Guid) && !Guid.TryParse(value.ToString(), out _)) value = Guid.NewGuid().ToString(); + // handle broken guids, github issue #2 + if (body.Type == typeof(Guid) && !Guid.TryParse(value.ToString(), out _)) value = Guid.NewGuid().ToString(); - var converter = TypeDescriptor.GetConverter(body.Type); - value = converter.ConvertFromString(value.ToString())!; - } + var converter = TypeDescriptor.GetConverter(body.Type); + value = converter.ConvertFromString(value.ToString())!; } catch (FormatException) { // return no records in case of any exception in formatting return Expression.Lambda(Expression.Constant(false), parameter); // q => false } + } + else if (valueExpression.IsCaseInsensitive) + { + value = value?.ToString().ToLower(); + body = Expression.Call(body, GetToLowerMethod()); + } Expression be; @@ -224,16 +232,18 @@ private static LambdaExpression GetExpressionWithNullCheck(MemberExpression prop private static MethodInfo GetAnyMethod(Type @type) => typeof(Enumerable).GetMethods().Single(m => m.Name == "Any" && m.GetParameters().Length == 2).MakeGenericMethod(@type); - private static MethodInfo GetEndsWithMethod() => typeof(string).GetMethod("EndsWith", new[] {typeof(string)})!; + private static MethodInfo GetEndsWithMethod() => typeof(string).GetMethod("EndsWith", new[] { typeof(string) })!; - private static MethodInfo GetStartWithMethod() => typeof(string).GetMethod("StartsWith", new[] {typeof(string)})!; + private static MethodInfo GetStartWithMethod() => typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!; - private static MethodInfo GetContainsMethod() => typeof(string).GetMethod("Contains", new[] {typeof(string)})!; + private static MethodInfo GetContainsMethod() => typeof(string).GetMethod("Contains", new[] { typeof(string) })!; + private static MethodInfo GetToLowerMethod() => typeof(string).GetMethod("ToLower", new Type[] { })!; private static MethodInfo GetToStringMethod() => typeof(object).GetMethod("ToString")!; + internal static (Expression> Expression, bool IsNested) - GenerateQuery(ExpressionSyntax expression, IGridifyMapper mapper,bool isParenthesisOpen=false) + GenerateQuery(ExpressionSyntax expression, IGridifyMapper mapper, bool isParenthesisOpen = false) { while (true) switch (expression.Kind) @@ -245,9 +255,9 @@ internal static (Expression> Expression, bool IsNested) if (bExp!.Left is FieldExpressionSyntax && bExp.Right is ValueExpressionSyntax) return ConvertBinaryExpressionSyntaxToQuery(bExp, mapper) ?? throw new GridifyFilteringException("Invalid expression"); - (Expression> exp,bool isNested) leftQuery; - (Expression> exp,bool isNested) rightQuery; - + (Expression> exp, bool isNested) leftQuery; + (Expression> exp, bool isNested) rightQuery; + if (bExp.Left is ParenthesizedExpressionSyntax lpExp) { leftQuery = GenerateQuery(lpExp.Expression, mapper, true); @@ -257,14 +267,14 @@ internal static (Expression> Expression, bool IsNested) if (bExp.Right is ParenthesizedExpressionSyntax rpExp) - rightQuery = GenerateQuery(rpExp.Expression, mapper,true); + rightQuery = GenerateQuery(rpExp.Expression, mapper, true); else rightQuery = GenerateQuery(bExp.Right, mapper); // check for nested collections if (isParenthesisOpen && - CheckIfCanMerge(leftQuery, rightQuery,bExp.OperatorToken.Kind) is Expression> mergedResult) - return (mergedResult,true); + CheckIfCanMerge(leftQuery, rightQuery, bExp.OperatorToken.Kind) is Expression> mergedResult) + return (mergedResult, true); var result = bExp.OperatorToken.Kind switch { @@ -285,7 +295,7 @@ internal static (Expression> Expression, bool IsNested) } private static LambdaExpression? CheckIfCanMerge((Expression> exp, bool isNested) leftQuery, - (Expression> exp, bool isNested) rightQuery,SyntaxKind op) + (Expression> exp, bool isNested) rightQuery, SyntaxKind op) { if (leftQuery.isNested && rightQuery.isNested) { @@ -303,7 +313,7 @@ internal static (Expression> Expression, bool IsNested) if (leftLambda is null || rightLambda is null) return null; - var visitedRight= new PredicateBuilder.ReplaceExpressionVisitor(rightLambda.Parameters[0], leftLambda.Parameters[0]) + var visitedRight = new PredicateBuilder.ReplaceExpressionVisitor(rightLambda.Parameters[0], leftLambda.Parameters[0]) .Visit(rightLambda.Body); var mergedExpression = op switch @@ -312,12 +322,13 @@ internal static (Expression> Expression, bool IsNested) SyntaxKind.Or => Expression.OrElse(leftLambda.Body, visitedRight), _ => throw new InvalidOperationException() }; - + var mergedLambda = Expression.Lambda(mergedExpression, leftLambda.Parameters); var newLambda = GetAnyExpression(leftMember, mergedLambda) as Expression>; return newLambda; } } + return null; } @@ -325,12 +336,10 @@ private static MethodCallExpression ParseNestedExpression(Expression exp) { return exp switch { - BinaryExpression {Right: MethodCallExpression cExp} => cExp, + BinaryExpression { Right: MethodCallExpression cExp } => cExp, MethodCallExpression mcExp => mcExp, _ => throw new InvalidExpressionException() }; } - - } } \ No newline at end of file diff --git a/src/Gridify/Syntax/ValueExpressionSyntax.cs b/src/Gridify/Syntax/ValueExpressionSyntax.cs index 709a9887..9abaf3a7 100644 --- a/src/Gridify/Syntax/ValueExpressionSyntax.cs +++ b/src/Gridify/Syntax/ValueExpressionSyntax.cs @@ -4,9 +4,10 @@ namespace Gridify.Syntax { internal sealed class ValueExpressionSyntax : ExpressionSyntax { - public ValueExpressionSyntax(SyntaxToken valueToken) + public ValueExpressionSyntax(SyntaxToken valueToken, bool isCaseInsensitive) { ValueToken = valueToken; + IsCaseInsensitive = isCaseInsensitive; } public override SyntaxKind Kind => SyntaxKind.ValueExpression; @@ -17,5 +18,6 @@ public override IEnumerable GetChildren() } public SyntaxToken ValueToken { get; } + public bool IsCaseInsensitive { get; } } } \ No newline at end of file diff --git a/test/EntityFramework6IntegrationTests.cs/EntityFramework6Tests.cs b/test/EntityFramework6IntegrationTests.cs/EntityFramework6Tests.cs index ad350290..bc68d9ce 100644 --- a/test/EntityFramework6IntegrationTests.cs/EntityFramework6Tests.cs +++ b/test/EntityFramework6IntegrationTests.cs/EntityFramework6Tests.cs @@ -55,7 +55,7 @@ public void EntityFramework6_FilteringAndOrdering() // Issue #8 using (var context = new EntityContext(connection)) { var gm = new GridifyQuery() { OrderBy = "name desc" , Filter = "id > 3"}; - var expected = context.Customers.ApplyFilterAndOrdering(gm).ToList(); + var expected = context.Customers.ApplyFilteringAndOrdering(gm).ToList(); var actual = context.Customers .Where(q=>q.Id > 3) .OrderByDescending(q => q.Name) diff --git a/test/Gridify.Tests/GridifyExtensionsShould.cs b/test/Gridify.Tests/GridifyExtensionsShould.cs index b7ab8136..27da189f 100644 --- a/test/Gridify.Tests/GridifyExtensionsShould.cs +++ b/test/Gridify.Tests/GridifyExtensionsShould.cs @@ -34,7 +34,7 @@ public void ApplyFiltering_SingleField() [Fact] public void ApplyFiltering_SingleField_GridifyQuery() { - var gq = new GridifyQuery { Filter = "name=John" }; + var gq = new GridifyQuery {Filter = "name=John"}; var actual = _fakeRepository.AsQueryable() .ApplyFiltering(gq) .ToList(); @@ -62,6 +62,7 @@ public void ApplyFiltering_NullHandlingUsingCustomConvertor() Assert.Equal(expected, actual); Assert.True(actual.Any()); } + [Fact] public void ApplyFiltering_NullHandlingUsingMapper() { @@ -78,7 +79,7 @@ public void ApplyFiltering_NullHandlingUsingMapper() Assert.Equal(expected, actual); Assert.True(actual.Any()); } - + [Fact] public void ApplyFiltering_DisableNullHandlingUsingMapper() { @@ -116,7 +117,7 @@ public void ApplyFiltering_DuplicateFiledName() [InlineData(@" name =LI \| AM", @"LI | AM")] public void ApplyFiltering_EscapeSpecialCharacters(string textFilter, string rawText) { - var gq = new GridifyQuery { Filter = textFilter }; + var gq = new GridifyQuery {Filter = textFilter}; var actual = _fakeRepository.AsQueryable() .ApplyFiltering(gq) .ToList(); @@ -129,7 +130,7 @@ public void ApplyFiltering_EscapeSpecialCharacters(string textFilter, string raw [Fact] public void ApplyFiltering_ParenthesisQueryWithoutEscapeShouldThrowException() { - var gq = new GridifyQuery { Filter = @"name=(LI,AM)" }; + var gq = new GridifyQuery {Filter = @"name=(LI,AM)"}; Action act = () => _fakeRepository.AsQueryable() .ApplyFiltering(gq); @@ -142,7 +143,7 @@ public void ApplyFiltering_SingleGuidField() { var guidString = "e2cec5dd-208d-4bb5-a852-50008f8ba366"; var guid = Guid.Parse(guidString); - var gq = new GridifyQuery { Filter = "myGuid=" + guidString }; + var gq = new GridifyQuery {Filter = "myGuid=" + guidString}; var actual = _fakeRepository.AsQueryable() .ApplyFiltering(gq) .ToList(); @@ -156,7 +157,7 @@ public void ApplyFiltering_SingleGuidField() public void ApplyFiltering_SingleBrokenGuidField() { var brokenGuidString = "e2cec5dd-208d-4bb5-a852-"; - var gq = new GridifyQuery { Filter = "myGuid=" + brokenGuidString }; + var gq = new GridifyQuery {Filter = "myGuid=" + brokenGuidString}; var actual = _fakeRepository.AsQueryable() .ApplyFiltering(gq) @@ -170,7 +171,7 @@ public void ApplyFiltering_SingleBrokenGuidField() public void ApplyFiltering_SingleBrokenGuidField_NotEqual() { var brokenGuidString = "e2cec5dd-208d-4bb5-a852-"; - var gq = new GridifyQuery { Filter = "myGuid!=" + brokenGuidString }; + var gq = new GridifyQuery {Filter = "myGuid!=" + brokenGuidString}; var actual = _fakeRepository.AsQueryable() .ApplyFiltering(gq) @@ -183,7 +184,7 @@ public void ApplyFiltering_SingleBrokenGuidField_NotEqual() [Fact] public void ApplyFiltering_InvalidFilterExpressionShouldThrowException() { - var gq = new GridifyQuery { Filter = "=guid,d=" }; + var gq = new GridifyQuery {Filter = "=guid,d="}; Assert.Throws(() => _fakeRepository.AsQueryable().ApplyFiltering(gq).ToList()); } @@ -191,7 +192,7 @@ public void ApplyFiltering_InvalidFilterExpressionShouldThrowException() [Fact] public void ApplyFiltering_InvalidCharacterShouldThrowException() { - var gq = new GridifyQuery { Filter = "@name=ali" }; + var gq = new GridifyQuery {Filter = "@name=ali"}; Assert.Throws(() => _fakeRepository.AsQueryable().ApplyFiltering(gq).ToList()); } @@ -359,7 +360,7 @@ public void ApplyFiltering_ComplexWithParenthesis() public void ApplyFiltering_NestedParenthesisWithSpace() { // we shouldn't add spaces for values - var gq = new GridifyQuery { Filter = " ( name =*J| ( name =*S , Id <5 ) )" }; + var gq = new GridifyQuery {Filter = " ( name =*J| ( name =*S , Id <5 ) )"}; var actual = _fakeRepository.AsQueryable() .ApplyFiltering(gq) .ToList(); @@ -372,7 +373,7 @@ public void ApplyFiltering_NestedParenthesisWithSpace() [Fact] public void ApplyFiltering_UsingChildClassProperty() { - var gq = new GridifyQuery { Filter = "Child_Name=Bob" }; + var gq = new GridifyQuery {Filter = "Child_Name=Bob"}; var gm = new GridifyMapper() .GenerateMappings() .AddMap("Child_name", q => q.ChildClass!.Name); @@ -382,7 +383,52 @@ public void ApplyFiltering_UsingChildClassProperty() .ApplyFiltering(gq, gm) .ToList(); - var expected = _fakeRepository.Where(q => q.ChildClass is { Name: "Bob" }).ToList(); + var expected = _fakeRepository.Where(q => q.ChildClass is {Name: "Bob"}).ToList(); + Assert.Equal(expected.Count, actual.Count); + Assert.Equal(expected, actual); + Assert.True(actual.Any()); + } + + [Fact] + public void ApplyFiltering_CaseInsensitiveSearchUsingConvertor() //issue #21 + { + var gq = new GridifyQuery { Filter = "name=BOB" }; + var gm = new GridifyMapper() + .AddMap("name", q => q.Name.ToLower() , c => c.ToLower()); + + var actual = _fakeRepository.AsQueryable() + .ApplyFiltering(gq, gm) + .ToList(); + + var expected = _fakeRepository.Where(q => q.Name.ToLower() == "BOB".ToLower()).ToList(); + Assert.Equal(expected.Count, actual.Count); + Assert.Equal(expected, actual); + Assert.True(actual.Any()); + } + + [Fact] + public void ApplyFiltering_CaseInsensitiveSearch() //issue #21 + { + var gq = new GridifyQuery { Filter = "name=BOB/i " }; + + var actual = _fakeRepository.AsQueryable() + .ApplyFiltering(gq) + .ToList(); + + var expected = _fakeRepository.Where(q => q.Name.ToLower() == "BOB".ToLower()).ToList(); + Assert.Equal(expected.Count, actual.Count); + Assert.Equal(expected, actual); + Assert.True(actual.Any()); + } + + [Fact] + public void ApplyFiltering_EscapeCaseInsensitiveSearch() //issue #21 + { + var actual = _fakeRepository.AsQueryable() + .ApplyFiltering(@"name=Case\/i" ) + .ToList(); + + var expected = _fakeRepository.Where(q => q.Name == "Case/i").ToList(); Assert.Equal(expected.Count, actual.Count); Assert.Equal(expected, actual); Assert.True(actual.Any()); @@ -395,7 +441,7 @@ public void ApplyFiltering_UsingChildClassProperty() [Fact] public void ApplyOrdering_OrderBy_Ascending() { - var gq = new GridifyQuery { OrderBy = "name" }; + var gq = new GridifyQuery {OrderBy = "name"}; var actual = _fakeRepository.AsQueryable() .ApplyOrdering(gq) .ToList(); @@ -406,7 +452,7 @@ public void ApplyOrdering_OrderBy_Ascending() [Fact] public void ApplyOrdering_OrderBy_DateTime() { - var gq = new GridifyQuery { OrderBy = "MyDateTime" }; + var gq = new GridifyQuery {OrderBy = "MyDateTime"}; var actual = _fakeRepository.AsQueryable() .ApplyOrdering(gq) .ToList(); @@ -419,7 +465,7 @@ public void ApplyOrdering_OrderBy_DateTime() [Fact] public void ApplyOrdering_OrderBy_Descending() { - var gq = new GridifyQuery { OrderBy = "Name desc" }; + var gq = new GridifyQuery {OrderBy = "Name desc"}; var actual = _fakeRepository.AsQueryable() .ApplyOrdering(gq) .ToList(); @@ -432,7 +478,7 @@ public void ApplyOrdering_OrderBy_Descending() [Fact] public void ApplyOrdering_MultipleOrderBy() { - var gq = new GridifyQuery { OrderBy = "MyDateTime desc , id , name asc" }; + var gq = new GridifyQuery {OrderBy = "MyDateTime desc , id , name asc"}; var actual = _fakeRepository.AsQueryable() .ApplyOrdering(gq) .ToList(); @@ -451,7 +497,7 @@ public void ApplyOrdering_MultipleOrderBy() [Fact] public void ApplyOrdering_SortUsingChildClassProperty() { - var gq = new GridifyQuery { OrderBy = "Child_Name desc" }; + var gq = new GridifyQuery {OrderBy = "Child_Name desc"}; var gm = new GridifyMapper() .GenerateMappings() .AddMap("Child_Name", q => q.ChildClass!.Name); @@ -517,7 +563,7 @@ public void ApplyPaging_UsingDefaultValues() [InlineData(20, 10)] public void ApplyPaging_UsingCustomValues(short page, int pageSize) { - var gq = new GridifyQuery { Page = page, PageSize = pageSize }; + var gq = new GridifyQuery {Page = page, PageSize = pageSize}; var actual = _fakeRepository.AsQueryable() .ApplyPaging(gq) .ToList(); @@ -548,7 +594,7 @@ public void Gridify_ActionOverload() var query = _fakeRepository.AsQueryable().Where(q => q.Name == "John").OrderByDescending(q => q.Name); var totalItems = query.Count(); var items = query.Skip(-2).Take(15).ToList(); - var expected = new Paging() { Data = items, Count = totalItems }; + var expected = new Paging() {Data = items, Count = totalItems}; Assert.Equal(expected.Count, actual.Count); Assert.Equal(expected.Data.Count(), actual.Data.Count()); @@ -567,7 +613,7 @@ public void Gridify_ActionOverload() public void ApplyOrderingAndPaging_UsingCustomValues(short page, int pageSize, bool isSortAsc) { var orderByExp = "name " + (isSortAsc ? "asc" : "desc"); - var gq = new GridifyQuery { Page = page, PageSize = pageSize, OrderBy = orderByExp }; + var gq = new GridifyQuery {Page = page, PageSize = pageSize, OrderBy = orderByExp}; // actual var actual = _fakeRepository.AsQueryable() .ApplyOrderingAndPaging(gq) @@ -596,21 +642,21 @@ private static IEnumerable GetSampleData() var lst = new List(); lst.Add(new TestClass(1, "John", null, Guid.NewGuid(), DateTime.Now)); lst.Add(new TestClass(2, "Bob", null, Guid.NewGuid(), DateTime.UtcNow)); - lst.Add(new TestClass(3, "Jack", (TestClass)lst[0].Clone(), Guid.Empty, DateTime.Now.AddDays(2))); + lst.Add(new TestClass(3, "Jack", (TestClass) lst[0].Clone(), Guid.Empty, DateTime.Now.AddDays(2))); lst.Add(new TestClass(4, "Rose", null, Guid.Parse("e2cec5dd-208d-4bb5-a852-50008f8ba366"))); lst.Add(new TestClass(5, "Ali", null)); - lst.Add(new TestClass(6, "Hamid", (TestClass)lst[0].Clone(), Guid.Parse("de12bae1-93fa-40e4-92d1-2e60f95b468c"))); - lst.Add(new TestClass(7, "Hasan", (TestClass)lst[1].Clone())); - lst.Add(new TestClass(8, "Farhad", (TestClass)lst[2].Clone(), Guid.Empty)); + lst.Add(new TestClass(6, "Hamid", (TestClass) lst[0].Clone(), Guid.Parse("de12bae1-93fa-40e4-92d1-2e60f95b468c"))); + lst.Add(new TestClass(7, "Hasan", (TestClass) lst[1].Clone())); + lst.Add(new TestClass(8, "Farhad", (TestClass) lst[2].Clone(), Guid.Empty)); lst.Add(new TestClass(9, "Sara", null)); lst.Add(new TestClass(10, "Jorge", null)); lst.Add(new TestClass(11, "joe", null)); - lst.Add(new TestClass(12, "jimmy", (TestClass)lst[0].Clone())); + lst.Add(new TestClass(12, "jimmy", (TestClass) lst[0].Clone())); lst.Add(new TestClass(13, "Nazanin", null)); lst.Add(new TestClass(14, "Reza", null)); - lst.Add(new TestClass(15, "Korosh", (TestClass)lst[0].Clone())); - lst.Add(new TestClass(16, "Kamran", (TestClass)lst[1].Clone())); - lst.Add(new TestClass(17, "Saeid", (TestClass)lst[2].Clone())); + lst.Add(new TestClass(15, "Korosh", (TestClass) lst[0].Clone())); + lst.Add(new TestClass(16, "Kamran", (TestClass) lst[1].Clone())); + lst.Add(new TestClass(17, "Saeid", (TestClass) lst[2].Clone())); lst.Add(new TestClass(18, "jessi=ca", null)); lst.Add(new TestClass(19, "Ped=ram", null)); lst.Add(new TestClass(20, "Peyman!", null)); @@ -619,6 +665,7 @@ private static IEnumerable GetSampleData() lst.Add(new TestClass(22, @"\Liam", null)); lst.Add(new TestClass(23, "LI | AM", null)); lst.Add(new TestClass(24, "(LI,AM)", null)); + lst.Add(new TestClass(25, "Case/i", null)); return lst; }