Comment forcer LINQ Sum () à retourner 0 alors que la collection source est vide

183

Fondamentalement, lorsque je fais la requête suivante, si aucune piste ne correspond, la requête suivante lève une exception. Dans ce cas, je préférerais que la somme égale 0 plutôt qu'une exception levée. Serait-ce possible dans la requête elle-même - je veux dire plutôt que de stocker la requête et de vérifier 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
la source
2
Le Wherene reviendrait pas nulls'il ne trouvait aucun enregistrement, il renverrait une liste de zéro élément. Quelle est l'exception?
Mike Perrenoud
3
Quelle est l'exception?
Toto du
3
J'obtiens l'exception: la conversion en valeur de type 'Int32' a échoué car la valeur matérialisée est nulle. Le paramètre générique du type de résultat ou la requête doit utiliser un type Nullable.
John Mayer du
1
@Stijn, non ce que vous avez fait n'aurait toujours pas fonctionné. Le problème est la façon dont le SQLest généré. Amountn'est pas réellement null, c'est vraiment un problème concernant la façon dont il gère les résultats zéro. Jetez un œil à la réponse qui a été fournie.
Mike Perrenoud du
39
Vous ne devriez pas utiliser le double pour les montants en dollars! Même des montants fractionnaires. Jamais, jamais, jamais utiliser le double lorsqu'une quantité exacte est prévue. Vos colonnes de base de données doivent être decimal, votre code doit utiliser decimal. Oubliez que vous ayez jamais connu floatet doubledans votre carrière de programmeur jusqu'au jour où quelqu'un vous dit de les utiliser, pour des statistiques ou la luminance des étoiles ou les résultats d'un processus stochastique ou de la charge d'un électron! Jusque-là, vous le faites mal .
ErikE

Réponses:

391

Essayez de modifier votre requête comme suit:

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 cette façon, votre requête sélectionnera uniquement le Amountchamp. Si la collection est vide, elle renverra un élément avec la valeur de 0, puis la somme sera appliquée.

Simon Bélanger
la source
Cela fait sûrement l'affaire, mais ne sélectionnerait-il pas d'abord une liste de valeurs Amount et Sumcelles - ci côté serveur plutôt que côté base de données? La solution de imo 2kay est plus optimale, au moins plus correcte sémantiquement.
Maksim Vi.
3
@MaksimVI EF va générer la requête lors de la première matérialisation, lorsque la IQueryable<T>chaîne s'arrête (généralement lorsque vous appelez ToList, AsEnumerable, etc .. et dans ce cas Sum). Sumest une méthode connue et gérée par le fournisseur de requêtes EF et générera l'instruction SQL associée.
Simon Belanger
@SimonBelanger Je reste corrigé, la somme est faite côté DB, mais elle est faite sur une sous-requête qui sélectionne d'abord les montants. Fondamentalement, la requête est SELECT SUM(a.Amount) FROM (SELECT Amount FROM Leads WHERE ...) AS aplutôt que juste SELECT SUM(Amount) FROM Leads. De plus, la sous-requête a un contrôle nul supplémentaire et une jointure externe étrange avec une table à une seule ligne.
Maksim Vi.
Pas une différence de performance significative et c'est probablement optimisé, mais je pense toujours que l'autre solution semble plus propre.
Maksim Vi.
5
Sachez que ce DefaultIfEmptyn'est pas pris en charge par un certain nombre de fournisseurs LINQ, vous devrez donc ajouter un ToList()ou quelque chose de similaire avant de l'utiliser dans ces cas afin qu'il soit appliqué dans le scénario LINQ to Objects .
Christopher King
188

Je préfère utiliser un autre 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
la source
18
Lorsque vous utilisez Linq to SQL, cela génère un code SQL beaucoup plus court que la réponse acceptée
wertzui
3
C'est la bonne réponse. Tous les autres échouent. Première conversion en nullable, puis comparaison du résultat final avec null.
Mohsen Afshin
3
C'est bien mieux que la réponse acceptée pour Linq To EF. Pour moi, le SQL généré fonctionne environ 3,8 fois mieux que DefaultIfEmpty.
Florian
2
C'est BEAUCOUP PLUS RAPIDE.
frakon
1
Je n'appellerais pas cela un hack, car c'est l'utilisation exacte pour laquelle les nullables sont conçus, c'est-à-dire montrant la différence entre aucune donnée et la valeur par défaut
MikeT
7

Essayez plutôt ceci, c'est plus court:

db.Leads.Where(..).Aggregate(0, (i, lead) => i + lead.Amount);
Kovács Róbert
la source
2
Cela évite-t-il l'exception?
Adrian Wragg
pourriez-vous élaborer?
DanielV
4

C'est gagnant pour moi:

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

Dans ma table LOGIN, dans le champ NUMBEROFLOGINS certaines valeurs sont NULL et d'autres ont un nombre INT. Je additionne ici le nombre total de NUMBEROFLOGINS de tous les utilisateurs d'une société (chaque identifiant).

Pedro Ramos
la source
1

Essayer:

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

La requête " SELECT SUM ([Amount]) " renverra NULL pour une liste vide. Mais si vous utilisez LINQ, il s'attend à ce que " Sum (l => l.Amount) " renvoie double et il ne vous permet pas d'utiliser l' opérateur " ?? " pour définir 0 pour une collection vide.

Afin d'éviter cette situation, vous devez faire en sorte que LINQ s'attende à " doubler? ". Vous pouvez le faire en lançant " (double?) L.Amount ".

Cela n'affecte pas la requête SQL, mais cela permet à LINQ de fonctionner pour des collections vides.

Maxim Lukoshko
la source
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
la source
1
Veuillez ajouter quelques informations à la réponse sur le code
Jaqen H'ghar
1
J'ai eu une erreur lorsque j'essaye sans ToList (), car il ne renvoie rien. Mais le ToList () fera la liste vide et il ne donne aucune erreur quand je fais ToList (). Sum ().
Mona
2
Vous ne voudriez probablement pas utiliser ToListici si vous ne voulez que la somme. Cela renverra l'ensemble de résultats complet (juste Amountpour chaque enregistrement dans ce cas) en mémoire, puis Sum()cet ensemble. Mieux vaut utiliser une autre solution qui effectue le calcul via SQL Server.
Josh M.