Pulling Apart Expression>

c# dapper extension-methods

Question

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.

Accepted Answer

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.


Popular Answer

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.



Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why