Encji nie można zbudować w zapytaniu LINQ to Entities

389

Istnieje typ jednostki o nazwie produkt, który jest generowany przez strukturę encji. Napisałem to zapytanie

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

Poniższy kod zgłasza następujący błąd:

„Encji lub typu złożonego Shop.Product nie można zbudować w zapytaniu LINQ to Entities”

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

Ale kiedy używam select pzamiast select new Product { Name = p.Name};tego działa poprawnie.

Jak mogę wykonać niestandardową sekcję wyboru?

Ghooti Farangi
źródło
System.NotSupportedException: „Encji lub typu złożonego„ StudentInfoAjax.Models.Student ”nie można zbudować w zapytaniu LINQ to Entities.”
Md Wahid

Odpowiedzi:

390

Nie można (i nie powinno być możliwe) rzutowanie na zmapowany obiekt. Możesz jednak rzutować na anonimowy typ lub na DTO :

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

A twoja metoda zwróci listę DTO.

public List<ProductDTO> GetProducts(int categoryID)
{
    return (from p in db.Products
            where p.CategoryID == categoryID
            select new ProductDTO { Name = p.Name }).ToList();
}
Jakimycz
źródło
152
Nie rozumiem, dlaczego nie powinienem być w stanie tego zrobić ... Byłoby to bardzo przydatne ...
Jonx
118
Cóż, zmapowane jednostki w EF zasadniczo reprezentują tabele bazy danych. Jeśli projektujesz na odwzorowaną encję, to w zasadzie to częściowo ładujesz encję, co nie jest prawidłowym stanem. EF nie będzie miał pojęcia, jak np. Obsługiwać aktualizację takiej encji w przyszłości (domyślnym zachowaniem byłoby prawdopodobnie zastąpienie nieobciążonych pól wartościami null lub cokolwiek, co masz w swoim obiekcie). Byłaby to niebezpieczna operacja, ponieważ groziłoby to utratą części danych w bazie danych, dlatego niedopuszczalne jest częściowe ładowanie jednostek (lub rzutowanie na zmapowane jednostki) w EF.
Jakimych
26
@Yakimych ma to sens, chyba że masz jakiś agregowany element, który generujesz / tworzysz za pomocą zapytania, a zatem jesteś w pełni świadomy / zamierza utworzyć zupełnie nowy byt, który będziesz następnie manipulował, a następnie dodawał. W tym przypadku albo zmusić uruchomić kwerendę lub pchania do DTO iz powrotem do jednostki, aby dodać - co jest frustrujące
Cargowire
16
@Cargowire - Zgadzam się, że ten scenariusz istnieje i jest frustrujący, gdy wiesz, co robisz, ale nie możesz tego robić z powodu ograniczeń. Gdyby jednak było to dozwolone, wielu sfrustrowanych programistów narzekałoby na utratę danych, np. Podczas próby zapisania częściowo załadowanych jednostek. IMO, błąd, który wysadza się z dużym hałasem (zgłaszanie wyjątku itp.), Jest lepszy niż zachowanie, które może powodować ukryte błędy, które są trudne do wyśledzenia i wyjaśnienia (rzeczy ładnie działają, zanim zaczniesz zauważać brakujące dane).
Jakimych,
14
DTO - Obiekty przesyłania danych
tkerwood
275

Możesz rzutować na anonimowy typ, a następnie z niego na typ modelu

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

Edycja : Będę bardziej szczegółowy, ponieważ to pytanie zyskało wiele uwagi.

Nie można rzutować bezpośrednio na typ modelu (ograniczenie EF), więc nie można tego obejść. Jedynym sposobem jest rzutowanie na typ anonimowy (pierwsza iteracja), a następnie na typ modelu (druga iteracja).

Należy również pamiętać, że gdy częściowe ładowanie w ten sposób jest częściowe, nie można ich aktualizować, więc powinny pozostać odłączone, ponieważ są.

Nigdy do końca nie rozumiałem, dlaczego nie jest to możliwe, a odpowiedzi w tym wątku nie dają mocnych powodów do tego (głównie mówiąc o częściowo załadowanych danych). Prawdą jest, że w stanie częściowo załadowanym nie można zaktualizować encji, ale wtedy ta encja zostanie odłączona, więc przypadkowe próby ich zapisania nie byłyby możliwe.

Zastanów się nad metodą, której użyłem powyżej: w rezultacie nadal mamy częściowo załadowany element modelu. Ten byt jest odłączony.

Rozważ ten (możliwy do istnienia) kod:

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

Może to również prowadzić do utworzenia listy odłączonych jednostek, więc nie musielibyśmy wykonywać dwóch iteracji. Kompilator byłby mądry, gdyby zobaczył, że użyto AsNoTracking (), co spowoduje odłączenie encji, co pozwoli nam to zrobić. Gdyby jednak pominięto AsNoTracking (), mógłby on zgłosić ten sam wyjątek, który jest teraz zgłaszany, aby ostrzec nas, że musimy być wystarczająco konkretni w kwestii oczekiwanego rezultatu.

Goran
źródło
3
To najczystsze rozwiązanie, gdy nie potrzebujesz / nie przejmujesz się stanem wybranego elementu, który chcesz wyświetlić.
Mário Meyrelles,
2
A kiedy nie obchodzi Cię, czy zwrócisz IEnumerable lub IQueryable;). Ale nadal otrzymujesz moją opinię, ponieważ to rozwiązanie działa teraz dla mnie.
Michael Brennt,
10
technicznie, rzut na typ modelu występuje poza zapytaniem i uważam, że wymaga również dodatkowej iteracji na liście. Nie będę używać tego rozwiązania dla mojego kodu, ale jest to rozwiązanie dla pytania. uptick.
1c1cle
4
Wolę to od przyjętego rozwiązania DTO - znacznie bardziej eleganckiego i czystego
Adam Hey
7
Tyle że z szacunkiem nie jest to właściwie odpowiedź na pytanie. Jest to odpowiedź na pytanie, jak wykonać rzutowanie Linq na obiekty, a nie rzutowanie zapytania Linq na obiekty. Tak więc opcja DTO jest jedyną opcją dotyczącą: Linq to Entities.
rism
78

Znalazłem inny sposób, w jaki działam, musisz zbudować klasę, która wywodzi się z klasy produktu i z niej korzystać. Na przykład:

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

Nie jestem pewien, czy jest to „dozwolone”, ale działa.

Tomasz Iniewicz
źródło
3
Sprytny! Wypróbowałem to teraz i działa. Jestem pewien, że jakoś mnie to poparzy.
Daniel
5
BTW to cię gryzie, jeśli spróbujesz utrwalić wyniki GetProducts (), ponieważ EF nie może znaleźć mapowania dla PseudoProduct, np. „System.InvalidOperationException: Nie można znaleźć informacji o mapowaniu i metadanych dla EntityType„ blah.PseudoProduct ””.
sming
4
Najlepsza odpowiedź i jedyna, która odpowiada w ramach parametrów pytania. Wszystkie pozostałe odpowiedzi zmieniają typ zwracanego obiektu lub przedwcześnie uruchamiają IQueryable i używają linq do obiektów
rdans
2
100% zszokowany to zadziałało ... w EF 6.1 to działa.
TravisWhidden
2
@mejobloggs Wypróbuj atrybut [NotMapped] na klasie pochodnej lub .Ignore <T>, jeśli używasz płynnego interfejsu API.
Dunc,
37

Oto jeden ze sposobów, aby to zrobić bez deklarowania dodatkowej klasy:

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

Można go jednak użyć tylko wtedy, gdy chcesz połączyć wiele jednostek w jedną całość. Powyższa funkcjonalność (proste mapowanie produktu na produkt) odbywa się w następujący sposób:

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;
}
Bojan Hrnkas
źródło
23

Kolejny prosty sposób :)

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;
}
Soren
źródło
dobra uwaga Właśnie nauczyłem się czegoś IQueryable z twoją miłą odpowiedzią. Byłoby miło, gdybyś wyjaśnił, DLACZEGO nie jest to przydatne po ToList (), a przyczyną jest to, że nie możesz używać list ogólnych w zapytaniu LINQ-to-SQL. Więc jeśli wiesz, że zawsze będziesz wypychać wyniki do innego zapytania przez osobę dzwoniącą, to z pewnością ma sens, aby być IQueryable. Ale jeśli nie ... jeśli później zamierzasz użyć go jako ogólnej listy, to użyj metody ToList () w metodzie, aby nie wykonywać ToList () na IQueryable przy każdym wywołaniu tej metody.
PositiveGuy,
Zupełnie w porządku, mój przyjacielu. Po prostu imituję podpis metody pytania, dlatego przekształcam go w zdolną do wysyłania zapytań ...;)
Soren
1
To działa, productList staje się nieedytowalny po ToList (). Jak mogę go edytować?
doncadavona,
Jeśli wpiszesz .ToListzapytanie, zostanie ono wykonane i wyciągnie dane z serwera, więc po co to robić ponownie AsQueryable?
Moshii
1
@Moshii tylko po to, aby spełnić metodę zwracania podpisu typu metody (jak powiedziałem w odpowiedzi, nie jest to już przydatne).
Soren
4

Możesz użyć tego i powinno działać -> Musisz użyć toListprzed utworzeniem nowej listy za pomocą select:

db.Products
    .where(x=>x.CategoryID == categoryID).ToList()
    .select(x=>new Product { Name = p.Name}).ToList(); 
Mohamed Adam
źródło
3
To jednak nadal wykonałoby „WYBIERZ * OD [..]”, a nie „WYBIERZ nazwę od [..]”
Timo Hermans
1

W odpowiedzi na inne pytanie oznaczone jako duplikat ( patrz tutaj ) wymyśliłem szybkie i łatwe rozwiązanie oparte na odpowiedzi Sorena:

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

Uwaga: To rozwiązanie działa tylko wtedy, gdy masz właściwość nawigacyjną (klucz obcy) w klasie Zadań (tutaj o nazwie „Incydent”). Jeśli tego nie masz, możesz po prostu użyć jednego z innych opublikowanych rozwiązań za pomocą „AsQueryable ()”.

JollyBrackets
źródło
1

Możesz rozwiązać ten problem, korzystając z obiektów przesyłania danych (DTO).

Są to trochę jak modele widokowe, w których umieszczasz potrzebne właściwości i możesz mapować je ręcznie w kontrolerze lub za pomocą rozwiązań innych firm, takich jak AutoMapper.

Z DTO możesz:

  • Ustaw szeregowanie danych (Json)
  • Pozbądź się okrągłych odniesień
  • Zmniejsz obciążenie sieci, pozostawiając niepotrzebne właściwości (zgodnie z modelem)
  • Użyj spłaszczania obiektów

Nauczyłem się tego w szkole w tym roku i jest to bardzo przydatne narzędzie.

Jelman
źródło
0

Jeśli używasz frameworka Entity, spróbuj usunąć właściwość z DbContext, który używa złożonego modelu, ponieważ Entity Miałem ten sam problem podczas mapowania wielu modeli na model widoku o nazwie Entity

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

Usunięcie wpisu z DbContext naprawiło mój błąd.

Vikas Suhag
źródło
0

jeśli wykonujesz Linq to Entity, nie możesz użyć ClassTypez newprzy selectzamykaniu zapytaniaonly anonymous types are allowed (new without type)

spójrz na ten fragment mojego projektu

//...
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

z dodałeś new keywordzamknięcie w Select nawet na tym complex propertiesotrzymasz ten błąd

tak kluczowe na zapytaniami ,,removeClassTypes from newLinq to Entity

ponieważ zostanie przekształcona w instrukcję SQL i wykonana na SqlServer

więc kiedy mogę użyć new with typespo selectzamknięciu?

możesz go użyć, jeśli masz do czynienia 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 

po wykonaniu ToListna zapytanie stało się in memory collection to, abyśmy mogli używać go new ClassTypesw select

Basheer AL-MOMANI
źródło
Jasne, że możesz używać anonimowych typów, ale nie możesz utworzyć encji w zapytaniu LINQ, nawet aby ustawić anonimowego członka, ponieważ LINQ-to-Entities nadal zgłasza ten sam wyjątek.
Suncat2000
0

W wielu przypadkach transformacja nie jest potrzebna. Pomyśl z tego powodu, dla którego chcesz silnie wpisać List, i oceń, czy chcesz tylko dane, na przykład w serwisie internetowym lub w celu ich wyświetlenia. Nie ma znaczenia rodzaj. Musisz tylko wiedzieć, jak go odczytać i sprawdzić, czy jest identyczny z właściwościami zdefiniowanymi w zdefiniowanym typie anonimowym. To jest scenariusz optymalizacyjny, który powoduje, że nie potrzebujesz wszystkich pól encji, i dlatego istnieje typ anonimowy.

Prostym sposobem jest zrobienie tego:

IEnumerable<object> list = dataContext.Table.Select(e => new { MyRequiredField = e.MyRequiredField}).AsEnumerable();
Sterling Diaz
źródło
0

Nie pozwoli Ci to odwzorować z powrotem na Produkt, ponieważ to jest twoja tabela, której szukasz. Potrzebujesz anonimowej funkcji, a następnie możesz dodać ją do ViewModel, dodać każdy ViewModel do a List<MyViewModel>i zwrócić je. To niewielka dygresja, ale uwzględniam zastrzeżenia dotyczące obchodzenia się z zerowymi datami, ponieważ są to problemy z tyłu, na wypadek, gdybyś miał. Tak sobie z tym poradziłem.

Mam nadzieję, że masz ProductViewModel:

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

Mam strukturę wstrzykiwania / repozytorium zależności, w której wywołuję funkcję w celu pobrania moich danych. Używając Twojego postu jako przykładu, w wywołaniu funkcji Kontrolera wyglądałoby to tak:

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

W klasie repozytorium:

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

Po powrocie do kontrolera wykonujesz:

 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>
vapcguy
źródło
-1

dodaj tylko AsEnumerable ():

public IQueryable<Product> GetProducts(int categoryID)
{
    return from p in db.Products.AsEnumerable()
           where p.CategoryID== categoryID
           select new Product { Name = p.Name};
}
HamidReza
źródło
8
Nigdy tego nie rób! Spowoduje to pobranie wszystkich danych z bazy danych, a następnie dokonanie wyboru.
Gh61
1
Dlatego w niektórych firmach Linq jest zabroniony.
hakan
-2

możesz dodać AsEnumerable do swojej kolekcji w następujący sposób:

public IQueryable<Product> GetProducts(int categoryID)
{
    return from p in db.Products.AsEnumerable()
           where p.CategoryID== categoryID
           select new Product { Name = p.Name};
}
HamidReza
źródło
Dlaczego jest to zła odpowiedź, mimo że działa ... Ponieważ AsNumerable kończy linq na byty. Klauzula Where i wszystko inne jest obsługiwane poza linq dla Encji. tzn. każdy produkt jest pobierany, a następnie filtrowany przez linq do obiektów. Poza tym jest prawie dokładnie taki sam, jak powyższa odpowiedź .ToList. stackoverflow.com/questions/5311034/…
KenF
1
Problem polega na tym, że jest to po prostu wykonanie * wyboru z ..., a nie wybranie nowego produktu {Nazwa = p.Nazwa}, ponieważ otrzymasz również cykliczne odwołanie. I chcesz tylko Imię.
Sterling Diaz