A entidade não pode ser construída em uma consulta LINQ to Entities

389

Há um tipo de entidade chamado produto que é gerado pela estrutura da entidade. Escrevi esta consulta

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

O código abaixo gera o seguinte erro:

"A entidade ou o tipo complexo Shop.Product não pode ser construído em uma consulta LINQ to Entities"

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

Mas quando eu uso, em select pvez disso select new Product { Name = p.Name};, funciona corretamente.

Como posso pré-formar uma seção de seleção personalizada?

Ghooti Farangi
fonte
System.NotSupportedException: 'A entidade ou o tipo complexo' StudentInfoAjax.Models.Student 'não pode ser construído em uma consulta LINQ to Entities.'
Md Wahid

Respostas:

390

Você não pode (e não deve ser capaz de) projetar em uma entidade mapeada. No entanto, você pode projetar em um tipo anônimo ou em um DTO :

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

E seu método retornará uma lista de DTOs.

public List<ProductDTO> GetProducts(int categoryID)
{
    return (from p in db.Products
            where p.CategoryID == categoryID
            select new ProductDTO { Name = p.Name }).ToList();
}
Yakimych
fonte
152
Eu não entendo por que eu não deveria ser capaz de fazer isso ... Isso seria muito útil ...
Jonx
118
Bem, entidades mapeadas no EF representam basicamente tabelas de banco de dados. Se você projeta em uma entidade mapeada, basicamente faz o carregamento parcial de uma entidade, que não é um estado válido. A EF não terá idéia de como, por exemplo, lidar com uma atualização dessa entidade no futuro (o comportamento padrão provavelmente substituirá os campos não carregados por nulos ou o que você tiver em seu objeto). Isso seria uma operação perigosa, pois você correria o risco de perder alguns dados no banco de dados; portanto, não é permitido carregar parcialmente entidades (ou projetar em entidades mapeadas) no EF.
Yakimych
26
@Yakimych que faz sentido, exceto se você tiver alguma entidade agregada que está gerando / criando por meio de uma consulta e, portanto, está totalmente ciente / pretende criar uma entidade totalmente nova que você manipulará e depois adicionará. Nesse caso, você deve forçar a execução da consulta ou enviar por push para um dto e retornar a uma entidade para adicionar - o que é frustrante
Cargowire
16
@Cargowire - Eu concordo que esse cenário existe e é frustrante quando você sabe o que está fazendo, mas não pode fazê-lo devido a limitações. No entanto, se isso fosse permitido, haveria muitos desenvolvedores frustrados reclamando sobre a perda de dados ao tentar salvar entidades parcialmente carregadas. O IMO, um erro que explode com muito ruído (lançando uma exceção etc.) é melhor do que o comportamento que pode causar bugs ocultos difíceis de rastrear e explicar (as coisas funcionam bem antes de você começar a perceber a falta de dados).
Yakimych 17/08/11
275

Você pode projetar em tipo anônimo e, a partir dele, em 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 });
}

Edit : Eu vou ser um pouco mais específico, pois esta pergunta recebeu muita atenção.

Você não pode projetar diretamente no tipo de modelo (restrição EF); portanto, não há como contornar isso. A única maneira é projetar em tipo anônimo (1ª iteração) e depois em tipo de modelo (2ª iteração).

Lembre-se também de que, quando você carrega parcialmente entidades dessa maneira, elas não podem ser atualizadas; portanto, devem permanecer desanexadas como estão.

Eu nunca entendi completamente por que isso não é possível, e as respostas neste segmento não dão razões fortes contra isso (principalmente falando sobre dados parcialmente carregados). É correto que a entidade no estado parcialmente carregado não possa ser atualizada, mas essa entidade seria desanexada, portanto, tentativas acidentais de salvá-las não seriam possíveis.

Considere o método que usei acima: ainda temos uma entidade de modelo parcialmente carregada como resultado. Esta entidade está desanexada.

Considere este código possível (desejo de existir):

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

Isso também pode resultar em uma lista de entidades desanexadas, portanto, não precisamos fazer duas iterações. Um compilador seria esperto ao ver que AsNoTracking () foi usado, o que resultará em entidades desanexadas, para que isso nos permita fazer isso. Se, no entanto, AsNoTracking () for omitido, poderá lançar a mesma exceção que está lançando agora, para nos alertar que precisamos ser específicos o suficiente sobre o resultado que queremos.

Goran
fonte
3
Essa é a solução mais limpa quando você não precisa / não se importa com o estado da entidade selecionada que deseja projetar.
Mário Meyrelles
2
E quando você não se importa se retorna IEnumerable ou IQueryable;). Mas você ainda recebe meu voto positivo, porque esta solução funciona para mim agora.
Michael Brennt
10
tecnicamente, a projeção para o tipo de modelo está ocorrendo fora da consulta e acredito que também exija uma iteração adicional na lista. Não usarei esta solução para o meu código, mas é a solução para a pergunta. uptick.
1c1cle
4
Eu prefiro este para a solução DTO aceita - muito mais elegante e limpo
Adam Hey
7
Exceto que, com respeito, não é realmente uma resposta para a pergunta. Esta é uma resposta sobre como fazer uma projeção Linq To Objects, não uma projeção de consulta Linq to Entities. Portanto, a opção DTO é a única opção referente a: Linq to Entities.
RISM
78

Há outra maneira que eu achei que funciona: você precisa criar uma classe que deriva da sua classe Product e usá-la. Por exemplo:

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

Não tenho certeza se isso é "permitido", mas funciona.

Tomasz Iniewicz
fonte
3
Inteligente! Tentei isso agora e funciona. Tenho certeza de que de alguma forma me queimará.
Daniel
5
BTW, isso o incomoda se você tentar persistir os resultados de GetProducts (), pois a EF não pode encontrar o mapeamento para PseudoProduct, por exemplo, "System.InvalidOperationException: informações de mapeamento e metadados não foram encontradas para EntityType 'blah.PseudoProduct'".
sming
4
Melhor resposta e a única que responde dentro dos parâmetros da pergunta. Todas as outras respostas mudar o tipo de retorno ou prematuramente executar o linq IQueryable e uso de objetos
rdans
2
100% chocado, funcionou ... na EF 6.1, isso está funcionando.
precisa saber é o seguinte
2
@mejobloggs Tente o atributo [NotMapped] na classe derivada ou .Ignore <T> se você estiver usando a API fluente.
Dunc
37

Aqui está uma maneira de fazer isso sem declarar classe 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;
}

No entanto, isso só deve ser usado se você deseja combinar várias entidades em uma única entidade. A funcionalidade acima (mapeamento simples de produto para produto) é feita assim:

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

Outra maneira simples :)

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
fonte
bom ponto, acabei de aprender algo com a sua boa resposta. Seria bom, se você explicasse POR QUE não é útil após um ToList () e o motivo é que você não pode usar listas genéricas em uma consulta LINQ-to-SQL. Portanto, se você sabe que sempre enviará os resultados para outra consulta pelo chamador, certamente faz sentido ser IQueryable. Mas se não ... se você for usá-lo como uma lista genérica depois, use o ToList () dentro do método para não fazer um ToList () na chamada IQueryable de todas e cada uma desse método.
PositiveGuy
Você totalmente bem meus friend.I apenas imitar a assinatura do método pergunta, por causa do que eu convertê-lo para uma consulta de poder ...;)
Soren
11
Isso funciona, o productList se torna não editável após o ToList (). Como posso editá-lo?
doncadavona
Se você colocar uma .ToListconsulta, ela é executada e extraída de dados do servidor, qual é o objetivo de fazê-la novamente AsQueryable?
Moshii 13/01
11
@ Moshii apenas para satisfazer a assinatura do tipo de retorno do método (como eu disse na resposta, não é mais útil).
Soren
4

Você pode usar isso e ele deve estar funcionando -> Você deve usar toListantes de fazer a nova lista usando select:

db.Products
    .where(x=>x.CategoryID == categoryID).ToList()
    .select(x=>new Product { Name = p.Name}).ToList(); 
Mohamed Adam
fonte
3
Isso, contudo, ainda fazer um 'SELECT * FROM [..]', não um 'SELECT nome FROM [..]'
Timo Hermans
1

Em resposta à outra pergunta que foi marcada como duplicada ( veja aqui ), descobri uma solução rápida e fácil com base na resposta 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 solução funciona apenas se você tiver uma propriedade de navegação (chave estrangeira) na classe Task (aqui chamada 'Incidente'). Se você não tiver, basta usar uma das outras soluções publicadas com "AsQueryable ()".

JollyBrackets
fonte
1

Você pode resolver isso usando objetos de transferência de dados (DTO).

Estes são um pouco como os modelos de visualização em que você coloca as propriedades necessárias e pode mapeá-las manualmente em seu controlador ou usando soluções de terceiros como o AutoMapper.

Com os DTOs, você pode:

  • Tornar dados serializáveis ​​(Json)
  • Livre-se das referências circulares
  • Reduza o tráfego de rede deixando propriedades que você não precisa (viewmodelwise)
  • Use achatamento de objetos

Aprendi isso na escola este ano e é uma ferramenta muito útil.

Jelman
fonte
0

Se você estiver usando a estrutura da Entidade, tente remover a propriedade do DbContext que usa seu modelo complexo como Entidade. Eu tive o mesmo problema ao mapear vários modelos em um modelo de exibição chamado Entidade

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

A remoção da entrada do DbContext corrigiu meu erro.

vikas suhag
fonte
0

se você estiver executando, Linq to Entitynão poderá usá ClassType-lo newno selectfechamento da consultaonly anonymous types are allowed (new without type)

dê uma olhada neste trecho do meu projeto

//...
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 você adicionou o new keywordfechamento no Select, mesmo no caso complex propertiesdeste erro

então removea ClassTypes from newpalavra-chave em Linq to Entityconsultas ,,

porque ele será transformado em instrução sql e executado no SqlServer

então quando posso usar new with typesno selectfechamento?

você pode usá-lo se estiver lidando com 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 

depois que eu executei ToListna consulta, tornou-se in memory collection para que possamos usar new ClassTypesem select

Basheer AL-MOMANI
fonte
Claro que você pode usar tipos anônimos, mas não pode criar uma entidade na consulta LINQ, mesmo para definir um membro anônimo, porque o LINQ to Entities ainda gera a mesma exceção.
Suncat2000
0

Em muitos casos, a transformação não é necessária. Pense no motivo pelo qual você deseja o tipo forte List e avalie se deseja apenas os dados, por exemplo, em um serviço da Web ou para exibi-lo. Não importa o tipo. Você só precisa saber como lê-lo e verificar se é idêntico às propriedades definidas no tipo anônimo que você definiu. Esse é o cenário otimizado, causa algo que você não precisa em todos os campos de uma entidade e é por isso que o tipo anônimo existe.

Uma maneira simples é fazer isso:

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

Não permitirá que você mapeie de volta para o produto, pois essa é a sua tabela que você está consultando. Você precisa de uma função anônima, pode adicioná-la a um ViewModel e adicionar cada ViewModel a List<MyViewModel>e retorná-las. É uma ligeira digressão, mas eu incluo advertências sobre como lidar com datas anuláveis, porque essas são uma dor nas costas por trás, para o caso de você ter alguma. É assim que eu lidei com isso.

Espero que você tenha um ProductViewModel:

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

Eu tenho uma estrutura de injeção / repositório de dependência onde chamo uma função para capturar meus dados. Usando sua postagem como exemplo, na sua chamada de função Controller, ficaria assim:

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

Na classe do repositório:

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 volta ao controlador, você faz:

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

adicione apenas 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
fonte
8
NUNCA faça isso! Isso buscará todos os dados do banco de dados e fará a seleção.
Gh61
11
É por isso que em algumas empresas o Linq é proibido de usar.
hakan
-2

você pode adicionar AsEnumerable à sua coleção da seguinte maneira:

public IQueryable<Product> GetProducts(int categoryID)
{
    return from p in db.Products.AsEnumerable()
           where p.CategoryID== categoryID
           select new Product { Name = p.Name};
}
HamidReza
fonte
Por que essa é uma resposta ruim, embora funcione ... .AsEnumerable finaliza o linq para entidades. A cláusula Where e todo o resto são tratados fora do linq para Entities. ou seja, todo produto é recuperado e depois filtrado pelo linq para objetos. Além disso, é praticamente o mesmo que a resposta .ToList acima. stackoverflow.com/questions/5311034/…
KenF
11
O problema é que é apenas uma seleção * de ... executada, não selecione o novo Produto {Nome = p.Nome}, pois você também receberá uma referência cíclica. E você quer apenas o nome.
Sterling Diaz