Joelty
Joelty

Reputation: 2009

How to apply operator to expression in more generic way

I'm dealing with dynamic queries and I'm not sure how to refactor this sample code code which basically would be giant if ladder if we wanted to support more operators / properties

var property = "AccountBalance";
var operator = ">";
var value = 5;

var query = _context.Users;
if (property == "AccountBalance")
{
    if (operator == ">")
    {
        query = query.Where(x => x.AccountBalance > value);
    }
}

I'd want to refactor it to something very generic and reusable like that:

// I simplified comparison here because afaik I'd have to use `CompareTo` due to IComparable   
// instead of >, but I don't think it is important right now

public Expression<Func<T, bool>> GetExpression<T, U>(string operator, U value, Func<T, object property) where U : IComparable, T : class
{
    switch (operator)
    {
        case ">":
            return property > value;    
        case "<":
            return property < value;
        (...)
        default:
            throw new Exception("undefined operator");
    }
}

Usage:

var expression = GetExpression<User, int>(">", 5, new Func<User> (x => x.AccountBalance));
query = query.Where(expression);

but I cannot figure out how to pass property into that function

new Func<User> (x => x.AccountBalance) that's pseudo code idea

Upvotes: 4

Views: 415

Answers (2)

Aleks Andreev
Aleks Andreev

Reputation: 7054

You can generate expression dynamically with Linq Expressions API

public static Expression<Func<T, bool>> GetExpression<T, U>(string property,
    string @operator, U value) where U : IComparable
{
    var p = Expression.Parameter(typeof(T), "p");
    var member = Expression.PropertyOrField(p, property);
    var constant = Expression.Constant(value);
    var compareToArgument = Expression.Convert(constant, typeof(object));

    var method = typeof(IComparable).GetMethod("CompareTo");
    var call = Expression.Call(member, method, compareToArgument);

    Expression body = null;

    if (@operator == ">")
        body = Expression.GreaterThan(call, Expression.Constant(0));
    else if (@operator == "<")
        body = Expression.LessThan(call, Expression.Constant(0));

    // todo: throw if @operator is unknown

    var lambda = Expression.Lambda<Func<T, bool>>(body, p);
    return lambda;
}

Usage:

var expression = GetExpression<User, int>("AccountBalance", ">", 5);
query = query.Where(expression);

If you want to support more operators you can omit call to the CompareTo method. Something like:

var p = Expression.Parameter(typeof(T), "p");
var member = Expression.PropertyOrField(p, property);
var constant = Expression.Constant(value);

Expression body = null;

if (@operator == ">=")
    body = Expression.GreaterThanOrEqual(member, constant);
if (@operator == "<=")
    body = Expression.LessThanOrEqual(member, constant);

Upvotes: 1

Christos
Christos

Reputation: 53958

You could add as a parameter of your method, GetExpression, a Func<T,U>, where T is the type of the object you want to pass one of it's properties and U is the type of the property. Then when you call your method you could pass a lambda.

E.g.

var expression = GetExpression<User, int>(">", 5, x => x.AccountBalance);

PS

You have to change the method signature as below:

public Expression<Func<T, bool>> GetExpression<T, U>
   (string operator, U value, Func<T, U> property) where U : IComparable, T : class

Update

You could try something like this:

public Expression<Func<T, bool>> GetExpression<T, U>
    (string op, U value, Func<T, U> property) 
        where  T: class 
        where U : IComparable
{
    switch (op)
    {
        case ">":
            return p => value.CompareTo(property(p)) > 0;
        case "<":
            return p => value.CompareTo(property(p)) < 0;
        default:
            throw new Exception("undefined operator");
    }
}

The very reason that you can't use operator > and < in this context can be found in the C# specification:

The < (less than), > (greater than), <= (less than or equal), and >= (greater than or equal) comparison, also known as relational, operators compare their operands. Those operators support all integral and floating-point numeric types.

U is a generic type argument that cannot be constrained to be either an integral or a floating-point numeric type. For that reason we want U to implement the IComparable interface. Demanding this give us the opportunity to do the comparison we want.

Upvotes: 2

Related Questions