Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating a LINQ Expression where parameter equals object

Given a primitive value age I know how to create an expression like this:

//assuming: age is an int or some other primitive type
employee => employee.Age == age

By doing this:

var entityType = typeof(Employee);
var propertyName = "Age";
int age = 30;
var parameter = Expression.Parameter(entityType, "entity");

var lambda = Expression.Lambda(
        Expression.Equal(
            Expression.Property(parameter, propertyName),
            Expression.Constant(age)
        )                    
    , parameter);

That works fine except in scenarios where the property and constant in question are not primitive types.

How would I construct a similar expression if the comparison is between objects?

With EF I can just write:

Location location = GetCurrentLocation();
employees = DataContext.Employees.Where(e => e.Location == location);

That also works, but if I try to create the same expression:

var entityType = typeof(Employee);
var propertyName = "Location";
var location = GetCurrentLocation();
var parameter = Expression.Parameter(entityType, "entity");

var lambda = Expression.Lambda(
        Expression.Equal(
            Expression.Property(parameter, propertyName),
            Expression.Constant(location)
        )                    
    , parameter);

I get an error that says:

Unable to create a constant value of type 'Location'. Only primitive types or enumeration types are supported in this context.

My suspicion is that Expression.Constant() only expects primitive types, so I need to use a different expression factory method. (maype Expression.Object? - I know that doesn't exist)

Is there a way to create an expression that compares objects? Why is that EF is able to interpret it correctly if its a compiled LINQ statement, but not when it is an expression?

like image 376
Alfero Chingono Avatar asked Apr 12 '13 17:04

Alfero Chingono


3 Answers

In addition to what has been mentioned in previous answers. A more specific solution would go as such:

public static Expression CreateExpression<T>(string propertyName, object valueToCompare)
{
    // get the type of entity
    var entityType = typeof(T);
    // get the type of the value object
    var valueType = valueToCompare.GetType();
    var entityProperty = entityType.GetProperty(propertyName);
    var propertyType = entityProperty.PropertyType;


    // Expression: "entity"
    var parameter = Expression.Parameter(entityType, "entity");

    // check if the property type is a value type
    // only value types work 
    if (propertyType.IsValueType || propertyType.Equals(typeof(string)))
    {
        // Expression: entity.Property == value
        return Expression.Equal(
            Expression.Property(parameter, entityProperty),
            Expression.Constant(valueToCompare)
        );
    }
    // if not, then use the key
    else
    {
        // get the key property
        var keyProperty = propertyType.GetProperties().FirstOrDefault(p => p.GetCustomAttributes(typeof(KeyAttribute), false).Length > 0);

        // Expression: entity.Property.Key == value.Key
        return Expression.Equal(
            Expression.Property(
                Expression.Property(parameter, entityProperty),
                keyProperty
            ),
            Expression.Constant(
                keyProperty.GetValue(valueToCompare),
                keyProperty.PropertyType
            )
        );
    }
}

IMPORTANT POINTS :

  1. Make sure to check for nulls
  2. Make sure propertyType and valueType are compatible (either they are the same type or are convertible)
  3. Several assumptions are made here (e.g. that you do assign a KeyAttribute)
  4. This code is not tested, so it is not exactly copy/paste ready.

Hope that helps.

like image 110
Alfero Chingono Avatar answered Nov 17 '22 00:11

Alfero Chingono


You can't do that because EF doesn't know how to translate equality comparisons on Location into a SQL expression.

However, if you know what properties of Location you want to compare, you can do this with anonymous types:

var location = GetCurrentLocation();
var locationObj = new { location.LocationName, location.LocationDescription };
employees = DataContext.Employees.Where(e => new { e.Location.LocationName, e.Location.Description } == locationObj);

Of course that's equivalent to:

var location = GetCurrentLocation();
employees = DataContext.Employees.Where(e => e.Location.LocationName == location.Name && 
                                             e.Location.Description == location.Description);
like image 30
p.s.w.g Avatar answered Nov 16 '22 23:11

p.s.w.g


Give the code below a run. I wanted to test your assumption that e => e.Location == location is compiling into something that can be constructed with Expression.Equal, Expression.Property, and Expression.Constant.

    class Program {
       static void Main(string[] args) {
          var location = new Location();
          Expression<Func<Employee, bool>> expression = e => e.Location == location;

          var untypedBody = expression.Body;

          //The untyped body is a BinaryExpression
           Debug.Assert(
              typeof(BinaryExpression).IsAssignableFrom(untypedBody.GetType()), 
              "Not Expression.Equal");

           var body = (BinaryExpression)untypedBody;
           var untypedLeft = body.Left;
           var untypedRight = body.Right;

           //The untyped left expression is a MemberExpression
           Debug.Assert(
              typeof(MemberExpression).IsAssignableFrom(untypedLeft.GetType()), 
              "Not Expression.Property");

           ////The untyped right expression is a ConstantExpression
          //Debug.Assert(
          //   typeof(ConstantExpression).IsAssignableFrom(untypedRight.GetType()),                 
          //   "Not Expression.Constant");

          //The untyped right expression is a MemberExpression?
          Debug.Assert(
               typeof(MemberExpression).IsAssignableFrom(untypedRight.GetType())));
    }
}

public class Employee
{
    public Location Location { get; set; }
}

public class Location { }

It seems like it isn't, and its because the right expression isn't a Constant. To see this, uncomment the commented out code.

What I don't understand is why the right expression is a MemberExpression. Perhaps someone who knows the linq expression compiler can shed more light onto this then I can.

Edit: This may have to do with closure in lambdas - a class is created behind the scenes which contains the closed over variables. The location might then be a member of that class. I'm not sure about this, but it's what I suspect.

This post may shed additional light on the situation.

like image 2
Doug Avatar answered Nov 16 '22 23:11

Doug