Skip to content

Commit

Permalink
adding the ability to build queries in stages (#157)
Browse files Browse the repository at this point in the history
  • Loading branch information
slorello89 authored Jul 17, 2022
1 parent 3dc1d6d commit 852682f
Show file tree
Hide file tree
Showing 8 changed files with 590 additions and 223 deletions.
16 changes: 6 additions & 10 deletions src/Redis.OM/Common/ExpressionTranslator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,14 +174,15 @@ public static RedisAggregation BuildAggregationFromExpression(Expression express
return aggregation;
}

/// <summary>
/// <summary>
/// Build's a query from the given expression.
/// </summary>
/// <param name="expression">The expression.</param>
/// <param name="type">The root type.</param>
/// <param name="mainBooleanExpression">The primary boolean expression to build the filter from.</param>
/// <returns>A Redis query.</returns>
/// <exception cref="InvalidOperationException">Thrown if type is missing indexing.</exception>
internal static RedisQuery BuildQueryFromExpression(Expression expression, Type type)
internal static RedisQuery BuildQueryFromExpression(Expression expression, Type type, Expression? mainBooleanExpression)
{
var attr = type.GetCustomAttribute<DocumentAttribute>();
if (attr == null)
Expand All @@ -206,9 +207,6 @@ internal static RedisQuery BuildQueryFromExpression(Expression expression, Type
{
switch (exp.Method.Name)
{
case "Where":
query.QueryText = TranslateWhereMethod(exp);
break;
case "OrderBy":
query.SortBy = TranslateOrderByMethod(exp, true);
break;
Expand All @@ -231,11 +229,6 @@ internal static RedisQuery BuildQueryFromExpression(Expression expression, Type
case "FirstOrDefault":
query.Limit ??= new SearchLimit { Offset = 0 };
query.Limit.Number = 1;
if (exp.Arguments.Count > 1)
{
query.QueryText = TranslateFirstMethod(exp);
}

break;
case "GeoFilter":
query.GeoFilter = ExpressionParserUtilities.TranslateGeoFilter(exp);
Expand All @@ -251,6 +244,9 @@ internal static RedisQuery BuildQueryFromExpression(Expression expression, Type
break;
}

query.QueryText = mainBooleanExpression == null ? "*" : BuildQueryFromExpression(
((LambdaExpression)mainBooleanExpression).Body);

return query;
}

Expand Down
70 changes: 70 additions & 0 deletions src/Redis.OM/PredicateBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace Redis.OM
{
/// <summary>
/// Enables the efficient, dynamic composition of query predicates.
/// 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.
/// </summary>
internal static class PredicateBuilder
{
/// <summary>
/// Combines the first predicate with the second using the logical "and".
/// </summary>
/// <param name="first">The first expression.</param>
/// <param name="second">The second expression.</param>
/// <typeparam name="T">The parameter type for the expression.</typeparam>
/// <returns>the combined expression.</returns>
internal static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
{
return first.Compose(second, Expression.AndAlso);
}

/// <summary>
/// Combines the first expression with the second using the specified merge function.
/// </summary>
private static Expression<T> Compose<T>(this Expression<T> first, Expression<T> second, Func<Expression, Expression, Expression> merge)
{
// zip parameters (map from parameters of second to parameters of first)
var map = first.Parameters
.Select((f, i) => new { f, s = second.Parameters[i] })
.ToDictionary(p => p.s, p => p.f);

// replace parameters in the second lambda expression with the parameters in the first
var secondBody = ParameterRebinder.ReplaceParameters(map, second.Body);

// create a merged lambda expression with parameters from the first expression
return Expression.Lambda<T>(merge(first.Body, secondBody), first.Parameters);
}

private class ParameterRebinder : ExpressionVisitor
{
private readonly Dictionary<ParameterExpression, ParameterExpression> _map;

public ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map)
{
this._map = map ?? new Dictionary<ParameterExpression, ParameterExpression>();
}

public static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map, Expression exp)
{
return new ParameterRebinder(map).Visit(exp);
}

protected override Expression VisitParameter(ParameterExpression p)
{
ParameterExpression replacement;

if (_map.TryGetValue(p, out replacement))
{
p = replacement;
}

return base.VisitParameter(p);
}
}
}
}
22 changes: 15 additions & 7 deletions src/Redis.OM/SearchExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,14 @@ public static RedisAggregationSet<T> Where<T>(this RedisAggregationSet<T> source
public static IRedisCollection<T> Where<T>(this IRedisCollection<T> source, Expression<Func<T, bool>> expression)
where T : notnull
{
var collection = (RedisCollection<T>)source;
var combined = collection.BooleanExpression == null ? expression : collection.BooleanExpression.And(expression);

var exp = Expression.Call(
null,
GetMethodInfo(Where, source, expression),
new[] { source.Expression, Expression.Quote(expression) });
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, source.ChunkSize);
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, combined, source.ChunkSize);
}

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

/// <summary>
Expand All @@ -126,11 +129,12 @@ public static IRedisCollection<TR> Select<T, TR>(this IRedisCollection<T> source
public static IRedisCollection<T> Skip<T>(this IRedisCollection<T> source, int count)
where T : notnull
{
var collection = (RedisCollection<T>)source;
var exp = Expression.Call(
null,
GetMethodInfo(Skip, source, count),
new[] { source.Expression, Expression.Constant(count) });
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, source.ChunkSize);
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, collection.BooleanExpression, source.ChunkSize);
}

/// <summary>
Expand All @@ -143,11 +147,12 @@ public static IRedisCollection<T> Skip<T>(this IRedisCollection<T> source, int c
public static IRedisCollection<T> Take<T>(this IRedisCollection<T> source, int count)
where T : notnull
{
var collection = (RedisCollection<T>)source;
var exp = Expression.Call(
null,
GetMethodInfo(Take, source, count),
new[] { source.Expression, Expression.Constant(count) });
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, source.ChunkSize);
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, collection.BooleanExpression, source.ChunkSize);
}

/// <summary>
Expand Down Expand Up @@ -465,6 +470,7 @@ public static RedisAggregationSet<T> RandomSample<T, TResult>(this RedisAggregat
public static IRedisCollection<T> GeoFilter<T>(this IRedisCollection<T> source, Expression<Func<T, GeoLoc?>> expression, double lon, double lat, double radius, GeoLocDistanceUnit unit)
where T : notnull
{
var collection = (RedisCollection<T>)source;
var exp = Expression.Call(
null,
GetMethodInfo(GeoFilter, source, expression, lon, lat, radius, unit),
Expand All @@ -474,7 +480,7 @@ public static IRedisCollection<T> GeoFilter<T>(this IRedisCollection<T> source,
Expression.Constant(lat),
Expression.Constant(radius),
Expression.Constant(unit));
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, source.ChunkSize);
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, collection.BooleanExpression, source.ChunkSize);
}

/// <summary>
Expand All @@ -488,11 +494,12 @@ public static IRedisCollection<T> GeoFilter<T>(this IRedisCollection<T> source,
public static IRedisCollection<T> OrderBy<T, TField>(this IRedisCollection<T> source, Expression<Func<T, TField>> expression)
where T : notnull
{
var collection = (RedisCollection<T>)source;
var exp = Expression.Call(
null,
GetMethodInfo(OrderBy, source, expression),
new[] { source.Expression, Expression.Quote(expression) });
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, source.ChunkSize);
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, collection.BooleanExpression, source.ChunkSize);
}

/// <summary>
Expand All @@ -506,11 +513,12 @@ public static IRedisCollection<T> OrderBy<T, TField>(this IRedisCollection<T> so
public static IRedisCollection<T> OrderByDescending<T, TField>(this IRedisCollection<T> source, Expression<Func<T, TField>> expression)
where T : notnull
{
var collection = (RedisCollection<T>)source;
var exp = Expression.Call(
null,
GetMethodInfo(OrderByDescending, source, expression),
new[] { source.Expression, Expression.Quote(expression) });
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, source.ChunkSize);
return new RedisCollection<T>((RedisQueryProvider)source.Provider, exp, source.StateManager, collection.BooleanExpression, source.ChunkSize);
}

/// <summary>
Expand Down
35 changes: 35 additions & 0 deletions src/Redis.OM/Searching/IRedisCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,5 +178,40 @@ public interface IRedisCollection<T> : IOrderedQueryable<T>, IAsyncEnumerable<T>
/// <param name="expression">The expression.</param>
/// <returns>The single instance.</returns>
Task<T?> SingleOrDefaultAsync(Expression<Func<T, bool>> expression);

/// <summary>
/// Retrieves the count of the collection async.
/// </summary>
/// <param name="expression">The predicate match.</param>
/// <returns>The Collection's count.</returns>
int Count(Expression<Func<T, bool>> expression);

/// <summary>
/// Returns the first item asynchronously.
/// </summary>
/// <param name="expression">The predicate match.</param>
/// <returns>First or default result.</returns>
T First(Expression<Func<T, bool>> expression);

/// <summary>
/// Returns the first or default asynchronously.
/// </summary>
/// <param name="expression">The predicate match.</param>
/// <returns>First or default result.</returns>
T? FirstOrDefault(Expression<Func<T, bool>> expression);

/// <summary>
/// Returns a single record or throws a <see cref="InvalidOperationException"/> if the sequence is empty or contains more than 1 record.
/// </summary>
/// <param name="expression">The expression.</param>
/// <returns>The single instance.</returns>
T Single(Expression<Func<T, bool>> expression);

/// <summary>
/// Returns a single record or the default if there are none, or more than 1.
/// </summary>
/// <param name="expression">The expression.</param>
/// <returns>The single instance.</returns>
T? SingleOrDefault(Expression<Func<T, bool>> expression);
}
}
Loading

0 comments on commit 852682f

Please sign in to comment.