diff --git a/README.md b/README.md index fcd92015..65b46a48 100644 --- a/README.md +++ b/README.md @@ -357,25 +357,26 @@ This is the performance improvement example when you use a compiled expression The QueryBuilder class is really useful if you want to manually build your query, also when you don't want to use the extension methods. -| Method | Description | -| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | -| AddCondition | Adds a string base Filtering query | -| AddOrderBy | Adds a string base Ordering query | -| ConfigurePaging | Configure Page and PageSize | -| AddQuery | Accepts a GridifyQuery object to configure filtering,ordering and paging | -| UseCustomMapper | Accepts a GridifyMapper to use in build methods | -| UseEmptyMapper | Setup an Empty new GridifyMapper without auto generated mappings | -| AddMap | Add a single Map to existing mapper | -| RemoveMap | Remove a single Map from existing mapper | -| ConfigureDefaultMapper | Configuring default mapper when we didn't use AddMapper method | -| Build | Applies filtering ordering and paging to a context | -| BuildFilteringExpression | Returns filtering expression | -| BuildOrderingExpression | Returns ordering expression | -| BuildQueryableEvaluator | Returns an evaluator delegate that can be use to evaluate an queryable context | -| BuildCollectionEvaluator | Returns an evaluator delegate that can be use to evaluate an enumerable context | -| BuildWithPaging | Applies filtering ordering and paging to a context, and returns paging result | -| BuildWithQueryablePaging | Applies filtering ordering and paging to a context, and returns queryable paging result | -| Evaluate | Directly Evaluate a context to check if all conditions are valid or not | +| Method | Description | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | +| AddCondition | Adds a string base Filtering query | +| AddOrderBy | Adds a string base Ordering query | +| ConfigurePaging | Configure Page and PageSize | +| AddQuery | Accepts a GridifyQuery object to configure filtering,ordering and paging | +| UseCustomMapper | Accepts a GridifyMapper to use in build methods | +| UseEmptyMapper | Setup an Empty new GridifyMapper without auto generated mappings | +| AddMap | Add a single Map to existing mapper | +| RemoveMap | Remove a single Map from existing mapper | +| ConfigureDefaultMapper | Configuring default mapper when we didn't use AddMapper method | +| Build | Applies filtering ordering and paging to a queryable context | +| BuildCompiled | Compiles the expressions and returns a delegate for applying filtering ordering and paging to a enumerable collection | +| BuildFilteringExpression | Returns filtering expression that can be compiled for later use for enumerable collections | +| BuildQueryableEvaluator | Returns an evaluator delegate that can be use to evaluate an queryable context | +| BuildCollectionEvaluator | Returns an evaluator delegate that can be use to evaluate an enumerable context | +| BuildWithPaging | Applies filtering ordering and paging to a context, and returns paging result | +| BuildWithPagingCompiled | Compiles the expressions and returns a delegate for applying filtering ordering and paging to a enumerable collection, that returns paging result | +| BuildWithQueryablePaging | Applies filtering ordering and paging to a context, and returns queryable paging result | +| Evaluate | Directly Evaluate a context to check if all conditions are valid or not | usage eg: ```c# diff --git a/benchmark/LibraryComparisionFilteringBenchmark.cs b/benchmark/LibraryComparisionFilteringBenchmark.cs index 00117fb2..725ef586 100644 --- a/benchmark/LibraryComparisionFilteringBenchmark.cs +++ b/benchmark/LibraryComparisionFilteringBenchmark.cs @@ -98,7 +98,7 @@ public void Sieve() } - private static IEnumerable GetSampleData() + public static IEnumerable GetSampleData() { var lst = new List(); lst.Add(new TestClass(1, "John", null, Guid.NewGuid(), DateTime.Now)); @@ -129,4 +129,4 @@ private static IEnumerable GetSampleData() return lst; } } -} \ No newline at end of file +} diff --git a/benchmark/Program.cs b/benchmark/Program.cs index e3d5c3d2..54f7345e 100644 --- a/benchmark/Program.cs +++ b/benchmark/Program.cs @@ -7,9 +7,9 @@ public class Program { private static void Main() { - BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + BenchmarkRunner.Run(); Console.Read(); } - } -} \ No newline at end of file +} diff --git a/benchmark/QueryBuilderBuildBenchmark.cs b/benchmark/QueryBuilderBuildBenchmark.cs new file mode 100644 index 00000000..f77e6001 --- /dev/null +++ b/benchmark/QueryBuilderBuildBenchmark.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Order; +using Gridify; +using Gridify.Tests; + +namespace Benchmarks +{ + [MemoryDiagnoser] + [RPlotExporter] + [Orderer(SummaryOrderPolicy.FastestToSlowest)] + public class QueryBuilderBuildBenchmark + { + private readonly IEnumerable _data; + private static readonly Consumer Consumer = new(); + private readonly Func, IEnumerable> BuildCompiledFuc; + private readonly Func, IQueryable> BuildFunc; + private readonly Func BuildFilteringExpressionFunc; + private readonly Func, Paging> BuildWithPagingCompiledFunc; + private readonly Func, Paging> BuildWithPagingFunc; + + + public QueryBuilderBuildBenchmark() + { + _data = LibraryComparisionFilteringBenchmark.GetSampleData().ToArray(); + + var builder = new QueryBuilder() + .AddCondition("id>2") + .AddCondition("name=*a"); + + BuildCompiledFuc = builder.BuildCompiled(); + BuildFunc = builder.Build(); + BuildFilteringExpressionFunc = builder.BuildFilteringExpression().Compile(); + BuildWithPagingCompiledFunc = builder.BuildWithPagingCompiled(); + BuildWithPagingFunc = builder.BuildWithPaging(); + + TestOutputs(); + } + + [Benchmark(Baseline = true)] // this method is only for filtering operations + public void UseGetFilteringExpression() + { + _data.Where(BuildFilteringExpressionFunc).Consume(Consumer); + } + + [Benchmark] + public void Build() + { + BuildFunc(_data.AsQueryable()).Consume(Consumer); + } + + [Benchmark] + public void BuildCompiled() + { + BuildCompiledFuc(_data).Consume(Consumer); + } + + [Benchmark] + public void BuildWithPaging() + { + BuildWithPagingFunc(_data.AsQueryable()).Data.Consume(Consumer); + } + + [Benchmark] + public void BuildWithPagingCompiled() + { + BuildWithPagingCompiledFunc(_data.AsQueryable()).Data.Consume(Consumer); + } + + private void TestOutputs() + { + if (AllSame(BuildCompiledFuc(_data).Count(), BuildFunc(_data.AsQueryable()).Count(), _data.Where(BuildFilteringExpressionFunc).Count()) && + AllSame(BuildCompiledFuc(_data).First().Id, BuildFunc(_data.AsQueryable()).First().Id, + _data.Where(BuildFilteringExpressionFunc).First().Id) && + AllSame(BuildCompiledFuc(_data).Last().Id, BuildFunc(_data.AsQueryable()).Last().Id, + _data.Where(BuildFilteringExpressionFunc).Last().Id) && + BuildCompiledFuc(_data).Count() < 2) + { + throw new Exception("MISS MATCH OUTPUT"); + } + } + + private static bool AllSame(params T[] items) + { + var first = true; + T comparand = default; + foreach (var i in items) + { + if (first) comparand = i; + else if (!i.Equals(comparand)) return false; + first = false; + } + + return true; + } + } +} diff --git a/src/Gridify.EntityFramework/Gridify.EntityFramework.csproj b/src/Gridify.EntityFramework/Gridify.EntityFramework.csproj index 1e868f66..e4e8aa66 100644 --- a/src/Gridify.EntityFramework/Gridify.EntityFramework.csproj +++ b/src/Gridify.EntityFramework/Gridify.EntityFramework.csproj @@ -9,7 +9,7 @@ netstandard2.0 Gridify.EntityFramework - 2.4.1 + 2.4.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 feade085..f8ecf979 100644 --- a/src/Gridify/Gridify.csproj +++ b/src/Gridify/Gridify.csproj @@ -3,7 +3,7 @@ netstandard2.0 Gridify - 2.4.1 + 2.4.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 abc01762..a8b76cc6 100644 --- a/src/Gridify/GridifyExtensions.cs +++ b/src/Gridify/GridifyExtensions.cs @@ -36,7 +36,6 @@ private static IGridifyPagination FixPagingData(this IGridifyPagination gridifyP #endregion - // TODO: should have some tests public static Expression> GetFilteringExpression(this IGridifyFiltering gridifyFiltering, IGridifyMapper? mapper = null) { if (string.IsNullOrWhiteSpace(gridifyFiltering.Filter)) diff --git a/src/Gridify/IQueryBuilder.cs b/src/Gridify/IQueryBuilder.cs index 069c79ca..8d050c7c 100644 --- a/src/Gridify/IQueryBuilder.cs +++ b/src/Gridify/IQueryBuilder.cs @@ -60,15 +60,141 @@ public interface IQueryBuilder IQueryBuilder AddMap(string from, Expression> to, Func? convertor = null, bool overwrite = true); IQueryBuilder RemoveMap(IGMap map); Expression> BuildFilteringExpression(); - IEnumerable>> BuildOrderingExpression(); Func, bool> BuildQueryableEvaluator(); Func, bool> BuildCollectionEvaluator(); bool Evaluate(IQueryable query); bool Evaluate(IEnumerable collection); + + /// + /// Returns a delegate that can be used to apply the filtering, ordering and paging to a queryable. + /// + /// + /// + /// var func = builder.Build(); + /// var query = func(queryableContext); + /// + /// + /// A delegate as type , IQueryable>]]> + Func, IQueryable> Build(); + + /// + /// Returns a delegate that can be used to apply the filtering, ordering and paging to a collection. + /// also, Internally it compiles the expressions to increase performance. + /// + /// + /// + /// var func = builder.BuildCompiled(); + /// var result = func(enumerableCollection); + /// + /// + /// A delegate as type , IEnumerable>]]> + Func, IEnumerable> BuildCompiled(); + + /// + /// Directly applies the filtering, ordering and paging to a queryable. + /// + /// + /// + /// var query = builder.Build(queryableContext); + /// + /// + /// ]]> IQueryable Build(IQueryable context); + + /// + /// Directly applies the filtering, ordering and paging to a enumerable collection. + /// + /// + /// + /// var result = builder.Build(enumerableCollection); + /// + /// + /// ]]> IEnumerable Build(IEnumerable collection); + + /// + /// Directly applies the filtering, ordering and paging to a enumerable collection. + /// also returns the total count of the collection. + /// + /// + /// + /// var pagingResult = builder.BuildWithPaging(enumerableCollection); + /// // or + /// var (count, result) = builder.BuildWithPaging(enumerableCollection); + /// + /// + /// ]]> Paging BuildWithPaging(IEnumerable collection); + + /// + /// Directly applies the filtering, ordering and paging to a queryable. + /// also load the data and returns the total count of the records. + /// + /// + /// + /// var pagingResult = builder.BuildWithPaging(queryableContext); + /// // or + /// var (count, result) = builder.BuildWithPaging(queryableContext); + /// + /// + /// ]]> Paging BuildWithPaging(IQueryable collection); + + /// + /// Directly applies the filtering, ordering and paging to a queryable. + /// also returns the total count of the records. + /// + /// + /// + /// var queryablePaging = builder.BuildWithQueryablePaging(queryableContext); + /// // or + /// var (count, query) = builder.BuildWithQueryablePaging(queryableContext); + /// + /// + /// ]]> QueryablePaging BuildWithQueryablePaging(IQueryable collection); + + /// + /// Returns a delegate that can be used to apply the filtering, ordering and paging to a queryable. + /// also returns the total count of the records. + /// + /// + /// + /// var func = builder.BuildWithQueryablePaging(); + /// var query = func(queryableContext); + /// + /// + /// A delegate as type ,QueryablePaging>]]> + Func,QueryablePaging> BuildWithQueryablePaging(); + + /// + /// Returns a delegate that can be used to apply the filtering, ordering and paging to a queryable. + /// also load the data and returns the total count of the records. + /// + /// + /// + /// var func = builder.BuildWithPaging(); + /// var pagingQuery = func(queryableContext); + /// // or + /// var (count, query) = func(queryableContext); + /// + /// + /// ,Paging> ]]> + Func,Paging> BuildWithPaging(); + + /// + /// Returns a delegate that can be used to apply the filtering, ordering and paging to a enumerable collection. + /// also load the data and returns the total count of the records. + /// + /// + /// + /// var func = builder.BuildWithPagingAsEnumerable(); + /// var pagingResult = func(enumerableCollection); + /// // or + /// var (count, result) = func(enumerableCollection); + /// + /// + /// ,Paging> ]]> + Func,Paging> BuildWithPagingCompiled(); } } diff --git a/src/Gridify/QueryBuilder.cs b/src/Gridify/QueryBuilder.cs index bbac4699..4e92772c 100644 --- a/src/Gridify/QueryBuilder.cs +++ b/src/Gridify/QueryBuilder.cs @@ -60,7 +60,7 @@ public IQueryBuilder AddQuery(IGridifyQuery gridifyQuery) AddOrderBy(gridifyQuery.OrderBy!); if (gridifyQuery.PageSize == 0) gridifyQuery.PageSize = GridifyExtensions.DefaultPageSize; - ConfigurePaging(gridifyQuery.Page, gridifyQuery.PageSize); + ConfigurePaging(gridifyQuery.Page, gridifyQuery.PageSize); return this; } @@ -136,16 +136,6 @@ public Expression> BuildFilteringExpression() => x is null ? y : x.And(y)) as Expression>)!; } - /// - public IEnumerable>> BuildOrderingExpression() - { - if (string.IsNullOrEmpty(_orderBy)) throw new GridifyOrderingException("Please use 'AddOrderBy' to specify at least an single order"); - - var gm = new GridifyQuery { OrderBy = _orderBy }; - _mapper ??= new GridifyMapper(true); - return gm.GetOrderingExpressions(_mapper); - } - /// public Func, bool> BuildQueryableEvaluator() { @@ -197,6 +187,31 @@ public IQueryable Build(IQueryable context) return query; } + /// + public Func, IQueryable> Build() + { + return Build; + } + + /// + public Func, IEnumerable> BuildCompiled() + { + var compiled = BuildFilteringExpression().Compile(); + return collection => + { + if (_conditions.Count > 0) + collection = collection.Where(compiled); + + if (!string.IsNullOrEmpty(_orderBy)) // TODO: this also should be compiled + collection = collection.AsQueryable().ApplyOrdering(_orderBy); + + if (_paging.HasValue) + collection = collection.Skip(_paging.Value.page * _paging.Value.pageSize).Take(_paging.Value.pageSize); + + return collection; + }; + } + /// public IEnumerable Build(IEnumerable collection) { @@ -226,6 +241,39 @@ public Paging BuildWithPaging(IQueryable collection) return new Paging(count, query); } + /// + public Func, QueryablePaging> BuildWithQueryablePaging() + { + return BuildWithQueryablePaging; + } + + /// + public Func, Paging> BuildWithPaging() + { + return BuildWithPaging; + } + + public Func, Paging> BuildWithPagingCompiled() + { + var compiled = BuildFilteringExpression().Compile(); + return collection => + { + if (_conditions.Count > 0) + collection = collection.Where(compiled); + + if (!string.IsNullOrEmpty(_orderBy)) // TODO: this also should be compiled + collection = collection.AsQueryable().ApplyOrdering(_orderBy); + + var result = collection.ToList(); + var count = result.Count(); + + return _paging.HasValue + ? new Paging(count, result.Skip(_paging.Value.page * _paging.Value.pageSize).Take(_paging.Value.pageSize)) + : new Paging(count, result); + }; + } + + /// public QueryablePaging BuildWithQueryablePaging(IQueryable collection) { diff --git a/test/Gridify.Tests/QueryBuilderShould.cs b/test/Gridify.Tests/QueryBuilderShould.cs index ca1fb80e..3d914c4d 100644 --- a/test/Gridify.Tests/QueryBuilderShould.cs +++ b/test/Gridify.Tests/QueryBuilderShould.cs @@ -56,5 +56,36 @@ public void Evaluator_Should_Check_All_Conditions_Without_And() Assert.True(isQueryValid); } + [Fact] + public void Build() + { + var builder = new QueryBuilder() + .AddCondition("name =*al") + .AddOrderBy("name"); + + var compiled = builder.Build(); + var result = compiled(_fakeRepository.AsQueryable()); + Assert.True(result.Any()); + + } + + [Fact] + public void BuildFilteringExpression_Should_Return_Correct_Expression() + { + var builder = new QueryBuilder() + .AddCondition("name =*a") + .AddCondition("id > 2"); + + var expectedExpressionString = new GridifyQuery() { Filter = "name=*a,id>2" }.GetFilteringExpression().ToString(); + var actualExpression = builder.BuildFilteringExpression(); + + var expectedResult = _fakeRepository.Where(q => + q.Id > 2 && q.Name != null && q.Name.Contains("a")); + + var actualResult = _fakeRepository.AsQueryable().Where(actualExpression); + + Assert.Equal(expectedExpressionString, actualExpression.ToString()); + Assert.Equal(expectedResult, actualResult); + } } }