Как сделать агрегаты Linq, когда может быть пустой набор?

У меня есть набор Linq Things, где Thing имеет свойство Amount (десятичное).

Я пытаюсь сделать агрегат для этого для определенного подмножества вещей:

var total = myThings.Sum(t => t.Amount);

и это работает хорошо. Но затем я добавил условие, которое оставило меня без Вещей в результате:

var total = myThings.Where(t => t.OtherProperty == 123).Sum(t => t.Amount);

И вместо получения total = 0 или null, я получаю сообщение об ошибке:

System.InvalidOperationException: нулевое значение не может быть присвоено член с типом System.Decimal, который является типом с недействительным значением.

Это действительно противно, потому что я не ожидал такого поведения. Я бы ожидал, что итоговое значение будет равно нулю, может быть, ноль - но, конечно же, не будет генерировать исключение!

Что я делаю неправильно? Какое обходное решение/исправление?

EDIT - пример

Спасибо всем за ваши комментарии. Здесь некоторый код, скопированный и вставленный (не упрощенный). Это LinqToSql (возможно, поэтому вы не смогли воспроизвести мою проблему):

var claims = Claim.Where(cl => cl.ID < 0);
var count = claims.Count(); // count=0
var sum = claims.Sum(cl => cl.ClaimedAmount); // throws exception

Ответ 1

Я могу воспроизвести вашу проблему со следующим запросом LINQPad против Northwind:

Employees.Where(e => e.EmployeeID == -999).Sum(e => e.EmployeeID)

Здесь есть две проблемы:

  • Sum() перегружен
  • LINQ to SQL следует семантике SQL, а не семантике С#.

В SQL, SUM(no rows) возвращает null, а не ноль. Однако вывод типа для вашего запроса дает decimal как параметр типа, а не decimal?. Исправление состоит в том, чтобы помочь ввести тип вывода, выбрать правильный тип, т.е.:

Employees.Where(e => e.EmployeeID == -999).Sum(e => (int?)e.EmployeeID)

Теперь будет использована правильная перегрузка Sum().

Ответ 2

Чтобы получить результат с нулевым значением, вам нужно указать сумму в тип с нулевым значением, а затем обработать случай Sum, возвращающий значение null.

decimal total = myThings.Sum(t => (decimal?)t.Amount) ?? 0;

Еще один вопрос, посвященный обоснованию (ir).

Ответ 3

он выдает исключение, потому что результат объединенного запроса sql равен null, и этот атрибут не может быть назначен десятичному var. Если вы сделали следующее, то ваша переменная была бы нулевой (я предполагаю, что ClaimedAmount десятичный):

var claims = Claim.Where(cl => cl.ID < 0);
var count = claims.Count(); // count=0
var sum = claims.Sum(cl => cl.ClaimedAmount as decimal?);

тогда вы должны получить желаемую функциональность.

Вы также можете сделать ToList() в точке оператора where, а затем сумма вернет 0, но это может испортить то, что было сказано в другом месте об агрегатах LINQ.

Ответ 4

Кажется, что лучше всего придерживаться чего-то простого, как

decimal total = decimal.Zero;

foreach (Thing myThing in myThings) {
    if (myThing.OtherProperty == 123) {
        total = total + myThing.Amount;
    }
}

Кроме того, этот пример работает для меня (как предложил Крейг)

Использование этого класса...

public class Location
{
    public string Map { get; set; }
    public int Top { get; set; }
    public int Left { get; set; }
}

И эта настройка...

        List<Location> myThings = new List<Location>();
        myThings.Add(new Location()
        {
            Map = "A",
            Top = 10,
            Left = 10
        });

        var total = myThings.Where(t => t.Map == "B").Sum(t => t.Top);

Получите в общей сложности 0.

Ответ 5

Если t имеет свойство, подобное "HasValue", тогда я бы изменил выражение на:

var total = 
     myThings.Where(t => (t.HasValue) && (t.OtherProperty == 123)).Sum(t => t.Amount);