1

I have a query from a database I need to make. I understand how to write the query in T-SQL. The real query is much more complicated, but a simple illustration of the pattern is something like this:

SELECT * FROM [dbo].[A] AS a
    LEFT JOIN dbo.[B] AS b ON a.ID = b.ParentID
    LEFT JOIN dbo.[C] AS c ON y.ID = c.ParentID
    LEFT JOIN 
    (
        SELECT * FROM dbo.[D]
        WHERE OtherID = @otherID
    ) AS d ON c.ID = d.ParentID
    LEFT JOIN
   (
        SELECT * FROM dbo.[E]
        WHERE OtherID = @otherID
    ) AS e ON e.ID = e.ParentID
WHERE A.ID = @Id

I need to write that SQL in C# linq to sql (for entityframework core) such that it generates the equivalent of the filtered inline views above. The goal obviously is to return a result set that always contains the tree A->B->C and contains D or E if and only if those nodes also match the secondary filtering. Note that it is quite easy to do the filtering within the inline view, but very difficult to do it outside the inline view because filtering outside the inline view tends to cause C nodes to disappear when there is no matching D child. That is not the intention.

Thanks

PS: To clarify, you might make a first attempt to write the above as:

query = from a in context.A
join bt in context.B on a.ID equals bt.ParentID into btent
from b in btent.DefaultIfEmpty()
join ct in context.C on b.ID equals ct.ParentID into ctent
from c in ctent.DefaultIfEmpty()
join dt in context.D on c.ID equals dt.ParentID into dtent
from d in dtent.DefaultIfEmpty()
.Include(a => a.B).ThenInclude(b => b.C).ThenInclude(c => c.D)
.Where(a => a.ID = myPrimaryID && d.OtherId = myOtherID)

The trouble is that a where clause on the 'd' entity returns only those rows where D entity exists, so the entire stack will be empty if it isn't. If you try to get cute and say filter where the 'd' entity is null or matches the filter, if you inspect the sql generated by EF in that situation, it is incorrect. The correct filtering has to happen within the 'join', as with the T-SQL above.

PPS: Yes, if you aren't filtering except for the parent object, you can dispense with this entirely and just write the includes and the where clause, but I think on reflection you'll realize that filtering by a term that applies to a great-grand-child but doesn't filter the grand-child is complex. If you can write out the query in either 'form', I'd appreciate it.

  • Please add Entity definitions with Navigation properties. Typically in EF A you'd just use `Include` to bring back A, along with the related B's, C's, D's, and E's. No need to flatten it all out or use outer joins. – David Browne - Microsoft Jul 23 '20 at 17:20
  • 1
    I am well enough versed in EF to know how to use Includes, and you may assume I have correctly constructed a list of '.Include(a => a.B).ThenInclude...' etc. I know how to use EF 'typically'. The real problem is much more complex than what I wrote here, so I won't try to cut and paste the navigation properties. You may assume that A has many B, B has many C, and C has many D and E, but that no child is guaranteed to exist. – Matthew Reynolds Jul 23 '20 at 17:40

2 Answers2

1

Apart from the lack of natural left outer join syntax, select being last, and select * requires anonymous/concrete type projection (but it could contain whole entities), LINQ supports the same constructs as standard SQL, including inline subqueries.

So it's possible to write LINQ query the exact way as sample SQL query:

from a in db.A
join b in db.B on a.ID equals b.ParentID
into a_b from b in a_b.DefaultIfEmpty()
join c in (from c in db.C where c.OtherID == myOtherID select c) on b.ID equals c.ParentID
into b_c from c in b_c.DefaultIfEmpty()
join d in (from d in db.D where d.OtherID == myOtherID2 select d) on c.ID equals d.ParentID
into c_d from d in c_d.DefaultIfEmpty()
select new { a, b, c, d }

which is translated by EF Core to:

SELECT [s].[ID], [s0].[ID], [s0].[ParentID], [t].[ID], [t].[OtherID], [t].[ParentID], [t0].[ID], [t0].[OtherID], [t0].[ParentID]
FROM [SO6_A] AS [s]
LEFT JOIN [SO6_B] AS [s0] ON [s].[ID] = [s0].[ParentID]
LEFT JOIN (
    SELECT [s1].[ID], [s1].[OtherID], [s1].[ParentID]
    FROM [SO6_C] AS [s1]
    WHERE [s1].[OtherID] = @__myOtherID_0
) AS [t] ON [s0].[ID] = [t].[ParentID]
LEFT JOIN (
    SELECT [s2].[ID], [s2].[OtherID], [s2].[ParentID]
    FROM [SO6_D] AS [s2]
    WHERE [s2].[OtherID] = @__myOtherID2_1
) AS [t0] ON [t].[ID] = [t0].[ParentID]

Another standard LINQ way is to push the predicates into join conditions (thus not filtering out the outer join result) by using composite join keys:

from a in db.A
join b in db.B on a.ID equals b.ParentID
into a_b from b in a_b.DefaultIfEmpty()
join c in db.C on new { K1 = b.ID, K2 = myOtherID } equals new { K1 = c.ParentID, K2 = c.OtherID }
into b_c from c in b_c.DefaultIfEmpty()
join d in db.D on new { K1 = c.ID, K2 = myOtherID2 } equals new { K1 = d.ParentID, K2 = d.OtherID }
into c_d from d in c_d.DefaultIfEmpty()
select new { a, b, c, d }

which is translated to:

SELECT [s].[ID], [s0].[ID], [s0].[ParentID], [s1].[ID], [s1].[OtherID], [s1].[ParentID], [s2].[ID], [s2].[OtherID], [s2].[ParentID]
FROM [SO6_A] AS [s]
LEFT JOIN [SO6_B] AS [s0] ON [s].[ID] = [s0].[ParentID]
LEFT JOIN [SO6_C] AS [s1] ON ([s0].[ID] = [s1].[ParentID]) AND (@__myOtherID_0 = [s1].[OtherID])
LEFT JOIN [SO6_D] AS [s2] ON ([s1].[ID] = [s2].[ParentID]) AND (@__myOtherID2_1 = [s2].[OtherID])

More compact LINQ way is to use correlated sub queries instead of joins:

from a in db.A
from b in db.B.Where(b => a.ID == b.ParentID).DefaultIfEmpty()
from c in db.C.Where(c => b.ID == c.ParentID && c.OtherID == myOtherID).DefaultIfEmpty()
from d in db.D.Where(d => c.ID == d.ParentID && d.OtherID == myOtherID2).DefaultIfEmpty()
select new { a, b, c, d }

which is happily translated by EF Core to:

  SELECT [s].[ID], [s0].[ID], [s0].[ParentID], [t].[ID], [t].[OtherID], [t].[ParentID], [t0].[ID], [t0].[OtherID], [t0].[ParentID]
  FROM [SO6_A] AS [s]
  LEFT JOIN [SO6_B] AS [s0] ON [s].[ID] = [s0].[ParentID]
  LEFT JOIN (
      SELECT [s1].[ID], [s1].[OtherID], [s1].[ParentID]
      FROM [SO6_C] AS [s1]
      WHERE [s1].[OtherID] = @__myOtherID_0
  ) AS [t] ON [s0].[ID] = [t].[ParentID]
  LEFT JOIN (
      SELECT [s2].[ID], [s2].[OtherID], [s2].[ParentID]
      FROM [SO6_D] AS [s2]
      WHERE [s2].[OtherID] = @__myOtherID2_1
  ) AS [t0] ON [t].[ID] = [t0].[ParentID]

Finally, the most compact and preferred way in EF Core is to use navigation properties instead of manual joins in LINQ to Entities query:

from a in db.A
from b in a.Bs.DefaultIfEmpty()
from c in b.Cs.Where(c => c.OtherID == myOtherID).DefaultIfEmpty()
from d in c.Ds.Where(d => d.OtherID == myOtherID2).DefaultIfEmpty()
select new { a, b, c, d }

which is also translated by EF Core to:


  SELECT [s].[ID], [s0].[ID], [s0].[ParentID], [t].[ID], [t].[OtherID], [t].[ParentID], [t0].[ID], [t0].[OtherID], [t0].[ParentID]
  FROM [SO6_A] AS [s]
  LEFT JOIN [SO6_B] AS [s0] ON [s].[ID] = [s0].[ParentID]
  LEFT JOIN (
      SELECT [s1].[ID], [s1].[OtherID], [s1].[ParentID]
      FROM [SO6_C] AS [s1]
      WHERE [s1].[OtherID] = @__myOtherID_0
  ) AS [t] ON [s0].[ID] = [t].[ParentID]
  LEFT JOIN (
      SELECT [s2].[ID], [s2].[OtherID], [s2].[ParentID]
      FROM [SO6_D] AS [s2]
      WHERE [s2].[OtherID] = @__myOtherID2_1
  ) AS [t0] ON [t].[ID] = [t0].[ParentID]
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • Thanks. I wrote the query with projections, but that solution is incredibly fragile. I'll utilize your answers and see if that produces a better solution. – Matthew Reynolds Jul 28 '20 at 21:26
  • I was testing your patterns here and I noticed something about what you wrote that I didn't realize was particularly critical but which seems so now, and that is that you ended by writing "select new { a, b, c, d}", and I would think that would generate a list of entities not attached to one another in the tree that I'm intending to produce. I would like to just write 'select a', but if I do that and use includes to chain a, b, c, and d together, the results aren't filtered as I would expect. Instead, if I "include" objects, EF core generates SQL that evades the intent of the filter. – Matthew Reynolds Jul 30 '20 at 17:56
  • Not sure I follow. You asked for LINQ equivalent of SQL `SELECT * FROM a join b ...` and LINQ `select new { a, b }` is the exact equivalent of SQL `*`. If you are seeking for different result shape (nested collections) or filtered `Include`, these in fact have no SQL equivalent, so you should formulate the question differently. And since there are already answers to this question, by SO you should ask new question. – Ivan Stoev Jul 30 '20 at 18:10
  • I guess that is a fair response, but the result set I am getting is fact an anonymous collection of proxy entities which I will then need to project into a set of entities that I really want and presumably manually sew together all the references for those entities. And if that's the case, what advantages have I here over writing a sql query that returns results sets that I can then use to populate entities and set their properties manually? Regardless of what I do, I seem to be evading the functionality promised by EF and delivered by EF in at least 'toy' examples. – Matthew Reynolds Jul 30 '20 at 18:36
  • Well, you get anonymous object which contains fully populated entities. Also in case this is tracking query (the default), you can always use the well known trick with EF navigation property fix-up. Add to the end of any of the above LINQ queries `(...).AsEnumerable().Select(e => e.a).ToList()`, and you'll get a populated hierarchy. – Ivan Stoev Jul 30 '20 at 20:02
0

Fair enough. 99.9% percent of EF questions about translating LEFT JOIN are a simple failure to use Navigation Properties.

EF Core is adding filtered includes in the next version see Filtering on Include in EF Core.

Or you can project A, along with selected child collections something like this:

var q = from a in db.A
        select new 
        {
          a, 
          Bs = a.Bs,
          Ds = a.Bs.SelectMany( b => b.Ds ).Where(d => d.OtherID = dOtherId)
        };
David Browne - Microsoft
  • 80,331
  • 6
  • 39
  • 67
  • Well, "it can't be done" does seem to be a fair answer. If I'm forced to do some sort of projection, I don't understand your syntax. That seems to be returning an anonymous type? Wouldn't I have to map that back to entities? – Matthew Reynolds Jul 23 '20 at 19:37