5

I am trying to implement a LEFT OUTER JOIN in Linq against an Entity Framework Core 2.0 DbContext. It's important that the query is translated to SQL, rather than evaluated locally. I've reviewed several StackOverflow solutions including this one which is good, but none are using EF Core.

The problem I get is that EF Core returns the following warning/error for the DefaultIfEmpty() method:

The LINQ expression 'DefaultIfEmpty()' could not be translated and will be evaluated locally

Without the DefaultIfEmpty() method an INNER JOIN is used. My LINQ query looks like this:

var join = context.Portfolios
           .Where(p => p.IsActive)
           .GroupJoin(context.BankAccounts, 
                      prt => prt.Id, 
                      bnk => bnk.PortfolioId, 
                      (prt, bnks) => new {Portfolio=prt,Account=bnks.DefaultIfEmpty()})
           .SelectMany(r => r.Accounts.DefaultIfEmpty(),
                       (p, b) => new 
                           {
                               Id = p.Portfolio.Id,
                               BankAccount = b.BankAccountNumber,
                               BankRef = b.BeneficiaryReference,
                               Code = p.Portfolio.Code,
                               Description = p.Portfolio.DisplayName
                           });

Does anyone know a way around this?

Peter
  • 5,455
  • 7
  • 46
  • 68
  • Not sure what's the value of this question. `left` joins in LINQ are always performed using the pattern from the link (which you didn't apply correctly - there should be only one `DefaultIfEmpty()`, not two). And in EF (Core) preferred way is to **not** use joins at all, but navigation properties. – Ivan Stoev Mar 29 '18 at 10:09
  • Thanks Ivan, I will try out other options and see if I get it to work as a LEFT JOIN. – Peter Mar 29 '18 at 10:25

1 Answers1

16

OK, this is my mistake, based on a comment in another SO question that noted that DefaultIfEmpty() is necessary to make the query an OUTER JOIN. Looking at the underlying SQL, a LEFT JOIN is being submitted to the database when I remove the DefaultIfEmpty() specification. I'm not sure if this differs from doing a LEFT JOIN over in-memory collections, but it has solved my problem.

The SQL as generated by EF Core for this Linq query looks like this:

SELECT [p].[ID], 
       [bnk].[BankAccountNumber] AS [BankAccount], 
       [bnk].[BeneficiaryReference] AS [BankRef], 
       [p].[Code], 
       [p].[DisplayName] AS [Description]
    FROM [Portfolio] AS [p]
    LEFT JOIN [BankAccount] AS [bnk] ON [p].[ID] = [bnk].[PortfolioId]
WHERE (([p].[IsActive] = 1)))

EDIT: Found time to test this out and @Ivan Stoev is correct: if your navigation properties are correctly setup in the EF context definition, EF will generate the LEFT JOIN. This is a better approach when using EF.

EF navigation property on Portfolio:

public virtual ICollection<BankAccount> BankAccounts { get; set; }

LINQ query via navigation property:

var join = context.Portfolios
                  .Where(p => p.IsActive)
                  .SelectMany(p => p.BankAccounts.DefaultIfEmpty(), (p, b) => new
                                                {
                                                    Id = p.Id,
                                                    BankAccount = b.BankAccountNumber,
                                                    BankRef = b.BeneficiaryReference,
                                                    Code = p.Code,
                                                    Description = p.DisplayName
                                                });

Resulting SQL code:

SELECT [p].[ID], [p.BankAccounts].[BankAccountNumber] AS [BankAccount], [p.BankAccounts].[BeneficiaryReference] AS [BankRef], [p].[Code], [p].[DisplayName] AS [Description]
    FROM [core].[Portfolio] AS [p]
    LEFT JOIN [ims].[BankAccount] AS [p.BankAccounts] ON [p].[ID] = [p.BankAccounts].[PortfolioId]
WHERE (([p].[IsActive] = 1))

Note that dropping the DefaultIfEmpty() from the LINQ query results in an INNER JOIN.

Peter
  • 5,455
  • 7
  • 46
  • 68