Skip to content

Commit 2b516c0

Browse files
authored
Adding arg-less count to RedisCollection (#426)
* argless count to RedisCollection * fixing multi-bool logic parsing logic
1 parent eda5816 commit 2b516c0

File tree

5 files changed

+146
-8
lines changed

5 files changed

+146
-8
lines changed

src/Redis.OM/Common/ExpressionParserUtilities.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,19 +77,25 @@ internal static string GetOperandString(MethodCallExpression exp)
7777
/// <param name="parameters">The parameters.</param>
7878
/// <param name="treatEnumsAsInt">Treat enum as an integer.</param>
7979
/// <param name="negate">Whether or not to negate the result.</param>
80+
/// <param name="treatBooleanMemberAsUnary">Treats a boolean member expression as unary.</param>
8081
/// <returns>the operand string.</returns>
8182
/// <exception cref="ArgumentException">thrown if expression is un-parseable.</exception>
82-
internal static string GetOperandStringForQueryArgs(Expression exp, List<object> parameters, bool treatEnumsAsInt = false, bool negate = false)
83+
internal static string GetOperandStringForQueryArgs(Expression exp, List<object> parameters, bool treatEnumsAsInt = false, bool negate = false, bool treatBooleanMemberAsUnary = false)
8384
{
8485
var res = exp switch
8586
{
8687
ConstantExpression constExp => ValueToString(constExp.Value),
87-
MemberExpression member => GetOperandStringForMember(member, treatEnumsAsInt),
88+
MemberExpression member => GetOperandStringForMember(member, treatEnumsAsInt, negate: negate, treatBooleanMemberAsUnary: treatBooleanMemberAsUnary),
8889
MethodCallExpression method => TranslateMethodStandardQuerySyntax(method, parameters),
89-
UnaryExpression unary => GetOperandStringForQueryArgs(unary.Operand, parameters, treatEnumsAsInt, unary.NodeType == ExpressionType.Not),
90+
UnaryExpression unary => GetOperandStringForQueryArgs(unary.Operand, parameters, treatEnumsAsInt, unary.NodeType == ExpressionType.Not, treatBooleanMemberAsUnary: treatBooleanMemberAsUnary),
9091
_ => throw new ArgumentException("Unrecognized Expression type")
9192
};
9293

94+
if (treatBooleanMemberAsUnary && exp is MemberExpression memberExp && memberExp.Type == typeof(bool) && negate)
95+
{
96+
negate = false;
97+
}
98+
9399
if (negate)
94100
{
95101
return $"-{res}";
@@ -308,7 +314,7 @@ internal static string EscapeTagField(string text)
308314
return sb.ToString();
309315
}
310316

311-
private static string GetOperandStringForMember(MemberExpression member, bool treatEnumsAsInt = false)
317+
private static string GetOperandStringForMember(MemberExpression member, bool treatEnumsAsInt = false, bool negate = false, bool treatBooleanMemberAsUnary = false)
312318
{
313319
var memberPath = new List<string>();
314320
var parentExpression = member.Expression;
@@ -414,6 +420,12 @@ private static string GetOperandStringForMember(MemberExpression member, bool tr
414420
if (searchField != null)
415421
{
416422
var propertyName = GetSearchFieldNameFromMember(member);
423+
if (member.Type == typeof(bool) && treatBooleanMemberAsUnary)
424+
{
425+
var val = negate ? "false" : "true";
426+
return $"@{propertyName}:{{{val}}}";
427+
}
428+
417429
return $"@{propertyName}";
418430
}
419431

src/Redis.OM/Common/ExpressionTranslator.cs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -349,22 +349,22 @@ internal static string TranslateBinaryExpression(BinaryExpression binExpression,
349349
sb.Append("(");
350350
sb.Append(TranslateBinaryExpression(left, parameters));
351351
sb.Append(SplitPredicateSeporators(binExpression.NodeType));
352-
sb.Append(ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Right, parameters));
352+
sb.Append(ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Right, parameters, treatBooleanMemberAsUnary: true));
353353
sb.Append(")");
354354
}
355355
else if (binExpression.Right is BinaryExpression right)
356356
{
357357
sb.Append("(");
358-
sb.Append(ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Left, parameters));
358+
sb.Append(ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Left, parameters, treatBooleanMemberAsUnary: true));
359359
sb.Append(SplitPredicateSeporators(binExpression.NodeType));
360360
sb.Append(TranslateBinaryExpression(right, parameters));
361361
sb.Append(")");
362362
}
363363
else
364364
{
365-
var leftContent = ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Left, parameters);
365+
var leftContent = ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Left, parameters, treatBooleanMemberAsUnary: true);
366366

367-
var rightContent = ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Right, parameters);
367+
var rightContent = ExpressionParserUtilities.GetOperandStringForQueryArgs(binExpression.Right, parameters, treatBooleanMemberAsUnary: true);
368368

369369
if (binExpression.Left is MemberExpression member)
370370
{
@@ -737,6 +737,24 @@ private static RedisSortBy TranslateOrderByMethod(MethodCallExpression expressio
737737
return sb;
738738
}
739739

740+
private static string TranslateUnaryOrMemberExpressionIntoBooleanQuery(Expression expression, List<object> parameters)
741+
{
742+
if (expression is MemberExpression member && member.Type == typeof(bool))
743+
{
744+
var propertyName = ExpressionParserUtilities.GetOperandStringForQueryArgs(member, parameters);
745+
return $"{propertyName}:{{true}}";
746+
}
747+
748+
if (expression is UnaryExpression uni && uni.Operand is MemberExpression uniMember && uniMember.Type == typeof(bool) && uni.NodeType is ExpressionType.Not)
749+
{
750+
var propertyName = ExpressionParserUtilities.GetOperandStringForQueryArgs(uniMember, parameters);
751+
return $"{propertyName}:{{false}}";
752+
}
753+
754+
throw new InvalidOperationException(
755+
$"Could not translate expression of type {expression.Type} to a boolean expression");
756+
}
757+
740758
private static string BuildQueryFromExpression(Expression exp, List<object> parameters)
741759
{
742760
if (exp is BinaryExpression binExp)
@@ -751,6 +769,12 @@ private static string BuildQueryFromExpression(Expression exp, List<object> para
751769

752770
if (exp is UnaryExpression uni)
753771
{
772+
if (uni.Operand is MemberExpression uniMember && uniMember.Type == typeof(bool) && uni.NodeType is ExpressionType.Not)
773+
{
774+
var propertyName = ExpressionParserUtilities.GetOperandString(uniMember);
775+
return $"{propertyName}:{{false}}";
776+
}
777+
754778
var operandString = BuildQueryFromExpression(uni.Operand, parameters);
755779
if (uni.NodeType == ExpressionType.Not)
756780
{
@@ -786,6 +810,8 @@ private static string BuildQueryPredicate(ExpressionType expType, string left, s
786810
ExpressionType.LessThanOrEqual => $"{left}:[-inf {right}]",
787811
ExpressionType.Equal => BuildEqualityPredicate(memberExpression, right),
788812
ExpressionType.NotEqual => BuildEqualityPredicate(memberExpression, right, true),
813+
ExpressionType.And or ExpressionType.AndAlso => $"{left} {right}",
814+
ExpressionType.Or or ExpressionType.OrElse => $"{left} | {right}",
789815
_ => string.Empty
790816
};
791817
return queryPredicate;

src/Redis.OM/Searching/IRedisCollection.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,12 @@ public interface IRedisCollection<T> : IOrderedQueryable<T>, IAsyncEnumerable<T>
275275
/// <returns>The Collection's count.</returns>
276276
int Count(Expression<Func<T, bool>> expression);
277277

278+
/// <summary>
279+
/// Retrieves the count of the collection async.
280+
/// </summary>
281+
/// <returns>The Collection's count.</returns>
282+
int Count();
283+
278284
/// <summary>
279285
/// Returns the first item asynchronously.
280286
/// </summary>

src/Redis.OM/Searching/RedisCollection.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,14 @@ public int Count(Expression<Func<T, bool>> expression)
459459
return (int)_connection.Search<T>(query).DocumentCount;
460460
}
461461

462+
/// <inheritdoc />
463+
public int Count()
464+
{
465+
var query = ExpressionTranslator.BuildQueryFromExpression(Expression, typeof(T), BooleanExpression, RootType);
466+
query.Limit = new SearchLimit { Number = 0, Offset = 0 };
467+
return (int)_connection.Search<T>(query).DocumentCount;
468+
}
469+
462470
/// <inheritdoc />
463471
public T First(Expression<Func<T, bool>> expression)
464472
{

test/Redis.OM.Unit.Tests/RediSearchTests/SearchTests.cs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2877,6 +2877,92 @@ public void SearchTagFieldAndTextListContainsWithEscapes()
28772877
"100");
28782878
}
28792879

2880+
[Fact]
2881+
public void ChainTwoBooleans()
2882+
{
2883+
int count;
2884+
_substitute.ClearSubstitute();
2885+
_substitute.Execute(Arg.Any<string>(), Arg.Any<object[]>()).Returns(_mockReply);
2886+
IRedisCollection<ObjectWithStringLikeValueTypes> collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_substitute);
2887+
collection = collection.Where(x => x.Boolean);
2888+
collection = collection.Where((x => x.Boolean));
2889+
count = collection.Count();
2890+
_substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "(@Boolean:{true} @Boolean:{true})", "LIMIT", "0", "0");
2891+
Assert.Equal(1, count);
2892+
2893+
collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_substitute);
2894+
collection = collection.Where(x => x.Boolean || x.Boolean);
2895+
count = collection.Count();
2896+
_substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "(@Boolean:{true} | @Boolean:{true})", "LIMIT", "0", "0");
2897+
Assert.Equal(1, count);
2898+
2899+
collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_substitute);
2900+
collection = collection.Where(x => !x.Boolean || x.Boolean);
2901+
count = collection.Count();
2902+
_substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "(@Boolean:{false} | @Boolean:{true})", "LIMIT", "0", "0");
2903+
Assert.Equal(1, count);
2904+
2905+
collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_substitute);
2906+
collection = collection.Where(x => !x.Boolean);
2907+
collection = collection.Where((x => !x.Boolean));
2908+
count = collection.Count();
2909+
_substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "(@Boolean:{false} @Boolean:{false})", "LIMIT", "0", "0");
2910+
Assert.Equal(1, count);
2911+
2912+
collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_substitute);
2913+
collection = collection.Where(x => x.Boolean);
2914+
collection = collection.Where((x => !x.Boolean));
2915+
count = collection.Count();
2916+
_substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "(@Boolean:{true} @Boolean:{false})", "LIMIT", "0", "0");
2917+
Assert.Equal(1, count);
2918+
2919+
collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_substitute);
2920+
collection = collection.Where(x => !x.Boolean);
2921+
collection = collection.Where((x => x.Boolean));
2922+
count = collection.Count();
2923+
_substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "(@Boolean:{false} @Boolean:{true})", "LIMIT", "0", "0");
2924+
Assert.Equal(1, count);
2925+
2926+
collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_substitute);
2927+
collection = collection.Where(x => !x.Boolean);
2928+
collection = collection.Where((x => x.Boolean));
2929+
collection = collection.Where((x => x.Boolean));
2930+
count = collection.Count();
2931+
_substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "((@Boolean:{false} @Boolean:{true}) @Boolean:{true})", "LIMIT", "0", "0");
2932+
Assert.Equal(1, count);
2933+
2934+
collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_substitute);
2935+
collection = collection.Where(x => !x.Boolean);
2936+
collection = collection.Where((x => x.Boolean));
2937+
collection = collection.Where((x => !x.Boolean));
2938+
count = collection.Count();
2939+
_substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "((@Boolean:{false} @Boolean:{true}) @Boolean:{false})", "LIMIT", "0", "0");
2940+
Assert.Equal(1, count);
2941+
2942+
collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_substitute);
2943+
collection = collection.Where(x => !x.Boolean || x.Boolean || !x.Boolean);
2944+
count = collection.Count();
2945+
_substitute.Received().Execute("FT.SEARCH", "objectwithstringlikevaluetypes-idx", "((@Boolean:{false} | @Boolean:{true}) | @Boolean:{false})", "LIMIT", "0", "0");
2946+
Assert.Equal(1, count);
2947+
}
2948+
2949+
[Fact]
2950+
public void SearchWithEmptyCount()
2951+
{
2952+
_substitute.ClearSubstitute();
2953+
_substitute.Execute(Arg.Any<string>(), Arg.Any<object[]>()).Returns(_mockReply);
2954+
var collection = new RedisCollection<ObjectWithStringLikeValueTypes>(_substitute);
2955+
var count = collection.Count(x => x.Boolean && x.AnEnum == AnEnum.three && x.Boolean == false);
2956+
_substitute.Received().Execute(
2957+
"FT.SEARCH",
2958+
"objectwithstringlikevaluetypes-idx",
2959+
"((@Boolean:{true} (@AnEnum:{three})) (@Boolean:{False}))",
2960+
"LIMIT",
2961+
"0",
2962+
"0");
2963+
Assert.Equal(1, count);
2964+
}
2965+
28802966
[Fact]
28812967
public void SearchWithEmptyAny()
28822968
{

0 commit comments

Comments
 (0)