Search code examples
c#lambdaexpressionravendb

Building a generic search for RavenDb - failing to create Expression<Func<T,object>>


The standard search method on IRavenQueryable looks like this:

public static IRavenQueryable<T> Search<T>(this IQueryable<T> self, Expression<Func<T, object>> fieldSelector, string searchTerms, decimal boost = 1m, SearchOptions options = SearchOptions.Guess, SearchOperator @operator = SearchOperator.Or)
{
    MethodInfo method = (MethodInfo)MethodBase.GetCurrentMethod();
    return (IRavenQueryable<T>)self.Provider.CreateQuery(Expression.Call(null, method, self.Expression, fieldSelector, Expression.Constant(searchTerms), Expression.Constant(boost), Expression.Constant(options), Expression.Constant(@operator)));
}

The full source of the method can be found here

I'd like to create another extension method that allows me to specify which property to search on using its name. So I came up with this

public static IRavenQueryable<T> Search<T>(this IQueryable<T> self, string fieldName, string searchTerms, decimal boost = 1m, SearchOptions options = SearchOptions.Guess, SearchOperator @operator = SearchOperator.Or)
{
    MethodInfo method = (MethodInfo)MethodBase.GetCurrentMethod();
    var info = typeof(T).GetProperty(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
        ?? throw new Exception($"{fieldName} is not a valid property on {typeof(T).Name}");
    var parTarget = Expression.Parameter(info.DeclaringType, "target");
    var exprGetter = Expression.Lambda(typeof(Func<,>).MakeGenericType(info.DeclaringType, typeof(object)), Expression.Property(parTarget, info), parTarget);
    return (IRavenQueryable<T>)self.Provider.CreateQuery(Expression.Call(null, method, self.Expression, exprGetter, Expression.Constant(searchTerms), Expression.Constant(boost), Expression.Constant(options), Expression.Constant(@operator)));
}

If I call this, I'm getting an ArgumentException (the Search I'm making is on T being NoSqlModels.PhoneBookCategory, and the property used in fieldName is of type string)

System.ArgumentException: 'Expression of type 'System.Func2[NoSqlModels.PhoneBookCategory,System.String]' cannot be used for parameter of type 'System.String' of method 'Raven.Client.Documents.Linq.IRavenQueryable1[NoSqlModels.PhoneBookCategory] Search[PhoneBookCategory](System.Linq.IQueryable`1[NoSqlModels.PhoneBookCategory], System.String, System.String, System.Decimal, Raven.Client.Documents.SearchOptions, Raven.Client.Documents.Queries.SearchOperator)' (Parameter 'arguments1')'

So it seems that my exprGetter returns an Expression<T,string>, rather than the required Expression<T,object>. So what do I need to change to get this to work


Solution

  • Here's a solution that strips much of the complexity of the original approach:

    public static IRavenQueryable<T> Search<T>(this IQueryable<T> self, string fieldName, string searchTerms, decimal boost = 1m, SearchOptions options = SearchOptions.Guess, SearchOperator @operator = SearchOperator.Or)
    {
        var parameter = Expression.Parameter(typeof(T));
        var property = Expression.Property(parameter, propName);
        expGetter = Expression.Lambda<Func<T, P>>(property, parameter);
        return self.Search(expGetter, searchTerms, boost, options, @operator);
    }
    

    It just generates the field selector, then calls the original extension method.