Entity framework left join
Asked Answered
S

8

112

How do I change this query so it returns all u.usergroups?

from u in usergroups
from p in u.UsergroupPrices
select new UsergroupPricesList
{
UsergroupID = u.UsergroupID,
UsergroupName = u.UsergroupName,
Price = p.Price
};
Southeaster answered 4/4, 2011 at 11:46 Comment(1)
maybe this can help. it was on another question here on SOLaryngology
L
171

adapted from MSDN, how to left join using EF 4

var query = from u in usergroups
            join p in UsergroupPrices on u.UsergroupID equals p.UsergroupID into gj
            from x in gj.DefaultIfEmpty()
            select new { 
                UsergroupID = u.UsergroupID,
                UsergroupName = u.UsergroupName,
                Price = (x == null ? String.Empty : x.Price) 
            };
Laryngology answered 4/4, 2011 at 12:44 Comment(10)
I like this better than where gj.DefaultIfEmpty() at the end because I can use x in the where or select!Gallicism
Can you explain the 'from x in gj.DefaultIfEmpty()' line?Colum
@AlexDresko this part takes all the results from the join , and for the ones that have no right hand value , gives you null (default of object being null). hthLaryngology
Thanks @Menahem, but it seems like it would be null by default? What happens if you don't do the DefaultIfEmpty thing? Does it not do a left outer join? It seems like a really weird thing to have to do.Colum
@AlexDresko , it's clunky i agree. i`m guessing this is because the group join operation isn't really an SQL like join , as it returns a hierarchical result rather than a flat table.Laryngology
@Laryngology u equals p.UsergroupID is this correct ? I think we need u.columnNameTuraco
what if price isn't a string but an int?Bondswoman
@AlexDresko its a bit late but without DefaultIfEmpty you end up with an inner join instead of a full outer join.Aeriell
What if there are more than two tables?Charger
This changed slightly with efcore; from x in gj.DefaultIfEmpty() becomes from p in gj.DefaultIfEmpty(). learn.microsoft.com/en-us/ef/core/querying/…Jitter
M
51

Please make your life easier (don't use join into group):

var query = from ug in UserGroups
            from ugp in UserGroupPrices.Where(x => x.UserGroupId == ug.Id).DefaultIfEmpty()
            select new 
            { 
                UserGroupID = ug.UserGroupID,
                UserGroupName = ug.UserGroupName,
                Price = ugp != null ? ugp.Price : 0 //this is to handle nulls as even when Price is non-nullable prop it may come as null from SQL (result of Left Outer Join)
            };
Mayhew answered 6/11, 2017 at 15:33 Comment(6)
Avoiding join into group is a matter of opinion, but it's certainly a valid opinion. Price = ugp.Price may fail if Price is a non-nullable property and the left join doesn't give any results though.Everyplace
Agree with above, but with more than two tables this approach is so much easier to read and maintain.Mayhew
We can check if ugp == NULL and set a default value for Price.Mitchum
just perfect :)Charger
Awesome! I prefer this solution for readability. Also, this makes more joins (i.e. from 3 or more table) much easier! I used it successfully for 2 left joins (i.e. 3 tables).Keening
What if we want a list from right table ?? not a single value like price.Tesstessa
F
49

It might be a bit of an overkill, but I wrote an extension method, so you can do a LeftJoin using the Join syntax (at least in method call notation):

persons.LeftJoin(
    phoneNumbers,
    person => person.Id,
    phoneNumber => phoneNumber.PersonId,
    (person, phoneNumber) => new
        {
            Person = person,
            PhoneNumber = phoneNumber?.Number
        }
);

My code does nothing more than adding a GroupJoin and a SelectMany call to the current expression tree. Nevertheless, it looks pretty complicated because I have to build the expressions myself and modify the expression tree specified by the user in the resultSelector parameter to keep the whole tree translatable by LINQ-to-Entities.

public static class LeftJoinExtension
{
    public static IQueryable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(
        this IQueryable<TOuter> outer,
        IQueryable<TInner> inner,
        Expression<Func<TOuter, TKey>> outerKeySelector,
        Expression<Func<TInner, TKey>> innerKeySelector,
        Expression<Func<TOuter, TInner, TResult>> resultSelector)
    {
        MethodInfo groupJoin = typeof (Queryable).GetMethods()
                                                 .Single(m => m.ToString() == "System.Linq.IQueryable`1[TResult] GroupJoin[TOuter,TInner,TKey,TResult](System.Linq.IQueryable`1[TOuter], System.Collections.Generic.IEnumerable`1[TInner], System.Linq.Expressions.Expression`1[System.Func`2[TOuter,TKey]], System.Linq.Expressions.Expression`1[System.Func`2[TInner,TKey]], System.Linq.Expressions.Expression`1[System.Func`3[TOuter,System.Collections.Generic.IEnumerable`1[TInner],TResult]])")
                                                 .MakeGenericMethod(typeof (TOuter), typeof (TInner), typeof (TKey), typeof (LeftJoinIntermediate<TOuter, TInner>));
        MethodInfo selectMany = typeof (Queryable).GetMethods()
                                                  .Single(m => m.ToString() == "System.Linq.IQueryable`1[TResult] SelectMany[TSource,TCollection,TResult](System.Linq.IQueryable`1[TSource], System.Linq.Expressions.Expression`1[System.Func`2[TSource,System.Collections.Generic.IEnumerable`1[TCollection]]], System.Linq.Expressions.Expression`1[System.Func`3[TSource,TCollection,TResult]])")
                                                  .MakeGenericMethod(typeof (LeftJoinIntermediate<TOuter, TInner>), typeof (TInner), typeof (TResult));

        var groupJoinResultSelector = (Expression<Func<TOuter, IEnumerable<TInner>, LeftJoinIntermediate<TOuter, TInner>>>)
                                      ((oneOuter, manyInners) => new LeftJoinIntermediate<TOuter, TInner> {OneOuter = oneOuter, ManyInners = manyInners});

        MethodCallExpression exprGroupJoin = Expression.Call(groupJoin, outer.Expression, inner.Expression, outerKeySelector, innerKeySelector, groupJoinResultSelector);

        var selectManyCollectionSelector = (Expression<Func<LeftJoinIntermediate<TOuter, TInner>, IEnumerable<TInner>>>)
                                           (t => t.ManyInners.DefaultIfEmpty());

        ParameterExpression paramUser = resultSelector.Parameters.First();

        ParameterExpression paramNew = Expression.Parameter(typeof (LeftJoinIntermediate<TOuter, TInner>), "t");
        MemberExpression propExpr = Expression.Property(paramNew, "OneOuter");

        LambdaExpression selectManyResultSelector = Expression.Lambda(new Replacer(paramUser, propExpr).Visit(resultSelector.Body), paramNew, resultSelector.Parameters.Skip(1).First());

        MethodCallExpression exprSelectMany = Expression.Call(selectMany, exprGroupJoin, selectManyCollectionSelector, selectManyResultSelector);

        return outer.Provider.CreateQuery<TResult>(exprSelectMany);
    }

    private class LeftJoinIntermediate<TOuter, TInner>
    {
        public TOuter OneOuter { get; set; }
        public IEnumerable<TInner> ManyInners { get; set; }
    }

    private class Replacer : ExpressionVisitor
    {
        private readonly ParameterExpression _oldParam;
        private readonly Expression _replacement;

        public Replacer(ParameterExpression oldParam, Expression replacement)
        {
            _oldParam = oldParam;
            _replacement = replacement;
        }

        public override Expression Visit(Expression exp)
        {
            if (exp == _oldParam)
            {
                return _replacement;
            }

            return base.Visit(exp);
        }
    }
}
Frustrate answered 13/9, 2013 at 9:30 Comment(5)
Thanks for this extension fero.Ansilma
This is still great. Thanks!Hypnotism
Tested this within .NET Framework 4.6.2 and it works as expected (i.e. generates a LEFT OUTER JOIN). I am wondering if it works on .NET Core though. Thanks.Reunite
Why ef doesn't default to left join is mind boggling. Many database engines will do an entire table scan against the other table even if no rows are returned from the main table.Arnoldoarnon
I confirm this works in EF CoreInteroffice
S
8

If you prefer method call notation, you can force a left join using SelectMany combined with DefaultIfEmpty. At least on Entity Framework 6 hitting SQL Server. For example:

using(var ctx = new MyDatabaseContext())
{
    var data = ctx
    .MyTable1
    .SelectMany(a => ctx.MyTable2
      .Where(b => b.Id2 == a.Id1)
      .DefaultIfEmpty()
      .Select(b => new
      {
        a.Id1,
        a.Col1,
        Col2 = b == null ? (int?) null : b.Col2,
      }));
}

(Note that MyTable2.Col2 is a column of type int). The generated SQL will look like this:

SELECT 
    [Extent1].[Id1] AS [Id1], 
    [Extent1].[Col1] AS [Col1], 
    CASE WHEN ([Extent2].[Col2] IS NULL) THEN CAST(NULL AS int) ELSE  CAST( [Extent2].[Col2] AS int) END AS [Col2]
    FROM  [dbo].[MyTable1] AS [Extent1]
    LEFT OUTER JOIN [dbo].[MyTable2] AS [Extent2] ON [Extent2].[Id2] = [Extent1].[Id1]
Spermaceti answered 24/5, 2019 at 18:44 Comment(2)
For me this is generating some extremely slow query with "CROSS APPLY" in it.Mongrel
Thanks a million, Helped me so much.Transfinite
A
3

For 2 and more left joins (left joining creatorUser and initiatorUser )

IQueryable<CreateRequestModel> queryResult = from r in authContext.Requests
                                             join candidateUser in authContext.AuthUsers
                                             on r.CandidateId equals candidateUser.Id
                                             join creatorUser in authContext.AuthUsers
                                             on r.CreatorId equals creatorUser.Id into gj
                                             from x in gj.DefaultIfEmpty()
                                             join initiatorUser in authContext.AuthUsers
                                             on r.InitiatorId equals initiatorUser.Id into init
                                             from x1 in init.DefaultIfEmpty()

                                             where candidateUser.UserName.Equals(candidateUsername)
                                             select new CreateRequestModel
                                             {
                                                 UserName = candidateUser.UserName,
                                                 CreatorId = (x == null ? String.Empty : x.UserName),
                                                 InitiatorId = (x1 == null ? String.Empty : x1.UserName),
                                                 CandidateId = candidateUser.UserName
                                             };
Aerie answered 4/5, 2020 at 18:13 Comment(0)
N
2

I was able to do this by calling the DefaultIfEmpty() on the main model. This allowed me to left join on lazy loaded entities, seems more readable to me:

        var complaints = db.Complaints.DefaultIfEmpty()
            .Where(x => x.DateStage1Complete == null || x.DateStage2Complete == null)
            .OrderBy(x => x.DateEntered)
            .Select(x => new
            {
                ComplaintID = x.ComplaintID,
                CustomerName = x.Customer.Name,
                CustomerAddress = x.Customer.Address,
                MemberName = x.Member != null ? x.Member.Name: string.Empty,
                AllocationName = x.Allocation != null ? x.Allocation.Name: string.Empty,
                CategoryName = x.Category != null ? x.Category.Ssl_Name : string.Empty,
                Stage1Start = x.Stage1StartDate,
                Stage1Expiry = x.Stage1_ExpiryDate,
                Stage2Start = x.Stage2StartDate,
                Stage2Expiry = x.Stage2_ExpiryDate
            });
Naxos answered 22/3, 2017 at 11:0 Comment(1)
Here, you don't need .DefaultIfEmpty() at all: it only affects what happens when db.Complains is empty. db.Complains.Where(...).OrderBy(...).Select(x => new { ..., MemberName = x.Member != null ? x.Member.Name : string.Empty, ... }), without any .DefaultIfEmpty(), would already perform a left join (assuming the Member property is marked as optional).Everyplace
R
1

If UserGroups has a one to many relationship with UserGroupPrices table, then in EF, once the relationship is defined in code like:

//In UserGroups Model
public List<UserGroupPrices> UserGrpPriceList {get;set;}

//In UserGroupPrices model
public UserGroups UserGrps {get;set;}

You can pull the left joined result set by simply this:

var list = db.UserGroupDbSet.ToList();

assuming your DbSet for the left table is UserGroupDbSet, which will include the UserGrpPriceList, which is a list of all associated records from the right table.

Rudin answered 23/4, 2018 at 16:4 Comment(0)
S
-1

That's how to write a LEFT JOIN with lambda syntax:

var result = _db.TableOne
    .Where(one => one.Id < 10000)
    .GroupJoin(_db.TableTwo,
        one => one.TwoId,
        two => two.Id,
        (one, twos) => new { one, twos })
    .SelectMany(
        item => item.twos.DefaultIfEmpty(),
        (g, two) => new { g.one, two });

Note: this is not an answer to the question - it's a note for googlers who land here after searching for "ef left join lambda".

Samarskite answered 13/3 at 14:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.