diff --git a/src/Redis.OM/Common/ExpressionParserUtilities.cs b/src/Redis.OM/Common/ExpressionParserUtilities.cs index a56e5b43..f502a609 100644 --- a/src/Redis.OM/Common/ExpressionParserUtilities.cs +++ b/src/Redis.OM/Common/ExpressionParserUtilities.cs @@ -77,19 +77,25 @@ internal static string GetOperandString(MethodCallExpression exp) /// The parameters. /// Treat enum as an integer. /// Whether or not to negate the result. + /// Treats a boolean member expression as unary. /// the operand string. /// thrown if expression is un-parseable. - internal static string GetOperandStringForQueryArgs(Expression exp, List parameters, bool treatEnumsAsInt = false, bool negate = false) + internal static string GetOperandStringForQueryArgs(Expression exp, List parameters, bool treatEnumsAsInt = false, bool negate = false, bool treatBooleanMemberAsUnary = false) { var res = exp switch { ConstantExpression constExp => ValueToString(constExp.Value), - MemberExpression member => GetOperandStringForMember(member, treatEnumsAsInt), + MemberExpression member => GetOperandStringForMember(member, treatEnumsAsInt, negate: negate, treatBooleanMemberAsUnary: treatBooleanMemberAsUnary), MethodCallExpression method => TranslateMethodStandardQuerySyntax(method, parameters), - UnaryExpression unary => GetOperandStringForQueryArgs(unary.Operand, parameters, treatEnumsAsInt, unary.NodeType == ExpressionType.Not), + UnaryExpression unary => GetOperandStringForQueryArgs(unary.Operand, parameters, treatEnumsAsInt, unary.NodeType == ExpressionType.Not, treatBooleanMemberAsUnary: treatBooleanMemberAsUnary), _ => throw new ArgumentException("Unrecognized Expression type") }; + if (treatBooleanMemberAsUnary && exp is MemberExpression memberExp && memberExp.Type == typeof(bool) && negate) + { + negate = false; + } + if (negate) { return $"-{res}"; @@ -308,7 +314,7 @@ internal static string EscapeTagField(string text) return sb.ToString(); } - private static string GetOperandStringForMember(MemberExpression member, bool treatEnumsAsInt = false) + private static string GetOperandStringForMember(MemberExpression member, bool treatEnumsAsInt = false, bool negate = false, bool treatBooleanMemberAsUnary = false) { var memberPath = new List(); var parentExpression = member.Expression; @@ -414,6 +420,12 @@ private static string GetOperandStringForMember(MemberExpression member, bool tr if (searchField != null) { var propertyName = GetSearchFieldNameFromMember(member); + if (member.Type == typeof(bool) && treatBooleanMemberAsUnary) + { + var val = negate ? "false" : "true"; + return $"@{propertyName}:{{{val}}}"; + } + return $"@{propertyName}"; } diff --git a/src/Redis.OM/Common/ExpressionTranslator.cs b/src/Redis.OM/Common/ExpressionTranslator.cs index 6de9866a..93fe4d69 100644 --- a/src/Redis.OM/Common/ExpressionTranslator.cs +++ b/src/Redis.OM/Common/ExpressionTranslator.cs @@ -349,22 +349,22 @@ internal static string TranslateBinaryExpression(BinaryExpression binExpression, sb.Append("("); sb.Append(TranslateBinaryExpression(left, parameters)); sb.Append(SplitPredicateSeporators(binExpression.NodeType)); - sb.Append(ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Right, parameters)); + sb.Append(ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Right, parameters, treatBooleanMemberAsUnary: true)); sb.Append(")"); } else if (binExpression.Right is BinaryExpression right) { sb.Append("("); - sb.Append(ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Left, parameters)); + sb.Append(ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Left, parameters, treatBooleanMemberAsUnary: true)); sb.Append(SplitPredicateSeporators(binExpression.NodeType)); sb.Append(TranslateBinaryExpression(right, parameters)); sb.Append(")"); } else { - var leftContent = ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Left, parameters); + var leftContent = ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Left, parameters, treatBooleanMemberAsUnary: true); - var rightContent = ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Right, parameters); + var rightContent = ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Right, parameters, treatBooleanMemberAsUnary: true); if (binExpression.Left is MemberExpression member) { @@ -737,6 +737,24 @@ private static RedisSortBy TranslateOrderByMethod(MethodCallExpression expressio return sb; } + private static string TranslateUnaryOrMemberExpressionIntoBooleanQuery(Expression expression, List parameters) + { + if (expression is MemberExpression member && member.Type == typeof(bool)) + { + var propertyName = ExpressionParserUtilities.GetOperandStringForQueryArgs(member, parameters); + return $"{propertyName}:{{true}}"; + } + + if (expression is UnaryExpression uni && uni.Operand is MemberExpression uniMember && uniMember.Type == typeof(bool) && uni.NodeType is ExpressionType.Not) + { + var propertyName = ExpressionParserUtilities.GetOperandStringForQueryArgs(uniMember, parameters); + return $"{propertyName}:{{false}}"; + } + + throw new InvalidOperationException( + $"Could not translate expression of type {expression.Type} to a boolean expression"); + } + private static string BuildQueryFromExpression(Expression exp, List parameters) { if (exp is BinaryExpression binExp) @@ -751,6 +769,12 @@ private static string BuildQueryFromExpression(Expression exp, List para if (exp is UnaryExpression uni) { + if (uni.Operand is MemberExpression uniMember && uniMember.Type == typeof(bool) && uni.NodeType is ExpressionType.Not) + { + var propertyName = ExpressionParserUtilities.GetOperandString(uniMember); + return $"{propertyName}:{{false}}"; + } + var operandString = BuildQueryFromExpression(uni.Operand, parameters); if (uni.NodeType == ExpressionType.Not) { @@ -786,6 +810,8 @@ private static string BuildQueryPredicate(ExpressionType expType, string left, s ExpressionType.LessThanOrEqual => $"{left}:[-inf {right}]", ExpressionType.Equal => BuildEqualityPredicate(memberExpression, right), ExpressionType.NotEqual => BuildEqualityPredicate(memberExpression, right, true), + ExpressionType.And or ExpressionType.AndAlso => $"{left} {right}", + ExpressionType.Or or ExpressionType.OrElse => $"{left} | {right}", _ => string.Empty }; return queryPredicate; diff --git a/src/Redis.OM/Searching/IRedisCollection.cs b/src/Redis.OM/Searching/IRedisCollection.cs index 93040bba..2fc123f9 100644 --- a/src/Redis.OM/Searching/IRedisCollection.cs +++ b/src/Redis.OM/Searching/IRedisCollection.cs @@ -275,6 +275,12 @@ public interface IRedisCollection : IOrderedQueryable, IAsyncEnumerable /// The Collection's count. int Count(Expression> expression); + /// + /// Retrieves the count of the collection async. + /// + /// The Collection's count. + int Count(); + /// /// Returns the first item asynchronously. /// diff --git a/src/Redis.OM/Searching/RedisCollection.cs b/src/Redis.OM/Searching/RedisCollection.cs index b73b9f6f..c0a0bdc5 100644 --- a/src/Redis.OM/Searching/RedisCollection.cs +++ b/src/Redis.OM/Searching/RedisCollection.cs @@ -459,6 +459,14 @@ public int Count(Expression> expression) return (int)_connection.Search(query).DocumentCount; } + /// + public int Count() + { + var query = ExpressionTranslator.BuildQueryFromExpression(Expression, typeof(T), BooleanExpression, RootType); + query.Limit = new SearchLimit { Number = 0, Offset = 0 }; + return (int)_connection.Search(query).DocumentCount; + } + /// public T First(Expression> expression) { diff --git a/test/Redis.OM.Unit.Tests/RediSearchTests/SearchTests.cs b/test/Redis.OM.Unit.Tests/RediSearchTests/SearchTests.cs index b78780f9..f54eb564 100644 --- a/test/Redis.OM.Unit.Tests/RediSearchTests/SearchTests.cs +++ b/test/Redis.OM.Unit.Tests/RediSearchTests/SearchTests.cs @@ -2877,6 +2877,92 @@ public void SearchTagFieldAndTextListContainsWithEscapes() "100"); } + [Fact] + public void ChainTwoBooleans() + { + int count; + _substitute.ClearSubstitute(); + _substitute.Execute(Arg.Any(), Arg.Any()).Returns(_mockReply); + IRedisCollection collection = new RedisCollection(_substitute); + collection = collection.Where(x => x.Boolean); + collection = collection.Where((x => x.Boolean)); + count = collection.Count(); + _substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "(@Boolean:{true} @Boolean:{true})", "LIMIT", "0", "0"); + Assert.Equal(1, count); + + collection = new RedisCollection(_substitute); + collection = collection.Where(x => x.Boolean || x.Boolean); + count = collection.Count(); + _substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "(@Boolean:{true} | @Boolean:{true})", "LIMIT", "0", "0"); + Assert.Equal(1, count); + + collection = new RedisCollection(_substitute); + collection = collection.Where(x => !x.Boolean || x.Boolean); + count = collection.Count(); + _substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "(@Boolean:{false} | @Boolean:{true})", "LIMIT", "0", "0"); + Assert.Equal(1, count); + + collection = new RedisCollection(_substitute); + collection = collection.Where(x => !x.Boolean); + collection = collection.Where((x => !x.Boolean)); + count = collection.Count(); + _substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "(@Boolean:{false} @Boolean:{false})", "LIMIT", "0", "0"); + Assert.Equal(1, count); + + collection = new RedisCollection(_substitute); + collection = collection.Where(x => x.Boolean); + collection = collection.Where((x => !x.Boolean)); + count = collection.Count(); + _substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "(@Boolean:{true} @Boolean:{false})", "LIMIT", "0", "0"); + Assert.Equal(1, count); + + collection = new RedisCollection(_substitute); + collection = collection.Where(x => !x.Boolean); + collection = collection.Where((x => x.Boolean)); + count = collection.Count(); + _substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "(@Boolean:{false} @Boolean:{true})", "LIMIT", "0", "0"); + Assert.Equal(1, count); + + collection = new RedisCollection(_substitute); + collection = collection.Where(x => !x.Boolean); + collection = collection.Where((x => x.Boolean)); + collection = collection.Where((x => x.Boolean)); + count = collection.Count(); + _substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "((@Boolean:{false} @Boolean:{true}) @Boolean:{true})", "LIMIT", "0", "0"); + Assert.Equal(1, count); + + collection = new RedisCollection(_substitute); + collection = collection.Where(x => !x.Boolean); + collection = collection.Where((x => x.Boolean)); + collection = collection.Where((x => !x.Boolean)); + count = collection.Count(); + _substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "((@Boolean:{false} @Boolean:{true}) @Boolean:{false})", "LIMIT", "0", "0"); + Assert.Equal(1, count); + + collection = new RedisCollection(_substitute); + collection = collection.Where(x => !x.Boolean || x.Boolean || !x.Boolean); + count = collection.Count(); + _substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "((@Boolean:{false} | @Boolean:{true}) | @Boolean:{false})", "LIMIT", "0", "0"); + Assert.Equal(1, count); + } + + [Fact] + public void SearchWithEmptyCount() + { + _substitute.ClearSubstitute(); + _substitute.Execute(Arg.Any(), Arg.Any()).Returns(_mockReply); + var collection = new RedisCollection(_substitute); + var count = collection.Count(x => x.Boolean && x.AnEnum == AnEnum.three && x.Boolean == false); + _substitute.Received().Execute( + "FT.SEARCH", + "objectwithstringlikevaluetypes-idx", + "((@Boolean:{true} (@AnEnum:{three})) (@Boolean:{False}))", + "LIMIT", + "0", + "0"); + Assert.Equal(1, count); + } + [Fact] public void SearchWithEmptyAny() {