3

I can’t seem to come up with the right corresponding LINQ to SQL statement to generate the following T-SQL. Essentially, I'm trying to return payment information with only one of the customer's addresses... the AR address, if it exists, then the primary address, if it exists, then any address.

SELECT < payment and address columns >
FROM   Payment AS p
       INNER JOIN Customer AS c ON c.CustomerID = p.CustomerID
       OUTER APPLY (
                       SELECT   TOP 1 < address columns >
                       FROM     Address AS a
                       WHERE    a.person_id = c.PersonID
                       ORDER BY CASE WHEN a.BusinessType = 'AR' THEN 0
                                     ELSE 1
                                END
                              , a.IsPrimary DESC
                                END
                   ) AS pa
WHERE  p.Posted = 1

We’re using the Repository Pattern to access the DB, so inside a method of the Payment Repository, I’ve tried:

var q = GetAll()
            .Where(p => p.Posted == true)
            .SelectMany(p => p.Customer
                              .Address
                              .OrderBy(a => a.BusinessType != "AR")
                              .ThenBy(a => a.Primary != true)
                              .Take(1)
                              .DefaultIfEmpty()
            .Select(a => new
            {
                < only the columns I need from p and a >
            });

But when I execute .ToList(), it throws the NullReferenceException (Object reference not set to an instance of an object) on a record where the customer has no addresses set up. So, I tried:

var q1 = GetAll().Where(p => p.Posted == true);

var q2 = q11.SelectMany(p => p.Customer
                              .Address
                              .OrderBy(a => a.BusinessType != "AR")
                              .ThenBy(a => a.Primary != true));

var q3 = q1.SelectMany(p => q2.Where(a => a.PersonID == p.Customer.PersonID)
                              .Take(1)
                              .DefaultIfEmpty()
           .Select(a => new
           {
                < only the columns I need from p and a >
           });

This returns the correct results, but the T-SQL it generates puts the entire T-SQL from above into the outer apply, which is then joined again on Payment and Customer. This seems somewhat inefficient and I wondered if it could be made more efficient because the T-SQL above returns in 6ms for the test case I’m using.

Additional Info:
Q: I think the problem here is that GetAll() returns IEnumerable, not IQueryable ... it would help to see this GetAll() method. - Gert Arnold
A: Actually, GetAll(), when traced all the way back, returns Table<TEntity> System.Data.Linq.GetTable<TEntity>() and Table<TEntity> does implement IQueryable.

However, DefaultIfEmpty() does return IEnumerable<Address>, which is what is throwing the exception, if I'm not mistaken, as I mentioned in the first L2S code section.

SOLUTION UPDATE

Okay, I knew I could fall back to simply going straight to joining the tables and foregoing the use of the navigation properties, and in this case, I now know that is how it should be done. It all makes sense now. I just had become accustomed to preferring using the navigation properties, but here, it’s best to go straight to joining tables.

The reason the T-SQL generated by the second L2S code section was so inefficient was because in order to get to the Address table, it required the inclusion of the Payment/Customer data.

When I simply go straight to joining the tables, the generated T-SQL, while not ideal, is much closer to the desired script code section. That’s because it didn’t require the inclusion of the Payment/Customer data. And that’s when the “well, duh” light bulb flashed on.

Thanks for all who helped on this path to discovery!

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
  • 1
    Why are you using SelectMany instead of just Select? – jdweng Jul 27 '18 at 16:19
  • 1
    Since Address represents a 0:m relationship, I thought I needed to use SelectMany to essentially flatten out that collection and then return only the first one based on the ordering. It would also allow me to return columns for both payments and addresses, whereas Select would only return Address information. If I'm wrong, please feel free to educate me. – DeuceyPoo's Dad Jul 27 '18 at 16:42
  • 1
    It looks like you are correct. I would use .FirstOrDefault() instead of .Take(1).DefaultIfEmpty. Take returns an array object even if you have only one item. So you would have SelectMany(p => p.Where(a => a.PersonID == p.Customer.PersonID).FirstOrDefault()).Select(a =>new ......)ToList();. FirstOrDefault gives you a singleton while Take(1) still gives you an array. – jdweng Jul 27 '18 at 17:00
  • I think the problem here is that `GetAll()` returns `IEnumerable`, not `IQueryable`, so the query can't be translated into SQL (which isn't null-reference sensitive). It would help to see this `GetAll()` method. – Gert Arnold Jul 27 '18 at 18:33
  • By the way, are you really using (outdated) LINQ-to-SQL, not Entity Framework? – Gert Arnold Jul 27 '18 at 18:36
  • 1
    @GertArnold - See updated question above. HA! I can’t fully answer that question without getting into (even more) trouble, but you’ve just been added to the quotes I’ve collected I show my boss occasionally to justify using EF (as if it needs justification). – DeuceyPoo's Dad Jul 27 '18 at 19:24
  • Ah,OK, actually I asked because many people aren't aware of the difference and they're actually using EF. If programmers make this mistake it shouldn't be hard to fool your boss :) But about your edit. That takes us a level deeper (which is always the problem with all these custom repository implementations): does `this.Query()` return `IQueryable`? This exception *really* indicates a query executed as LINQ-to-Objects, i.e. operating on `IEnumerable`. – Gert Arnold Jul 27 '18 at 19:35
  • @GertArnold - My utter apologies... see updated updated question above. – DeuceyPoo's Dad Jul 27 '18 at 19:55

1 Answers1

1

When trying a similar query it turned out that this DefaultIfEpty() call knocks down LINQ-to-SQL. The exception's stack trace shows that things go wrong in System.Data.Linq.SqlClient.SqlBinder.Visitor.IsOuterDependent, i.e. during SQL query building.

Contrary to your conclusion it's not advisable to abandon the use of navigation properties and return to explicit joins. The question is: how to use the best parts of LINQ (which includes nav properties) without troubling LINQ-to-SQL. This, by the way, is true for each ORM with LINQ support.

In this particular case I'd switch to query syntax for the main query and use the keyword let. Something like:

from p in context.Payments
let address = p.Customer
    .Addresses
    .OrderBy(a => a.BusinessType != "AR")
    .ThenBy(a => a.Primary != true)
    .FirstOrDefault()
select new 
{
    p.PropertyX,
    address.PropertyY
    ...
}

This will be translated into one SQL statement and it avoids LINQ-to-SQL's apparent issue with DefaultIfEmpty.

Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
  • 1
    – The T-SQL generated using `let` seemed to generate a sub select containing the entire contents of the `let` clause (the joins and ordering) for each `Address` property returned in the final select. IOW, to return City, State, and PostalCode, it created 3 `SELECT TOP 1` sub selects. This doesn’t seem as efficient as the T-SQL generated by the explicit joins. Now, granted, I simplified my schema a bit (but not much) for this post to make it easier to understand. When I get back to the office, I’ll create a schema that matches this post's to see if `let` does the same thing. – DeuceyPoo's Dad Jul 28 '18 at 17:15
  • 1
    Most ORMs (including Entity Framework) will produce *decent* SQL at best. Often a better manually crafted statement can be wrought and once *that* is done, it may be possible to massage the LINQ statement until it produces SQL that gets close. But normally, when the generated SQL performs well enough, there's no reason to go to these lengths. Anyway, the point here is that it's clear what caused the NRE and how it can be circumvented. – Gert Arnold Jul 29 '18 at 14:17