EF6 с использованием настраиваемого свойства в запросе linq

У меня есть класс, который имеет следующее свойство:

[NotMapped]
public string Key
{
    get
    {
        return string.Format("{0}_{1}", Process.Name, LocalSequenceNumber);
    }
}

Локальный порядковый номер — это вычисляемое целое число, поддерживаемое кешем в форме параллельного словаря.

Я хочу использовать указанное выше свойство Key в запросе LINQ, но получаю исключение:

Член указанного типа "Key" не поддерживается в LINQ to Entities. Поддерживаются только инициализаторы, члены сущностей и свойства навигации сущностей.

Я понимаю, почему я получаю эту ошибку, но я не слишком уверен в том, как ее исправить. В настоящее время свойство Key обеспечивает хорошую инкапсуляцию моего класса, с которой я не хочу расставаться. Любые предложения с точки зрения библиотек или простых шаблонов, чтобы обойти это?

Изменить: вот запрос, который вызывает исключение:

db.Cars.SingleOrDefault(c => c.Id == id && c.Key == key);

person Riz    schedule 15.10.2018    source источник
comment
Без кода, который на самом деле генерирует исключение, довольно сложно сказать, почему.   -  person Erik Philips    schedule 15.10.2018
comment
я добавил запрос.   -  person Riz    schedule 15.10.2018
comment
Ну, вы сказали EF не сопоставлять ключ со столбцом базы данных, тогда вы хотите использовать его в запросе. Не удивительно, что это не работает.   -  person Erik Philips    schedule 15.10.2018
comment
Помните, что LINQ to Entities переводит оператор в SQL, если вы используете NotMappedAttribute, то свойство не может быть преобразовано в оператор SQL.   -  person Tetsuya Yamamoto    schedule 15.10.2018
comment
Правильно, я уже ясно дал понять, и я понимаю проблему. Это вычисляемое свойство, которое я хотел бы использовать как часть запроса. Мой вопрос в том, есть ли способ обойти это.   -  person Riz    schedule 15.10.2018
comment
Вычисляемое свойство должно использовать Select для проецирования его в класс модели. Прочитайте аналогичную проблему здесь: stackoverflow.com/questions/25867470/.   -  person Tetsuya Yamamoto    schedule 15.10.2018
comment
@Riz: хотя это звучит как перебор, но можете ли вы сопоставить свою сущность с классом модели, используя что-то вроде AutoMapper, а затем попробовать этот запрос?   -  person Syed Ali Taqi    schedule 15.10.2018
comment
хорошее замечание Тецуя. Спасибо за ваш полезный комментарий. Теперь я вызываю .ToList(), чтобы сначала материализовать мой список на основе первой части запроса, а затем запрашиваю его по вычисляемому свойству, и это работает как шарм. Если вы опубликуете свой ответ в ответе, я буду рад отметить его как таковой.   -  person Riz    schedule 15.10.2018


Ответы (3)


Пакет DelegateDecompiler https://github.com/hazzik/DelegateDecompiler обрабатывает сценарии такого типа.

Украсьте свое свойство атрибутом Computed, тогда запросы, подобные следующим, должны работать, если вы добавите метод Decompile:

db.Cars.Decompile().SingleOrDefault(c => c.Id == id && c.Key == key)
person Jonas Høgh    schedule 15.10.2018

Существует множество сторонних пакетов, которые могут решить эту проблему. Я также считаю, что в EF.Core есть методы, которые могут помочь, однако я предложу 2 «чистых» решения Entity Framework 6.

  1. Выполните свой запрос в двух частях - часть SQL, затем часть "в коде".

db.Cars.Where(c => c.Id == id).ToList().SingleOrDefault(c => c.Key == key)

это по-прежнему будет держать вашу логику инкапсулированной в классе, но вы не получите преимущества выполнения SQL.

  1. То, что я называю паттерном «проектор». Этот немного более затянут.

По сути, вы создаете «представление» EF POCO, представляющее объект передачи данных. он имеет свойства, необходимые для вашего представления, а также определяет, как проецировать данные из базы данных в представление.

// Poco:
public class Car {
  public int Id {get;set;}
  public string LocalSequenceNumber {get;set;}
  public int ProcessId {get;set; }
  public virtual Process Process {get;set;}
  // ...
}
public class Process {
 // ...
}

// View+Projector:
public class CarView
{ 
  public int Id {get;set;}
  public string Color {get;set;}
  public string Key {get;set;}
  public static Expression<Func<Car, CarView>> Projector = car => new CarView {
    Id = car.Id,
    Color = car.Color,
    Key = car.Process.Name + " " + car.LocalSequenceNumber 
  }
}

// calling code
var car = db.Cars.Select(CarView.Project).SingleOrDefault(cv => cv.Id == id && cv.Key == key)

Это оценит весь код в базе данных, инкапсулируя вашу бизнес-логику в код.

person AndrewP    schedule 15.10.2018
comment
Да, самый простой подход - это 1, где .ToList() выполняет отложенный запрос и получает результаты с сервера, а затем Linq .SingleOrDefault(c => c.Key == key) просто запускается в памяти IEnumerable и не пытается выполнить через LinqToEntities. - person Stephen York; 16.10.2018

Увы, вы забыли сообщить нам, что такое Process.Name и LocalSequenceNumber. Судя по идентификаторам, они не являются частью вашего Cars, а являются значениями в вашем локальном процессе. Почему бы не рассчитать ключ перед вашим запросом?

var key = string.Format("{0}_{1}", Process.Name, LocalSequenceNumber);
db.Cars.SingleOrDefault(c => c.Id == id && c.Key == key);

Если, с другой стороны, Process.Name или LocalSequenceNumber являются свойствами Car, вам придется изменить IQueryable.Expression в вашем запросе LINQ, используя только те свойства и методы, которые могут быть преобразованы вашим IQueryable.Provider в SQL.

К счастью, ваш Provider знает ToSTring() и концепцию конкатенации строк.

Поскольку вы используете свойство Key в Queryable.Where, я предлагаю расширить IQueryable функцией WhereKey. Если функции расширения для вас немного волшебны, см. Демистификация методов расширения

public static IQueryable<Car> WhereKey(this IQueryable<Car> cars, int id, string key)
{
    return cars.Where(car => car.Id == id
            && key == car.Process.Name.ToString() + "_" + car.LocalSequenceNumber.ToString());
}

Использование:

int carId = ...
string carKey = ...
var result = myDbContext.Cars
    .WhereKey(carId, carKey)
    .FirstOrDefault();

Рассмотрите возможность создания WhereKey, который проверяет только ключ. Конкатенация с Where, которая выбирает Id.

 var result = myDbContext.Cars
    .Where(car => car.Id == id)
    .WhereKey(carKey)
    .FirstOrDefault();

Если Process.Name или LocalSequenceNumber не является частью Car, добавьте его в качестве параметра. Вы поняли суть.

Рассмотрите возможность создания WhereKey, который проверяет только ключ. Конкатенация с Where, которая выбирает Id.

При желании можно создать WhereKeyFirstOrDefault(), но сомневаюсь, что от этого будет толку.

person Harald Coppoolse    schedule 15.10.2018