Skip to content

Commit 852682f

Browse files
authored
adding the ability to build queries in stages (#157)
1 parent 3dc1d6d commit 852682f

File tree

8 files changed

+590
-223
lines changed

8 files changed

+590
-223
lines changed

src/Redis.OM/Common/ExpressionTranslator.cs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -174,14 +174,15 @@ public static RedisAggregation BuildAggregationFromExpression(Expression express
174174
return aggregation;
175175
}
176176

177-
/// <summary>
177+
/// <summary>
178178
/// Build's a query from the given expression.
179179
/// </summary>
180180
/// <param name="expression">The expression.</param>
181181
/// <param name="type">The root type.</param>
182+
/// <param name="mainBooleanExpression">The primary boolean expression to build the filter from.</param>
182183
/// <returns>A Redis query.</returns>
183184
/// <exception cref="InvalidOperationException">Thrown if type is missing indexing.</exception>
184-
internal static RedisQuery BuildQueryFromExpression(Expression expression, Type type)
185+
internal static RedisQuery BuildQueryFromExpression(Expression expression, Type type, Expression? mainBooleanExpression)
185186
{
186187
var attr = type.GetCustomAttribute<DocumentAttribute>();
187188
if (attr == null)
@@ -206,9 +207,6 @@ internal static RedisQuery BuildQueryFromExpression(Expression expression, Type
206207
{
207208
switch (exp.Method.Name)
208209
{
209-
case "Where":
210-
query.QueryText = TranslateWhereMethod(exp);
211-
break;
212210
case "OrderBy":
213211
query.SortBy = TranslateOrderByMethod(exp, true);
214212
break;
@@ -231,11 +229,6 @@ internal static RedisQuery BuildQueryFromExpression(Expression expression, Type
231229
case "FirstOrDefault":
232230
query.Limit ??= new SearchLimit { Offset = 0 };
233231
query.Limit.Number = 1;
234-
if (exp.Arguments.Count > 1)
235-
{
236-
query.QueryText = TranslateFirstMethod(exp);
237-
}
238-
239232
break;
240233
case "GeoFilter":
241234
query.GeoFilter = ExpressionParserUtilities.TranslateGeoFilter(exp);
@@ -251,6 +244,9 @@ internal static RedisQuery BuildQueryFromExpression(Expression expression, Type
251244
break;
252245
}
253246

247+
query.QueryText = mainBooleanExpression == null ? "*" : BuildQueryFromExpression(
248+
((LambdaExpression)mainBooleanExpression).Body);
249+
254250
return query;
255251
}
256252

src/Redis.OM/PredicateBuilder.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Linq.Expressions;
5+
6+
namespace Redis.OM
7+
{
8+
/// <summary>
9+
/// Enables the efficient, dynamic composition of query predicates.
10+
/// credit to the author Ano Mepani whose <see href="https://www.c-sharpcorner.com/UploadFile/c42694/dynamic-query-in-linq-using-predicate-builder/">post</see> this class was taken from, with some light edits.
11+
/// </summary>
12+
internal static class PredicateBuilder
13+
{
14+
/// <summary>
15+
/// Combines the first predicate with the second using the logical "and".
16+
/// </summary>
17+
/// <param name="first">The first expression.</param>
18+
/// <param name="second">The second expression.</param>
19+
/// <typeparam name="T">The parameter type for the expression.</typeparam>
20+
/// <returns>the combined expression.</returns>
21+
internal static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
22+
{
23+
return first.Compose(second, Expression.AndAlso);
24+
}
25+
26+
/// <summary>
27+
/// Combines the first expression with the second using the specified merge function.
28+
/// </summary>
29+
private static Expression<T> Compose<T>(this Expression<T> first, Expression<T> second, Func<Expression, Expression, Expression> merge)
30+
{
31+
// zip parameters (map from parameters of second to parameters of first)
32+
var map = first.Parameters
33+
.Select((f, i) => new { f, s = second.Parameters[i] })
34+
.ToDictionary(p => p.s, p => p.f);
35+
36+
// replace parameters in the second lambda expression with the parameters in the first
37+
var secondBody = ParameterRebinder.ReplaceParameters(map, second.Body);
38+
39+
// create a merged lambda expression with parameters from the first expression
40+
return Expression.Lambda<T>(merge(first.Body, secondBody), first.Parameters);
41+
}
42+
43+
private class ParameterRebinder : ExpressionVisitor
44+
{
45+
private readonly Dictionary<ParameterExpression, ParameterExpression> _map;
46+
47+
public ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map)
48+
{
49+
this._map = map ?? new Dictionary<ParameterExpression, ParameterExpression>();
50+
}
51+
52+
public static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map, Expression exp)
53+
{
54+
return new ParameterRebinder(map).Visit(exp);
55+
}
56+
57+
protected override Expression VisitParameter(ParameterExpression p)
58+
{
59+
ParameterExpression replacement;
60+
61+
if (_map.TryGetValue(p, out replacement))
62+
{
63+
p = replacement;
64+
}
65+
66+
return base.VisitParameter(p);
67+
}
68+
}
69+
}
70+
}

src/Redis.OM/SearchExtensions.cs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,14 @@ public static RedisAggregationSet<T> Where<T>(this RedisAggregationSet<T> source
9090
public static IRedisCollection<T> Where<T>(this IRedisCollection<T> source, Expression<Func<T, bool>> expression)
9191
where T : notnull
9292
{
93+
var collection = (RedisCollection<T>)source;
94+
var combined = collection.BooleanExpression == null ? expression : collection.BooleanExpression.And(expression);
95+
9396
var exp = Expression.Call(
9497
null,
9598
GetMethodInfo(Where, source, expression),
9699
new[] { source.Expression, Expression.Quote(expression) });
97-
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, source.ChunkSize);
100+
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, combined, source.ChunkSize);
98101
}
99102

100103
/// <summary>
@@ -113,7 +116,7 @@ public static IRedisCollection<TR> Select<T, TR>(this IRedisCollection<T> source
113116
null,
114117
GetMethodInfo(Select, source, expression),
115118
new[] { source.Expression, Expression.Quote(expression) });
116-
return new RedisCollection<TR>((RedisQueryProvider)source.Provider, exp, source.StateManager, source.ChunkSize);
119+
return new RedisCollection<TR>((RedisQueryProvider)source.Provider, exp, source.StateManager, null, source.ChunkSize);
117120
}
118121

119122
/// <summary>
@@ -126,11 +129,12 @@ public static IRedisCollection<TR> Select<T, TR>(this IRedisCollection<T> source
126129
public static IRedisCollection<T> Skip<T>(this IRedisCollection<T> source, int count)
127130
where T : notnull
128131
{
132+
var collection = (RedisCollection<T>)source;
129133
var exp = Expression.Call(
130134
null,
131135
GetMethodInfo(Skip, source, count),
132136
new[] { source.Expression, Expression.Constant(count) });
133-
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, source.ChunkSize);
137+
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, collection.BooleanExpression, source.ChunkSize);
134138
}
135139

136140
/// <summary>
@@ -143,11 +147,12 @@ public static IRedisCollection<T> Skip<T>(this IRedisCollection<T> source, int c
143147
public static IRedisCollection<T> Take<T>(this IRedisCollection<T> source, int count)
144148
where T : notnull
145149
{
150+
var collection = (RedisCollection<T>)source;
146151
var exp = Expression.Call(
147152
null,
148153
GetMethodInfo(Take, source, count),
149154
new[] { source.Expression, Expression.Constant(count) });
150-
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, source.ChunkSize);
155+
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, collection.BooleanExpression, source.ChunkSize);
151156
}
152157

153158
/// <summary>
@@ -465,6 +470,7 @@ public static RedisAggregationSet<T> RandomSample<T, TResult>(this RedisAggregat
465470
public static IRedisCollection<T> GeoFilter<T>(this IRedisCollection<T> source, Expression<Func<T, GeoLoc?>> expression, double lon, double lat, double radius, GeoLocDistanceUnit unit)
466471
where T : notnull
467472
{
473+
var collection = (RedisCollection<T>)source;
468474
var exp = Expression.Call(
469475
null,
470476
GetMethodInfo(GeoFilter, source, expression, lon, lat, radius, unit),
@@ -474,7 +480,7 @@ public static IRedisCollection<T> GeoFilter<T>(this IRedisCollection<T> source,
474480
Expression.Constant(lat),
475481
Expression.Constant(radius),
476482
Expression.Constant(unit));
477-
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, source.ChunkSize);
483+
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, collection.BooleanExpression, source.ChunkSize);
478484
}
479485

480486
/// <summary>
@@ -488,11 +494,12 @@ public static IRedisCollection<T> GeoFilter<T>(this IRedisCollection<T> source,
488494
public static IRedisCollection<T> OrderBy<T, TField>(this IRedisCollection<T> source, Expression<Func<T, TField>> expression)
489495
where T : notnull
490496
{
497+
var collection = (RedisCollection<T>)source;
491498
var exp = Expression.Call(
492499
null,
493500
GetMethodInfo(OrderBy, source, expression),
494501
new[] { source.Expression, Expression.Quote(expression) });
495-
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, source.ChunkSize);
502+
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, collection.BooleanExpression, source.ChunkSize);
496503
}
497504

498505
/// <summary>
@@ -506,11 +513,12 @@ public static IRedisCollection<T> OrderBy<T, TField>(this IRedisCollection<T> so
506513
public static IRedisCollection<T> OrderByDescending<T, TField>(this IRedisCollection<T> source, Expression<Func<T, TField>> expression)
507514
where T : notnull
508515
{
516+
var collection = (RedisCollection<T>)source;
509517
var exp = Expression.Call(
510518
null,
511519
GetMethodInfo(OrderByDescending, source, expression),
512520
new[] { source.Expression, Expression.Quote(expression) });
513-
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, source.ChunkSize);
521+
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, collection.BooleanExpression, source.ChunkSize);
514522
}
515523

516524
/// <summary>

src/Redis.OM/Searching/IRedisCollection.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,5 +178,40 @@ public interface IRedisCollection<T> : IOrderedQueryable<T>, IAsyncEnumerable<T>
178178
/// <param name="expression">The expression.</param>
179179
/// <returns>The single instance.</returns>
180180
Task<T?> SingleOrDefaultAsync(Expression<Func<T, bool>> expression);
181+
182+
/// <summary>
183+
/// Retrieves the count of the collection async.
184+
/// </summary>
185+
/// <param name="expression">The predicate match.</param>
186+
/// <returns>The Collection's count.</returns>
187+
int Count(Expression<Func<T, bool>> expression);
188+
189+
/// <summary>
190+
/// Returns the first item asynchronously.
191+
/// </summary>
192+
/// <param name="expression">The predicate match.</param>
193+
/// <returns>First or default result.</returns>
194+
T First(Expression<Func<T, bool>> expression);
195+
196+
/// <summary>
197+
/// Returns the first or default asynchronously.
198+
/// </summary>
199+
/// <param name="expression">The predicate match.</param>
200+
/// <returns>First or default result.</returns>
201+
T? FirstOrDefault(Expression<Func<T, bool>> expression);
202+
203+
/// <summary>
204+
/// Returns a single record or throws a <see cref="InvalidOperationException"/> if the sequence is empty or contains more than 1 record.
205+
/// </summary>
206+
/// <param name="expression">The expression.</param>
207+
/// <returns>The single instance.</returns>
208+
T Single(Expression<Func<T, bool>> expression);
209+
210+
/// <summary>
211+
/// Returns a single record or the default if there are none, or more than 1.
212+
/// </summary>
213+
/// <param name="expression">The expression.</param>
214+
/// <returns>The single instance.</returns>
215+
T? SingleOrDefault(Expression<Func<T, bool>> expression);
181216
}
182217
}

0 commit comments

Comments
 (0)