简体   繁体   中英

NullReferenceException in linq left join, how do I handle the null value?

I'm trying to join tree lists together with left joins in linq, but I'm getting an System.NullReferenceException in the select statement.

printing out dataCost gives data in the form { ParrentLineNo = 0, Cost = 230 } and dataPrice gives { ParrentLineNo = 0, Price = 500 }

I was hoping that dataJoined would hold data in the form {lineNo,Cost,Price}

I think the problem is lineNo = 9. It is not included in the dataCost, så I would expect the x.cost in dataJoined to be null.

I have tried with x.Cost?? line.Cost, but that does not work with doubles.

hope you can help med solve this.

This is my code:

using System;
using System.Collections.Generic;
using System.Linq;

namespace LinqGroup
{
    class Line
    {
        public int LineNo { get; set; }
        public int ParrentLineNo { get; set; }
        public double Cost { get; set; }
        public double Price { get; set; }
        public string IsItem { get; set; }
        public string VareType { get; set; }

        static public List<Line> Data()
        {
            Line line9 = new() { LineNo = 9, ParrentLineNo = 9, Cost = 99, Price = 999, IsItem = "No", VareType = "Vare" };
            Line line1 = new() { LineNo = 0, ParrentLineNo = 0, Cost = 100, Price = 500, IsItem = "No", VareType = "Stykliste" };
            Line line2 = new() { LineNo = 1, ParrentLineNo = 0, Cost = 110, Price = 0, IsItem = "Yes", VareType = "Vare" };
            Line line3 = new() { LineNo = 2, ParrentLineNo = 0, Cost = 120, Price = 0, IsItem = "Yes", VareType = "Vare" };
            Line line4 = new() { LineNo = 3, ParrentLineNo = 3, Cost = 130, Price = 1000, IsItem = "No", VareType = "Stykliste" };
            Line line5 = new() { LineNo = 4, ParrentLineNo = 3, Cost = 140, Price = 0, IsItem = "Yes", VareType = "Vare" };
            Line line6 = new() { LineNo = 5, ParrentLineNo = 3, Cost = 150, Price = 0, IsItem = "Yes", VareType = "Vare" };

            List<Line> lines = new() { line9, line1, line2, line3, line4, line5, line6 };
            return lines;
        }
    }
    class Program
    {
        static public void Main()
        {
            var lines = Line.Data();

            // Build pricedata
            var dataPrice = from line in lines
                            where line.IsItem == "No"
                            group line by line.ParrentLineNo into Group
                            select new
                            {
                                ParrentLineNo = Group.Key,
                                Price = Group.Sum(x => x.Price)
                            };

            // Build costdata
            var dataCost = from line in lines
                           where line.IsItem == "Yes"
                           group line by line.ParrentLineNo into Group
                           select new
                           {
                               ParrentLineNo = Group.Key,
                               Cost = Group.Sum(x => x.Cost)
                           };
    
            // Join it all
            var dataJoined = from line in lines
                             join x in dataCost on line.ParrentLineNo equals x.ParrentLineNo into mma
                             from x in mma.DefaultIfEmpty()
                             join y in dataPrice on line.ParrentLineNo equals y.ParrentLineNo into pma
                             from y in pma.DefaultIfEmpty()
                             select new
                             {
                                 line.LineNo,
                                 x.Cost,
                                 y.Price
                             };

            // Then display
            foreach (var d in dataJoined)
            {
                Console.WriteLine(d);
            }
        }
    }
}

The cause of the problem is you have lines like this, that assign DefaultIfEmpty to a variable:

from line in lines
join x in dataCost on line.ParrentLineNo equals x.ParrentLineNo into mma
from x in mma.DefaultIfEmpty()

Since DefaultIfEmpty will return the default of the type if the result is empty, and knowing that the default value for all classes is null , then we can't later do this:

select new
{
    line.LineNo,
    x.Cost,       // Can't do this when 'x' is 'null'
    y.Price
};

Because x is null , we can't access it's Cost property.

One way around this is to just use a null value for that anonymous type property in that case:

select new
{
    line.LineNo,
    x?.Cost,      // The '?.' operator returns 'null' if the left side is 'null'
    y?.Price
};

Note that we should treat y the same way, since with some data sets it may also be null and we'd have the same exception.


I saw in one of your comments you said,

" How can I get an output for lineNo 9 like { LineNo = 0, Cost = null, Price = 500 }"

To actually output "null" (as a string), we need to take over how the classes are output rather than relying on the default Property = value , since null produces just a blank space in the default implementation. Instead, we can do something like:

// The '??' operator returns the right side if the left side is null

foreach (var d in dataJoined)
{ 
    Console.WriteLine($"{{LineNo = {d.LineNo}, " + 
        $"Cost = {d.Cost?.ToString() ?? "null"}, " +  
        $"Price = {d.Price?.ToString() ?? "null"}}}");
}

Another solution would be to ignore cases where x is null (remove them from our results) by not using the DefaultIfEmpty method:

var dataJoined = from line in lines
    join x in dataCost on line.ParrentLineNo equals x.ParrentLineNo into mma
    from x in mma
    join y in dataPrice on line.ParrentLineNo equals y.ParrentLineNo into pma
    from y in pma
    select new
    {
        line.LineNo,
        x.Cost,
        y.Price
    };

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM