From e3660877ef448382a09d3a8e3752226ffe312a11 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 12 Feb 2018 21:27:09 -0600 Subject: [PATCH] collection handling. and auto null checking if enabled. --- .../ComplexQueryTest.cs | 86 ++++++++++++++++++- PoweredSoft.DynamicLinq/Constants.cs | 3 +- .../Fluent/QueryBuilder.cs | 31 +++++-- .../Fluent/QueryBuilderFilter.cs | 2 +- .../Helpers/QueryableHelpers.cs | 57 ++++++++---- 5 files changed, 148 insertions(+), 31 deletions(-) diff --git a/PoweredSoft.DynamicLinq.Test/ComplexQueryTest.cs b/PoweredSoft.DynamicLinq.Test/ComplexQueryTest.cs index e90cb9e..ead086e 100644 --- a/PoweredSoft.DynamicLinq.Test/ComplexQueryTest.cs +++ b/PoweredSoft.DynamicLinq.Test/ComplexQueryTest.cs @@ -99,6 +99,81 @@ namespace PoweredSoft.DynamicLinq.Test Assert.IsTrue(second.Id == 1); } + [TestMethod] + public void TestCreateFilterExpressionCheckNull() + { + var authors = new List() + { + new Author + { + Id = 1, + FirstName = "David", + LastName = "Lebee", + Posts = new List + { + new Post + { + Id = 1, + AuthorId = 1, + Title = "Match", + Content = "ABC", + Comments = new List() + { + new Comment() + { + Id = 1, + DisplayName = "John Doe", + CommentText = "!@#$!@#!@#", + Email = "john.doe@me.com" + } + } + }, + new Post + { + Id = 2, + AuthorId = 1, + Title = "Match", + Content = "ABC" + } + } + }, + new Author + { + Id = 2, + FirstName = "Chuck", + LastName = "Norris", + Posts = new List + { + new Post + { + Id = 3, + AuthorId = 2, + Title = "Match", + Content = "ASD" + }, + new Post + { + Id = 4, + AuthorId = 2, + Title = "DontMatch", + Content = "ASD" + } + } + } + }; + + // the query. + var query = authors.AsQueryable(); + + query = query.Query(qb => + { + qb.NullChecking(); + qb.And("Posts.Comments.Email", ConditionOperators.Equal, "john.doe@me.com", collectionHandling: QueryCollectionHandling.Any); + }); + + Assert.AreEqual(1, query.Count()); + } + [TestMethod] public void TestCreateFilterExpression() { @@ -133,7 +208,8 @@ namespace PoweredSoft.DynamicLinq.Test Id = 2, AuthorId = 1, Title = "Match", - Content = "ABC" + Content = "ABC", + Comments = new List() } } }, @@ -150,6 +226,7 @@ namespace PoweredSoft.DynamicLinq.Test AuthorId = 2, Title = "Match", Content = "ASD", + Comments = new List() }, new Post { @@ -157,6 +234,7 @@ namespace PoweredSoft.DynamicLinq.Test AuthorId = 2, Title = "DontMatch", Content = "ASD", + Comments = new List() } } } @@ -165,10 +243,12 @@ namespace PoweredSoft.DynamicLinq.Test // the query. var query = authors.AsQueryable(); - var allExpression = QueryableHelpers.CreateFilterExpression("Posts.Title", ConditionOperators.Equal, "Match", QueryConvertStrategy.ConvertConstantToComparedPropertyOrField, QueryCollectionCondition.All); - var anyExpression = QueryableHelpers.CreateFilterExpression("Posts.Title", ConditionOperators.Equal, "Match", QueryConvertStrategy.ConvertConstantToComparedPropertyOrField, QueryCollectionCondition.Any); + var allExpression = QueryableHelpers.CreateFilterExpression("Posts.Title", ConditionOperators.Equal, "Match", QueryConvertStrategy.ConvertConstantToComparedPropertyOrField, QueryCollectionHandling.All); + var anyExpression = QueryableHelpers.CreateFilterExpression("Posts.Title", ConditionOperators.Equal, "Match", QueryConvertStrategy.ConvertConstantToComparedPropertyOrField, QueryCollectionHandling.Any); + var anyExpression2 = QueryableHelpers.CreateFilterExpression("Posts.Comments.Email", ConditionOperators.Equal, "John.doe@me.com", QueryConvertStrategy.ConvertConstantToComparedPropertyOrField, QueryCollectionHandling.Any); Assert.AreEqual(1, query.Count(allExpression)); Assert.AreEqual(2, query.Count(anyExpression)); + Assert.AreEqual(1, query.Count(anyExpression2)); } } } diff --git a/PoweredSoft.DynamicLinq/Constants.cs b/PoweredSoft.DynamicLinq/Constants.cs index dbc22af..eb36959 100644 --- a/PoweredSoft.DynamicLinq/Constants.cs +++ b/PoweredSoft.DynamicLinq/Constants.cs @@ -10,6 +10,7 @@ namespace PoweredSoft.DynamicLinq public enum ConditionOperators { Equal, + NotEqual, GreaterThan, GreaterThanOrEqual, LessThan, @@ -25,7 +26,7 @@ namespace PoweredSoft.DynamicLinq ConvertConstantToComparedPropertyOrField } - public enum QueryCollectionCondition + public enum QueryCollectionHandling { Any, All diff --git a/PoweredSoft.DynamicLinq/Fluent/QueryBuilder.cs b/PoweredSoft.DynamicLinq/Fluent/QueryBuilder.cs index 352357a..f39ac7d 100644 --- a/PoweredSoft.DynamicLinq/Fluent/QueryBuilder.cs +++ b/PoweredSoft.DynamicLinq/Fluent/QueryBuilder.cs @@ -15,6 +15,8 @@ namespace PoweredSoft.DynamicLinq.Fluent public Type QueryableType { get; set; } + public bool IsNullCheckingEnabled { get; protected set; } = false; + public List Filters { get; protected set; } = new List(); public List Sorts { get; protected set; } = new List(); @@ -24,9 +26,15 @@ namespace PoweredSoft.DynamicLinq.Fluent Query = query; } + public QueryBuilder NullChecking(bool check = true) + { + IsNullCheckingEnabled = check; + return this; + } + public virtual QueryBuilder Compare(string path, ConditionOperators conditionOperators, object value, QueryConvertStrategy convertStrategy = QueryConvertStrategy.ConvertConstantToComparedPropertyOrField, - bool and = true) + bool and = true, QueryCollectionHandling collectionHandling = QueryCollectionHandling.Any) { Filters.Add(new QueryBuilderFilter { @@ -34,7 +42,8 @@ namespace PoweredSoft.DynamicLinq.Fluent ConditionOperator = conditionOperators, Path = path, Value = value, - ConvertStrategy = convertStrategy + ConvertStrategy = convertStrategy, + CollectionHandling = collectionHandling }); return this; @@ -55,6 +64,7 @@ namespace PoweredSoft.DynamicLinq.Fluent { // create query builder for same type. var qb = new QueryBuilder(Query); + qb.NullChecking(IsNullCheckingEnabled); // callback. subQuery(qb); @@ -69,11 +79,13 @@ namespace PoweredSoft.DynamicLinq.Fluent return this; } - public QueryBuilder And(string path, ConditionOperators conditionOperator, object value, QueryConvertStrategy convertStrategy = QueryConvertStrategy.ConvertConstantToComparedPropertyOrField) - => Compare(path, conditionOperator, value, convertStrategy: convertStrategy, and: true); + public QueryBuilder And(string path, ConditionOperators conditionOperator, object value, + QueryConvertStrategy convertStrategy = QueryConvertStrategy.ConvertConstantToComparedPropertyOrField, QueryCollectionHandling collectionHandling = QueryCollectionHandling.Any) + => Compare(path, conditionOperator, value, convertStrategy: convertStrategy, collectionHandling: collectionHandling, and: true); - public QueryBuilder Or(string path, ConditionOperators conditionOperator, object value, QueryConvertStrategy convertStrategy = QueryConvertStrategy.ConvertConstantToComparedPropertyOrField) - => Compare(path, conditionOperator, value, convertStrategy: convertStrategy, and: false); + public QueryBuilder Or(string path, ConditionOperators conditionOperator, object value, + QueryConvertStrategy convertStrategy = QueryConvertStrategy.ConvertConstantToComparedPropertyOrField, QueryCollectionHandling collectionHandling = QueryCollectionHandling.Any) + => Compare(path, conditionOperator, value, convertStrategy: convertStrategy, collectionHandling: collectionHandling, and: false); public QueryBuilder And(Action> subQuery) => SubQuery(subQuery, true); @@ -189,9 +201,9 @@ namespace PoweredSoft.DynamicLinq.Fluent else { if (filter.And) - temp = Expression.Lambda>(Expression.And(temp.Body, innerExpression.Body), parameter); + temp = Expression.Lambda>(Expression.AndAlso(temp.Body, innerExpression.Body), parameter); else - temp = Expression.Lambda>(Expression.Or(temp.Body, innerExpression.Body), parameter); + temp = Expression.Lambda>(Expression.OrElse(temp.Body, innerExpression.Body), parameter); } }); @@ -207,7 +219,8 @@ namespace PoweredSoft.DynamicLinq.Fluent filter.Value, filter.ConvertStrategy, filter.CollectionHandling, - parameter: parameter + parameter: parameter, + nullChecking: IsNullCheckingEnabled ); return ret; diff --git a/PoweredSoft.DynamicLinq/Fluent/QueryBuilderFilter.cs b/PoweredSoft.DynamicLinq/Fluent/QueryBuilderFilter.cs index 277fac4..b9b727f 100644 --- a/PoweredSoft.DynamicLinq/Fluent/QueryBuilderFilter.cs +++ b/PoweredSoft.DynamicLinq/Fluent/QueryBuilderFilter.cs @@ -14,6 +14,6 @@ namespace PoweredSoft.DynamicLinq.Fluent public bool And { get; set; } public QueryConvertStrategy ConvertStrategy { get; set; } public List Filters { get; set; } = new List(); - public QueryCollectionCondition CollectionHandling { get; set; } + public QueryCollectionHandling CollectionHandling { get; set; } } } diff --git a/PoweredSoft.DynamicLinq/Helpers/QueryableHelpers.cs b/PoweredSoft.DynamicLinq/Helpers/QueryableHelpers.cs index 3378c1f..1a09465 100644 --- a/PoweredSoft.DynamicLinq/Helpers/QueryableHelpers.cs +++ b/PoweredSoft.DynamicLinq/Helpers/QueryableHelpers.cs @@ -26,6 +26,8 @@ namespace PoweredSoft.DynamicLinq.Helpers if (conditionOperator == ConditionOperators.Equal) ret = Expression.Equal(member, constant); + else if (conditionOperator == ConditionOperators.NotEqual) + ret = Expression.NotEqual(member, constant); else if (conditionOperator == ConditionOperators.GreaterThan) ret = Expression.GreaterThan(member, constant); else if (conditionOperator == ConditionOperators.GreaterThanOrEqual) @@ -119,8 +121,8 @@ namespace PoweredSoft.DynamicLinq.Helpers return query; } - internal static Expression InternalCreateFilterExpression(int recursionStep, Type type, ParameterExpression parameter, Expression current, List parts, - ConditionOperators condition, object value, QueryConvertStrategy convertStrategy, QueryCollectionCondition collectionHandling) + internal static Expression InternalCreateFilterExpression(int recursionStep, Type type, ParameterExpression parameter, Expression current, List parts, + ConditionOperators condition, object value, QueryConvertStrategy convertStrategy, QueryCollectionHandling collectionHandling, bool nullChecking) { var partStr = parts.First(); var isLast = parts.Count == 1; @@ -130,8 +132,9 @@ namespace PoweredSoft.DynamicLinq.Helpers // TODO : maybe support that last part is collection but what do we do? // not supported yet. - if (isLast && IsEnumerable(memberExpression)) - throw new NotSupportedException("Last part must not be a collection"); + if (isLast && IsEnumerable(memberExpression) && value != null) + throw new NotSupportedException("Can only compare collection to null"); + // create the expression and return it. if (isLast) @@ -141,30 +144,49 @@ namespace PoweredSoft.DynamicLinq.Helpers var lambda = Expression.Lambda(filterExpression, parameter); return lambda; } - + + // null check. + Expression nullCheckExpression = null; + if (nullChecking) + nullCheckExpression = Expression.NotEqual(memberExpression, Expression.Constant(null)); + if (IsEnumerable(memberExpression)) { var listGenericArgumentType = memberExpression.Type.GetGenericArguments().First(); var innerParameter = Expression.Parameter(listGenericArgumentType, $"t{++recursionStep}"); - var innerLambda = InternalCreateFilterExpression(recursionStep, listGenericArgumentType, innerParameter, innerParameter, parts.Skip(1).ToList(), condition, value, convertStrategy, collectionHandling); + var innerLambda = InternalCreateFilterExpression(recursionStep, listGenericArgumentType, innerParameter, innerParameter, parts.Skip(1).ToList(), condition, value, convertStrategy, collectionHandling, nullChecking); // the collection method. var collectionMethod = GetCollectionMethod(collectionHandling); var genericMethod = collectionMethod.MakeGenericMethod(listGenericArgumentType); var callResult = Expression.Call(genericMethod, memberExpression, innerLambda); - var expressionResult = Expression.Lambda(callResult, parameter); - return expressionResult; - } - // standard property or field. - return InternalCreateFilterExpression(recursionStep, type, parameter, memberExpression, parts.Skip(1).ToList(), condition, value, convertStrategy, collectionHandling); + if (nullCheckExpression != null) + { + var nullCheckResult = Expression.AndAlso(nullCheckExpression, callResult); + return Expression.Lambda(nullCheckResult, parameter); + } + + return Expression.Lambda(callResult, parameter); + } + else + { + if (nullCheckExpression != null) + { + var pathExpr = InternalCreateFilterExpression(recursionStep, type, parameter, memberExpression, parts.Skip(1).ToList(), condition, value, convertStrategy, collectionHandling, nullChecking); + var nullCheckResult = Expression.AndAlso(nullCheckExpression, pathExpr); + return nullCheckResult; + } + + return InternalCreateFilterExpression(recursionStep, type, parameter, memberExpression, parts.Skip(1).ToList(), condition, value, convertStrategy, collectionHandling, nullChecking); + } } - public static MethodInfo GetCollectionMethod(QueryCollectionCondition collectionHandling) + public static MethodInfo GetCollectionMethod(QueryCollectionHandling collectionHandling) { - if (collectionHandling == QueryCollectionCondition.All) + if (collectionHandling == QueryCollectionHandling.All) return Constants.AllMethod; - else if (collectionHandling == QueryCollectionCondition.Any) + else if (collectionHandling == QueryCollectionHandling.Any) return Constants.AnyMethod; throw new NotSupportedException($"{collectionHandling} is not supported"); @@ -174,14 +196,15 @@ namespace PoweredSoft.DynamicLinq.Helpers ConditionOperators condition, object value, QueryConvertStrategy convertStrategy, - QueryCollectionCondition collectionHandling = QueryCollectionCondition.Any, - ParameterExpression parameter = null) + QueryCollectionHandling collectionHandling = QueryCollectionHandling.Any, + ParameterExpression parameter = null, + bool nullChecking = false) { if (parameter == null) parameter = Expression.Parameter(typeof(T), "t"); var parts = path.Split('.').ToList(); - var result = InternalCreateFilterExpression(1, typeof(T), parameter, parameter, parts, condition, value, convertStrategy, collectionHandling); + var result = InternalCreateFilterExpression(1, typeof(T), parameter, parameter, parts, condition, value, convertStrategy, collectionHandling, nullChecking); var ret = result as Expression>; return ret; }