Skip to content

Commit

Permalink
v2.1.0 (#22)
Browse files Browse the repository at this point in the history
Add support Case-Insensitive search using `/i` operator. #21
  • Loading branch information
alirezanet authored Sep 21, 2021
1 parent 2c5bfb8 commit 116795e
Show file tree
Hide file tree
Showing 12 changed files with 192 additions and 82 deletions.
31 changes: 31 additions & 0 deletions .github/workflows/publush.yml
Original file line number Diff line number Diff line change
@@ -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/[email protected]
with:
PROJECT_FILE_PATH: src/Gridify/Gridify.csproj
NUGET_KEY: ${{secrets.NUGET_API_KEY}}
- name: Publish Gridify.EntityFramework
uses: brandedoutcast/[email protected]
with:
PROJECT_FILE_PATH: src/Gridify.EntityFramework/Gridify.EntityFramework.csproj
NUGET_KEY: ${{secrets.NUGET_API_KEY}}
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
name: .NET
name: Build Pull Requests

on:
push:
branches: [ master , version2]
pull_request:
branches: [ master , version2]
branches: [ master ]

jobs:
build:
Expand Down
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>\ </code> 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 <code>\ </code> 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,
Expand Down
4 changes: 2 additions & 2 deletions src/Gridify.EntityFramework/Gridify.EntityFramework.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageId>Gridify.EntityFramework</PackageId>
<Version>2.0.0</Version>
<Version>2.1.0</Version>
<Authors>Alireza Sabouri</Authors>
<Company>TuxTeam</Company>
<PackageDescription>Gridify (EntityFramework), Easy and optimized way to apply Filtering, Sorting, and Pagination using text-based data.</PackageDescription>
<RepositoryUrl>https://github.com/alirezanet/Gridify/tree/version2</RepositoryUrl>
<RepositoryUrl>https://github.com/alirezanet/Gridify</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
Expand Down
4 changes: 2 additions & 2 deletions src/Gridify/Gridify.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageId>Gridify</PackageId>
<Version>2.0.0</Version>
<Version>2.1.0</Version>
<Authors>Alireza Sabouri</Authors>
<Company>TuxTeam</Company>
<PackageDescription>Gridify, Easy and optimized way to apply Filtering, Sorting, and Pagination using text-based data.</PackageDescription>
<RepositoryUrl>https://github.com/alirezanet/Gridify/tree/version2</RepositoryUrl>
<RepositoryUrl>https://github.com/alirezanet/Gridify</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<LangVersion>latest</LangVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
Expand Down
6 changes: 5 additions & 1 deletion src/Gridify/Syntax/Lexer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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++, "<");
Expand Down Expand Up @@ -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();
Expand Down
10 changes: 9 additions & 1 deletion src/Gridify/Syntax/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion src/Gridify/Syntax/SyntaxKind.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public enum SyntaxKind
ValueToken,
ParenthesizedExpression,
NotStartsWith,
NotEndsWith
NotEndsWith,
CaseInsensitive
}
}
79 changes: 44 additions & 35 deletions src/Gridify/Syntax/SyntaxTreeToQueryConvertor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ private static (Expression<Func<T, bool>> Expression, bool IsNested)? ConvertBin
BinaryExpressionSyntax binarySyntax, IGridifyMapper<T> 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;
Expand All @@ -41,15 +41,15 @@ private static (Expression<Func<T, bool>> Expression, bool IsNested)? ConvertBin
private static Expression<Func<T, bool>>? GenerateNestedExpression<T>(
IGridifyMapper<T> mapper,
IGMap<T> gMap,
string stringValue,
ValueExpressionSyntax value,
SyntaxNode op)
{
var body = gMap.To.Body;

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;
Expand All @@ -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!);
Expand Down Expand Up @@ -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<string, object>? 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;

Expand Down Expand Up @@ -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<Func<T, bool>> Expression, bool IsNested)
GenerateQuery<T>(ExpressionSyntax expression, IGridifyMapper<T> mapper,bool isParenthesisOpen=false)
GenerateQuery<T>(ExpressionSyntax expression, IGridifyMapper<T> mapper, bool isParenthesisOpen = false)
{
while (true)
switch (expression.Kind)
Expand All @@ -245,9 +255,9 @@ internal static (Expression<Func<T, bool>> Expression, bool IsNested)
if (bExp!.Left is FieldExpressionSyntax && bExp.Right is ValueExpressionSyntax)
return ConvertBinaryExpressionSyntaxToQuery(bExp, mapper) ?? throw new GridifyFilteringException("Invalid expression");

(Expression<Func<T, bool>> exp,bool isNested) leftQuery;
(Expression<Func<T, bool>> exp,bool isNested) rightQuery;
(Expression<Func<T, bool>> exp, bool isNested) leftQuery;
(Expression<Func<T, bool>> exp, bool isNested) rightQuery;

if (bExp.Left is ParenthesizedExpressionSyntax lpExp)
{
leftQuery = GenerateQuery(lpExp.Expression, mapper, true);
Expand All @@ -257,14 +267,14 @@ internal static (Expression<Func<T, bool>> 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<Func<T, bool>> mergedResult)
return (mergedResult,true);
CheckIfCanMerge(leftQuery, rightQuery, bExp.OperatorToken.Kind) is Expression<Func<T, bool>> mergedResult)
return (mergedResult, true);

var result = bExp.OperatorToken.Kind switch
{
Expand All @@ -285,7 +295,7 @@ internal static (Expression<Func<T, bool>> Expression, bool IsNested)
}

private static LambdaExpression? CheckIfCanMerge<T>((Expression<Func<T, bool>> exp, bool isNested) leftQuery,
(Expression<Func<T, bool>> exp, bool isNested) rightQuery,SyntaxKind op)
(Expression<Func<T, bool>> exp, bool isNested) rightQuery, SyntaxKind op)
{
if (leftQuery.isNested && rightQuery.isNested)
{
Expand All @@ -303,7 +313,7 @@ internal static (Expression<Func<T, bool>> 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
Expand All @@ -312,25 +322,24 @@ internal static (Expression<Func<T, bool>> 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<Func<T, bool>>;
return newLambda;
}
}

return null;
}

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()
};
}


}
}
4 changes: 3 additions & 1 deletion src/Gridify/Syntax/ValueExpressionSyntax.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,5 +18,6 @@ public override IEnumerable<SyntaxNode> GetChildren()
}

public SyntaxToken ValueToken { get; }
public bool IsCaseInsensitive { get; }
}
}
Loading

0 comments on commit 116795e

Please sign in to comment.