Cómo forzar a LINQ Sum () a devolver 0 mientras la colección de origen está vacía

183

Básicamente, cuando hago la siguiente consulta, si no se encontraron coincidencias, la siguiente consulta arroja una excepción. En ese caso, preferiría que la suma igualara 0 en lugar de que se lanzara una excepción. ¿Sería esto posible en la consulta en sí misma, quiero decir, en lugar de almacenar la consulta y 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
fuente
2
El Whereno volvería nullsi no encontrara ningún registro, devolvería una lista de cero elementos. ¿Cual es la excepción?
Mike Perrenoud
3
¿Cual es la excepción?
Toto
3
Recibo la excepción: el tipo de conversión al valor 'Int32' falló porque el valor materializado es nulo. El parámetro genérico del tipo de resultado o la consulta deben usar un tipo anulable.
John Mayer
1
@Stijn, no, lo que hiciste aún no hubiera funcionado. El problema es la forma en que SQLse genera. Amounten realidad no lo es null, es realmente un problema en torno a cómo maneja los resultados cero. Echa un vistazo a la respuesta que se proporcionó.
Mike Perrenoud
39
¡No deberías usar el doble por montos en dólares! Incluso cantidades fraccionarias en dólares. Nunca, nunca, use el doble cuando se pretende una cantidad exacta. Las columnas de su base de datos deben ser decimal, su código debe usar decimal. ¡Olvídate de que alguna vez conociste floaty doubleen tu carrera de programación hasta el día en que alguien te dice que los uses, para estadísticas o luminancia estelar o los resultados de un proceso estocástico o carga de un electrón! Hasta entonces, lo estás haciendo mal .
ErikE

Respuestas:

391

Intente cambiar su consulta a esto:

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

De esta manera, su consulta solo seleccionará el Amountcampo. Si la colección está vacía, devolverá un elemento con el valor de 0y luego se aplicará la suma.

Simon Belanger
fuente
Seguramente funciona, pero ¿no seleccionaría primero una lista de valores de Cantidad y Sumellos en el lado del servidor, en lugar del lado de la base de datos? La solución de imo 2kay es más óptima, al menos más semánticamente correcta.
Maksim Vi.
3
@MaksimVI EF generará la consulta sobre la primera materialización, cuando la IQueryable<T>cadena se detiene (por lo general cuando se llama ToList, AsEnumerableetc .. y en este caso Sum). Sumes un método conocido y manejado por el proveedor EF Queryable y generará la instrucción SQL relacionada.
Simon Belanger
@SimonBelanger Estoy corregido, la suma se realiza en el lado de la base de datos, pero se realiza en una subconsulta que selecciona Montos primero. Básicamente la consulta es en SELECT SUM(a.Amount) FROM (SELECT Amount FROM Leads WHERE ...) AS alugar de solo SELECT SUM(Amount) FROM Leads. Además, la subconsulta tiene una comprobación nula adicional y una combinación externa extraña con una sola tabla de filas.
Maksim Vi.
No es una diferencia de rendimiento significativa y probablemente esté optimizada, pero sigo pensando que la otra solución parece más limpia.
Maksim Vi.
55
Tenga en cuenta que DefaultIfEmptyno es compatible con una serie de proveedores de LINQ, por lo que deberá agregar ToList()algo similar antes de usarlo en esos casos para que se aplique en el escenario LINQ to Objects .
Christopher King
188

Prefiero usar otro truco:

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
fuente
18
Al utilizar LINQ a SQL esto genera un código SQL mucho más corta que la respuesta aceptada
wertzui
3
Esta es la respuesta correcta. Todos los demás fallan. Primero lanzando a anulable y luego comparando el resultado final contra nulo.
Mohsen Afshin
3
Esto es mucho mejor que la respuesta aceptada para Linq To EF. Para mí, el SQL generado funciona 3,8 veces mejor que DefaultIfEmpty.
Florian
2
Esto es MUCHO MÁS RÁPIDO.
frakon
1
No llamaría a esto un truco, ya que este es el uso exacto para el que están diseñados los valores
nulos
7

Intente esto en su lugar, es más corto:

db.Leads.Where(..).Aggregate(0, (i, lead) => i + lead.Amount);
Kovács Róbert
fuente
2
¿Esto evita la excepción?
Adrian Wragg
podrías elaborar?
DanielV
4

Eso es una victoria para mí:

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

En mi tabla INICIAR SESIÓN, en el campo NUMBEROFLOGINS, algunos valores son NULL y otros tienen un número INT. Resumo aquí NUMBEROFLOGINS totales de todos los usuarios de una Corporación (cada Id).

Pedro Ramos
fuente
1

Tratar:

ganancias dobles = db.Leads.Where (l => l.ShouldBeIncluded) .Sum (l => (double?) l.Amount) ?? 0 ;

La consulta " SELECCIONAR SUMA ([Cantidad]) " devolverá NULL para la lista vacía. Pero si usa LINQ, espera que la " Suma (l => l.Amount) " devuelva el doble y no le permita usar " ?? operador " para establecer 0 para la colección vacía.

Para evitar esta situación, debe hacer que LINQ espere " ¿doble? ". Puedes hacerlo lanzando " (¿doble?) L.Amount ".

No afecta la consulta a SQL pero hace que LINQ funcione para colecciones vacías.

Maxim Lukoshko
fuente
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
fuente
1
Agregue información a la respuesta sobre el código
Jaqen H'ghar
1
Recibí un error cuando lo intento sin ToList (), ya que no devuelve nada. Pero ToList () hará la lista vacía y no dará ningún error cuando haga ToList (). Sum ().
Mona
2
Probablemente no quieras usar ToListaquí si todo lo que quieres es la suma. Esto devolverá todo el conjunto de resultados (solo Amountpara cada registro en este caso) en la memoria y luego Sum()ese conjunto. Mucho mejor usar otra solución que haga el cálculo a través de SQL Server.
Josh M.