diff --git a/src/Gridify.EntityFramework/Gridify.EntityFramework.csproj b/src/Gridify.EntityFramework/Gridify.EntityFramework.csproj index 3dae83c4..37abc4aa 100644 --- a/src/Gridify.EntityFramework/Gridify.EntityFramework.csproj +++ b/src/Gridify.EntityFramework/Gridify.EntityFramework.csproj @@ -9,7 +9,7 @@ netstandard2.0 Gridify.EntityFramework - 2.3.1 + 2.3.2 Alireza Sabouri TuxTeam Gridify (EntityFramework), Easy and optimized way to apply Filtering, Sorting, and Pagination using text-based data. diff --git a/src/Gridify/Gridify.csproj b/src/Gridify/Gridify.csproj index ce08fce2..b62cf6d6 100644 --- a/src/Gridify/Gridify.csproj +++ b/src/Gridify/Gridify.csproj @@ -3,7 +3,7 @@ netstandard2.0 Gridify - 2.3.1 + 2.3.2 Alireza Sabouri TuxTeam Gridify, Easy and optimized way to apply Filtering, Sorting, and Pagination using text-based data. diff --git a/src/Gridify/GridifyExtensions.cs b/src/Gridify/GridifyExtensions.cs index 08a20b2a..5595a78f 100644 --- a/src/Gridify/GridifyExtensions.cs +++ b/src/Gridify/GridifyExtensions.cs @@ -6,13 +6,14 @@ using Gridify.Syntax; [assembly: InternalsVisibleTo("Gridify.EntityFramework")] + namespace Gridify { public static partial class GridifyExtensions { internal static bool EntityFrameworkCompatibilityLayer { get; set; } public static int DefaultPageSize { get; set; } = 20; - + #region "Private" /// @@ -132,13 +133,37 @@ public static IQueryable ApplyOrdering(this IQueryable query, IGridifyO : ProcessOrdering(query, gridifyOrdering.OrderBy!, startWithThenBy, mapper); } + /// + /// adds Ordering to the query + /// + /// the original(target) queryable object + /// the ordering fields + /// this is an optional parameter to apply ordering using a custom mapping configuration + /// if you already have an ordering with start with ThenBy, new orderings will add on top of your orders + /// type of target entity + /// returns user query after applying Ordering + public static IQueryable ApplyOrdering(this IQueryable query, string orderBy, IGridifyMapper? mapper = null, + bool startWithThenBy = false) + { + mapper = mapper.FixMapper(); + return string.IsNullOrWhiteSpace(orderBy) + ? query + : ProcessOrdering(query, orderBy, startWithThenBy, mapper); + } + private static IQueryable ProcessOrdering(IQueryable query, string orderings, bool startWithThenBy, IGridifyMapper mapper) { var isFirst = !startWithThenBy; foreach (var (member, isAscending) in ParseOrderings(orderings)) { - // skip if there is no mappings available - if (!mapper.HasMap(member)) continue; + if (!mapper.HasMap(member)) + { + // skip if there is no mappings available + if (mapper.Configuration.IgnoreNotMappedFields) + continue; + + throw new GridifyMapperException($"Mapping '{member}' not found"); + } if (isFirst) { diff --git a/src/Gridify/GridifyMapper.cs b/src/Gridify/GridifyMapper.cs index af2f9e77..ef00cda8 100644 --- a/src/Gridify/GridifyMapper.cs +++ b/src/Gridify/GridifyMapper.cs @@ -135,7 +135,7 @@ public IEnumerable> GetCurrentMaps() /// /// Converts current mappings to a comma seperated list of map names. - /// eg, filed1,field2,field3 + /// eg, field1,field2,field3 /// /// a comma seperated string public override string ToString() => string.Join(",", _mappings.Select(q => q.From)); diff --git a/src/Gridify/GridifyMapperConfiguration.cs b/src/Gridify/GridifyMapperConfiguration.cs index 6c4054bd..e5acb279 100644 --- a/src/Gridify/GridifyMapperConfiguration.cs +++ b/src/Gridify/GridifyMapperConfiguration.cs @@ -3,6 +3,16 @@ namespace Gridify public record GridifyMapperConfiguration { public bool CaseSensitive { get; set; } + + /// + /// This option enables the 'null' keyword in filtering operations + /// public bool AllowNullSearch { get; set; } = true; + /// + /// If true, in filtering and ordering operations, + /// gridify doesn't return any exceptions when a mapping + /// is not defined for the fields + /// + public bool IgnoreNotMappedFields { get; set; } } } \ No newline at end of file diff --git a/src/Gridify/Syntax/SyntaxTreeToQueryConvertor.cs b/src/Gridify/Syntax/SyntaxTreeToQueryConvertor.cs index a500327e..fa39ddb3 100644 --- a/src/Gridify/Syntax/SyntaxTreeToQueryConvertor.cs +++ b/src/Gridify/Syntax/SyntaxTreeToQueryConvertor.cs @@ -12,9 +12,8 @@ public static class ExpressionToQueryConvertor private static (Expression> Expression, bool IsNested)? ConvertBinaryExpressionSyntaxToQuery( BinaryExpressionSyntax binarySyntax, IGridifyMapper mapper) { - var fieldExpression = binarySyntax.Left as FieldExpressionSyntax; - + var left = fieldExpression?.FieldToken.Text.Trim(); var right = (binarySyntax.Right as ValueExpressionSyntax); var op = binarySyntax.OperatorToken; @@ -23,10 +22,10 @@ private static (Expression> Expression, bool IsNested)? ConvertBin var gMap = mapper.GetGMap(left); - if (gMap == null) return null; - + if (gMap == null) throw new GridifyMapperException($"Mapping '{left}' not found"); + if (fieldExpression!.IsCollection) - gMap.To = UpdateExpressionIndex(gMap.To,fieldExpression.Index); + gMap.To = UpdateExpressionIndex(gMap.To, fieldExpression.Index); if (gMap.IsNestedCollection) { @@ -44,11 +43,11 @@ private static (Expression> Expression, bool IsNested)? ConvertBin private static LambdaExpression UpdateExpressionIndex(LambdaExpression exp, int index) { - var parameter = exp.Parameters[0]; + var parameter = exp.Parameters[0]; var unary = exp.Body as UnaryExpression; var body = unary!.Operand as MemberExpression; - var newBody = new PredicateBuilder.ReplaceExpressionVisitor(exp.Parameters[1],Expression.Constant(index,typeof(int))).Visit(body!); - return Expression.Lambda(newBody,parameter); + var newBody = new PredicateBuilder.ReplaceExpressionVisitor(exp.Parameters[1], Expression.Constant(index, typeof(int))).Visit(body!); + return Expression.Lambda(newBody, parameter); } private static Expression>? GenerateNestedExpression( @@ -382,7 +381,19 @@ internal static (Expression> Expression, bool IsNested) var bExp = expression as BinaryExpressionSyntax; if (bExp!.Left is FieldExpressionSyntax && bExp.Right is ValueExpressionSyntax) - return ConvertBinaryExpressionSyntaxToQuery(bExp, mapper) ?? throw new GridifyFilteringException("Invalid expression"); + { + try + { + return ConvertBinaryExpressionSyntaxToQuery(bExp, mapper) ?? throw new GridifyFilteringException("Invalid expression"); + } + catch (GridifyMapperException e) + { + if (mapper.Configuration.IgnoreNotMappedFields) + return (_ => true, false); + + throw; + } + } (Expression> exp, bool isNested) leftQuery; (Expression> exp, bool isNested) rightQuery; diff --git a/test/Gridify.Tests/GridifyExtensionsShould.cs b/test/Gridify.Tests/GridifyExtensionsShould.cs index 12ea3abb..f2d8ddf1 100644 --- a/test/Gridify.Tests/GridifyExtensionsShould.cs +++ b/test/Gridify.Tests/GridifyExtensionsShould.cs @@ -97,7 +97,7 @@ public void ApplyFiltering_DisableNullHandlingUsingMapper() } [Fact] - public void ApplyFiltering_DuplicateFiledName() + public void ApplyFiltering_DuplicatefieldName() { const string gq = "name=John|name=Sara"; var actual = _fakeRepository.AsQueryable() @@ -457,6 +457,7 @@ public void ApplyFiltering_GreaterThanBetweenTwoStrings() Assert.Equal(expected, actual); Assert.True(actual.Any()); } + [Fact] // issue #27 public void ApplyFiltering_LessThanBetweenTwoStrings() @@ -551,6 +552,43 @@ public void ApplyFiltering_NotEqual_ProcessingNullOrDefaultValueNonStringTypes() Assert.Equal(expected, actual); Assert.True(actual.Any()); } + + [Fact] // issue #33 + public void ApplyFiltering_WithSpaces() + { + var actual = _fakeRepository.AsQueryable().ApplyFiltering("name =ali reza").ToList(); + var expected = _fakeRepository.Where(q => q.Name == "ali reza" ).ToList(); + + Assert.Equal(expected.Count, actual.Count); + Assert.Equal(expected, actual); + Assert.True(actual.Any()); + } + + + [Fact] // issue #34 + public void ApplyFiltering_UnmappedFields_ShouldThrowException() + { + var gm = new GridifyMapper() + .AddMap("Id", q => q.Id); + + var exp = Assert.Throws(() => _fakeRepository.AsQueryable().ApplyFiltering("name=John,id>0", gm).ToList()); + Assert.Equal("Mapping 'name' not found",exp.Message); + } + + [Fact] // issue #34 + public void ApplyFiltering_UnmappedFields_ShouldSkipWhenIgnored() + { + var gm = new GridifyMapper(configuration => configuration.IgnoreNotMappedFields = true) + .AddMap("Id", q => q.Id); + + // name=*a filter should be ignored + var actual = _fakeRepository.AsQueryable().ApplyFiltering("name=*a, id>15", gm).ToList(); + var expected = _fakeRepository.Where(q => q.Id > 15).ToList(); + + Assert.Equal(expected.Count, actual.Count); + Assert.Equal(expected, actual); + Assert.True(actual.Any()); + } #endregion @@ -644,12 +682,38 @@ public void ApplyOrdering_EmptyOrderBy_ShouldSkip() [Fact] public void ApplyOrdering_NullGridifyQuery_ShouldSkip() { + GridifyQuery gq = null; var actual = _fakeRepository.AsQueryable() - .ApplyOrdering(null) + .ApplyOrdering(gq) .ToList(); var expected = _fakeRepository.ToList(); Assert.Equal(expected, actual); } + + [Fact] // issue #34 + public void ApplyOrdering_UnmappedFields_ShouldThrowException() + { + var gm = new GridifyMapper() + .AddMap("Id", q => q.Id); + + var exp = Assert.Throws(() => _fakeRepository.AsQueryable().ApplyOrdering("name,id", gm).ToList()); + Assert.Equal("Mapping 'name' not found",exp.Message); + } + + [Fact] // issue #34 + public void ApplyOrdering_UnmappedFields_ShouldSkipWhenIgnored() + { + var gm = new GridifyMapper(configuration => configuration.IgnoreNotMappedFields = true) + .AddMap("Id", q => q.Id); + + // name orderBy should be ignored + var actual = _fakeRepository.AsQueryable().ApplyOrdering("name,id", gm).ToList(); + var expected = _fakeRepository.OrderBy(q => q.Id ).ToList(); + + Assert.Equal(expected.Count, actual.Count); + Assert.Equal(expected, actual); + Assert.True(actual.Any()); + } #endregion @@ -784,7 +848,8 @@ private static IEnumerable GetSampleData() lst.Add(new TestClass(23, "LI | AM", null)); lst.Add(new TestClass(24, "(LI,AM)", null, tag: string.Empty)); lst.Add(new TestClass(25, "Case/i", null, tag: string.Empty)); - lst.Add(new TestClass(26, "/iCase", null)); + lst.Add(new TestClass(26, "/iCase", null)); + lst.Add(new TestClass(27, "ali reza", null)); return lst; }