L'entité ne peut pas être construite dans une requête LINQ to Entities

389

Il existe un type d'entité appelé produit qui est généré par le framework d'entité. J'ai écrit cette requête

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

Le code ci-dessous renvoie l'erreur suivante:

"L'entité ou le type complexe Shop.Product ne peut pas être construit dans une requête LINQ to Entities"

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

Mais quand j'utilise select pau lieu de select new Product { Name = p.Name};cela fonctionne correctement.

Comment puis-je préformer une section de sélection personnalisée?

Ghooti Farangi
la source
System.NotSupportedException: 'L'entité ou le type complexe' StudentInfoAjax.Models.Student 'ne peut pas être construit dans une requête LINQ to Entities.'
Md Wahid

Réponses:

390

Vous ne pouvez pas (et ne devriez pas pouvoir) projeter sur une entité mappée. Vous pouvez cependant projeter sur un type anonyme ou sur un DTO :

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

Et votre méthode renverra une liste de DTO.

public List<ProductDTO> GetProducts(int categoryID)
{
    return (from p in db.Products
            where p.CategoryID == categoryID
            select new ProductDTO { Name = p.Name }).ToList();
}
Yakimych
la source
152
Je ne comprends pas pourquoi je ne devrais pas être capable de faire ça ... Ce serait très utile ...
Jonx
118
Eh bien, les entités mappées dans EF représentent essentiellement des tables de base de données. Si vous projetez sur une entité mappée, ce que vous faites essentiellement est de charger partiellement une entité, ce qui n'est pas un état valide. EF n'aura aucune idée de comment, par exemple, gérer une mise à jour d'une telle entité à l'avenir (le comportement par défaut serait probablement d'écraser les champs non chargés avec des valeurs nulles ou tout ce que vous aurez dans votre objet). Ce serait une opération dangereuse, car vous risqueriez de perdre certaines de vos données dans la base de données, il n'est donc pas autorisé de charger partiellement des entités (ou de projeter sur des entités mappées) dans EF.
Yakimych
26
@Yakimych qui a du sens, sauf si vous avez une entité agrégée que vous générez / créez via une requête et que vous êtes donc pleinement conscient / avez l'intention de créer une toute nouvelle entité que vous manipulerez puis ajouterez plus tard. Dans ce cas, vous devez soit forcer l'exécution de la requête, soit pousser dans un dto et revenir dans une entité pour ajouter - ce qui est frustrant
Cargowire
16
@Cargowire - Je suis d'accord, ce scénario existe et c'est frustrant quand vous savez ce que vous faites mais n'êtes pas autorisé à le faire en raison de limitations. Cependant, si cela avait été autorisé, il y aurait beaucoup de développeurs frustrés se plaignant de la perte de leurs données, par exemple en essayant de sauvegarder des entités partiellement chargées. IMO, une erreur qui explose avec beaucoup de bruit (levée d'une exception, etc.) est meilleure qu'un comportement qui peut provoquer des bogues cachés difficiles à localiser et à expliquer (les choses fonctionnent bien avant de commencer à remarquer des données manquantes).
Yakimych
275

Vous pouvez projeter dans un type anonyme, puis à partir de celui-ci vers le type de modèle

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

Éditer : Je vais être un peu plus précis car cette question a attiré beaucoup d'attention.

Vous ne pouvez pas projeter directement dans le type de modèle (restriction EF), il n'y a donc aucun moyen de contourner cela. La seule façon est de projeter en type anonyme (1ère itération), puis de modéliser le type (2ème itération).

Sachez également que lorsque vous chargez partiellement des entités de cette manière, elles ne peuvent pas être mises à jour, elles doivent donc rester détachées telles quelles.

Je n'ai jamais complètement compris pourquoi cela n'est pas possible, et les réponses sur ce fil ne donnent pas de raisons solides contre cela (parlant principalement de données partiellement chargées). Il est exact que, dans un état partiellement chargé, l'entité ne peut pas être mise à jour, mais alors, cette entité serait détachée, de sorte que des tentatives accidentelles de les enregistrer ne seraient pas possibles.

Considérez la méthode que j'ai utilisée ci-dessus: nous avons toujours une entité de modèle partiellement chargée. Cette entité est détachée.

Considérez ce code (souhait d'exister) possible:

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

Cela pourrait également entraîner une liste d'entités détachées, nous n'aurions donc pas besoin de faire deux itérations. Un compilateur serait intelligent de voir que AsNoTracking () a été utilisé, ce qui entraînera des entités détachées, il pourrait donc nous permettre de le faire. Si, cependant, AsNoTracking () était omis, il pourrait lever la même exception qu'il lance maintenant, pour nous avertir que nous devons être suffisamment précis sur le résultat que nous voulons.

Goran
la source
3
C'est la solution la plus propre lorsque vous n'avez pas besoin / ne vous souciez pas de l'état de l'entité sélectionnée que vous souhaitez projeter.
Mário Meyrelles
2
Et quand vous ne vous souciez pas si vous retournez IEnumerable ou IQueryable;). Mais vous obtenez toujours mon vote positif car cette solution fonctionne pour moi maintenant.
Michael Brennt
10
techniquement, la projection vers le type de modèle se produit en dehors de la requête, et je crois que cela nécessite également une itération supplémentaire dans la liste. Je n'utiliserai pas cette solution pour mon code, mais c'est une solution pour la question. uptick.
1c1cle
4
Je préfère cela à la solution DTO acceptée - beaucoup plus élégante et propre
Adam Hey
7
Sauf que, en toute déférence, ce n'est pas vraiment une réponse à la question. Il s'agit d'une réponse sur la façon de faire une projection Linq To Objects, pas une projection de requête Linq to Entities. L'option DTO est donc la seule option concernant Linq to Entities.
rism
78

Il y a une autre façon dont j'ai trouvé que ça marche, vous devez construire une classe qui dérive de votre classe Product et l'utiliser. Par exemple:

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

Je ne sais pas si cela est "autorisé", mais cela fonctionne.

Tomasz Iniewicz
la source
3
Intelligent! J'ai essayé ça maintenant et ça marche. Je suis sûr que cela me brûlera en quelque sorte.
Daniel
5
BTW, cela vous mord si vous essayez de conserver les résultats de GetProducts () car EF ne peut pas trouver le mappage pour PseudoProduct, par exemple "System.InvalidOperationException: les informations de mappage et de métadonnées sont introuvables pour EntityType 'blah.PseudoProduct'".
sming
4
Meilleure réponse, et la seule qui répond dans les paramètres de la question. Toutes les autres réponses modifient le type de retour ou exécutent prématurément IQueryable et utilisent linq pour les objets
rdans
2
100% choqué, cela a fonctionné ... dans EF 6.1, cela fonctionne.
TravisWhidden
2
@mejobloggs Essayez l'attribut [NotMapped] sur la classe dérivée, ou .Ignore <T> si vous utilisez l'API courante.
Dunc
37

Voici une façon de procéder sans déclarer de classe supplémentaire:

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

Cependant, cela ne doit être utilisé que si vous souhaitez combiner plusieurs entités en une seule entité. La fonctionnalité ci-dessus (cartographie simple de produit à produit) se fait comme suit:

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
la source
23

Un autre moyen simple :)

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
la source
bon point, je viens d'apprendre quelque chose IQueryable avec votre belle réponse. Cela aurait été bien si vous aviez expliqué POURQUOI ce n'est pas utile après une ToList () et la raison en est que vous ne pouvez pas utiliser de listes génériques dans une requête LINQ-to-SQL. Donc, si vous savez que vous allez toujours pousser les résultats dans une autre requête de l'appelant, il est certainement logique d'être IQueryable. Mais sinon ... si vous allez l'utiliser comme une liste générique après, alors utilisez la ToList () à l'intérieur de la méthode afin que vous ne fassiez pas une ToList () sur le IQueryable chaque appel à cette méthode.
PositiveGuy
Vous allez tout à fait bien mon ami, j'imite juste la signature de la méthode des questions, à cause de cela je la convertis en requête capable ...;)
Soren
1
Cela fonctionne, la productList devient non modifiable après la ToList (). Comment puis-je le rendre modifiable?
doncadavona
Si vous mettez .ToListen requête, elle est exécutée et extraite des données du serveur alors à quoi bon la refaire AsQueryable?.
Moshii
1
@Moshii juste pour satisfaire la signature de type de retour de méthode, (comme je l'ai dit dans la réponse, ce n'est plus utile).
Soren
4

Vous pouvez l'utiliser et cela devrait fonctionner -> Vous devez utiliser toListavant de créer la nouvelle liste en utilisant select:

db.Products
    .where(x=>x.CategoryID == categoryID).ToList()
    .select(x=>new Product { Name = p.Name}).ToList(); 
Mohamed Adam
la source
3
Cela ferait cependant toujours un 'SELECT * FROM [..]', pas un 'SELECT name FROM [..]'
Timo Hermans
1

En réponse à l'autre question qui a été marquée comme doublon ( voir ici ), j'ai trouvé une solution rapide et facile basée sur la réponse de Soren:

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

Remarque: Cette solution ne fonctionne que si vous avez une propriété de navigation (clé étrangère) sur la classe Task (appelée ici «Incident»). Si vous ne l'avez pas, vous pouvez simplement utiliser l'une des autres solutions publiées avec "AsQueryable ()".

JollyBrackets
la source
1

Vous pouvez résoudre ce problème en utilisant des objets de transfert de données (DTO).

Ce sont un peu comme des modèles de vue où vous mettez les propriétés dont vous avez besoin et vous pouvez les mapper manuellement dans votre contrôleur ou en utilisant des solutions tierces comme AutoMapper.

Avec les DTO, vous pouvez:

  • Rendre les données sérialisables (Json)
  • Débarrassez-vous des références circulaires
  • Réduisez le trafic réseau en laissant les propriétés dont vous n'avez pas besoin (viewmodelwise)
  • Utiliser l'aplatissement des objets

J'ai appris cela à l'école cette année et c'est un outil très utile.

Jelman
la source
0

Si vous utilisez le framework Entity, essayez de supprimer la propriété de DbContext qui utilise votre modèle complexe en tant qu'entité. J'ai eu le même problème lors du mappage de plusieurs modèles dans un modèle de vue nommé Entity

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

La suppression de l'entrée de DbContext a corrigé mon erreur.

vikas suhag
la source
0

si vous exécutez, Linq to Entityvous ne pouvez pas utiliser le ClassTypeavec newdans la selectfermeture de la requêteonly anonymous types are allowed (new without type)

regardez cet extrait de mon projet

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

vous avez ajouté la new keywordfermeture dans Select même sur le complex propertiesvous obtiendrez cette erreur

donc removele ClassTypes from newmot - clé sur les Linq to Entityrequêtes ,,

car il sera transformé en instruction sql et exécuté sur SqlServer

alors quand puis-je l'utiliser new with typesà la selectfermeture?

vous pouvez l'utiliser si vous avez affaire à 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 

après avoir exécuté ToListsur requête, il est devenu in memory collection que nous pouvons utiliser new ClassTypesdans certains

Basheer AL-MOMANI
la source
Bien sûr, vous pouvez utiliser des types anonymes, mais vous ne pouvez pas créer une entité dans la requête LINQ, même pour définir un membre anonyme, car LINQ-to-Entities lève toujours la même exception.
Suncat2000
0

Dans de nombreux cas, la transformation n'est pas nécessaire. Pensez à la raison pour laquelle vous voulez fortement taper List et évaluez si vous voulez simplement les données, par exemple, dans un service Web ou pour les afficher. Peu importe le type. Vous avez juste besoin de savoir comment le lire et vérifier qu'il est identique aux propriétés définies dans le type anonyme que vous avez défini. C'est le scénario optimun, car quelque chose dont vous n'avez pas besoin de tous les champs d'une entité, et c'est la raison pour laquelle le type anonyme existe.

Une façon simple de procéder est la suivante:

IEnumerable<object> list = dataContext.Table.Select(e => new { MyRequiredField = e.MyRequiredField}).AsEnumerable();
Sterling Diaz
la source
0

Il ne vous permettra pas de mapper à nouveau sur Product car c'est votre table que vous interrogez. Vous avez besoin d'une fonction anonyme, vous pouvez ensuite l'ajouter à un ViewModel, ajouter chaque ViewModel à un List<MyViewModel>et les renvoyer. C'est une légère digression, mais j'inclus des mises en garde sur la gestion des dates annulables car elles sont difficiles à gérer, au cas où vous en auriez. C'est ainsi que je l'ai géré.

J'espère que vous avez ProductViewModel:

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

J'ai un cadre d'injection de dépendances / référentiel où j'appelle une fonction pour récupérer mes données. En utilisant votre article comme exemple, dans votre appel de fonction Controller, cela ressemblerait à ceci:

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

Dans la classe de référentiel:

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

De retour dans le contrôleur, vous faites:

 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
la source
-1

ajoutez seulement 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
la source
8
NE JAMAIS le faire! Cela récupérera toutes les données de la base de données et fera ensuite la sélection.
Gh61
1
C'est pourquoi dans certaines entreprises, Linq est interdit d'utilisation.
hakan
-2

vous pouvez ajouter AsEnumerable à votre collection comme suit:

public IQueryable<Product> GetProducts(int categoryID)
{
    return from p in db.Products.AsEnumerable()
           where p.CategoryID== categoryID
           select new Product { Name = p.Name};
}
HamidReza
la source
Pourquoi c'est une mauvaise réponse bien que cela fonctionne ... Comme AsEnumerable termine linq aux entités. La clause Where et tout le reste est gérée en dehors de linq vers Entities. c'est-à-dire que chaque produit est récupéré puis filtré par linq en objets. En dehors de cela, c'est à peu près exactement la même chose que la réponse .ToList ci-dessus. stackoverflow.com/questions/5311034/…
KenF
1
Le problème est que c'est juste un select * from ... effectué, pas un nouveau produit {Name = p.Name}, car vous obtiendrez également une référence cyclique. Et vous voulez juste le nom.
Sterling Diaz