In this tutorial let us look into how to use Join Query in EF Core to load data from two, three or more tables. The LINQ join operator allows us to join multiple tables on one or more columns (multiple columns). By default, they perform the inner join of the tables. We also learn how to perform left joins in EF Core by using the join operator & DefaultIfEmpty method. Also left join with where clause.
Database:
The Database for this tutorial is taken from the chinook database.Source Code:
The source code of this project available in GitHub. It also contains the script of the database
It is always advisable to use navigational properties to query the related data. You can refer to the article Querying Related data using Navigational Properties from our last tutorial. You should use the Join Query operators only if the tables do not have any navigational properties defined on them or you want to fine-tune the generated queries for performance benefits.
Table of Contents
Using EF Core Join
Query Syntax
The joins Queries are easier with the Query Syntax.
The following query joins Track
and MediaType
table using the Join
query operator. The Join
operator uses the Equals
Keyword to compare the two or more columns. In this example, we use the column MediaTypeId
. The query looks very similar to the normal database SQL Join Queries.
The query begins with outer table i.e Track
. We are using o
as range variable, you can also use the name of table also.
1 2 3 | from o in db.Track |
Use the join
operator to join the inner table.
1 2 3 | join i in db.MediaType |
Mention the condition on which you want to join them. Note that we use the equals
& not ==
or =
. Also we can only compare for equality. Other comparison are not yet supported.
1 2 3 | on o.MediaTypeId equals i.MediaTypeId |
Finally to select the columns we make use of the projection queries. You can select the properties using the range variable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | private void joinTwoTablesExample1() { //Query Syntax using (ChinookContext db = new ChinookContext()) { var Track = (from o in db.Track join i in db.MediaType on o.MediaTypeId equals i.MediaTypeId select new { Name = o.Name, Composer = o.Composer, MediaType = i.Name }).Take(5); foreach (var t in Track) { Console.WriteLine("{0} {1} {2}", t.Name, t.Composer, t.MediaType); } } Console.WriteLine("Press any key to continue"); Console.ReadKey(); } |
The above query translates into the following SQL Query. You will notice that it creates an SQL INNER JOIN.
1 2 3 4 5 6 | //SQL Query SELECT TOP(@__p_0) [t].[Name], [t].[Composer], [m].[Name] AS [MediaType] FROM [Track] AS [t] INNER JOIN [MediaType] AS [m] ON [t].[MediaTypeId] = [m].[MediaTypeId] |
Method Syntax
The method query syntax uses the join
method. join
method is an extension method, which takes the following syntax.
1 2 3 4 5 6 7 8 9 | public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>( this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector ) |
IEnumerable<TOuter> outer
The first sequence to join
. In our case it is Track
table.
1 2 3 | var Track = db.Track |
IEnumerable<TInner> inner
The sequence to join
to the first sequence. Here we use MediaType
table, which we are going to join
to the Track
table.
1 2 3 | .Join(db.MediaType, |
Func<TOuter, TKey> outerKeySelector
A function to extract the join
key from the first sequence (Track
table). Here we use a lambda expression l => l.MediaTypeId
. We use the MediaTypeId
of the Track
table is to join
the two tables
1 2 3 | o => o.MediaTypeId, |
Func<TInner, TKey> innerKeySelector
A function to extract the join
key from the second sequence (MediaType
table). This is similar to the outerKeySelector
, but the specify the key to be used from the second table. In our case is MediaTypeId
field from MediaTypetable
.
1 2 3 | i => i.MediaTypeId, |
Func<TOuter, TInner, TResult>
A function to create a result element from two matching elements. Here the two matching elements are TOuter
which is our Track
table ( o
) & TInner
which is MediaType
table (i
).
We use the projection to an anonymous type to return the result.
1 2 3 4 5 6 7 8 | (o, i) => new { Name = o.Name, Composer = o.Composer, MediaType = i.Name } |
The final query is as follows
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | private void joinTwoTablesExample1() { //Method Syntax using (ChinookContext db = new ChinookContext()) { var Track = db.Track .Join(db.MediaType, o => o.MediaTypeId, i => i.MediaTypeId, (o, i) => new { Name = o.Name, Composer = o.Composer, MediaType = i.Name } ).Take(5); foreach (var t in Track) { Console.WriteLine("{0} {1} {2}", t.Name, t.Composer, t.MediaType); } } Console.WriteLine("Press any key to continue"); Console.ReadKey(); } //SQL Query SELECT TOP(@__p_0) [t].[Name], [t].[Composer], [m].[Name] AS [MediaType] FROM [Track] AS [t] INNER JOIN [MediaType] AS [m] ON [t].[MediaTypeId] = [m].[MediaTypeId] |
Here is an another example, where we use the join
clause to join customer
& employee
table. Both in method & query syntax.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | private void joinTwoTablesExample2() { //Method Syntax using (ChinookContext db = new ChinookContext()) { var Track = db.Customer .Join(db.Employee, f => f.SupportRepId, s => s.EmployeeId, (f, s) => new { CustomerName = f.FirstName, CustomerState = f.State, EmployeeName = s.FirstName, EmployeeState = s.State, } ).Take(5); foreach (var t in Track) { Console.WriteLine("{0} {1} {2} {3}", t.CustomerName, t.CustomerState, t.EmployeeName, t.EmployeeState); } } Console.WriteLine("Press any key to continue"); Console.ReadKey(); //Query Syntax using (ChinookContext db = new ChinookContext()) { var Track = (from f in db.Customer join s in db.Employee on f.SupportRepId equals s.EmployeeId select new { CustomerName = f.FirstName, CustomerState = f.State, EmployeeName = s.FirstName, EmployeeState = s.State, }).Take(5); foreach (var t in Track) { Console.WriteLine("{0} {1} {2} {3}", t.CustomerName, t.CustomerState, t.EmployeeName, t.EmployeeState); } } Console.WriteLine("Press any key to continue"); Console.ReadKey(); } |
The SQL Query of the above method
1 2 3 4 5 6 | //SELECT TOP(@__p_0) [c].[FirstName] AS[CustomerName], [c].[State] AS[CustomerState], [e].[FirstName] AS[EmployeeName], [e].[State] //AS[EmployeeState] //FROM[Customer] AS[c] //INNER JOIN[Employee] AS[e] ON[c].[SupportRepId] = [e].[EmployeeId] |
LINQ Join on Multiple Columns
To join two tables on more than one column (join by using composite keys), we need to define an anonymous type with the values we want to compare
Query Syntax
For Example in query example inst
1 2 3 4 5 6 7 8 9 10 11 12 | var result = (from m1 in db.model1 join m2 in db.model2 on new { m1.field1 , m1.field2 } equals new { m2.field1 , m2.field2 } select new { field1 = m1.field1, field2 = m1.field2, someField = m2.someField }).ToList(); |
In the above we are creating the anonymous type to compare the fields
1 2 3 | on new { m1.field1 , m1.field2 } equals new {m2.field1, m2.field2 } |
The above works only if the data types and the names of the properties in the anonymous types match
If the property names do not match, then you should name the properties of the anonymous type as shown below
1 2 3 | on new { p1=m1.field1 , p2=m1.field2 } equals new {p1=m2.fld1, p2=m2.fld2 } |
Example (Query Syntax)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | var Track = db.Customer .Join(db.Employee, f => new { f1 = f.SupportRepId.Value, f2 = f.State }, s => new { f1 = s.EmployeeId, f2 = s.State }, (f, s) => new { CustomerName = f.FirstName, CustomerState = f.State, EmployeeName = s.FirstName, EmployeeState = s.State, } ).Take(5); |
Example Method Syntax
1 2 3 4 5 6 7 8 9 10 11 12 | var Track = (from f in db.Customer join s in db.Employee on new { f1 = f.SupportRepId.Value, f2 = f.State } equals new { f1 = s.EmployeeId, f2 = s.State } select new { CustomerName = f.FirstName, CustomerState = f.State, EmployeeName = s.FirstName, EmployeeState = s.State, }).Take(5); |
Note that we use f.SupportRepId.Value
instead of f.SupportRepId
. i.e because data type of SupportRepId
is int?
(Nullable int). While that of EmployeeId
is int
. The join fails if the data type does not match. Hence we use f.SupportRepId.Value
to convert int?
to int
Joining three or more Tables
The following queries demonstrate the use of Join queries between three or more tables. The query below queries for all invoices of track Bohemian Rhapsody
with their qty
& amount
. This query involves joining three tables Track
, InvoiceLine
& Invoice
Query Syntax
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | private void joinThreeTablesQuerySyntax() { using (ChinookContext db = new ChinookContext()) { var Track = (from t in db.Track join il in db.InvoiceLine on t.TrackId equals il.TrackId join i in db.Invoice on il.InvoiceId equals i.InvoiceId where t.Name == "Bohemian Rhapsody" select (new { TrackName = t.Name, TrackId = t.TrackId, InvoiceId = i.InvoiceId, InvoiceDate = i.InvoiceDate, Quantity = il.Quantity, UnitPrice = il.UnitPrice }) ).ToList(); foreach (var r in Track) { Console.WriteLine("{0} {1} {2} {3}", r.TrackName, r.InvoiceDate, r.Quantity, r.UnitPrice); } } } |
1 2 3 4 5 6 7 | SELECT[t].[Name] AS[TrackName], [t].[TrackId], [i0].[InvoiceId], [i0].[InvoiceDate], [i].[Quantity], [i].[UnitPrice] FROM[Track] AS[t] INNER JOIN[InvoiceLine] AS[i] ON[t].[TrackId] = [i].[TrackId] INNER JOIN[Invoice] AS[i0] ON[i].[InvoiceId] = [i0].[InvoiceId] WHERE[t].[Name] = N'Bohemian Rhapsody' |
Method Syntax
The method syntax achieves this by chaining the join method.
The first join joins the outer table Tack
with inner table InvoiceLine
on TrackId
and uses the projection to create an anonymous object.
For the second join, the previous result (anonymous object) acts as the outer table. The inner table is Invoice
. Use the projection again to create another anonymous object.
You can keep repeat the join again for more tables. Finally use the ToList
method execute the query.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | private void joinThreeTablesMethodSyntax() { //Method Syntax using (ChinookContext db = new ChinookContext()) { var Track = db.Track .Join(db.InvoiceLine, f => f.TrackId, s => s.TrackId, (f, s) => new { TrackName = f.Name, TrackId = f.TrackId, InvoiceId = s.InvoiceId, Quantity = s.Quantity, UnitPrice = s.UnitPrice } ) .Join(db.Invoice, f => f.InvoiceId, s => s.InvoiceId, (f, s) => new { TrackName = f.TrackName, TrackId = f.TrackId, InvoiceId = f.InvoiceId, InvoiceDate = s.InvoiceDate, Quantity = f.Quantity, UnitPrice = f.UnitPrice } ).Where(r => r.TrackName == "Bohemian Rhapsody") .ToList(); foreach (var r in Track) { Console.WriteLine("{0} {1} {2} {3}", r.TrackName, r.InvoiceDate, r.Quantity, r.UnitPrice); } } } |
The SQL Query is as follows
1 2 3 4 5 6 7 | SELECT[t].[Name] AS[TrackName], [t].[TrackId], [i].[InvoiceId], [i0].[InvoiceDate], [i].[Quantity], [i].[UnitPrice] FROM[Track] AS[t] INNER JOIN[InvoiceLine] AS[i] ON[t].[TrackId] = [i].[TrackId] INNER JOIN[Invoice] AS[i0] ON[i].[InvoiceId] = [i0].[InvoiceId] WHERE[t].[Name] = N'Bohemian Rhapsody' |
The query can also be written as shown below. Notice the difference in projection of the first join and this one.
1 2 3 4 5 6 7 8 9 10 11 12 13 | var Track = db.Track .Join(db.InvoiceLine, f => f.TrackId, s => s.TrackId, (Track, InvoiceLine) => new { Track, InvoiceLine } //Projecting entire row from both tables ) .Join(db.Invoice, f => f.InvoiceLine.InvoiceId, s => s.InvoiceId, (f, s) => new { TrackName = f.Track.Name, TrackId = f.Track.TrackId, InvoiceId = f.InvoiceLine.InvoiceId, InvoiceDate = s.InvoiceDate, Quantity = f.InvoiceLine.Quantity, UnitPrice = f.InvoiceLine.UnitPrice } ) .Where(r => r.TrackName == "Bohemian Rhapsody") .ToList(); |
Left Join
The EF Core converts to above joins into an INNER JOIN. But the other most used join is a SQL left join. To use a left join we use the methodDefaultIfEmpty
in the Query Syntax.
To implement left join, first we start of with a normal join of two tables. Use the into j1
to push the results of this join into a temp variable j1
1 2 3 4 | var model = (from t in db.Track join il in db.InvoiceLine on t.TrackId equals il.TrackId into j1 |
Now assume j1
is another table and start another join as shown below. Here make use of the method DefaultIfEmpty
, which instructs the EF Core to use Left Join. Use the into j2
to push the results of this join into a temp variable j2
1 2 3 4 | from j in j1.DefaultIfEmpty() join i in db.Invoice on j.InvoiceId equals i.InvoiceId into j2 |
You can continue with this for more joins.
Finally in the last from
statement, use the projection to select the properties of the output. Remember that range variables from join
clause (i.e il
& i
) will not be available. Only the range variable in the from
clause are available i.e (t
,j
,r
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | public void joinLeftQuerySyntaxExample1() { //Console.WriteLine("******************* Query Syntax ******************* "); using (ChinookContext db = new ChinookContext()) { var model = (from t in db.Track join il in db.InvoiceLine on t.TrackId equals il.TrackId into j1 from j in j1.DefaultIfEmpty() join i in db.Invoice on j.InvoiceId equals i.InvoiceId into j2 from r in j2.DefaultIfEmpty() select new { TrackName = t.Name, TrackId = t.TrackId, InvoiceId = r.InvoiceId, InvoiceDate = r.InvoiceDate, Quantity = j.Quantity, UnitPrice = j.UnitPrice }) .Where(r => r.TrackName == "Bohemian Rhapsody") .ToList(); foreach (var item in model) { Console.WriteLine("{0} {1} {2} {3}", item.TrackName, item.InvoiceDate, item.Quantity, item.UnitPrice); } } Console.WriteLine("Press any key to continue /Query Syntax 1"); Console.ReadKey(); } |
The above code results in the following SQL statement.
1 2 3 4 5 6 7 | SELECT [t].[Name] AS [TrackName], [t].[TrackId], [i0].[InvoiceId], [i0].[InvoiceDate], [i].[Quantity], [i].[UnitPrice] FROM [Track] AS [t] LEFT JOIN [InvoiceLine] AS [i] ON [t].[TrackId] = [i].[TrackId] LEFT JOIN [Invoice] AS [i0] ON [i].[InvoiceId] = [i0].[InvoiceId] WHERE [t].[Name] = N'Bohemian Rhapsody' |
Note that if you omit DefaultIfEmpty
, then the SQL Server will perform an inner join. You can make use of it to create a query with left and inner joins
Using Where Clause
You can also make use of the following query, where we have use the join condition in where clause and used DefaultIfEmpty
, But you must check the final query before using it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | using (ChinookContext db = new ChinookContext()) { var model = (from t in db.Track from il in db.InvoiceLine.Where(il => il.TrackId == t.TrackId).DefaultIfEmpty() from i in db.Invoice.Where(i => i.InvoiceId == il.InvoiceId).DefaultIfEmpty() select new { TrackName = t.Name, TrackId = t.TrackId, InvoiceId = i.InvoiceId, InvoiceDate = i.InvoiceDate, Quantity = il.Quantity, UnitPrice = il.UnitPrice }) .Where(r => r.TrackName == "Bohemian Rhapsody") .ToList(); foreach (var item in model) { Console.WriteLine("{0} {1} {2} {3}", item.TrackName, item.InvoiceDate, item.Quantity, item.UnitPrice); } } |
References
Summary
Performing joins is one of the common tasks you perform. In EF Core we use the join
operator to achieve this. The join performs an inner join. You can also perform left join DefaultIfEmpty
Thank you very much!
Hey thank you so much for taking your time and posting this. I needed to get some syntax right and your post really helped.
Legend!
Not cool. The examples are too simple. For instance, I have issue with creating Linq query between several tables with nested projections and collections… And I cannot find proper example anywhere to figure out if it is the bug of EfCore5 or something wrong with my code.
Thanks!! and this query..
SELECT ..
FROM table_a A
INNER JOIN table_b B ON A.key_B=B.key_B
LEFT JOIN table_c C ON B.key_C=C.key_C
Thanks. You could have added method syntax for left join also