LINQ to Entities não reconhece o método 'System.String Format (System.String, System.Object, System.Object)'

88

Eu tenho esta consulta linq:

private void GetReceivedInvoiceTasks(User user, List<Task> tasks)
{
    var areaIds = user.Areas.Select(x => x.AreaId).ToArray();

    var taskList = from i in _db.Invoices
                   join a in _db.Areas on i.AreaId equals a.AreaId
                   where i.Status == InvoiceStatuses.Received && areaIds.Contains(a.AreaId)
                   select new Task {
                       LinkText = string.Format(Invoice {0} has been received from {1}, i.InvoiceNumber, i.Organisation.Name),
                       Link = Views.Edit
                   };
}

Porém, tem problemas. Estou tentando criar tarefas. Para cada nova tarefa, quando eu definir o texto do link para uma string constante como "Olá", tudo bem. No entanto, acima estou tentando construir o linktext de propriedade usando propriedades da fatura.

Eu recebo este erro:

base {System.SystemException} = {"LINQ to Entities não reconhece o método 'System.String Format (System.String, System.Object, System.Object)' e este método não pode ser convertido em uma expressão de armazenamento." }

Alguém sabe por quê? Alguém conhece uma forma alternativa de fazer isso para que funcione?

AnonyMouse
fonte
Sim, perdi isso originalmente
AnonyMouse

Respostas:

148

Entity Framework está tentando executar sua projeção no lado SQL, onde não há equivalente a string.Format. Use AsEnumerable()para forçar a avaliação daquela peça com Linq to Objects.

Com base na resposta anterior que dei a você, reestruturaria sua consulta assim:

int statusReceived = (int)InvoiceStatuses.Received;
var areaIds = user.Areas.Select(x=> x.AreaId).ToArray();

var taskList = (from i in _db.Invoices
               where i.Status == statusReceived && areaIds.Contains(i.AreaId)
               select i)
               .AsEnumerable()
               .Select( x => new Task()
               {
                  LinkText = string.Format("Invoice {0} has been received from {1}", x.InvoiceNumber, x.Organisation.Name),
                  Link = Views.Edit
                });

Também vejo que você usa entidades relacionadas na consulta ( Organisation.Name), certifique-se de adicionar o apropriado Includeà sua consulta ou materializar especificamente essas propriedades para uso posterior, ou seja:

var taskList = (from i in _db.Invoices
               where i.Status == statusReceived && areaIds.Contains(i.AreaId)
               select new { i.InvoiceNumber, OrganisationName = i.Organisation.Name})
               .AsEnumerable()
               .Select( x => new Task()
               {
                  LinkText = string.Format("Invoice {0} has been received from {1}", x.InvoiceNumber, x.OrganisationName),
                  Link = Views.Edit
                });
Vidro quebrado
fonte
Além do fato de que a parte de seleção de nova tarefa não pode acontecer no lado do servidor devido à tradução da árvore de expressão, também deve ser observado que isso é indesejável. Presumivelmente, você deseja que as tarefas sejam criadas no lado do cliente. Portanto, a separação de consulta e criação de tarefas poderia ser ainda mais explícita.
Tormod,
3
Eu também recomendaria selecionar um tipo anônimo que contenha apenas o InvoiceNumber e o Organisation.Name necessários. Se a entidade de faturas for grande, o select i com o AsEnumerable subsequente puxará de volta todas as colunas, embora você esteja usando apenas duas.
Devin,
1
@Devin: Sim, concordo - na verdade, é exatamente isso que o segundo exemplo de consulta está fazendo.
BrokenGlass
15

IQueryablederiva de IEnumerable, a principal semelhança é que quando você faz sua consulta, ela é postada no mecanismo de banco de dados em sua linguagem, o momento tênue é quando você diz ao C # para lidar com os dados no servidor (não no lado do cliente) ou para dizer ao SQL para lidar dados.

Então, basicamente, quando você diz IEnumerable.ToString(), C # obtém a coleta de dados e chama ToString()o objeto. Mas quando você diz, IQueryable.ToString()C # informa ao SQL para chamar ToString()o objeto, mas não existe tal método no SQL.

A desvantagem é que, quando você lida com dados em C #, toda a coleção que você está examinando deve ser construída na memória antes que o C # aplique os filtros.

A maneira mais eficiente de fazer isso é fazer a consulta IQueryablecom todos os filtros que você pode aplicar.

E então acumule-o na memória e faça a formatação dos dados em C #.

IQueryable<Customer> dataQuery = Customers.Where(c => c.ID < 100 && c.ZIP == 12345 && c.Name == "John Doe");

 var inMemCollection = dataQuery.AsEnumerable().Select(c => new
                                                  {
                                                     c.ID
                                                     c.Name,
                                                     c.ZIP,
                                                     c.DateRegisterred.ToString("dd,MMM,yyyy")
                                                   });
Nikolay
fonte
3

Embora o SQL não saiba o que fazer com um, string.Formatele pode realizar a concatenação de strings.

Se você executar o código a seguir, deverá obter os dados que procura.

var taskList = from i in _db.Invoices
               join a in _db.Areas on i.AreaId equals a.AreaId
               where i.Status == InvoiceStatuses.Received && areaIds.Contains(a.AreaId)
               select new Task {
                   LinkText = "Invoice " + i.InvoiceNumber + "has been received from " + i.Organisation.Name),
                   Link = Views.Edit
               };

Depois que você realmente executa a consulta, isso deve ser um pouco mais rápido do que usar AsEnumerable(pelo menos foi o que encontrei em meu próprio código depois de ter o mesmo erro original que você). Se você estiver fazendo algo mais complexo com C #, ainda precisará usar AsEnumerable.

d219
fonte
2
Não sei por que o Linq não pôde ser adaptado para usar a função FORMATMESSAGE docs.microsoft.com/en-us/sql/t-sql/functions/… Até então, a sua é a solução (sem forçar a materialização)
MemeDeveloper
2
Dependendo da estrutura do banco de dados e do número de colunas relacionadas, usar esse método em vez de AsEnumerable()pode ser muito mais eficiente. Evite AsEnumerable()e ToList()até que você realmente queira trazer todos os resultados para a memória.
Chris Schaller