Суб'єкт не може бути побудований у запиті LINQ to Entities


389

Існує тип сутності, який називається продуктом, який генерується структурою сутності. Я написав цей запит

public IQueryable<Product> GetProducts(int categoryID)
{
    return from p in db.Products
           where p.CategoryID== categoryID
           select new Product { Name = p.Name};
}

У наведеному нижче коді випливає така помилка:

"Суб'єкт або комплексний тип Shop.Product не може бути побудований у запиті LINQ to Entities"

var products = productRepository.GetProducts(1).Tolist();

Але коли я використовую select pзамість select new Product { Name = p.Name};нього, працює правильно.

Як я можу зробити попередній розділ вибору?


System.NotSupportedException: 'Суб'єкт або комплексний тип' StudentInfoAjax.Models.Student 'не може бути побудований у запиті LINQ для Entities.'
Md Wahid

Відповіді:


390

Ви не можете (і не повинні бути в змозі) проектувати на відображену сутність. Однак можна проектувати на анонімний тип або на DTO :

public class ProductDTO
{
    public string Name { get; set; }
    // Other field you may need from the Product entity
}

І ваш метод поверне Список DTO.

public List<ProductDTO> GetProducts(int categoryID)
{
    return (from p in db.Products
            where p.CategoryID == categoryID
            select new ProductDTO { Name = p.Name }).ToList();
}

152
Я не розумію, чому я не зміг би цього зробити ... Це було б дуже корисно ...
Jonx

118
Ну, відображені сутності в EF в основному представляють таблиці баз даних. Якщо ви проектуєте на відображену сутність, то, що ви робите, це частково завантажувати об'єкт, який не є дійсним станом. EF не матиме поняття, як, наприклад, обробляти оновлення такої сутності в майбутньому (поведінка за замовчуванням, ймовірно, буде перезаписати незавантажені поля нулями або все, що ви будете мати у своєму об'єкті). Це було б небезпечною операцією, оскільки ви ризикуєте втратити частину своїх даних у БД, тому часткове завантаження об'єктів (або проектування на відображені об'єкти) у EF заборонено.
Якімич

26
@Yakimych, що має сенс, за винятком випадків, коли у вас є якась сукупна сутність, яку ви генеруєте / створюєте за допомогою запиту, і тому ви повністю усвідомлюєте / маєте намір створити абсолютно нову сутність, якою ви потім будете маніпулювати та пізніше додавати. У цьому випадку вам або доведеться змусити запустити запит, або натиснути на dto і повернутись у сутність, щоб додати - що засмучує
Cargowire

16
@Cargowire - Я погоджуюся, що такий сценарій існує, і це засмучує, коли ти знаєш, що ти робиш, але заборонено це робити через обмеження. Однак, якби це було дозволено, було б багато розчарованих розробників, які скаржаться на втрату своїх даних, наприклад, намагаючись зберегти частково завантажені об'єкти. IMO, помилка, яка вибухає з великим рівнем шуму (кидання винятку тощо), є кращою, ніж поведінка, яка може спричинити приховані помилки, які важко відстежити та пояснити (такі речі добре працюють, перш ніж ви почнете помічати відсутні дані).
Якимич


275

Ви можете проектувати в анонімний тип, а потім з нього на тип моделі

public IEnumerable<Product> GetProducts(int categoryID)
{
    return (from p in Context.Set<Product>()
            where p.CategoryID == categoryID
            select new { Name = p.Name }).ToList()
           .Select(x => new Product { Name = x.Name });
}

Редагувати : Я буду трохи більш конкретним, оскільки це питання привернуло багато уваги.

Не можна запроектувати тип моделі безпосередньо (обмеження EF), тому не обійтися. Єдиний спосіб - проектувати в анонімний тип (1-а ітерація), а потім моделювати тип (2-а ітерація).

Будь ласка, майте на увазі, що коли ви частково завантажуєте об'єкти таким чином, вони не можуть бути оновлені, тому вони повинні залишатися відокремленими, як вони є.

Я ніколи не розумів, чому це неможливо, і відповіді на цю тему не дають вагомих причин проти цього (переважно кажучи про частково завантажені дані). Правильно, що в частково завантаженому державному об'єкті не можна оновити, але тоді ця сутність буде від'єднана, тому випадкові спроби їх збереження були б неможливими.

Розглянемо метод, який я використав вище: у нас все ще є частково завантажена модельна сутність. Ця організація відокремлена.

Розглянемо цей (бажає існувати) можливий код:

return (from p in Context.Set<Product>()
        where p.CategoryID == categoryID
        select new Product { Name = p.Name }).AsNoTracking().ToList();

Це також може призвести до переліку відокремлених об'єктів, тому нам не потрібно буде робити дві ітерації. Компілятору було б розумно бачити, що використовується AsNoTracking (), що призведе до відокремлених об'єктів, тому це може нам дозволити це зробити. Якщо, однак, AsNoTracking () було опущено, воно може кинути той самий виняток, що і зараз, щоб попередити нас, що нам потрібно бути досить конкретним щодо результату, який ми хочемо.


3
Це найчистіше рішення, коли вам не потрібно / небайдуже стан обраної сутності, яку ви хочете спроектувати.
Mário Meyrelles

2
І коли вам все одно, чи повернете ви IEnumerable чи IQueryable;). Але все-таки ви отримаєте мою нагоду, тому що це рішення працює для мене зараз.
Майкл Бреннт

10
технічно проекція на тип моделі відбувається поза запитом, і я вважаю, що також потрібна додаткова ітерація через список. Я не буду використовувати це рішення для свого коду, але це рішення для питання. підступ.
1c1cle

4
Я вважаю за краще це прийняте рішення DTO - набагато більш елегантне та чисте
Adam Hey,

7
За винятком того, що з повагою це насправді не відповідь на питання. Це відповідь на те, як зробити проекцію Linq To Objects, а не проекцію запитів Linq to Entities. Тож варіант DTO є єдиним варіантом, що стосується: Linq to Entities.
rism

78

Існує ще один спосіб, коли я знайшов твори, ви повинні створити клас, який походить з вашого класу Product і використовувати це. Наприклад:

public class PseudoProduct : Product { }

public IQueryable<Product> GetProducts(int categoryID)
{
    return from p in db.Products
           where p.CategoryID== categoryID
           select new PseudoProduct() { Name = p.Name};
}

Не впевнений, чи це "дозволено", але це працює.


3
Розумний! Спробував це зараз, і це працює. Я впевнений, що це якось спалить мене.
Даніель

5
BTW це вас укусить, якщо ви намагаєтесь зберегти результати GetProducts (), оскільки EF не може знайти відображення для PseudoProduct, наприклад, "System.InvalidOperationException: Не вдалося знайти картографічну інформацію та інформацію метаданих для EntityType" blah.PseudoProduct "".
посміхається

4
Найкраща відповідь і єдина, яка відповідає в межах параметрів питання. Усі інші відповіді змінюють тип повернення або передчасно виконують IQueryable і використовують linq для об'єктів
rdans

2
На 100% шоковано це спрацювало ... в EF 6.1 це працює.
TravisWhidden

2
@mejobloggs Спробуйте атрибут [NotMapped] у похідному класі, або .Ignore <T>, якщо ви використовуєте вільний API.
Данк

37

Ось один із способів зробити це без оголошення додаткового класу:

public List<Product> GetProducts(int categoryID)
{
    var query = from p in db.Products
            where p.CategoryID == categoryID
            select new { Name = p.Name };
    var products = query.ToList().Select(r => new Product
    {
        Name = r.Name;
    }).ToList();

    return products;
}

Однак це може бути використане лише в тому випадку, якщо ви хочете об'єднати кілька об'єктів в одну сутність. Вищенаведена функціональність (просте відображення продукту на продукт) виконується так:

public List<Product> GetProducts(int categoryID)
{
    var query = from p in db.Products
            where p.CategoryID == categoryID
            select p;
    var products = query.ToList();

    return products;
}

23

Ще один простий спосіб :)

public IQueryable<Product> GetProducts(int categoryID)
{
    var productList = db.Products
        .Where(p => p.CategoryID == categoryID)
        .Select(item => 
            new Product
            {
                Name = item.Name
            })
        .ToList()
        .AsQueryable(); // actually it's not useful after "ToList()" :D

    return productList;
}

хороший момент, я щойно дізнався щось IQueyable з вашою приємною відповіддю. Було б добре, хоч якби ви пояснили, ЧОМУ це не корисно після ToList (), і причина полягає в тому, що ви не можете використовувати загальні списки в запиті LINQ до SQL. Тож якщо ви знаєте, що ви завжди збираєтесь підштовхувати результати до іншого запиту абоненту, то, безумовно, має сенс бути IQueryable. Якщо ж ні, якщо ви збираєтесь використовувати його як загальний список після цього, тоді використовуйте ToList () всередині методу, щоб ви не робили ToList () в IQueryable кожен виклик цього методу.
PositiveGuy

Ви абсолютно добре, мій друг. Я просто імітую підпис методу запитання, через що я перетворюю його на запит, здатний ...;)
Soren

1
Це працює, список продуктів стає непридатним після ToList (). Як зробити його редагованим?
doncadavona

Якщо ви ставите .ToListзапит, він виконується і витягує дані з сервера, тоді який сенс зробити це знову AsQueryable?.
Мошій

1
@Moshii лише для того, щоб задовольнити підпис методу повернення типу (як я вже говорив у відповіді, це вже не корисно).
Сорен

4

Ви можете використовувати це, і це повинно працювати -> Ви повинні використовувати toListперед створенням нового списку, вибравши:

db.Products
    .where(x=>x.CategoryID == categoryID).ToList()
    .select(x=>new Product { Name = p.Name}).ToList(); 

3
Однак це все-таки зробить "ВИБРАТИ * ВІД [..]", а не "ВИБРАТИ ім'я від [..]"
Тимо Германс

1

У відповідь на інше питання, яке було позначене як дублікат ( див. Тут ), я зрозумів швидке та просте рішення, засноване на відповіді Сорена:

data.Tasks.AddRange(
    data.Task.AsEnumerable().Select(t => new Task{
        creator_id   = t.ID,
        start_date   = t.Incident.DateOpened,
        end_date     = t.Incident.DateCLosed,
        product_code = t.Incident.ProductCode
        // so on...
    })
);
data.SaveChanges();

Примітка. Це рішення працює лише в тому випадку, якщо у вас є властивість навігації (зовнішній ключ) класу Завдання (тут називається "Інцидент"). Якщо у вас цього немає, ви можете просто скористатись одним із інших розміщених рішень за допомогою "AsQueryable ()".


1

Ви можете вирішити це за допомогою об'єктів передачі даних (DTO).

Це трохи схожі на перегляд моделей, де ви вводите потрібні вам властивості, і ви можете їх відобразити вручну в контролері або за допомогою сторонніх рішень, таких як AutoMapper.

З DTO ви можете:

  • Зробити серіалізацію даних (Json)
  • Позбудьтеся кругових посилань
  • Зменшіть мережевий трафік, залишивши властивості, які вам не потрібні
  • Використовуйте обшивку об'єктів

Я цього року навчався в школі, і це дуже корисний інструмент.


0

Якщо ви використовуєте Entity Framework, то спробуйте видалити властивість з DbContext, який використовує вашу складну модель як Entity У мене була така ж проблема, коли відображення декількох моделей у моделі перегляду з назвою Entity

public DbSet<Entity> Entities { get; set; }

Видалення запису з DbContext виправило мою помилку.


0

якщо ви Виконавчі Linq to Entityви не можете використовувати ClassTypeз newвselect закритті запитуonly anonymous types are allowed (new without type)

погляньте на цей фрагмент мого проекту

//...
var dbQuery = context.Set<Letter>()
                .Include(letter => letter.LetterStatus)
                .Select(l => new {Title =l.Title,ID = l.ID, LastModificationDate = l.LastModificationDate, DateCreated = l.DateCreated,LetterStatus = new {ID = l.LetterStatusID.Value,NameInArabic = l.LetterStatus.NameInArabic,NameInEnglish = l.LetterStatus.NameInEnglish} })
                               ^^ without type__________________________________________________________________________________________________________^^ without type

з вас додали new keywordзакриття в Select, навіть якщо complex propertiesви отримаєте цю помилку

так ключових слів на запити ,,removeClassTypes from newLinq to Entity

тому що він буде перетворений у sql-оператор та виконаний на SqlServer

тож коли я можу використовувати new with typesдаліselect закритті?

ви можете використовувати його, якщо ви маєте справу LINQ to Object (in memory collection)

//opecations in tempList , LINQ to Entities; so we can not use class types in select only anonymous types are allowed
var tempList = dbQuery.Skip(10).Take(10).ToList();// this is list of <anonymous type> so we have to convert it so list of <letter>

//opecations in list , LINQ to Object; so we can use class types in select
list = tempList.Select(l => new Letter{ Title = l.Title, ID = l.ID, LastModificationDate = l.LastModificationDate, DateCreated = l.DateCreated, LetterStatus = new LetterStatus{ ID = l.LetterStatus.ID, NameInArabic = l.LetterStatus.NameInArabic, NameInEnglish = l.LetterStatus.NameInEnglish } }).ToList();
                                ^^^^^^ with type 

після того, як я виконав ToListна запит, це стало, in memory collection щоб ми могли використовувати new ClassTypesв select


Звичайно, ви можете використовувати анонімні типи, але ви не можете створити об'єкт у запиті LINQ, навіть щоб встановити анонімного члена, оскільки LINQ до-Entities все ще кидає той самий виняток.
Suncat2000

0

У багатьох випадках трансформація не потрібна. Подумайте, чому ви хочете сильно ввести список, і оцініть, чи потрібно вам просто дані, наприклад, у веб-службі або для їх відображення. Не має значення тип. Вам просто потрібно знати, як його читати і перевіряти, чи відповідає ідентичність властивостей, визначених у визначеному вами анонімному типі. Це оптимум сценарій, викликати те, що вам не потрібно всіх полів об'єкта, і тому існує анонімний тип.

Простий спосіб зробити це:

IEnumerable<object> list = dataContext.Table.Select(e => new { MyRequiredField = e.MyRequiredField}).AsEnumerable();

0

Це не дозволить повернути вас до продукту, оскільки це ваша таблиця, яку ви запитуєте. Вам потрібна анонімна функція, потім ви можете додати її до ViewModel, а кожен ViewModel додати до а List<MyViewModel>та повернути їх. Це невеликий відступ, але я включаю застереження щодо поводження з нестабільними датами, оскільки це болі ззаду для вирішення, на всякий випадок, якщо у вас є такі. Ось як я впорався.

Сподіваємось, у вас є ProductViewModel:

public class ProductViewModel
{
    [Key]
    public string ID { get; set; }
    public string Name { get; set; }
}

У мене є структура введення / зберігання залежностей, де я викликаю функцію, щоб захопити свої дані. Використовуючи свою посаду як приклад, у виклику функції контролера це виглядатиме так:

int categoryID = 1;
var prods = repository.GetProducts(categoryID);

У класі сховища:

public IEnumerable<ProductViewModel> GetProducts(int categoryID)
{
   List<ProductViewModel> lstPVM = new List<ProductViewModel>();

   var anonymousObjResult = from p in db.Products
                            where p.CategoryID == categoryID 
                            select new
                            {
                                CatID = p.CategoryID,
                                Name = p.Name
                            };

        // NOTE: If you have any dates that are nullable and null, you'll need to
        // take care of that:  ClosedDate = (DateTime?)p.ClosedDate ?? DateTime.Now

        // If you want a particular date, you have to define a DateTime variable,
        // assign your value to it, then replace DateTime.Now with that variable. You
        // cannot call a DateTime.Parse there, unfortunately. 
        // Using 
        //    new Date("1","1","1800"); 
        // works, though. (I add a particular date so I can edit it out later.)

        // I do this foreach below so I can return a List<ProductViewModel>. 
        // You could do: return anonymousObjResult.ToList(); here
        // but it's not as clean and is an anonymous type instead of defined
        // by a ViewModel where you can control the individual field types

        foreach (var a in anonymousObjResult)
        {                
            ProductViewModel pvm = new ProductViewModel();
            pvm.ID = a.CatID;  
            pvm.Name = a.Name;
            lstPVM.Add(rvm);
        }

        // Obviously you will just have ONE item there, but I built it 
        // like this so you could bring back the whole table, if you wanted
        // to remove your Where clause, above.

        return lstPVM;
    }

Повернувшись до контролера, ви робите:

 List<ProductViewModel> lstProd = new List<ProductViewModel>();

 if (prods != null) 
 {
    // For setting the dates back to nulls, I'm looking for this value:
    // DateTime stdDate = DateTime.Parse("01/01/1800");

    foreach (var a in prods)
    {
        ProductViewModel o_prod = new ReportViewModel();
        o_prod.ID = a.ID;
        o_prod.Name = a.Name;
       // o_prod.ClosedDate = a.ClosedDate == stdDate ? null : a.ClosedDate;
        lstProd.Add(o_prod);
    }
}
return View(lstProd);  // use this in your View as:   @model IEnumerable<ProductViewModel>

-1

додайте лише AsEnumerable ():

public IQueryable<Product> GetProducts(int categoryID)
{
    return from p in db.Products.AsEnumerable()
           where p.CategoryID== categoryID
           select new Product { Name = p.Name};
}

8
НІКОЛИ не робіть цього! Це отримає всі дані з БД, а потім зробить вибір.
Gh61

1
Ось чому в деяких компаніях Linq заборонено використовувати.
хакан

-2

ви можете додати AsEnumerable до своєї колекції таким чином:

public IQueryable<Product> GetProducts(int categoryID)
{
    return from p in db.Products.AsEnumerable()
           where p.CategoryID== categoryID
           select new Product { Name = p.Name};
}

Чому це погана відповідь, хоча це і працює ... Пункт «Де» та все інше обробляються поза зв’язком з Суб’єктами. тобто кожен продукт витягується, після чого відфільтровується по об'єктах. Крім цього, це майже точно так само, як відповідь .ToList вище. stackoverflow.com/questions/5311034 / ...
KenF

1
Проблема в цьому полягає в тому, що виконується лише вибраний * з ..., а не вибір нового продукту {Name = p.Name}, оскільки ви отримаєте також циклічну посилання. І ви хочете просто Ім'я.
Стерлінг Діаз
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.