From d046ff6ea20486528748eaf0c74f8b3d0da6b823 Mon Sep 17 00:00:00 2001 From: AliReZa Sabouri Date: Fri, 21 Jun 2024 17:36:02 +0200 Subject: [PATCH 01/13] refactor(lexer): to use null-coalescing operator and add diagnostics collection method Simplify initialization of _diagnostics list Use null-coalescing operator for initializing _diagnostics Add new private method AddDiagnostics for managing diagnostics Improve readability by clearly separating concerns --- src/Gridify/Syntax/Lexer.cs | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/Gridify/Syntax/Lexer.cs b/src/Gridify/Syntax/Lexer.cs index bd89b267..00a1fa62 100644 --- a/src/Gridify/Syntax/Lexer.cs +++ b/src/Gridify/Syntax/Lexer.cs @@ -5,21 +5,14 @@ namespace Gridify.Syntax; -internal ref struct Lexer +internal ref struct Lexer(string text, IEnumerable customOperators) { - private readonly List _diagnostics = new(); - private readonly ReadOnlySpan _text; - private readonly IEnumerable _customOperators; + private List? _diagnostics = null; + private readonly ReadOnlySpan _text = text.AsSpan(); private int _position; private bool _waitingForValue; - public Lexer(string text, IEnumerable customOperators) - { - _text = text.AsSpan(); - _customOperators = customOperators; - } - - public IEnumerable Diagnostics => _diagnostics; + public IEnumerable Diagnostics => _diagnostics ?? Enumerable.Empty(); private char Current => _position >= _text.Length ? '\0' : _text[_position]; @@ -116,9 +109,9 @@ public SyntaxToken NextToken() ? new SyntaxToken(SyntaxKind.GreaterOrEqualThan, _position += 2, ">=") : new SyntaxToken(SyntaxKind.GreaterThan, _position++, ">"); } - case '#' when _customOperators.Any(): // Custom Operators + case '#' when customOperators.Any(): // Custom Operators { - foreach (var cOp in _customOperators) + foreach (var cOp in customOperators) { var op = cOp.GetOperator(); var opSlice = op.AsSpan(); @@ -164,7 +157,7 @@ public SyntaxToken NextToken() return new SyntaxToken(SyntaxKind.FieldToken, start, text.ToString()); } - _diagnostics.Add($"bad character input: '{Current.ToString()}', at index {_position.ToString()}"); + AddDiagnostics($"bad character input: '{Current.ToString()}', at index {_position.ToString()}"); return new SyntaxToken(SyntaxKind.BadToken, _position++, string.Empty); } @@ -189,7 +182,7 @@ private bool TryParseIndexer(char peek, out SyntaxToken nextToken) } } - _diagnostics.Add($"Indexer is not closed: '{peek.ToString()}' at {_position++.ToString()}. expected ']' "); + AddDiagnostics($"Indexer is not closed: '{peek.ToString()}' at {_position++.ToString()}. expected ']' "); { nextToken = new SyntaxToken(SyntaxKind.BadToken, _position, Current.ToString()); return true; @@ -259,4 +252,10 @@ private bool TryToReadTheValue(out SyntaxToken? valueToken) valueToken = null; return false; } + + private void AddDiagnostics(string message) + { + _diagnostics ??= []; + _diagnostics.Add(message); + } } From 7909f0c1830fa0df0e841cbd5b6d4650fadd8936 Mon Sep 17 00:00:00 2001 From: AliReZa Sabouri Date: Fri, 21 Jun 2024 17:59:54 +0200 Subject: [PATCH 02/13] chore: add native linq comparision benchmark --- benchmark/NativeLinqComparisionBenchmark.cs | 52 +++++++++++++++++++++ benchmark/Program.cs | 3 +- 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 benchmark/NativeLinqComparisionBenchmark.cs diff --git a/benchmark/NativeLinqComparisionBenchmark.cs b/benchmark/NativeLinqComparisionBenchmark.cs new file mode 100644 index 00000000..8a764f59 --- /dev/null +++ b/benchmark/NativeLinqComparisionBenchmark.cs @@ -0,0 +1,52 @@ +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 NativeLinqComparisionBenchmark +{ + private static readonly Consumer Consumer = new(); + private TestClass[] _data; + private GridifyMapper _gm; + + private IQueryable Ds => _data.AsQueryable(); + + [GlobalSetup] + public void Setup() + { + _data = LibraryComparisionFilteringBenchmark.GetSampleData().ToArray(); + _gm = new GridifyMapper(true); + } + + + [Benchmark(Baseline = true)] + public void Native_LINQ() + { + Ds.Where(q => q.Name.Contains('a')).Consume(Consumer); + Ds.Where(q => q.Id > 5).Consume(Consumer); + Ds.Where(q => q.Name == "Ali").Consume(Consumer); + } + + [Benchmark] + public void Gridify() + { + Ds.ApplyFiltering("Name=*a", _gm).Consume(Consumer); + Ds.ApplyFiltering("Id>5", _gm).Consume(Consumer); + Ds.ApplyFiltering("Name=Ali", _gm).Consume(Consumer); + } + + [Benchmark] + public void Gridify_WithoutMapper() + { + Ds.ApplyFiltering("Name=*a").Consume(Consumer); + Ds.ApplyFiltering("Id>5").Consume(Consumer); + Ds.ApplyFiltering("Name=Ali").Consume(Consumer); + } +} diff --git a/benchmark/Program.cs b/benchmark/Program.cs index 6cbb7ef7..5f85ee45 100644 --- a/benchmark/Program.cs +++ b/benchmark/Program.cs @@ -7,7 +7,8 @@ public class Program { private static void Main() { - BenchmarkRunner.Run(); + // BenchmarkRunner.Run(); + BenchmarkRunner.Run(); // BenchmarkRunner.Run(); // BenchmarkRunner.Run(); // BenchmarkRunner.Run(); From a32f8cd11af3a72682180bfb31fb31660b3674c6 Mon Sep 17 00:00:00 2001 From: AliReZa Sabouri Date: Fri, 21 Jun 2024 18:02:03 +0200 Subject: [PATCH 03/13] perf(syntax): Refactor syntax nodes to use ISyntaxNode interface - Replace SyntaxNode with ISyntaxNode throughout the project - Update ExpressionSyntax and derived classes to implement ISyntaxNode - Modify constructors and methods to accommodate the new interface - Ensure consistency in method signatures using ISyntaxNode --- .../ElasticsearchQueryBuilder.cs | 4 +-- src/Gridify/GridifyExtensions.cs | 4 +-- src/Gridify/QueryBuilders/BaseQueryBuilder.cs | 6 ++-- src/Gridify/QueryBuilders/LinqQueryBuilder.cs | 8 ++--- src/Gridify/Syntax/BinaryExpressionSyntax.cs | 6 ++-- src/Gridify/Syntax/ExpressionSyntax.cs | 10 ++++-- src/Gridify/Syntax/FieldExpressionSyntax.cs | 2 +- src/Gridify/Syntax/ISyntaxNode.cs | 9 ++++++ src/Gridify/Syntax/Lexer.cs | 4 +-- .../Syntax/ParenthesizedExpressionSyntax.cs | 10 +++--- src/Gridify/Syntax/Parser.cs | 31 ++++++++++++------- src/Gridify/Syntax/SyntaxNode.cs | 9 ------ src/Gridify/Syntax/SyntaxToken.cs | 22 +++++-------- src/Gridify/Syntax/ValueExpressionSyntax.cs | 8 ++--- 14 files changed, 69 insertions(+), 64 deletions(-) create mode 100644 src/Gridify/Syntax/ISyntaxNode.cs delete mode 100644 src/Gridify/Syntax/SyntaxNode.cs diff --git a/src/Gridify.Elasticsearch/ElasticsearchQueryBuilder.cs b/src/Gridify.Elasticsearch/ElasticsearchQueryBuilder.cs index ead4bbca..9261a60a 100644 --- a/src/Gridify.Elasticsearch/ElasticsearchQueryBuilder.cs +++ b/src/Gridify.Elasticsearch/ElasticsearchQueryBuilder.cs @@ -13,7 +13,7 @@ internal class ElasticsearchQueryBuilder(IGridifyMapper mapper) : BaseQuer private readonly IGridifyMapper _mapper = mapper; protected override Query BuildNestedQuery( - Expression body, IGMap gMap, ValueExpressionSyntax value, SyntaxNode op) + Expression body, IGMap gMap, ValueExpressionSyntax value, ISyntaxNode op) { throw new NotSupportedException(); } @@ -40,7 +40,7 @@ protected override object BuildQueryAccordingToValueType( Expression body, ParameterExpression parameter, object? value, - SyntaxNode op, + ISyntaxNode op, ValueExpressionSyntax valueExpression) { if (valueExpression.IsCaseInsensitive) diff --git a/src/Gridify/GridifyExtensions.cs b/src/Gridify/GridifyExtensions.cs index 06a90170..e3a88363 100644 --- a/src/Gridify/GridifyExtensions.cs +++ b/src/Gridify/GridifyExtensions.cs @@ -197,9 +197,9 @@ internal static IGridifyMapper FixMapper(this IGridifyMapper? mapper, S return mapper; } - private static IEnumerable Descendants(this SyntaxNode root) + private static IEnumerable Descendants(this ISyntaxNode root) { - var nodes = new Stack(new[] { root }); + var nodes = new Stack(new[] { root }); while (nodes.Any()) { var node = nodes.Pop(); diff --git a/src/Gridify/QueryBuilders/BaseQueryBuilder.cs b/src/Gridify/QueryBuilders/BaseQueryBuilder.cs index 94f59e30..41252874 100644 --- a/src/Gridify/QueryBuilders/BaseQueryBuilder.cs +++ b/src/Gridify/QueryBuilders/BaseQueryBuilder.cs @@ -16,7 +16,7 @@ internal TQuery Build(ExpressionSyntax expression) } protected abstract TQuery? BuildNestedQuery( - Expression body, IGMap gMap, ValueExpressionSyntax value, SyntaxNode op); + Expression body, IGMap gMap, ValueExpressionSyntax value, ISyntaxNode op); protected abstract TQuery BuildAlwaysTrueQuery(); @@ -31,7 +31,7 @@ internal TQuery Build(ExpressionSyntax expression) Expression body, ParameterExpression parameter, object? value, - SyntaxNode op, + ISyntaxNode op, ValueExpressionSyntax valueExpression); protected abstract TQuery CombineWithAndOperator(TQuery left, TQuery right); @@ -157,7 +157,7 @@ private static object AddIndexerNullCheck(IGMap gMap, object query) Expression body, ParameterExpression parameter, ValueExpressionSyntax valueExpression, - SyntaxNode op, + ISyntaxNode op, Func? convertor) { // Remove the boxing for value types diff --git a/src/Gridify/QueryBuilders/LinqQueryBuilder.cs b/src/Gridify/QueryBuilders/LinqQueryBuilder.cs index 9caaa14d..d98ecb3c 100644 --- a/src/Gridify/QueryBuilders/LinqQueryBuilder.cs +++ b/src/Gridify/QueryBuilders/LinqQueryBuilder.cs @@ -10,7 +10,7 @@ namespace Gridify.QueryBuilders; internal class LinqQueryBuilder(IGridifyMapper mapper) : BaseQueryBuilder>, T>(mapper) { protected override Expression>? BuildNestedQuery( - Expression body, IGMap gMap, ValueExpressionSyntax value, SyntaxNode op) + Expression body, IGMap gMap, ValueExpressionSyntax value, ISyntaxNode op) { while (true) switch (body) @@ -105,7 +105,7 @@ protected override Expression> BuildAlwaysFalseQuery(ParameterExpr Expression body, ParameterExpression parameter, object? value, - SyntaxNode op, + ISyntaxNode op, ValueExpressionSyntax valueExpression) { Expression be; @@ -229,7 +229,7 @@ protected override Expression> BuildAlwaysFalseQuery(ParameterExpr break; case SyntaxKind.CustomOperator: - var token = op as SyntaxToken; + var token = (SyntaxToken)op; var customOperator = GridifyGlobalConfiguration.CustomOperators.Operators.First(q => q.GetOperator() == token!.Text); var customExp = customOperator.OperatorHandler(); @@ -257,7 +257,7 @@ protected override Expression> CombineWithOrOperator(Expression>? GenerateNestedExpression(Expression body, IGMap gMap, ValueExpressionSyntax value, SyntaxNode op) + private Expression>? GenerateNestedExpression(Expression body, IGMap gMap, ValueExpressionSyntax value, ISyntaxNode op) { while (true) switch (body) diff --git a/src/Gridify/Syntax/BinaryExpressionSyntax.cs b/src/Gridify/Syntax/BinaryExpressionSyntax.cs index 29cad2c4..bf53a370 100644 --- a/src/Gridify/Syntax/BinaryExpressionSyntax.cs +++ b/src/Gridify/Syntax/BinaryExpressionSyntax.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Gridify.Syntax; @@ -17,10 +17,10 @@ public BinaryExpressionSyntax(ExpressionSyntax left, SyntaxToken operatorToken, public override SyntaxKind Kind => SyntaxKind.BinaryExpression; - public override IEnumerable GetChildren() + public override IEnumerable GetChildren() { yield return Left; yield return OperatorToken; yield return Right; } -} \ No newline at end of file +} diff --git a/src/Gridify/Syntax/ExpressionSyntax.cs b/src/Gridify/Syntax/ExpressionSyntax.cs index a569aeb5..2981a565 100644 --- a/src/Gridify/Syntax/ExpressionSyntax.cs +++ b/src/Gridify/Syntax/ExpressionSyntax.cs @@ -1,5 +1,9 @@ -namespace Gridify.Syntax; +using System.Collections.Generic; -public abstract class ExpressionSyntax : SyntaxNode +namespace Gridify.Syntax; + +public abstract class ExpressionSyntax : ISyntaxNode { -} \ No newline at end of file + public abstract SyntaxKind Kind { get; } + public abstract IEnumerable GetChildren(); +} diff --git a/src/Gridify/Syntax/FieldExpressionSyntax.cs b/src/Gridify/Syntax/FieldExpressionSyntax.cs index 922ee2bd..1a1f8c4d 100644 --- a/src/Gridify/Syntax/FieldExpressionSyntax.cs +++ b/src/Gridify/Syntax/FieldExpressionSyntax.cs @@ -12,7 +12,7 @@ internal FieldExpressionSyntax(SyntaxToken fieldToken, string? indexer = default public override SyntaxKind Kind => SyntaxKind.FieldExpression; - public override IEnumerable GetChildren() + public override IEnumerable GetChildren() { yield return FieldToken; } diff --git a/src/Gridify/Syntax/ISyntaxNode.cs b/src/Gridify/Syntax/ISyntaxNode.cs new file mode 100644 index 00000000..def89e3c --- /dev/null +++ b/src/Gridify/Syntax/ISyntaxNode.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Gridify.Syntax; + +public interface ISyntaxNode +{ + public SyntaxKind Kind { get; } + public IEnumerable GetChildren(); +} diff --git a/src/Gridify/Syntax/Lexer.cs b/src/Gridify/Syntax/Lexer.cs index 00a1fa62..d9b80295 100644 --- a/src/Gridify/Syntax/Lexer.cs +++ b/src/Gridify/Syntax/Lexer.cs @@ -192,7 +192,7 @@ private bool TryParseIndexer(char peek, out SyntaxToken nextToken) return false; } - private bool TryToReadTheValue(out SyntaxToken? valueToken) + private bool TryToReadTheValue(out SyntaxToken valueToken) { if (_waitingForValue) { @@ -249,7 +249,7 @@ private bool TryToReadTheValue(out SyntaxToken? valueToken) } } - valueToken = null; + valueToken = new SyntaxToken(); return false; } diff --git a/src/Gridify/Syntax/ParenthesizedExpressionSyntax.cs b/src/Gridify/Syntax/ParenthesizedExpressionSyntax.cs index b69fab9f..5083edba 100644 --- a/src/Gridify/Syntax/ParenthesizedExpressionSyntax.cs +++ b/src/Gridify/Syntax/ParenthesizedExpressionSyntax.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Gridify.Syntax; @@ -7,18 +7,18 @@ internal sealed class ParenthesizedExpressionSyntax : ExpressionSyntax public SyntaxToken OpenParenthesisToken { get; } public ExpressionSyntax Expression { get; } public SyntaxToken CloseParenthesisToken { get; } - public override SyntaxKind Kind => SyntaxKind.ParenthesizedExpression; + public override SyntaxKind Kind => SyntaxKind.ParenthesizedExpression; - public ParenthesizedExpressionSyntax(SyntaxToken openParenthesisToken,ExpressionSyntax expression,SyntaxToken closeParenthesisToken) + public ParenthesizedExpressionSyntax(SyntaxToken openParenthesisToken, ExpressionSyntax expression, SyntaxToken closeParenthesisToken) { OpenParenthesisToken = openParenthesisToken; Expression = expression; CloseParenthesisToken = closeParenthesisToken; } - public override IEnumerable GetChildren() + public override IEnumerable GetChildren() { yield return OpenParenthesisToken; yield return Expression; yield return CloseParenthesisToken; } -} \ No newline at end of file +} diff --git a/src/Gridify/Syntax/Parser.cs b/src/Gridify/Syntax/Parser.cs index 27714f03..5719e54b 100644 --- a/src/Gridify/Syntax/Parser.cs +++ b/src/Gridify/Syntax/Parser.cs @@ -3,10 +3,10 @@ namespace Gridify.Syntax; -internal class Parser +internal struct Parser { - private readonly List _diagnostics = new(); - private readonly SyntaxToken[] _tokens; + private List? _diagnostics = null; + private readonly List _tokens = []; private int _position; private static bool IsOperator(SyntaxKind kind) @@ -28,17 +28,16 @@ SyntaxKind.NotEndsWith or public Parser(string text, IEnumerable customOperators) { - var tokens = new List(); var lexer = new Lexer(text, customOperators); SyntaxToken token; do { token = lexer.NextToken(); - if (token.Kind != SyntaxKind.WhiteSpace) tokens.Add(token); + if (token.Kind != SyntaxKind.WhiteSpace) _tokens.Add(token); } while (token.Kind != SyntaxKind.End); - _tokens = tokens.ToArray(); - _diagnostics.AddRange(lexer.Diagnostics); + if (lexer.Diagnostics.Any()) + AddDiagnostics(lexer.Diagnostics); } private SyntaxToken Current => Peek(0); @@ -46,14 +45,14 @@ public Parser(string text, IEnumerable customOperators) private SyntaxToken Peek(int offset) { var index = _position + offset; - return index >= _tokens.Length ? _tokens[_tokens.Length - 1] : _tokens[index]; + return index >= _tokens.Count ? _tokens[_tokens.Count - 1] : _tokens[index]; } public SyntaxTree Parse() { var expression = ParseTerm(); var end = Match(SyntaxKind.End, GetExpectation(expression.Kind)); - return new SyntaxTree(_diagnostics, expression, end); + return new SyntaxTree(_diagnostics ?? Enumerable.Empty(), expression, end); } private SyntaxKind GetExpectation(SyntaxKind kind) @@ -141,8 +140,8 @@ private SyntaxToken Match(SyntaxKind kind, SyntaxKind? expectation = null) expectation ??= kind; - if (!_diagnostics.Any(q => q.StartsWith("Unexpected token"))) - _diagnostics.Add($"Unexpected token <{Current.Kind}> at index {Current.Position}, expected <{expectation}>"); + if (_diagnostics != null && !_diagnostics.Any(q => q.StartsWith("Unexpected token"))) + AddDiagnostics($"Unexpected token <{Current.Kind}> at index {Current.Position}, expected <{expectation}>"); return new SyntaxToken(kind, Current.Position, Current.Text); } @@ -165,4 +164,14 @@ private ExpressionSyntax ParseFieldExpression() ? new FieldExpressionSyntax(fieldToken, fieldIndexer.Text) : new FieldExpressionSyntax(fieldToken); } + private void AddDiagnostics(string message) + { + _diagnostics ??= []; + _diagnostics.Add(message); + } + private void AddDiagnostics(IEnumerable messages) + { + _diagnostics ??= []; + _diagnostics.AddRange(messages); + } } diff --git a/src/Gridify/Syntax/SyntaxNode.cs b/src/Gridify/Syntax/SyntaxNode.cs deleted file mode 100644 index 4089043f..00000000 --- a/src/Gridify/Syntax/SyntaxNode.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Collections.Generic; - -namespace Gridify.Syntax; - -public abstract class SyntaxNode -{ - public abstract SyntaxKind Kind { get; } - public abstract IEnumerable GetChildren(); -} \ No newline at end of file diff --git a/src/Gridify/Syntax/SyntaxToken.cs b/src/Gridify/Syntax/SyntaxToken.cs index 904080b6..78884060 100644 --- a/src/Gridify/Syntax/SyntaxToken.cs +++ b/src/Gridify/Syntax/SyntaxToken.cs @@ -3,26 +3,18 @@ namespace Gridify.Syntax; -public class SyntaxToken : SyntaxNode +public struct SyntaxToken(SyntaxKind kind, int position, string text) : ISyntaxNode { - public override SyntaxKind Kind { get; } - public int Position { get; } - public string Text { get; } + public int Position { get; } = position; + public string Text { get; } = text; + public SyntaxKind Kind { get; } = kind; - public override IEnumerable GetChildren() + public IEnumerable GetChildren() { - return Enumerable.Empty(); + return Enumerable.Empty(); } - public SyntaxToken(SyntaxKind kind, int position, string text) + public SyntaxToken() : this(SyntaxKind.End, 0, string.Empty) { - Kind = kind; - Position = position; - Text = text; - } - - public SyntaxToken() - { - Text = string.Empty; } } diff --git a/src/Gridify/Syntax/ValueExpressionSyntax.cs b/src/Gridify/Syntax/ValueExpressionSyntax.cs index 0145cbe9..5032154d 100644 --- a/src/Gridify/Syntax/ValueExpressionSyntax.cs +++ b/src/Gridify/Syntax/ValueExpressionSyntax.cs @@ -1,10 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Gridify.Syntax; internal sealed class ValueExpressionSyntax : ExpressionSyntax { - public ValueExpressionSyntax(SyntaxToken valueToken,bool isCaseInsensitive, bool isNullOrDefault) + public ValueExpressionSyntax(SyntaxToken valueToken, bool isCaseInsensitive, bool isNullOrDefault) { ValueToken = valueToken; IsCaseInsensitive = isCaseInsensitive; @@ -13,7 +13,7 @@ public ValueExpressionSyntax(SyntaxToken valueToken,bool isCaseInsensitive, bool public override SyntaxKind Kind => SyntaxKind.ValueExpression; - public override IEnumerable GetChildren() + public override IEnumerable GetChildren() { yield return ValueToken; } @@ -21,4 +21,4 @@ public override IEnumerable GetChildren() public SyntaxToken ValueToken { get; } public bool IsCaseInsensitive { get; } public bool IsNullOrDefault { get; } -} \ No newline at end of file +} From fd362c3f272802bbe316c66bd7f5dc3d385d1f01 Mon Sep 17 00:00:00 2001 From: AliReZa Sabouri Date: Fri, 21 Jun 2024 23:41:34 +0200 Subject: [PATCH 04/13] perf: Refactor OperatorManager and Parser --- src/Gridify/Syntax/OperatorManager.cs | 14 ++++++++------ src/Gridify/Syntax/Parser.cs | 8 ++++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Gridify/Syntax/OperatorManager.cs b/src/Gridify/Syntax/OperatorManager.cs index aaf415b2..4598fdad 100644 --- a/src/Gridify/Syntax/OperatorManager.cs +++ b/src/Gridify/Syntax/OperatorManager.cs @@ -1,25 +1,26 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; namespace Gridify.Syntax; public class OperatorManager { - private readonly ConcurrentDictionary _operators = new(); - internal IEnumerable Operators => _operators.Values; + private ConcurrentDictionary? _operators; + internal IEnumerable Operators => _operators?.Values ?? Enumerable.Empty(); public void Register() where TOperator : IGridifyOperator { - var gridifyOperator = (IGridifyOperator) Activator.CreateInstance(typeof(TOperator))!; + var gridifyOperator = (IGridifyOperator)Activator.CreateInstance(typeof(TOperator))!; Register(gridifyOperator); } public void Register(IGridifyOperator gridifyOperator) { Validate(gridifyOperator.GetOperator()); - + _operators ??= new(); _operators.Add(gridifyOperator); } @@ -28,15 +29,16 @@ public void Register(string @operator, Expression handler) Validate(@operator); var customOperator = new GridifyOperator(@operator, handler); + _operators ??= new(); _operators.Add(customOperator); } public void Remove() where TOperator : IGridifyOperator { - var gridifyOperator = (IGridifyOperator) Activator.CreateInstance(typeof(TOperator))!; + var gridifyOperator = (IGridifyOperator)Activator.CreateInstance(typeof(TOperator))!; Remove(gridifyOperator.GetOperator()); } - public void Remove(string @operator) => _operators.Remove(@operator); + public void Remove(string @operator) => _operators?.Remove(@operator); private static void Validate(string @operator) { diff --git a/src/Gridify/Syntax/Parser.cs b/src/Gridify/Syntax/Parser.cs index 5719e54b..b41b9fab 100644 --- a/src/Gridify/Syntax/Parser.cs +++ b/src/Gridify/Syntax/Parser.cs @@ -55,7 +55,7 @@ public SyntaxTree Parse() return new SyntaxTree(_diagnostics ?? Enumerable.Empty(), expression, end); } - private SyntaxKind GetExpectation(SyntaxKind kind) + private static SyntaxKind GetExpectation(SyntaxKind kind) { switch (kind) { @@ -103,7 +103,7 @@ private ExpressionSyntax ParseFactor() return left; } - private ExpressionSyntax ParseValueExpression() + private ValueExpressionSyntax ParseValueExpression() { // field= if (Current.Kind != SyntaxKind.ValueToken) @@ -140,7 +140,7 @@ private SyntaxToken Match(SyntaxKind kind, SyntaxKind? expectation = null) expectation ??= kind; - if (_diagnostics != null && !_diagnostics.Any(q => q.StartsWith("Unexpected token"))) + if (_diagnostics is null || !_diagnostics.Any(q => q.StartsWith("Unexpected token"))) AddDiagnostics($"Unexpected token <{Current.Kind}> at index {Current.Position}, expected <{expectation}>"); return new SyntaxToken(kind, Current.Position, Current.Text); @@ -156,7 +156,7 @@ private ExpressionSyntax ParsePrimaryExpression() return new ParenthesizedExpressionSyntax(left, expression, right); } - private ExpressionSyntax ParseFieldExpression() + private FieldExpressionSyntax ParseFieldExpression() { var fieldToken = Match(SyntaxKind.FieldToken); From 702dab660c0483acf909c87fc6f79f80ceaf4baa Mon Sep 17 00:00:00 2001 From: AliReZa Sabouri Date: Sat, 22 Jun 2024 00:52:39 +0200 Subject: [PATCH 05/13] perf: simplifies iteration and error handling in FixMapper Removed redundant catch block around AddMap calls Improved syntax tree traversal with direct descendant extraction Directly throw exceptions if fields are not mapped correctly --- src/Gridify/GridifyExtensions.cs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Gridify/GridifyExtensions.cs b/src/Gridify/GridifyExtensions.cs index e3a88363..ea7ede95 100644 --- a/src/Gridify/GridifyExtensions.cs +++ b/src/Gridify/GridifyExtensions.cs @@ -178,25 +178,25 @@ internal static IGridifyMapper FixMapper(this IGridifyMapper? mapper, S mapper = new GridifyMapper(); - var fields = syntaxTree.Root.Descendants() - .Where(q => q.Kind == SyntaxKind.FieldExpression) - .Cast() - .Distinct(new FieldExpressionComparer()); + var root = syntaxTree.Root; + var fields = root.Descendants() + .OfType() + .Distinct(new FieldExpressionComparer()); - foreach (var field in fields) - try - { - mapper.AddMap(field.FieldToken.Text); - } - catch (Exception) - { - if (!mapper.Configuration.IgnoreNotMappedFields) - throw new GridifyMapperException($"Property '{field.FieldToken.Text}' not found."); - } + try + { + foreach (var field in fields) mapper.AddMap(field.FieldToken.Text); + } + catch (Exception) + { + if (!mapper.Configuration.IgnoreNotMappedFields) + throw; + } return mapper; } + private static IEnumerable Descendants(this ISyntaxNode root) { var nodes = new Stack(new[] { root }); From b3fe64e0a212ba66ea1f3ec16b35cb0afcba8135 Mon Sep 17 00:00:00 2001 From: AliReZa Sabouri Date: Sat, 22 Jun 2024 20:13:51 +0200 Subject: [PATCH 06/13] fix: typo --- ...ComparisionBenchmark.cs => NativeLinqComparisonBenchmark.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename benchmark/{NativeLinqComparisionBenchmark.cs => NativeLinqComparisonBenchmark.cs} (96%) diff --git a/benchmark/NativeLinqComparisionBenchmark.cs b/benchmark/NativeLinqComparisonBenchmark.cs similarity index 96% rename from benchmark/NativeLinqComparisionBenchmark.cs rename to benchmark/NativeLinqComparisonBenchmark.cs index 8a764f59..80c0b5be 100644 --- a/benchmark/NativeLinqComparisionBenchmark.cs +++ b/benchmark/NativeLinqComparisonBenchmark.cs @@ -10,7 +10,7 @@ namespace Benchmarks; [MemoryDiagnoser] [RPlotExporter] [Orderer(SummaryOrderPolicy.FastestToSlowest)] -public class NativeLinqComparisionBenchmark +public class NativeLinqComparisonBenchmark { private static readonly Consumer Consumer = new(); private TestClass[] _data; From d4f766b6d63bb67bc3da13fa9389d699caa83375 Mon Sep 17 00:00:00 2001 From: AliReZa Sabouri Date: Sat, 22 Jun 2024 04:01:00 +0200 Subject: [PATCH 07/13] feat: support mapping generation for list of simple types --- src/Gridify/GridifyMapper.cs | 64 ++++++++++++++++++++++++++++++++---- 1 file changed, 58 insertions(+), 6 deletions(-) diff --git a/src/Gridify/GridifyMapper.cs b/src/Gridify/GridifyMapper.cs index 64abe906..b170f5b1 100644 --- a/src/Gridify/GridifyMapper.cs +++ b/src/Gridify/GridifyMapper.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Reflection; namespace Gridify; @@ -47,7 +48,10 @@ public IGridifyMapper AddMap(string from, Func? convertor = n Expression> to; try { - to = CreateExpression(from); + var expression = CreateExpression(from); + if (expression is null) + return this; + to = expression; } catch (Exception) { @@ -210,16 +214,64 @@ public IEnumerable> GetCurrentMaps() /// a comma seperated string public override string ToString() => string.Join(",", _mappings.Select(q => q.From)); - internal static Expression> CreateExpression(string from) + internal static Expression>? CreateExpression(string from) // TODO: handle possible nulls { // Param_x => var parameter = Expression.Parameter(typeof(T), "__" + typeof(T).Name); // Param_x.Name, Param_x.yyy.zz.xx var mapProperty = from.Split('.').Aggregate(parameter, Expression.Property); - // (object)Param_x.Name - var convertedExpression = Expression.Convert(mapProperty, typeof(object)); - // Param_x => (object)Param_x.Name - return Expression.Lambda>(convertedExpression, parameter); + + if (!IsListOrArrayOfPrimitivesOrString(mapProperty.Type, out var genericType)) + { + // (object)Param_x.Name + var convertedExpression = Expression.Convert(mapProperty, typeof(object)); + // Param_x => (object)Param_x.Name + return Expression.Lambda>(convertedExpression, parameter); + } + + var selectMethod = GetSelectMethod([genericType!, genericType!]); + if (selectMethod is null) + return null; + var predicateParameter = Expression.Parameter(genericType!); + var predicate = Expression.Lambda(predicateParameter, predicateParameter); + // Param_x.Name.Select(fc => fc) + var body = Expression.Call(selectMethod, mapProperty, predicate); + return Expression.Lambda>(body, parameter); + } + + private static MethodInfo? GetSelectMethod(Type[] type) + { + return typeof(Enumerable).GetMethods().First(m => m.Name == "Select").MakeGenericMethod(type); + } + + private static bool IsListOrArrayOfPrimitivesOrString(Type type, out Type? itemType) + { + itemType = null; + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) + { + var arguments = type.GetGenericArguments(); + if (arguments.Length != 1 || !IsPrimitiveOrKnownType(arguments[0])) return false; + + itemType = arguments[0]; + return true; + } + if (!type.IsArray) return false; + + var elementType = type.GetElementType(); + if (elementType == null || !IsPrimitiveOrKnownType(elementType)) return false; + itemType = elementType; + return true; + } + + private static bool IsPrimitiveOrKnownType(Type type) + { + // Primitive types in C# include: int, float, double, char, bool, etc. + // Also consider the known primitive types and string explicitly + return type.IsPrimitive || + (type == typeof(string)) || + (type == typeof(decimal)) || + (type == typeof(DateTime)) || + (type == typeof(Guid)); } } From cd92e17e469ac3205a59eba6b0403bba06ba42d2 Mon Sep 17 00:00:00 2001 From: AliReZa Sabouri Date: Sat, 22 Jun 2024 04:01:37 +0200 Subject: [PATCH 08/13] test: mapping generation for simple types --- .../Gridify.Tests/IssueTests/Issue168Tests.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 test/Gridify.Tests/IssueTests/Issue168Tests.cs diff --git a/test/Gridify.Tests/IssueTests/Issue168Tests.cs b/test/Gridify.Tests/IssueTests/Issue168Tests.cs new file mode 100644 index 00000000..40144c09 --- /dev/null +++ b/test/Gridify.Tests/IssueTests/Issue168Tests.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Xunit; + +namespace Gridify.Tests.IssueTests; + +public class Issue168Tests +{ + [Fact] + [SuppressMessage("ReSharper", "InconsistentNaming")] + public void IssueStory() + { + // arrange + var dataSource = new List() + { + new() {FavouriteColors = ["Green", "Blue"]}, + new() {FavouriteColors = ["White", "Yellow"]}, + }.AsQueryable(); + + var mapper = new GridifyMapper() + .AddMap("FavouriteColors", w => w.FavouriteColors.Select(Param_0 => Param_0)); + var expected = dataSource.ApplyFiltering("FavouriteColors=Red|FavouriteColors=Blue", mapper); + + // act + var actual = dataSource.ApplyFiltering("FavouriteColors=Red|FavouriteColors=Blue"); + + // assert + Assert.Equal(expected.ToString(), actual.ToString()); + var actualList = actual.ToList(); + Assert.Equal(expected.ToList(), actualList); + Assert.Single(actualList); + } + private class Artist + { + public List FavouriteColors { get; set; } = []; + } +} From 62d21186e1d524190198e25098c54fa0d5554996 Mon Sep 17 00:00:00 2001 From: AliReZa Sabouri Date: Sat, 22 Jun 2024 17:48:37 +0200 Subject: [PATCH 09/13] feat: generate Contains method instead of Any for SimpleType collections --- src/Gridify/GridifyMapper.cs | 52 +++---------------- src/Gridify/QueryBuilders/LinqQueryBuilder.cs | 44 +++++++++++++--- src/Gridify/Syntax/SimpleTypeHelper.cs | 44 ++++++++++++++++ 3 files changed, 86 insertions(+), 54 deletions(-) create mode 100644 src/Gridify/Syntax/SimpleTypeHelper.cs diff --git a/src/Gridify/GridifyMapper.cs b/src/Gridify/GridifyMapper.cs index b170f5b1..d5a0bcf8 100644 --- a/src/Gridify/GridifyMapper.cs +++ b/src/Gridify/GridifyMapper.cs @@ -1,9 +1,8 @@ -#nullable enable using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Reflection; +using Gridify.Syntax; namespace Gridify; @@ -48,10 +47,7 @@ public IGridifyMapper AddMap(string from, Func? convertor = n Expression> to; try { - var expression = CreateExpression(from); - if (expression is null) - return this; - to = expression; + to = CreateExpression(from); ; } catch (Exception) { @@ -85,7 +81,7 @@ private void GenerateMappingsRecursive(Type type, string prefix, ushort maxNesti var fullName = string.IsNullOrEmpty(prefix) ? propertyName : $"{prefix}.{propertyName}"; // Skip classes if nestingLevel is exceeded - if (item.PropertyType.IsClass && item.PropertyType != typeof(string)) + if (item.PropertyType.IsClass && item.PropertyType != typeof(string) && !item.PropertyType.IsSimpleTypeCollection(out _)) { if (currentDepth >= maxNestingDepth) { @@ -214,14 +210,14 @@ public IEnumerable> GetCurrentMaps() /// a comma seperated string public override string ToString() => string.Join(",", _mappings.Select(q => q.From)); - internal static Expression>? CreateExpression(string from) // TODO: handle possible nulls + internal static Expression> CreateExpression(string from) // TODO: handle possible nulls { // Param_x => var parameter = Expression.Parameter(typeof(T), "__" + typeof(T).Name); // Param_x.Name, Param_x.yyy.zz.xx var mapProperty = from.Split('.').Aggregate(parameter, Expression.Property); - if (!IsListOrArrayOfPrimitivesOrString(mapProperty.Type, out var genericType)) + if (!mapProperty.Type.IsSimpleTypeCollection(out var genericType)) { // (object)Param_x.Name var convertedExpression = Expression.Convert(mapProperty, typeof(object)); @@ -229,9 +225,7 @@ public IEnumerable> GetCurrentMaps() return Expression.Lambda>(convertedExpression, parameter); } - var selectMethod = GetSelectMethod([genericType!, genericType!]); - if (selectMethod is null) - return null; + var selectMethod = genericType!.GetSimpleTypeSelectMethod(); var predicateParameter = Expression.Parameter(genericType!); var predicate = Expression.Lambda(predicateParameter, predicateParameter); // Param_x.Name.Select(fc => fc) @@ -239,39 +233,5 @@ public IEnumerable> GetCurrentMaps() return Expression.Lambda>(body, parameter); } - private static MethodInfo? GetSelectMethod(Type[] type) - { - return typeof(Enumerable).GetMethods().First(m => m.Name == "Select").MakeGenericMethod(type); - } - - private static bool IsListOrArrayOfPrimitivesOrString(Type type, out Type? itemType) - { - itemType = null; - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) - { - var arguments = type.GetGenericArguments(); - if (arguments.Length != 1 || !IsPrimitiveOrKnownType(arguments[0])) return false; - - itemType = arguments[0]; - return true; - } - if (!type.IsArray) return false; - - var elementType = type.GetElementType(); - if (elementType == null || !IsPrimitiveOrKnownType(elementType)) return false; - itemType = elementType; - return true; - } - - private static bool IsPrimitiveOrKnownType(Type type) - { - // Primitive types in C# include: int, float, double, char, bool, etc. - // Also consider the known primitive types and string explicitly - return type.IsPrimitive || - (type == typeof(string)) || - (type == typeof(decimal)) || - (type == typeof(DateTime)) || - (type == typeof(Guid)); - } } diff --git a/src/Gridify/QueryBuilders/LinqQueryBuilder.cs b/src/Gridify/QueryBuilders/LinqQueryBuilder.cs index d98ecb3c..a4e91e47 100644 --- a/src/Gridify/QueryBuilders/LinqQueryBuilder.cs +++ b/src/Gridify/QueryBuilders/LinqQueryBuilder.cs @@ -27,7 +27,7 @@ internal class LinqQueryBuilder(IGridifyMapper mapper) : BaseQueryBuilder< if (conditionExp is not LambdaExpression lambdaExp) return null; - return ParseMethodCallExpression(selectExp, lambdaExp) as Expression>; + return LinqQueryBuilder.ParseMethodCallExpression(selectExp, lambdaExp, op) as Expression>; } case ConditionalExpression cExp: { @@ -175,10 +175,10 @@ protected override Expression> BuildAlwaysFalseQuery(ParameterExpr be = GetLessThanOrEqualExpression(body, valueExpression, value); break; case SyntaxKind.Like: - be = Expression.Call(body, GetContainsMethod(), GetValueExpression(body.Type, value)); + be = Expression.Call(body, GetStringContainsMethod(), GetValueExpression(body.Type, value)); break; case SyntaxKind.NotLike: - be = Expression.Not(Expression.Call(body, GetContainsMethod(), GetValueExpression(body.Type, value))); + be = Expression.Not(Expression.Call(body, GetStringContainsMethod(), GetValueExpression(body.Type, value))); break; case SyntaxKind.StartsWith: if (body.Type != typeof(string)) @@ -269,7 +269,7 @@ protected override Expression> CombineWithOrOperator(Expression>; + return LinqQueryBuilder.ParseMethodCallExpression(selectExp, lambdaExp, op) as Expression>; } case ConditionalExpression cExp: { @@ -296,18 +296,27 @@ protected override Expression> CombineWithOrOperator(Expression.ParseMethodCallExpression(subExp, newPredicate, op); } case MethodCallExpression { Method.Name: "Select" } subExp when subExp.Arguments.Last() is LambdaExpression @@ -317,13 +326,32 @@ when subExp.Arguments.Last() is LambdaExpression { var newExp = new ReplaceExpressionVisitor(predicate.Parameters[0], lambdaMember).Visit(predicate.Body); var newPredicate = GetExpressionWithNullCheck(lambdaMember, lambda.Parameters[0], newExp); - return ParseMethodCallExpression(subExp, newPredicate); + return LinqQueryBuilder.ParseMethodCallExpression(subExp, newPredicate, op); } default: throw new InvalidOperationException(); } } + private static LambdaExpression GetContainsExpression(MemberExpression member, BinaryExpression binaryExpression, ISyntaxNode op) + { + var param = GetParameterExpression(member); + var prop = GetPropertyOrField(member, param); + + var tp = prop.Type.IsGenericType + ? prop.Type.GenericTypeArguments.First() // list + : prop.Type.GetElementType(); // array + + if (tp == null) throw new GridifyFilteringException($"Can not detect the '{member.Member.Name}' property type."); + var containsMethod = typeof(Enumerable).GetMethods().First(x => x.Name == "Contains").MakeGenericMethod(tp); + Expression containsExp = Expression.Call(containsMethod, prop, binaryExpression.Right); + if (op.Kind == SyntaxKind.NotEqual) + { + containsExp = Expression.Not(containsExp); + } + return GetExpressionWithNullCheck(prop, param, containsExp); + } + private static ParameterExpression GetParameterExpression(MemberExpression member) { return member.Expression switch @@ -451,7 +479,7 @@ private static MethodInfo GetStartWithMethod() return typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!; } - private static MethodInfo GetContainsMethod() + private static MethodInfo GetStringContainsMethod() { return typeof(string).GetMethod("Contains", new[] { typeof(string) })!; } diff --git a/src/Gridify/Syntax/SimpleTypeHelper.cs b/src/Gridify/Syntax/SimpleTypeHelper.cs new file mode 100644 index 00000000..c88c0eac --- /dev/null +++ b/src/Gridify/Syntax/SimpleTypeHelper.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Gridify.Syntax; + +internal static class SimpleTypeHelper +{ + internal static bool IsSimpleTypeCollection(this Type type, out Type? itemType) + { + itemType = null; + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) + { + var arguments = type.GetGenericArguments(); + if (arguments.Length != 1 || !IsSimpleType(arguments[0])) return false; + + itemType = arguments[0]; + return true; + } + if (!type.IsArray) return false; + + var elementType = type.GetElementType(); + if (elementType == null || !IsSimpleType(elementType)) return false; + itemType = elementType; + return true; + } + + internal static bool IsSimpleType(this Type type) + { + // Primitive types in C# include: int, float, double, char, bool, etc. + // Also consider the known primitive types and string explicitly + return type.IsPrimitive || type.IsValueType || + (type == typeof(string)) || + (type == typeof(decimal)) || + (type == typeof(DateTime)) || + (type == typeof(Guid)); + } + + public static MethodInfo GetSimpleTypeSelectMethod(this Type type) + { + return typeof(Enumerable).GetMethods().First(m => m.Name == "Select").MakeGenericMethod([type, type]); + } +} From 3399e4210f39789b5fa6bc562e3e277221ac9621 Mon Sep 17 00:00:00 2001 From: AliReZa Sabouri Date: Sat, 22 Jun 2024 17:49:15 +0200 Subject: [PATCH 10/13] test: simpleType collections --- .../Gridify.Tests/IssueTests/Issue168Tests.cs | 141 ++++++++++++++++-- 1 file changed, 130 insertions(+), 11 deletions(-) diff --git a/test/Gridify.Tests/IssueTests/Issue168Tests.cs b/test/Gridify.Tests/IssueTests/Issue168Tests.cs index 40144c09..d8d42fdc 100644 --- a/test/Gridify.Tests/IssueTests/Issue168Tests.cs +++ b/test/Gridify.Tests/IssueTests/Issue168Tests.cs @@ -5,25 +5,26 @@ namespace Gridify.Tests.IssueTests; +[SuppressMessage("ReSharper", "InconsistentNaming")] +[SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract")] public class Issue168Tests { [Fact] - [SuppressMessage("ReSharper", "InconsistentNaming")] - public void IssueStory() + public void ApplyFiltering_WithoutMapper_ShouldGenerateMappingsForListOfStrings() { // arrange - var dataSource = new List() + var dataSource = new List() { - new() {FavouriteColors = ["Green", "Blue"]}, - new() {FavouriteColors = ["White", "Yellow"]}, + new() {FavouriteColorList = ["Green", "Blue"]}, + new() {FavouriteColorList = ["White", "Yellow"]}, }.AsQueryable(); - var mapper = new GridifyMapper() - .AddMap("FavouriteColors", w => w.FavouriteColors.Select(Param_0 => Param_0)); - var expected = dataSource.ApplyFiltering("FavouriteColors=Red|FavouriteColors=Blue", mapper); + var mapper = new GridifyMapper() + .AddMap("FavouriteColorList", w => w.FavouriteColorList.Select(Param_0 => Param_0)); + var expected = dataSource.ApplyFiltering("FavouriteColorList=Red|FavouriteColorList=Blue", mapper); // act - var actual = dataSource.ApplyFiltering("FavouriteColors=Red|FavouriteColors=Blue"); + var actual = dataSource.ApplyFiltering("FavouriteColorList=Red|FavouriteColorList=Blue"); // assert Assert.Equal(expected.ToString(), actual.ToString()); @@ -31,8 +32,126 @@ public void IssueStory() Assert.Equal(expected.ToList(), actualList); Assert.Single(actualList); } - private class Artist + + [Fact] + public void GenerateMappings_ShouldGenerateMappingsForSimpleTypes() + { + var gm = new GridifyMapper() + .GenerateMappings(); + + Assert.Equal(typeof(Test).GetProperties().Length, gm.GetCurrentMaps().Count()); + } + + [Fact] + public void ApplyFiltering_WithoutMapper_ShouldGenerateMappingsForArrayOfStrings() + { + // arrange + var dataSource = new List() + { + new() {FavouriteColorsArray = ["Green", "Blue"]}, + new() {FavouriteColorsArray = ["White", "Yellow"]}, + }.AsQueryable(); + + var mapper = new GridifyMapper() + .AddMap("FavouriteColorsArray", w => w.FavouriteColorsArray.Select(Param_0 => Param_0)); + var expected = dataSource.ApplyFiltering("FavouriteColorsArray=Red|FavouriteColorsArray=Blue", mapper); + + // act + var actual = dataSource.ApplyFiltering("FavouriteColorsArray=Red|FavouriteColorsArray=Blue"); + + // assert + Assert.Equal(expected.ToString(), actual.ToString()); + var actualList = actual.ToList(); + Assert.Equal(expected.ToList(), actualList); + Assert.Single(actualList); + } + + [Fact] + public void ApplyFiltering_WithoutMapper_ShouldGenerateTheCorrectQuery() + { + // arrange + var dataSource = new List() + { + new() {NumbersArray = [1, 2]}, + new() {NumbersArray = [3, 4]}, + }.AsQueryable(); + + var expected = dataSource.Where(__Test => __Test.NumbersArray != null && __Test.NumbersArray.Contains(2)); + + // act + var actual = dataSource.ApplyFiltering("NumbersArray=2"); + + // assert + Assert.Equal(expected.ToString(), actual.ToString()); + var actualList = actual.ToList(); + Assert.Equal(expected.ToList(), actualList); + Assert.Single(actualList); + GridifyGlobalConfiguration.DisableNullChecks = false; + } + + [Fact] + public void ApplyFiltering_WithoutMapperWithMultipleConditions_ShouldGenerateTheCorrectQuery() { - public List FavouriteColors { get; set; } = []; + // arrange + var dataSource = new List() + { + new() {NumbersArray = [1, 2]}, + new() {NumbersArray = [3, 4]}, + }.AsQueryable(); + + var expected = dataSource.Where(__Test => + __Test.NumbersArray != null && __Test.NumbersArray.Contains(2) || __Test.NumbersArray != null && __Test.NumbersArray.Contains(5)); + + // act + var actual = dataSource.ApplyFiltering("NumbersArray=2|NumbersArray=5"); + + // assert + Assert.Equal(expected.ToString(), actual.ToString()); + var actualList = actual.ToList(); + Assert.Equal(expected.ToList(), actualList); + Assert.Single(actualList); + GridifyGlobalConfiguration.DisableNullChecks = false; + } + + [Fact] + public void ApplyFiltering_WithoutMapper_ShouldGenerateMappingsForEnumArray() + { + // arrange + var dataSource = new List() + { + new() {EnumsArray = [Test.MyEnum.Item1, Test.MyEnum.Item2]}, + new() {EnumsArray = [Test.MyEnum.Item1]}, + }.AsQueryable(); + + var expected = dataSource.Where(__Test => + __Test.EnumsArray != null && __Test.EnumsArray.Contains(Test.MyEnum.Item1) || + __Test.EnumsArray != null && __Test.EnumsArray.Contains(Test.MyEnum.Item2)); + + // act + var actual = dataSource.ApplyFiltering("EnumsArray=Item1|EnumsArray=Item2"); + + // assert + Assert.Equal(expected.ToString(), actual.ToString()); + var actualList = actual.ToList(); + Assert.Equal(expected.ToList(), actualList); + Assert.Equal(2, actualList.Count); + } + + + private class Test + { + public List FavouriteColorList { get; set; } = []; + public string[] FavouriteColorsArray { get; set; } = []; + + public int[] NumbersArray { get; set; } = []; + public IEnumerable NumbersEnumerable { get; set; } = []; + public MyEnum[] EnumsArray { get; set; } = []; + + public enum MyEnum + { + Item1, + Item2 + } + } } From 3e05abae14b7e4b89fa6de4eb489a7701474bac3 Mon Sep 17 00:00:00 2001 From: AliReZa Sabouri Date: Sat, 22 Jun 2024 17:53:48 +0200 Subject: [PATCH 11/13] refactor: LinqQueryBuilder.cs --- src/Gridify/QueryBuilders/LinqQueryBuilder.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Gridify/QueryBuilders/LinqQueryBuilder.cs b/src/Gridify/QueryBuilders/LinqQueryBuilder.cs index a4e91e47..8584f04f 100644 --- a/src/Gridify/QueryBuilders/LinqQueryBuilder.cs +++ b/src/Gridify/QueryBuilders/LinqQueryBuilder.cs @@ -27,7 +27,7 @@ internal class LinqQueryBuilder(IGridifyMapper mapper) : BaseQueryBuilder< if (conditionExp is not LambdaExpression lambdaExp) return null; - return LinqQueryBuilder.ParseMethodCallExpression(selectExp, lambdaExp, op) as Expression>; + return ParseMethodCallExpression(selectExp, lambdaExp, op) as Expression>; } case ConditionalExpression cExp: { @@ -269,7 +269,7 @@ protected override Expression> CombineWithOrOperator(Expression.ParseMethodCallExpression(selectExp, lambdaExp, op) as Expression>; + return ParseMethodCallExpression(selectExp, lambdaExp, op) as Expression>; } case ConditionalExpression cExp: { @@ -316,7 +316,7 @@ when subExp.Arguments.Last() is LambdaExpression { Body: MemberExpression lambdaMember }: { var newPredicate = GetAnyExpression(lambdaMember, predicate); - return LinqQueryBuilder.ParseMethodCallExpression(subExp, newPredicate, op); + return ParseMethodCallExpression(subExp, newPredicate, op); } case MethodCallExpression { Method.Name: "Select" } subExp when subExp.Arguments.Last() is LambdaExpression @@ -326,7 +326,7 @@ when subExp.Arguments.Last() is LambdaExpression { var newExp = new ReplaceExpressionVisitor(predicate.Parameters[0], lambdaMember).Visit(predicate.Body); var newPredicate = GetExpressionWithNullCheck(lambdaMember, lambda.Parameters[0], newExp); - return LinqQueryBuilder.ParseMethodCallExpression(subExp, newPredicate, op); + return ParseMethodCallExpression(subExp, newPredicate, op); } default: throw new InvalidOperationException(); @@ -343,7 +343,7 @@ private static LambdaExpression GetContainsExpression(MemberExpression member, B : prop.Type.GetElementType(); // array if (tp == null) throw new GridifyFilteringException($"Can not detect the '{member.Member.Name}' property type."); - var containsMethod = typeof(Enumerable).GetMethods().First(x => x.Name == "Contains").MakeGenericMethod(tp); + var containsMethod = GetContainsMethod(tp); Expression containsExp = Expression.Call(containsMethod, prop, binaryExpression.Right); if (op.Kind == SyntaxKind.NotEqual) { @@ -352,6 +352,8 @@ private static LambdaExpression GetContainsExpression(MemberExpression member, B return GetExpressionWithNullCheck(prop, param, containsExp); } + + private static ParameterExpression GetParameterExpression(MemberExpression member) { return member.Expression switch @@ -484,6 +486,11 @@ private static MethodInfo GetStringContainsMethod() return typeof(string).GetMethod("Contains", new[] { typeof(string) })!; } + private static MethodInfo GetContainsMethod(Type tp) + { + return typeof(Enumerable).GetMethods().First(x => x.Name == "Contains").MakeGenericMethod(tp); + } + private static MethodInfo GetIsNullOrEmptyMethod() { return typeof(string).GetMethod("IsNullOrEmpty", new[] { typeof(string) })!; From 445bbb4d5ed877f348e6c4506682addf53c053c3 Mon Sep 17 00:00:00 2001 From: AliReZa Sabouri Date: Sat, 22 Jun 2024 17:58:59 +0200 Subject: [PATCH 12/13] build: bump to v2.15.0-preview2 --- docs/.vitepress/configs/version.ts | 2 +- src/Gridify.Elasticsearch/Gridify.Elasticsearch.csproj | 2 +- src/Gridify.EntityFramework/Gridify.EntityFramework.csproj | 2 +- src/Gridify/Gridify.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/.vitepress/configs/version.ts b/docs/.vitepress/configs/version.ts index c2658c11..1cdaf898 100644 --- a/docs/.vitepress/configs/version.ts +++ b/docs/.vitepress/configs/version.ts @@ -1 +1 @@ -export const version: string = '2.15.0-preview1' +export const version: string = '2.15.0-preview2' diff --git a/src/Gridify.Elasticsearch/Gridify.Elasticsearch.csproj b/src/Gridify.Elasticsearch/Gridify.Elasticsearch.csproj index fd43b34e..b8491ae2 100644 --- a/src/Gridify.Elasticsearch/Gridify.Elasticsearch.csproj +++ b/src/Gridify.Elasticsearch/Gridify.Elasticsearch.csproj @@ -1,7 +1,7 @@ Gridify.Elasticsearch - 2.15.0-preview1 + 2.15.0-preview2 Alireza Sabouri; Dzmitry Koush Gridify (Elasticsearch), Easy way to apply Filtering, Sorting, and Pagination using text-based data. https://github.com/alirezanet/Gridify diff --git a/src/Gridify.EntityFramework/Gridify.EntityFramework.csproj b/src/Gridify.EntityFramework/Gridify.EntityFramework.csproj index 45853b62..f4ab0aac 100644 --- a/src/Gridify.EntityFramework/Gridify.EntityFramework.csproj +++ b/src/Gridify.EntityFramework/Gridify.EntityFramework.csproj @@ -1,7 +1,7 @@ Gridify.EntityFramework - 2.15.0-preview1 + 2.15.0-preview2 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 90820acc..139929a7 100644 --- a/src/Gridify/Gridify.csproj +++ b/src/Gridify/Gridify.csproj @@ -2,7 +2,7 @@ Gridify - 2.15.0-preview1 + 2.15.0-preview2 Alireza Sabouri TuxTeam Gridify, Easy and optimized way to apply Filtering, Sorting, and Pagination using text-based data. From bb2b85d1ea238960d0ba7a93e8b22342d1441154 Mon Sep 17 00:00:00 2001 From: AliReZa Sabouri Date: Sat, 22 Jun 2024 18:10:57 +0200 Subject: [PATCH 13/13] chore: remove extra comments --- src/Gridify/GridifyMapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gridify/GridifyMapper.cs b/src/Gridify/GridifyMapper.cs index d5a0bcf8..7d190ebd 100644 --- a/src/Gridify/GridifyMapper.cs +++ b/src/Gridify/GridifyMapper.cs @@ -210,7 +210,7 @@ public IEnumerable> GetCurrentMaps() /// a comma seperated string public override string ToString() => string.Join(",", _mappings.Select(q => q.From)); - internal static Expression> CreateExpression(string from) // TODO: handle possible nulls + internal static Expression> CreateExpression(string from) { // Param_x => var parameter = Expression.Parameter(typeof(T), "__" + typeof(T).Name);