views:

79

answers:

4

I have a Linq collection of Things, where Thing has an Amount (decimal) property.

I'm trying to do an aggregate on this for a certain subset of Things:

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

and that works nicely. But then I added a condition that left me with no Things in the result:

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

And instead of getting total = 0 or null, I get an error:

System.InvalidOperationException: The null value cannot be assigned to a member 
with type System.Decimal which is a non-nullable value type.

That is really nasty, because I didn't expect that behavior. I would have expected total to be zero, maybe null - but certainly not to throw an exception!

What am I doing wrong? What's the workaround/fix?

EDIT - example

Thanks to all for your comments. Here's some code, copied and pasted (not simplified). It's LinqToSql (perhaps that's why you couldn't reproduce my problem):

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

It almost seems better to stick with something as simple as

decimal total = decimal.Zero;

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

Except, this example works for me (as suggested by Craig)

Using this class...

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

And this set up...

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

Get's you a total of 0.

Sohnee
No, you *should not* do this, as it won't use the query provider's `Sum` implementation. E.g., with LINQ to SQL it will fetch all rows to the client instead of doing a `SUM` on the server.
Craig Stuntz
@Craig Stuntz - I'm not sure the level of detail exists in the question to confirm your statement is true.
Sohnee
What I wrote is true generally, not just for this question: **Don't re-invent LINQ aggregates!** They are implemented the way they are for very good reasons.
Craig Stuntz
A: 

If t has a property like a 'HasValue', then I would change the expression to:

var total = 
     myThings.Where(t => (t.HasValue) && (t.OtherProperty == 123)).Sum(t => t.Amount); 
JayD
Definitely not. The whole point is that the set is empty - there is no "t" to call HasValue on!
Shaul
+2  A: 

it throws an exception because the result of the combined sql query is null and this cant be assigned to the decimal var. If you did the following then your variable would be null (I assume ClaimedAmount is decimal):

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

then you should get the functionality you desire.

You could also do ToList() at the point of the where statement and then the sum would return 0 but that would fall foul of what has been said elsewhere about LINQ aggregates.

Leom Burke
+1 You are right - but that is very nasty of MS to throw an exception there. ClaimedAmount is defined as a decimal, so why should you have to declare it as a "decimal?" just so you can have it in an aggregate? That's dumb.
Shaul
You don't have to *declare* it as a `decimal?`. You *can* **cast** it, though!
Craig Stuntz
Yeah that's what I meant... :)
Shaul
+3  A: 

I can reproduce your problem with the following LINQPad query against Northwind:

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

There are two issues here:

  1. Sum() is overloaded
  2. LINQ to SQL follows SQL semantics, not C# semantics.

In SQL, SUM(no rows) returns null, not zero. However, the type inference for your query gives you decimal as the type parameter, instead of decimal?. The fix is to help type inference select the correct type, i.e.:

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

Now the correct Sum() overload will be used.

Craig Stuntz
+1 - and see my comment to @Leom Burke that I think this was a design mistake on Microsoft's part. It's obvious that the desired type is "decimal?", so forcing me to declare it explicitly is really dumb.
Shaul
It's not clear what L2S should do with the `decimal` overload for `Sum`, given that it follows SQL semantics. The C# compiler, OTOH, certainly *should not* use the `decimal?` overload because some random LINQ provider might not follow C# semantics. The compiler is doing the right thing, since you have not given it any type hints. L2S's response is at least arguable.
Craig Stuntz
+ answer credit - was wavering between you and @Leom Burke, coz you both answered it right. But you have given a lot more background and explanation, so you get the credit. Thanks! :)
Shaul