Создание выражения LINQ, где параметр равен объекту

Учитывая примитивный age ценности, я знаю, как создать такое выражение:

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

Делая это:

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);

Это работает отлично, за исключением сценариев, в которых свойство и константа не являются примитивными типами.

Как построить аналогичное выражение, если сравнение между объектами?

С EF я могу просто написать:

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

Это также работает, но если я попытаюсь создать одно и то же выражение:

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);

Я получаю сообщение об ошибке:

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

Мое подозрение в том, что Expression.Constant() только ожидает примитивных типов, поэтому мне нужно использовать другой метод фабрики выражений. (maype Expression.Object? - Я знаю, что этого не существует)

Есть ли способ создать выражение, которое сравнивает объекты? Почему EF может правильно интерпретировать его, если его скомпилированный оператор LINQ, но не когда он является выражением?

Ответ 1

В дополнение к тому, что было упомянуто в предыдущих ответах. Более конкретное решение было бы таким:

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
            )
        );
    }
}

ВАЖНЫЕ МОМЕНТЫ :

  1. Не забудьте проверить нули
  2. Убедитесь, что propertyType и valueType совместимы (либо они одного типа, либо конвертируемые)
  3. Здесь делается несколько предположений (например, что вы назначаете KeyAttribute)
  4. Этот код не проверен, поэтому он не совсем готов к копированию/вставке.

Надеюсь, это поможет.

Ответ 2

Вы не можете этого сделать, потому что EF не знает, как перевести сравнения сравнений в Location в выражение SQL.

Однако, если вы знаете, какие свойства Location вы хотите сравнить, вы можете сделать это с помощью анонимных типов:

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

Конечно, это эквивалентно:

var location = GetCurrentLocation();
employees = DataContext.Employees.Where(e => e.Location.LocationName == location.Name && 
                                             e.Location.Description == location.Description);

Ответ 3

Дайте код ниже пробега. Я хотел бы проверить ваше предположение, что e => e.Location == местоположение компилируется во что-то, что может быть создано с помощью Expression.Equal, Expression.Property и 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 { }

Похоже, что это не так, и потому, что правильное выражение не является константой. Чтобы увидеть это, раскомментируйте прокомментированный код.

Я не понимаю, почему правильным выражением является выражение MemberExpression. Возможно, кто-то, кто знает компилятор выражения linq, может пролить свет на это, тогда я могу.

Изменение: это может иметь отношение к закрытию в lambdas - класс создается за кулисами, который содержит закрытые переменные. Тогда местоположение может быть членом этого класса. Я не уверен в этом, но это то, что я подозреваю.

Это сообщение может проливать дополнительный свет на ситуацию.