thelastchief
thelastchief

Reputation: 211

LINQ, handle Null in Model

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

Answers (4)

Zdeněk Jel&#237;nek
Zdeněk Jel&#237;nek

Reputation: 2913

What is null?

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:

1. Prevent nulls

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
    }
}

2. Propagate and handle nulls

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 nulls 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

Scott Baldric
Scott Baldric

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

mjwills
mjwills

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

Emre Kabaoglu
Emre Kabaoglu

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

Related Questions