EF6 използва потребителско свойство в linq заявка

Имам клас, който има следното свойство:

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

Локалният пореден номер е изчислено цяло число, подкрепено от кеш под формата на паралелен речник.

Искам да използвам свойството Key по-горе в LINQ заявка, но получавам изключението:

Посоченият тип член „Ключ“ не се поддържа в 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 да не картографира Key към колона от базата данни, тогава искате да го използвате в заявка. Не е изненадващо, че не работи.   -  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