EF Core Join Query

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.

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.

from o in db.Track

Use the join operator to join the inner table.

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.

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.

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.

//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.

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.

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.

.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

 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.

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.

(o, i) => new
    {
      Name = o.Name,
      Composer = o.Composer,
      MediaType = i.Name
    }

The final query is as follows

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.

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

//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

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

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

on new { p1=m1.field1 , p2=m1.field2 } equals new {p1=m2.fld1, p2=m2.fld2 }

Example (Query Syntax)

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

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

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);
        }

    }
}
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.

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

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.

    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

        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

                     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)

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.

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.

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

  1. DefaultIfEmpty

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

5 thoughts on “EF Core Join Query”

  1. 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!

  2. 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.

  3. 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

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Scroll to Top