Reputation: 211
Im trying to pass AverageRating to my view. AverageRating is the result of querying items in an Icollection of Review Model. Review Model has a property of Rating. However i get this message:
System.ArgumentNullException
This happens whenever the get is performed, which is understandable. However how do i best handle null exceptions in my model or elsewhere when my code looks like the following:
public class MyModel
{
//Querying this navigation property
public ICollection<Review> Reviews { get; set; }
public double? AverageRating
{
get
{
//check that this is not null / handle null
return Math.Round(Reviews.Average(c => c.Rating), 1);
}
}
}
public class Review
{
[Key]
public int ReviewID { get; set; }
public int Rating { get; set; }
public string Comment { get; set; }
public int CoachID { get; set; }
public int? StudentID { get; set; }
public Coach Coach { get; set; }
public Student Student { get; set; }
}
Upvotes: 4
Views: 1693
Reputation: 2913
First off let's assume that null
means unknown
. Eric Lippert's Null Is Not Empty provides a great reasoning for that. It can be further tracked to the design of SQL and the principles three-state logic. A null
collection is not empty same as a null
int?
is not zero.
But even if you disagree, there are two basic philosophies of working with nulls properly:
Simply adjust your model so that nulls are always prevented during object's lifetime. This is not always possible to achieve through type system (especially when using .NET serialization). This may also lead to a lot of additional boilerplate code at places so use it wisely:
public class Model
{
// is non-null in any Model instance
public IReadOnlyList<ModelItem> Items { get; }
public Model(IEnumerable<ModelItem> items)
{
Items = new List<ModelItems>(items); // does not check if items contains null
}
}
When you already have a null
, it's best not to cover up (this hinders maintenance). You can either throw or return null
up the call stack to other place that is then either forced to handle the null
or throw.
public class ModelItem
{
public double? Value { get; set; }
}
public class Model
{
public ICollection<ModelItem> Items { get; set; } // for some reason, e.g. serialization, the Items collection can be null
public double? Average
{
get
{
if (Items == null)
{
// I don't know what items exist => the average is unknown
return null;
}
return Items.Average(i => i?.Value); // note the ?. here to prevent NullReferenceException
}
}
}
Note that Average<Nullable<double>>
doesn't throw InvalidOperationException
with empty sequence unlike the non-nullable variant, additional check should be added for non-nullable types
Also note that the code is not trying to resolve the null
s into anything else than other null
. If your null
gets handled somewhere, it is most probably a part of your application's business logic and should reside in respective layer (e.g. code that handles backward compatibility with the model of previous version that doesn't have a certain property, returning it as null
).
However, if your model class inherently assumes that a null
collection is an empty collection (I would strongly recommend against that for readability and maintenance reasons), the null should indeed not be propagated and should be handled inside that class, e.g. with a coalescing operator (??
).
Upvotes: 3
Reputation: 465
If you're making use of C# 6.0, you can use null propagation to help specify a default scenario.
The code would end up looking like the following:
return Math.Round(Reviews?.Average(c => c.Rating) ?? 0.0, 1);
This makes use of null propagation to ensure that the Reviews collection is not Null before accessing the Average
extension method.
If you have individual items that are NULL, then you can extend the checking inside the lambda as well with the following:
return Math.Round(Reviews?.Average(c => c?.Rating ?? 0.0) ?? 0.0, 1);
This will guard against Reviews being null or Review's items being null.
Here's a fiddle showing it in action: https://dotnetfiddle.net/qBTEyf
If you instead need to skip translating NULLs to 0, then you can remove NULL items from the collection with a Where
statement first.
return Math.Round(Reviews?.Where(c => c?.Rating != null).Average(c => c.Rating) ?? 0.0, 1);
This way removes any null item from the list before processing it into Average
.
EDIT
Per the comment below, you can use DefaultIfEmpty
to handle when the sequence itself is empty as below:
return Math.Round(Reviews?.DefaultIfEmpty().Average(c => c?.Rating ?? 0.0) ?? 0.0, 1);
Calling DefaultIfEmpty
will return an IEnumerable
will one null element in it. This will then be filtered out during the Average
and return 0.
This can also be combined with other methods in this post. The fiddle has been updated with a test example of using DefaultIfEmpty
.
Upvotes: 1
Reputation: 23864
This implementation may do what you need:
public double? AverageRating
{
get
{
return Reviews?.Average(x => x?.Rating);
}
}
It will deal with Reviews
being null
(it will return null
), due to use of ?
after Reviews
.
It will deal with individual Reviews
being null
(they will be ignored in terms of calculating the Average), due to use of ?
after x
.
Upvotes: 3
Reputation: 13146
You can use DefaultIfEmpty
to set 0
value for empty sets and to exclude possible null values from Average
calculation, you should eliminate them;
public double? AverageRating
{
get
{
if (Reviews == null)
{
return null;
}
return Math.Round(Reviews.Where(x => x.Rating.HasValue).Select(x => x.Rating).DefaultIfEmpty(0).Average().Value, 1);
}
}
Upvotes: 1