Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pulling Apart Expression<Func<T, object>>

I am busy creating wrapper extension methods on top of Dapper and DapperExtensions. At the moment I am trying to add filtering to the GetList<T> extension method, similar to LINQ's Where<T> extension method. I have seen this question but it seems I cannot implement what Marc Gravell suggested because there isn't a type EqualsExpression in .NET 4.5. Here is some demo code to help with the explanation of my problem:

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq.Expressions;
using DapperExtensions;

namespace Dapper.Extensions.Demo
{
    public class Program
    {
        private static readonly string ConnectionString = ConfigurationManager.ConnectionStrings["DapperDbContext"].ConnectionString;
        public static IDbConnection Connection { get { return new SqlConnection(ConnectionString); } }

        public static void Main(string[] args)
        {
            const int marketId = 2;
            var matchingPeople = Connection.Get<Person>(p => p.MarketId, marketId); // This works

            // Below is a LambdaExpression. expression.Body is, bizarrely, a UnaryExpression with a Convert
            //var matchingPeople = Connection.Get<Person>(p => p.MarketId == marketId); // Does not work

            foreach (var person in matchingPeople)
            {
                Console.WriteLine(person);
            }

            if (Debugger.IsAttached)
                Console.ReadLine();
        }
    }

    public static class SqlConnectionExtensions
    {
        public static IEnumerable<T> Get<T>(this IDbConnection connection, Expression<Func<T, object>> expression, object value = null) where T : class
        {
            using (connection)
            {
                connection.Open();

                // I want to be able to pass in: t => t.Id == id then:
                // Expression<Func<T, object>> expressionOnLeftOfFilterClause = t => t.Id;
                // string operator = "==";
                // object valueFromLambda = id;
                // and call Predicates.Field(expressionOnLeftOfFilterClause, Operator.Eq, valueFromLambda)

                var predicate = Predicates.Field(expression, Operator.Eq, value);
                var entities = connection.GetList<T>(predicate, commandTimeout: 30);
                connection.Close();
                return entities;
            }
        }
    }

    public class Person
    {
        public int Id { get; set; }

        public string FirstName { get; set; }

        public string Surname { get; set; }

        public int MarketId { get; set; }

        public override string ToString()
        {
            return string.Format("{0}: {1}, {2} - MarketId: {3}", Id, Surname, FirstName, MarketId);
        }
    }
}

Paying particular attention to my Get<T> extension method: when I pass in either p => p.MarketId or p => p.MarketId == marketId, expression.Body is of type UnaryExpression. For the latter, expression.Body actually contains {Convert((p.MarketId == 2))}.

Attempting

var binaryExpression = expression as BinaryExpression;

returns null, which is unfortunate because there are Left and Right properties which I could have found useful.

So, does anyone know how to achieve what I want? Further down the line I would like to be able to pick the Operator enum based on the lambda expression passed in. Any help would be much appreciated.

like image 372
Sameer Singh Avatar asked Mar 01 '13 09:03

Sameer Singh


2 Answers

I have figured out how to achieve what I want.

In summary:

  1. I need an extension method which wraps DapperExtension's GetList<T> extension method.
  2. The latter may take in a predicate of type IFieldPredicate which I can use to add a filter to the SQL query to be executed. I can achieve this by using Predicates.Field<T>(Expression<Func<T, object>> expression, Operator op, object value).
  3. The problem lies in transforming a simple lambda expression t => t.Id == id into parameters for Predicates.Field<T>. So, conceptually, I need to pull apart the lambda expression into three parts: t => t.Id, Operator.Eq, and id.

With help from @Iridium, @Eduard and @Jon, my final solution is:

public static class SqlConnectionExtensions
{
    public static IEnumerable<T> Get<T>(this IDbConnection connection, Expression<Func<T, object>> expression) where T : class
    {
        using (connection)
        {
            connection.Open();

            var binaryExpression = (BinaryExpression)((UnaryExpression) expression.Body).Operand;

            var left = Expression.Lambda<Func<T, object>>(Expression.Convert(binaryExpression.Left, typeof(object)), expression.Parameters[0]);
            var right = binaryExpression.Right.GetType().GetProperty("Value").GetValue(binaryExpression.Right);
            var theOperator = DetermineOperator(binaryExpression);

            var predicate = Predicates.Field(left, theOperator, right);
            var entities = connection.GetList<T>(predicate, commandTimeout: 30);

            connection.Close();
            return entities;
        }
    }

    private static Operator DetermineOperator(Expression binaryExpression)
    {
        switch (binaryExpression.NodeType)
        {
            case ExpressionType.Equal:
                return Operator.Eq;
            case ExpressionType.GreaterThan:
                return Operator.Gt;
            case ExpressionType.GreaterThanOrEqual:
                return Operator.Ge;
            case ExpressionType.LessThan:
                return Operator.Lt;
            case ExpressionType.LessThanOrEqual:
                return Operator.Le;
            default:
                return Operator.Eq;
        }
    }
}

I can now do this:

var matchingPeople = Connection.Get<Person>(p => p.MarketId == marketId);

I know how brittle this is - it will break if I pass in anything more complex, or even something that looks to be equivalent, like var matchingPeople = Connection.Get<Person>(p => p.MarketId.Equals(marketId));. It does solve 90% of my cases though so I am content to leave it as-is.

like image 196
Sameer Singh Avatar answered Sep 22 '22 12:09

Sameer Singh


This is the problem:

Expression<Func<T, object>> expression

Your function has to return object. The type of p.MarketId == marketId is bool. It therefore needs to be boxed to object, hence the Convert.

If the expression is always meant to be a predicate, you should change it to:

Expression<Func<T, bool>> expression

At that point, I'd expect you to see the appropriate binary expression. On the other hand, that then won't work for p => p.MarketId...

To be honest, it's not really clear what the parameters are meant to mean. It feels like maybe you want two methods - one for a single parameter which is a predicate, and one for two parameters: a projection and a target value.

like image 29
Jon Skeet Avatar answered Sep 21 '22 12:09

Jon Skeet