我将如何在 Linq 中执行带合并的左连接?

How would I perform a left join with coalesce in Linq?

考虑表格

对于给定的发明,&inventionID,我想对所有组件进行'covering'左连接,而不仅仅是使用的组件。

SQL 类似于

select 
  I.name as inventionName
, C.name as componentName
, coalese (IC.count, 0) as componentCount
from
  (select &inventionID as inventionID, ID, name from components) C -- all components applied to some &inventionID
left join
  inventionComponents IC
on
  C.ID = IC.ComponentID 
  and C.inventionID = IC.inventionID
join
  inventions I
on
  I.ID = C.inventionID

.NET fiddle 中的示例数据和 Linq 查询 https://dotnetfiddle.net/cx4bHp 导致异常

[System.NullReferenceException: Object reference not set to an instance of an object.]

问题:应如何修改 Linq 查询以执行所需的覆盖查询?

为了完整起见,C# fiddle 代码在此处重复

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

public class Program
{
  public static void Main()
  {
    var components = new List<Component>{
      new Component { ID=1, Name = "Florgebit" },
      new Component { ID=2, Name = "Phadron" },
      new Component { ID=3, Name = "Goobstem" },
      new Component { ID=4, Name = "Larchwren" },
      new Component { ID=5, Name = "Zangponder" },
      new Component { ID=6, Name = "Spoofork" },
      new Component { ID=7, Name = "Forkoon" },
      new Component { ID=8, Name = "Blidget" },
      new Component { ID=9, Name = "Wazzawim" },
      new Component { ID=10, Name = "Klackberg" },
    };
    var inventions = new List<Invention>{
      new Invention { ID=21, Name = "Swazzlute" },
      new Invention { ID=22, Name = "Corpocran" },
      new Invention { ID=23, Name = "Fillyboof" },
    };
    var inventionComponents  = new List<InventionComponent>{
      new InventionComponent { ID=100, InventionID=21, ComponentID=1, Count=1 },
      new InventionComponent { ID=101, InventionID=21, ComponentID=2, Count=2 },
      new InventionComponent { ID=102, InventionID=21, ComponentID=8, Count=3 },
      new InventionComponent { ID=103, InventionID=23, ComponentID=5, Count=4 },
      new InventionComponent { ID=104, InventionID=23, ComponentID=6, Count=5 },
      new InventionComponent { ID=105, InventionID=23, ComponentID=3, Count=4 },
      new InventionComponent { ID=106, InventionID=21, ComponentID=4, Count=3 },
      new InventionComponent { ID=107, InventionID=22, ComponentID=5, Count=2 },
      new InventionComponent { ID=108, InventionID=22, ComponentID=4, Count=1 },
      new InventionComponent { ID=109, InventionID=22, ComponentID=1, Count=6 },
      new InventionComponent { ID=110, InventionID=22, ComponentID=7, Count=1 },
      new InventionComponent { ID=111, InventionID=21, ComponentID=9, Count=1 },
    };

    var details =
      from A in inventions
      join B in inventionComponents on A.ID equals B.InventionID
      join C in components on B.ComponentID equals C.ID
      orderby A.Name, C.Name
      select new {
        InventionName = A.Name,
        ComponentName = C.Name,
        ComponentCount = B.Count
      };
/*
    foreach(var d in details)
    {
      Console.WriteLine("Invention: {0}, Component: {1}, Count: {2}", d.InventionName, d.ComponentName, d.ComponentCount);
    }
*/
    var inventionID = 22;
    var index = 1;

    // want full coverage of inventionID, componentID with applied counts
    // 22,1,6
    // 22,2,**0**
    // 22,3,**0**
    // 22,4,1
    // 22,5,2
    // 22,6,**0**
    // 22,7,1
    // 22,8,**0**
    // 22,9,**0**
    // 22,10,**0**

    var corpcheck = 
      from C in components select new { InventionID = inventionID, ComponentID = C.ID, ComponentName = C.Name } into allcomps
      join B in inventionComponents on new { allcomps.InventionID, allcomps.ComponentID } equals new { B.InventionID, B.ComponentID } into join1
      // from j1 in Join1 // inner join
      from j1 in join1.DefaultIfEmpty() // causes exception
      orderby allcomps.ComponentName
      select new {
          RowNum = index++,
        InventionID = allcomps.InventionID,
        ComponentName = allcomps.ComponentName,
        ComponentCount = j1.Count,
      };


    foreach(var x in corpcheck)
    {
      Console.WriteLine("InventionID: {0}, RowNum: {1}, ComponentName: {2}, Count: {3}", x.InventionID, x.RowNum, x.ComponentName, x.ComponentCount);
    }
  }

  public class Invention
  {
    public int ID { get; set; }
    public string Name { get; set; }
  }

  public class InventionComponent
  {
    public int ID { get; set; }
    public int InventionID { get; set; }
    public int ComponentID { get; set; }
    public int Count { get; set; }
  }

  public class Component
  {
    public int ID { get; set; }
    public string Name { get; set; }
  }
}

通过添加 DefaultIfEmpty()j1 对于某些组件可以为 null。如果 j1 为空,我假设您希望计数为 0:

  from C in components select new { InventionID = inventionID, ComponentID = C.ID, ComponentName = C.Name } into allcomps
  join B in inventionComponents on new { allcomps.InventionID, allcomps.ComponentID } equals new { B.InventionID, B.ComponentID } into join1
  from j1 in join1.DefaultIfEmpty()
  orderby allcomps.ComponentName
  select new {
      RowNum = index++,
      InventionID = allcomps.InventionID,
      ComponentName = allcomps.ComponentName,
      ComponentCount = j1 == null ? 0 : j1.Count, // add null check
  };

在 LINQ-to-objects 中,您也可以使用 ComponentCount = j1?.Count ?? 0。但我假设您将在 LINQ 中将其用于 SQL 后端。