La entidad no se puede construir en una consulta LINQ to Entities

389

Hay un tipo de entidad llamada producto que es generado por el marco de la entidad. He escrito esta consulta

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

El siguiente código arroja el siguiente error:

"La entidad o tipo complejo Shop.Product no se puede construir en una consulta LINQ to Entities"

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

Pero cuando lo uso en select plugar de select new Product { Name = p.Name};funciona correctamente.

¿Cómo puedo preformar una sección de selección personalizada?

Ghooti Farangi
fuente
System.NotSupportedException: 'La entidad o tipo complejo' StudentInfoAjax.Models.Student 'no se puede construir en una consulta LINQ to Entities'.
Md Wahid

Respuestas:

390

No puede (y no debería poder) proyectar en una entidad asignada. Sin embargo, puede proyectar en un tipo anónimo o en un DTO :

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

Y su método devolverá una Lista 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
fuente
152
No entiendo por qué no debería poder hacer esto ... Esto sería muy útil ...
Jonx
118
Bueno, las entidades mapeadas en EF básicamente representan tablas de bases de datos. Si proyecta en una entidad asignada, lo que básicamente hace es cargar parcialmente una entidad, que no es un estado válido. EF no tendrá idea de cómo, por ejemplo, manejar una actualización de dicha entidad en el futuro (el comportamiento predeterminado probablemente sería sobrescribir los campos no cargados con valores nulos o lo que sea que tenga en su objeto). Esta sería una operación peligrosa, ya que correría el riesgo de perder algunos de sus datos en la base de datos, por lo tanto, no está permitido cargar entidades parcialmente (o proyectar en entidades asignadas) en EF.
Yakimych
26
@Yakimych tiene sentido, excepto si tiene alguna entidad agregada que está generando / creando a través de una consulta y, por lo tanto, tiene plena conciencia / tiene la intención de crear una nueva entidad que luego manipulará y luego agregará. En este caso, debe forzar la ejecución de la consulta o ingresar a un dto y volver a una entidad para agregar, lo cual es frustrante
Cargowire
16
@Cargowire - Estoy de acuerdo, ese escenario existe, y es frustrante cuando sabes lo que estás haciendo, pero no puedes hacerlo debido a limitaciones. Sin embargo, si esto se hubiera permitido, habría muchos desarrolladores frustrados quejándose de que sus datos se pierden cuando, por ejemplo, intentan salvar entidades parcialmente cargadas. En mi opinión, un error que explota con mucho ruido (lanzar una excepción, etc.) es mejor que el comportamiento que puede causar errores ocultos que son difíciles de rastrear y explicar (las cosas funcionan bastante bien antes de comenzar a notar datos faltantes).
Yakimych
275

Puede proyectar en tipo anónimo, y luego de él a tipo de modelo

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

Editar : Voy a ser un poco más específico ya que esta pregunta llamó mucho la atención.

No puede proyectar directamente en el tipo de modelo (restricción de EF), por lo que no hay forma de evitarlo. La única forma es proyectar en tipo anónimo (primera iteración), y luego modelar el tipo (segunda iteración).

Tenga en cuenta también que cuando carga entidades parcialmente de esta manera, no se pueden actualizar, por lo que deben permanecer separadas, tal como están.

Nunca entendí completamente por qué esto no es posible, y las respuestas en este hilo no dan fuertes razones en su contra (principalmente hablando de datos parcialmente cargados). Es correcto que en el estado de carga parcial la entidad no se pueda actualizar, pero luego, esta entidad se desconectaría, por lo que no serían posibles los intentos accidentales de salvarlos.

Considere el método que utilicé anteriormente: todavía tenemos una entidad modelo parcialmente cargada como resultado. Esta entidad está separada.

Considere este posible código (deseo de existir):

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

Esto también podría dar como resultado una lista de entidades separadas, por lo que no necesitaríamos hacer dos iteraciones. Un compilador sería inteligente al ver que se ha utilizado AsNoTracking (), lo que dará como resultado entidades separadas, por lo que podría permitirnos hacer esto. Sin embargo, si se omitió AsNoTracking (), podría lanzar la misma excepción que está lanzando ahora, para advertirnos que necesitamos ser lo suficientemente específicos sobre el resultado que queremos.

Goran
fuente
3
Esta es la solución más limpia cuando no necesita / no le importa el estado de la entidad seleccionada que desea proyectar.
Mário Meyrelles
2
Y cuando no le importa si devuelve IEnumerable o IQueryable;). Pero aún así recibes mi voto positivo porque esta solución funciona para mí ahora.
Michael Brennt
10
técnicamente, la proyección al tipo de modelo se produce fuera de la consulta, y creo que también requiere una iteración adicional a través de la lista. No usaré esta solución para mi código, pero es la solución para la pregunta. repunte
1c1cle
44
Prefiero esto a la solución DTO aceptada, mucho más elegante y limpia
Adam Hey
77
Excepto que, con respeto, en realidad no es una respuesta a la pregunta. Esta es una respuesta sobre cómo hacer una proyección de Linq a objetos, no una proyección de consulta de Linq a entidades. Entonces, la opción DTO es la única opción re: Linq to Entities.
rism
78

Hay otra forma en que encontré que funciona, tienes que construir una clase que se derive de tu clase de Producto y usarla. Por ejemplo:

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

No estoy seguro si esto está "permitido", pero funciona.

Tomasz Iniewicz
fuente
3
¡Inteligente! Intenté esto ahora y funciona. Sin embargo, estoy seguro de que de alguna manera me quemará.
Daniel
55
Por cierto, esto lo muerde si intenta persistir los resultados de GetProducts () ya que EF no puede encontrar la asignación para PseudoProduct, por ejemplo, "System.InvalidOperationException: no se pudo encontrar la información de mapeo y metadatos para EntityType 'blah.PseudoProduct'".
Sming
44
La mejor respuesta, y la única que responde dentro de los parámetros de la pregunta. Todas las demás respuestas cambian el tipo de retorno o ejecutan prematuramente el IQueryable y usan linq para los objetos
rdans
2
100% sorprendido funcionó ... en EF 6.1 esto está funcionando.
TravisWhidden
2
@mejobloggs Pruebe el atributo [NotMapped] en la clase derivada, o .Ignore <T> si está utilizando la API fluida.
Dunc
37

Aquí hay una manera de hacer esto sin declarar una clase adicional:

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

Sin embargo, esto solo se debe usar si desea combinar varias entidades en una sola entidad. La funcionalidad anterior (mapeo simple de producto a producto) se realiza así:

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
fuente
23

Otra forma 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
fuente
buen punto acabo de aprender algo IQueryable con tu buena respuesta. Sin embargo, hubiera sido bueno si hubieras explicado POR QUÉ no es útil después de ToList () y la razón es que no puedes usar listas genéricas en una consulta LINQ-to-SQL. Entonces, si sabe que siempre va a insertar los resultados en otra consulta de la persona que llama, entonces ciertamente tiene sentido ser IQueryable. Pero si no ... si lo va a usar como una lista genérica después, use ToList () dentro del método para no hacer ToList () en IQueryable en todas y cada una de las llamadas a este método.
PositiveGuy
Estás bien, amigo mío. Simplemente imito la firma del método de la pregunta, por eso la convierto en consultable ...;)
Soren
1
Esto funciona, la lista de productos no se puede editar después de ToList (). ¿Cómo puedo hacerlo editable?
doncadavona
Si realiza una .ToListconsulta, se ejecuta y extrae datos del servidor, ¿cuál es el punto para hacerlo nuevamente AsQueryable?
Moshii
1
@Moshii solo para satisfacer la firma del tipo de retorno del método (como dije en la respuesta, ya no es útil).
Soren
4

Puede usar esto y debería estar funcionando -> Debe usarlo toListantes de hacer la nueva lista usando select:

db.Products
    .where(x=>x.CategoryID == categoryID).ToList()
    .select(x=>new Product { Name = p.Name}).ToList(); 
Mohamed Adam
fuente
3
Sin embargo, esto todavía haría un 'SELECCIONAR * DE [..]', no un 'SELECCIONAR nombre DE [..]'
Timo Hermans
1

En respuesta a la otra pregunta que se marcó como duplicado ( ver aquí ), descubrí una solución rápida y fácil basada en la respuesta 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();

Nota: Esta solución solo funciona si tiene una propiedad de navegación (clave externa) en la clase Tarea (aquí llamada 'Incidente'). Si no tiene eso, puede usar una de las otras soluciones publicadas con "AsQueryable ()".

JollyBrackets
fuente
1

Puede resolver esto utilizando Objetos de transferencia de datos (DTO).

Estos son un poco como los modelos de vista en los que coloca las propiedades que necesita y puede asignarlos manualmente en su controlador o mediante el uso de soluciones de terceros como AutoMapper.

Con DTO's puedes:

  • Hacer datos serializables (Json)
  • Deshágase de las referencias circulares
  • Reduzca el tráfico de red dejando propiedades que no necesita (ver modelo)
  • Usar aplanamiento de objetos

He estado aprendiendo esto en la escuela este año y es una herramienta muy útil.

Jelman
fuente
0

Si está utilizando Entity Framework, intente eliminar la propiedad de DbContext que usa su modelo complejo como Entity. Tuve el mismo problema al mapear varios modelos en un modelo de vista llamado Entity

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

Eliminar la entrada de DbContext corrigió mi error.

vikas suhag
fuente
0

si está ejecutando Linq to Entityno puede usar el ClassTypecon newen el selectcierre de la consultaonly anonymous types are allowed (new without type)

mira este fragmento de mi proyecto

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

de usted agregó el new keywordcierre en Seleccionar incluso en el complex propertiesobtendrá este error

entonces removela ClassTypes from newpalabra clave en Linq to Entityconsultas ,,

porque se transformará en una instrucción sql y se ejecutará en SqlServer

así que cuando puedo usar new with typesen selectel cierre?

puedes usarlo si estás lidiando con 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 

después de ejecutar ToListen la consulta se convirtió in memory collection para que podamos usar new ClassTypesen select

Basheer AL-MOMANI
fuente
Claro que puede usar tipos anónimos, pero no puede crear una entidad dentro de la consulta LINQ, incluso para establecer un miembro anónimo, porque LINQ-to-Entities todavía arroja la misma excepción.
Suncat2000
0

En muchos casos, la transformación no es necesaria. Piense por la razón por la que desea escribir la lista, y evalúe si solo desea los datos, por ejemplo, en un servicio web o para mostrarlos. No importa el tipo. Solo necesita saber cómo leerlo y verificar que sea idéntico a las propiedades definidas en el tipo anónimo que definió. Ese es el escenario óptimo, porque algo no necesita todos los campos de una entidad, y esa es la razón por la que existe el tipo anónimo.

Una manera simple es hacer esto:

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

No le permitirá volver a mapear en Producto ya que esa es su tabla que está consultando. Necesita una función anónima, luego puede agregarla a un ViewModel y agregar cada ViewModel a unList<MyViewModel> y devolverlos. Es una ligera digresión, pero incluyo advertencias sobre el manejo de fechas anulables porque es un problema difícil de manejar, en caso de que tenga alguna. Así es como lo manejé.

Espero que tengas un ProductViewModel:

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

Tengo un marco de inyección / repositorio de dependencia donde llamo a una función para tomar mis datos. Usando su publicación como ejemplo, en su llamada a la función Controlador, se vería así:

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

En la clase de repositorio:

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 vuelta en el controlador, haces:

 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
fuente
-1

solo agregue 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
fuente
8
¡Nunca hacerlo! Esto buscará todos los datos de DB y luego hará la selección.
Gh61
1
Es por eso que en algunas empresas se prohíbe el uso de Linq.
Hakan
-2

Puede agregar AsEnumerable a su colección de la siguiente manera:

public IQueryable<Product> GetProducts(int categoryID)
{
    return from p in db.Products.AsEnumerable()
           where p.CategoryID== categoryID
           select new Product { Name = p.Name};
}
HamidReza
fuente
¿Por qué esta es una mala respuesta, aunque funciona ...? La cláusula Where y todo lo demás se maneja fuera de linq a Entities. es decir, cada producto se recupera y luego se filtra por linq a los objetos. Aparte de esto, es casi exactamente lo mismo que la respuesta .ToList anterior. stackoverflow.com/questions/5311034/…
KenF
1
El problema con esto es que es solo un select * from ... realizado, no selecciona un nuevo Producto {Name = p.Name}, ya que también obtendrá una referencia cíclica. Y solo quieres el nombre.
Sterling Diaz