Como forçar LINQ Sum () a retornar 0 enquanto a coleção de fontes está vazia

183

Basicamente, quando faço a consulta a seguir, se nenhum lead for correspondido, a consulta a seguir gera uma exceção. Nesse caso, eu preferiria que a soma igualasse 0, em vez de uma exceção ser lançada. Isso seria possível na própria consulta - quero dizer, em vez de armazenar a consulta e verificar query.Any()?

double earnings = db.Leads.Where(l => l.Date.Day == date.Day
                && l.Date.Month == date.Month
                && l.Date.Year == date.Year
                && l.Property.Type == ProtectedPropertyType.Password
                && l.Property.PropertyId == PropertyId).Sum(l => l.Amount);
John Mayer
fonte
2
O Wherenão retornaria nullse não encontrasse nenhum registro, retornaria uma lista de zero itens. Qual é a exceção?
Mike Perrenoud
3
Qual é a exceção?
Toto
3
Recebo a exceção: A conversão para o tipo de valor 'Int32' falhou porque o valor materializado é nulo. O parâmetro genérico do tipo de resultado ou a consulta devem usar um tipo anulável.
31513 John Mayer
1
@ Stijn, não, o que você fez ainda não teria funcionado. O problema é a maneira como o SQLé gerado. Amountnão é realmente null, é realmente um problema envolvendo como ele lida com zero resultados. Dê uma olhada na resposta que foi fornecida.
11133 Mike Perrenoud
39
Você não deve usar o dobro para valores em dólares! Mesmo valores fracionários em dólar. Nunca, nunca, use o dobro quando se pretende uma quantidade exata. Suas colunas do banco de dados devem ser decimal, seu código deve ser usado decimal. Esqueça que você já conheceu floate doubleem sua carreira de programador até o dia em que alguém lhe disser para usá-los, para estatísticas ou luminância estelar ou os resultados de um processo estocástico ou carga de um elétron! Até então, você está fazendo errado .
ErikE

Respostas:

391

Tente alterar sua consulta para isso:

db.Leads.Where(l => l.Date.Day == date.Day
            && l.Date.Month == date.Month
            && l.Date.Year == date.Year
            && l.Property.Type == ProtectedPropertyType.Password
            && l.Property.PropertyId == PropertyId)
         .Select(l => l.Amount)
         .DefaultIfEmpty(0)
         .Sum();

Dessa forma, sua consulta selecionará apenas o Amountcampo. Se a coleção estiver vazia, ele retornará um elemento com o valor de 0e a soma será aplicada.

Simon Belanger
fonte
Certamente, ele faz o truque, mas não selecionaria uma lista dos valores de Quantidade primeiro e Sumeles no lado do servidor, em vez de no lado do banco de dados? A solução da imo 2kay é mais ideal, pelo menos mais semanticamente correta.
Maksim Vi.
3
@MaksimVI EF irá gerar a consulta sobre a primeira materialização, quando a IQueryable<T>corrente pára (normalmente quando você chama ToList, AsEnumerable, etc .. e, neste caso Sum). Sumé um método conhecido e tratado pelo EF Queryable Provider e gerará a instrução SQL relacionada.
precisa
@SimonBelanger Estou corrigido, a soma é feita no lado do banco de dados, mas é feita em uma subconsulta que seleciona Montantes primeiro. Basicamente, a consulta é em SELECT SUM(a.Amount) FROM (SELECT Amount FROM Leads WHERE ...) AS avez de apenas SELECT SUM(Amount) FROM Leads. Além disso, a subconsulta possui uma verificação nula adicional e uma junção externa estranha com uma tabela de linha única.
Maksim Vi.
Não é uma diferença significativa de desempenho e provavelmente está otimizada, mas ainda acho que a outra solução parece mais limpa.
Maksim Vi.
5
Esteja ciente de que DefaultIfEmptynão há suporte para vários provedores LINQ; portanto, você precisará inserir um ToList()ou algo semelhante antes de usá-lo nesses casos, para que ele seja aplicado no cenário LINQ to Objects .
Christopher King
188

Eu prefiro usar outro hack:

double earnings = db.Leads.Where(l => l.Date.Day == date.Day
                                      && l.Date.Month == date.Month
                                      && l.Date.Year == date.Year
                                      && l.Property.Type == ProtectedPropertyType.Password
                                      && l.Property.PropertyId == PropertyId)
                          .Sum(l => (double?) l.Amount) ?? 0;
tukaef
fonte
18
Ao usar o LINQ to SQL isso gera um código SQL muito menor do que a resposta aceita
wertzui
3
Essa é a resposta correta. Todos os outros falham. Primeiro, faça a conversão para nulo e, em seguida, compare o resultado final com nulo.
Mohsen Afshin
3
Isso é muito melhor do que a resposta aceita para o Linq To EF. Para mim, o SQL gerado executa cerca de 3,8 vezes melhor que o DefaultIfEmpty.
Florian
2
Isso é muito mais rápido.
frakon
1
Eu não chamaria isso de um hack, como este é o nullables uso exato são projetados para, isto é, mostrando a diferença entre nenhum dado e padrão valor
miket
7

Tente isso em vez disso, é mais curto:

db.Leads.Where(..).Aggregate(0, (i, lead) => i + lead.Amount);
Kovács Róbert
fonte
2
Isso evita a exceção?
Adrian Wragg
você poderia elaborar?
DanielV
4

Isso é vitória para mim:

int Total = 0;
Total = (int)Db.Logins.Where(L => L.id == item.MyId).Sum(L => (int?)L.NumberOfLogins ?? 0);

Na minha tabela LOGIN, no campo NUMBEROFLOGINS, alguns valores são NULL e outros têm um número INT. Soma aqui o total de NUMBEROFLOGINS de todos os usuários de uma corporação (cada ID).

Pedro Ramos
fonte
1

Experimentar:

ganhos duplos = db.Leads.Where (l => l.ShouldBeIncluded) .Soma (l => (dobro?) l.Amount) ?? 0 ;

A consulta " SELECT SUM ([Amount]) " "retornará NULL para a lista vazia. Mas se você usar o LINQ, espera que o " Sum (l => l.Amount) " retorne duas vezes e não permita que você use o operador " ?? " para definir 0 para a coleção vazia.

Para evitar essa situação, você precisa fazer o LINQ esperar " dobrar? ". Você pode fazer isso lançando " (double?) L.mount ".

Não afeta a consulta ao SQL, mas faz o LINQ funcionar para coleções vazias.

Maxim Lukoshko
fonte
0
db.Leads.Where(l => l.Date.Day == date.Day
        && l.Date.Month == date.Month
        && l.Date.Year == date.Year
        && l.Property.Type == ProtectedPropertyType.Password
        && l.Property.PropertyId == PropertyId)
     .Select(l => l.Amount)
     .ToList()
     .Sum();
Mona
fonte
1
Adicione algumas informações à resposta sobre o código
Jaqen H'ghar
1
Eu recebi um erro quando tento sem ToList (), pois ele não retorna nada. Mas o ToList () fará a lista vazia e não está dando erro quando eu faço o ToList (). Sum ().
Mona
2
Você provavelmente não gostaria de usar ToListaqui se tudo que você quer é a soma. Isso retornará o conjunto de resultados inteiro (apenas Amountpara cada registro neste caso) na memória e, em seguida, Sum()nesse conjunto. Muito melhor usar outra solução que faça o cálculo via SQL Server.
27417 Josh M.Jul