If a database driven LINQ provider is used, a significantly more readable left outer join can be written as such:
from c in categories
from p in products.Where(c == p.Category).DefaultIfEmpty()
If you omit the DefaultIfEmpty()
you will have an inner join.
Take the accepted answer:
from c in categories
join p in products on c equals p.Category into ps
from p in ps.DefaultIfEmpty()
This syntax is very confusing, and it's not clear how it works when you want to left join MULTIPLE tables.
Note
It should be noted that from alias in Repo.SomeTable.Where(condition).DefaultIfEmpty()
is the same as an outer-apply/left-join-lateral, which any (decent) database-optimizer is perfectly capable of translating into a left join, as long as you don't introduce per-row-values (aka an actual outer apply). Don't do this in Linq-2-Objects, because there, there will be no DB-optimizer ...
Detailed Example
var query2 = (
from users in Repo.T_User
from mappings in Repo.T_User_Group
.Where(mapping => mapping.USRGRP_USR == users.USR_ID)
.DefaultIfEmpty() // <== makes join left join
from groups in Repo.T_Group
.Where(gruppe => gruppe.GRP_ID == mappings.USRGRP_GRP)
.DefaultIfEmpty() // <== makes join left join
// where users.USR_Name.Contains(keyword)
// || mappings.USRGRP_USR.Equals(666)
// || mappings.USRGRP_USR == 666
// || groups.Name.Contains(keyword)
select new
{
UserId = users.USR_ID
,UserName = users.USR_User
,UserGroupId = groups.ID
,GroupName = groups.Name
}
);
var xy = (query2).ToList();
When used with LINQ 2 SQL it will translate nicely to the following very legible SQL query:
SELECT
users.USR_ID AS UserId
,users.USR_User AS UserName
,groups.ID AS UserGroupId
,groups.Name AS GroupName
FROM T_User AS users
LEFT JOIN T_User_Group AS mappings
ON mappings.USRGRP_USR = users.USR_ID
LEFT JOIN T_Group AS groups
ON groups.GRP_ID == mappings.USRGRP_GRP
I stress again, if you are doing that in Linq-2-Objects (instead of Linq-2-SQL), you should do it the old-fashioned way (because LINQ to SQL translates this correctly to join operations, but over objects this method forces a full scan).
Here's the old fashioned way to do it, with actual left-joins:
var query2 = (
from users in Repo.T_Benutzer
join mappings in Repo.T_Benutzer_Benutzergruppen on mappings.BEBG_BE equals users.BE_ID into tmpMapp
join groups in Repo.T_Benutzergruppen on groups.ID equals mappings.BEBG_BG into tmpGroups
from mappings in tmpMapp.DefaultIfEmpty()
from groups in tmpGroups.DefaultIfEmpty()
select new
{
UserId = users.BE_ID
,UserName = users.BE_User
,UserGroupId = mappings.BEBG_BG
,GroupName = groups.Name
}
);
Now, as an example with a more complex query, if you need further explanation on how it works, consider this SQL statement:
DECLARE @BE_ID integer;
DECLARE @stichtag datetime;
DECLARE @in_standort uniqueidentifier;
DECLARE @in_gebaeude uniqueidentifier;
SET @BE_ID = 123;
SET @stichtag = CURRENT_TIMESTAMP;
SET @in_standort = '00000000-0000-0000-0000-000000000000';
SET @in_gebaeude = '00000000-0000-0000-0000-000000000000';
DECLARE @unixTimestamp bigint;
DECLARE @bl national character varying(MAX);
SET @unixTimestamp = DATEDIFF(SECOND, '1970-01-01T00:00:00.000', CONVERT(DATETIME, @stichtag, 1));
SET @bl = (
SELECT TOP 1 FC_Value
FROM T_FMS_Configuration
WHERE FC_Key = 'basicLink'
);
-- SELECT @unixTimestamp AS unix_ts, @bl AS bl;
SELECT
so.SO_Nr AS RPT_SO_Nr
,so.SO_Bezeichnung AS RPT_SO_Bezeichnung
,gb.GB_Bezeichnung
,gb.GB_GM_Lat
,gb.GB_GM_Lng
,objTyp.OBJT_Code
,@bl + '/Modules/App150/index.html'
+ '?Code=' + COALESCE(objTyp.OBJT_Code, 'BAD')
+ '&UID=' + COALESCE(CAST(gb.GB_UID AS national character varying(MAX)), '')
+ '&Timestamp=' + CONVERT(national character varying(MAX), @unixTimestamp, 126)
AS RPT_QR
FROM T_AP_Gebaeude AS gb
LEFT JOIN T_AP_Standort AS so ON gb.GB_SO_UID = so.SO_UID
LEFT JOIN T_OV_Ref_ObjektTyp AS objTyp ON 'GB' = objTyp.OBJT_Code
LEFT JOIN T_Benutzer AS benutzer ON benutzer.BE_ID = @BE_ID AND benutzer.BE_Status = 1
WHERE gb.GB_Status = 1
AND @stichtag >= gb.GB_DatumVon
AND @stichtag <= gb.GB_DatumBis
AND so.SO_Status = 1
AND @stichtag >= so.SO_DatumVon
AND @stichtag <= so.SO_DatumBis
AND (@in_standort = '00000000-0000-0000-0000-000000000000' OR so.SO_UID = @in_standort)
AND (@in_gebaeude = '00000000-0000-0000-0000-000000000000' OR gb.GB_UID = @in_gebaeude)
AND
(
benutzer.BE_ID IS NULL
OR benutzer.BE_ID < 0
OR benutzer.BE_usePRT = 0
OR EXISTS
(
SELECT 1
FROM T_COR_Objekte AS obj
INNER JOIN T_COR_ZO_ObjektRechte_Lesen AS objR
ON objR.ZO_OBJR_OBJ_UID = obj.OBJ_UID
AND objR.ZO_OBJR_OBJ_OBJT_Code = obj.OBJ_OBJT_Code
WHERE obj.OBJ_UID = gb.GB_UID
)
);
which yields the following LINQ:
(this is the DB context, dump is a method of LINQpad)
int BE_ID = 123;
System.DateTime stichtag = System.DateTime.Now;
System.Guid in_standort = System.Guid.Empty;
System.Guid in_gebaeude = System.Guid.Empty;
long unixTimestamp = (long)(stichtag.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds;
string bl = (
from c in this.T_FMS_Configuration
where c.FC_Key == "basicLink"
select c.FC_Value
).FirstOrDefault();
(
from gb in this.T_AP_Gebaeude
join so in this.T_AP_Standort on gb.GB_SO_UID equals so.SO_UID into gb_so
from so in gb_so.DefaultIfEmpty()
join objTyp in this.T_OV_Ref_ObjektTyp on "GB" equals objTyp.OBJT_Code into gb_objTyp
from objTyp in gb_objTyp.DefaultIfEmpty()
join benutzer in this.T_Benutzer.Where(b => b.BE_ID == BE_ID && b.BE_Status == 1) on 1 equals 1 into gb_benutzer
from benutzer in gb_benutzer.DefaultIfEmpty()
where gb.GB_Status == 1
&& stichtag >= gb.GB_DatumVon
&& stichtag <= gb.GB_DatumBis
&& so.SO_Status == 1
&& stichtag >= so.SO_DatumVon
&& stichtag <= so.SO_DatumBis
&& (in_standort == System.Guid.Empty|| so.SO_UID == in_standort)
&& (in_gebaeude == System.Guid.Empty || gb.GB_UID == in_gebaeude)
&&
(
benutzer == null
|| benutzer.BE_ID < 0
|| benutzer.BE_usePRT == false
|| this.T_COR_Objekte.Any(
obj => obj.OBJ_UID == gb.GB_UID
&& this.T_COR_ZO_ObjektRechte_Lesen.Any(objR => objR.ZO_OBJR_OBJ_UID == obj.OBJ_UID && objR.ZO_OBJR_OBJ_OBJT_Code == obj.OBJ_OBJT_Code)
)
)
select new {
RPT_SO_Nr = so.SO_Nr
,RPT_SO_Bezeichnung = so.SO_Bezeichnung
// ,RPT_GB_UID = gb.GB_UID
// ,gb.GB_Nr
,gb.GB_Bezeichnung
// ,adr = gb.GB_Strasse + " " + gb.GB_StrasseNr + ", CH-" + gb.GB_PLZ + " " + gb.GB_Ort
,gb.GB_GM_Lat
,gb.GB_GM_Lng
// ,objTyp.OBJT_UID
,objTyp.OBJT_Code
,RPT_QR = bl + "/Modules/App150/index.html"
+ "?Code=" + (objTyp.OBJT_Code ?? "BAD")
+ "&UID=" + (System.Convert.ToString(gb.GB_UID) ?? "" )
+ "&Timestamp=" + unixTimestamp.ToString(System.Globalization.CultureInfo.InvariantCulture)
}
).Dump();