A join in EF Core combines two query sources by matching their keys and can project values from both sources into a new result shape. You can use joins with entities that have a configured relationship or with sources that simply share compatible key values.
Inner Join
Use Join when you only want results whose keys match in both query sources.
var productsWithCategories = await context.Products
.Join(
context.Categories,
product => product.CategoryId,
category => category.CategoryId,
(product, category) => new
{
ProductName = product.Name,
CategoryName = category.Name
})
.ToListAsync();
This query joins Products and Categories by comparing Product.CategoryId with Category.CategoryId.
The result selector creates an anonymous type with two properties:
ProductNameCategoryName
The application therefore receives a list of projected objects with those two values rather than complete Product and Category entities. See Projection for more about shaping query results.
Because this is an inner join, only products whose CategoryId matches a category are returned. With a relational database provider, this query shape is typically translated to an SQL INNER JOIN.
Join() composes the query; it does not execute it. The result remains an IQueryable until ToListAsync() executes the query and materializes the results. See LINQ Queries for more about query composition and execution.
Left Join
Use LeftJoin when you want to keep every row from the first query source, even when there is no matching row in the second source.
var categoriesWithProducts = await context.Categories
.LeftJoin(
context.Products,
category => category.CategoryId,
product => product.CategoryId,
(category, product) => new
{
CategoryName = category.Name,
ProductName = product == null ? null : product.Name
})
.ToListAsync();
This query keeps every Category because Categories is the first query source. When a category has a matching product, ProductName contains the product name. When there is no match, the product side is optional and ProductName is null.
The result is a list of anonymous objects containing CategoryName and a nullable ProductName. For example, a Books category with no matching product still appears in the result with ProductName set to null.
LeftJoin is available in .NET 10, and EF Core 10 can translate it to a relational LEFT JOIN when supported by the provider. In earlier .NET versions, left joins were commonly expressed using the GroupJoin + SelectMany + DefaultIfEmpty pattern.
LeftJoin preserves the first query source.
Right Join
Use RightJoin when you want to keep every row from the second query source, even when there is no matching row in the first source.
var categoriesWithProducts = await context.Products
.RightJoin(
context.Categories,
product => product.CategoryId,
category => category.CategoryId,
(product, category) => new
{
CategoryName = category.Name,
ProductName = product == null ? null : product.Name
})
.ToListAsync();
This time, Categories is the second query source, so every category remains in the result. If no product matches a category, ProductName is null.
The result has the same shape as the left join example: an anonymous object containing CategoryName and a nullable ProductName.
RightJoin has the same .NET 10 and EF Core 10 version requirements as LeftJoin, and EF Core can translate it to a relational RIGHT JOIN when supported by the provider.
The key difference between the two operators is which source is preserved:
LeftJoinkeeps every row from the first query source.RightJoinkeeps every row from the second query source.
Group Join
Use GroupJoin when you want each element from the first sequence together with a group containing all matching elements from the second sequence.
Unlike the previous join examples, a grouped result contains a collection for each outer element. A GroupJoin that returns an outer element together with a collection of matching inner elements does not translate directly to the server in many cases.
For this collection-shaped result, a clear approach is to materialize the required data first and then perform the GroupJoin in memory:
var categories = await context.Categories
.Select(category => new
{
category.CategoryId,
category.Name
})
.ToListAsync();
var products = await context.Products
.Select(product => new
{
product.CategoryId,
product.Name
})
.ToListAsync();
var categoriesWithProducts = categories
.GroupJoin(
products,
category => category.CategoryId,
product => product.CategoryId,
(category, matchingProducts) => new
{
CategoryName = category.Name,
ProductNames = matchingProducts
.Select(product => product.Name)
.ToList()
})
.ToList();
The two ToListAsync() calls execute the EF Core queries and materialize the selected category and product values. After materialization, categories and products are in-memory lists rather than IQueryable sources, so GroupJoin runs over them using LINQ to Objects.
The result is a list of anonymous objects containing:
CategoryNameProductNames, aList<string>with the matching product names
For example, a Beverages category can contain Coffee and Tea, while a Books category with no matching products still appears with an empty ProductNames list.
This is different from SQL GROUP BY, which groups rows by keys for aggregation rather than attaching a collection of matches to each outer element.
Join on Composite Keys
Use Join with composite keys when matching rows requires more than one property.
In this example, InventoryEntry and PriceEntry are matched by both ProductId and StoreId:
var inventoryWithPrices = await context.InventoryEntries
.Join(
context.PriceEntries,
inventory => new
{
inventory.ProductId,
inventory.StoreId
},
price => new
{
price.ProductId,
price.StoreId
},
(inventory, price) => new
{
inventory.ProductId,
inventory.StoreId,
inventory.Quantity,
price.Price
})
.ToListAsync();
Each key selector creates an anonymous object with the same two components:
ProductIdStoreId
The anonymous key objects must have matching property names and corresponding compatible types so both key selectors produce the same key shape. These properties do not need to form the primary key of either entity; they are simply the values used by this join.
A match is produced only when both values match. For this anonymous-key pattern, EF Core translates the join condition by comparing the corresponding key components.
The result is a list of anonymous objects containing:
ProductIdStoreIdQuantityPrice
InventoryEntry and PriceEntry do not need a configured EF Core relationship for this join. The join is based on the key selectors in the query, not on a navigation property.
Join vs Include
Join and Include solve different problems.
Use Join when you want to combine query sources by matching keys and shape the result with values from both sources. The examples above return anonymous objects rather than loading complete related entities into navigation properties.
Use Include when you want to load related entities into the navigation properties of the root entities returned by the query.
In short:
Joincombines sources and creates the result shape you select.Includeloads related entities through configured navigation properties.
External Resources - Join
The following videos are useful if you want to see LeftJoin, RightJoin, and GroupJoin in practical examples. The first focuses on the new LeftJoin and RightJoin operators in .NET 10 with EF Core 10. The second adds a practical comparison with navigation-property queries, while the third helps illustrate the grouped result shape produced by LINQ GroupJoin. The fourth provides a concise walkthrough of LeftJoin and RightJoin with visual explanations, code, and executed results.
Video 1 - EF Core 10 Finally Adds LeftJoin + RightJoin (Too Little, Too Late?)
Milan Jovanović compares the historical left-join pattern with the direct LeftJoin and RightJoin operators available with .NET 10 and supported by EF Core 10. The video also demonstrates the resulting query behavior and inspects the SQL sent to the database.
Key timestamps:
- 0:18 — Visual explanation of how a left join preserves the left source and handles unmatched rows
- 6:20 — Building a query with the direct
LeftJoinmethod - 7:27 — Method syntax and the lack of a direct query-syntax form for the new operator
- 8:20 — Implementing the corresponding
RightJoin
Video 2 - EF Core just fixed one of its biggest limitations
Round The Code demonstrates practical LeftJoin and RightJoin queries in .NET 10, including DTO projections and a comparison with navigation-property queries. It also shows how query syntax still relies on the historical outer-join pattern.
Key timestamps:
- 1:11 — Using
Includefor a simple related-data query before moving to explicit joins - 4:51 — Implementing the query with the direct
LeftJoinmethod - 5:58 — Building a
RightJoinquery and projecting the result - 6:30 — Using the historical
join ... intoandDefaultIfEmpty()pattern with query syntax
Video 3 - LINQ GroupJoin Explained in C# | Master join into in .NET 10
Harshit Agarwal provides a focused introduction to LINQ GroupJoin and its hierarchical result shape. The examples use in-memory collections rather than EF Core, making this video most useful for understanding how each outer element is paired with a collection of matching inner elements.
Key timestamps:
- 0:07 — Understanding
GroupJoinand hierarchical results - 2:20 — Using
join ... intoto create and project a grouped result - 4:25 — Showing an outer element with an empty group when no matches exist
- 5:30 — Implementing the same grouped result with method-syntax
GroupJoin
Video 4 - The 2 NEW LINQ Methods in EF CORE 10 that make Joins EASY!
Israel Quiroz provides a concise introduction to the LeftJoin and RightJoin operators available in .NET 10 and supported by EF Core 10. He visually explains which side of each join is preserved, demonstrates both operators in code, and executes the query to inspect the resulting data.
The video is especially useful as a clear, practical walkthrough of the LeftJoin and RightJoin syntax and behavior, complementing the broader examples and explanations covered in the other resources in this section.
Key timestamps:
- 1:20 — Visual explanation of
LeftJoinand how it preserves the left side of the join - 2:59 — Showing the
RightJoinquery in code - 3:27 — Showing the corresponding
LeftJoinquery in code - 5:43 — Inspecting the executed
RightJoinresult and the rows preserved from the right side
Summary
EF Core can combine related or unrelated query sources by matching compatible key values.
- Use
Joinwhen you only want rows with matching keys. - Use
LeftJointo keep every row from the first query source. - Use
RightJointo keep every row from the second query source. - Use
GroupJoinwhen each outer element needs a collection of matching inner elements. - Use anonymous key objects when a join must match multiple properties.
Join, LeftJoin, and RightJoin compose an IQueryable and are executed when a terminal operation such as ToListAsync() materializes the results. For collection-shaped GroupJoin results, translation is more limited, so the example in this article materializes the required values before performing the grouping in memory.
Related Articles
- LINQ Methods — Learn how to use individual LINQ operators with EF Core.
- LINQ Queries — Learn how to build, compose, and execute EF Core queries.
- Include — Learn how to load related entities through navigation properties.
- Projection — Learn how to shape query results with only the data you need.
FAQ
Can I join entities that do not have a relationship in EF Core?
Yes. Join does not require a configured navigation property or EF Core relationship. The query can join two sources as long as the key selectors provide compatible values to compare.
What is the difference between Join and Include?
Join combines query sources by matching keys and lets you shape a new result from values on both sides.
Include loads related entities into navigation properties of the root entities returned by the query.
Does Join() execute the query immediately?
No. When used with EF Core IQueryable sources, Join() composes the query. A terminal operation such as ToListAsync() executes it and materializes the results.
What versions support LeftJoin and RightJoin?
The direct LeftJoin and RightJoin LINQ operators were introduced in .NET 10, and EF Core 10 can translate these operators for relational providers that support the required SQL. Earlier versions use other LINQ patterns to express outer joins.
Does GroupJoin translate to SQL?
Not in every result shape. A GroupJoin that returns each outer element together with a collection of matching inner elements does not translate directly to the server in many cases. In the example in this article, the required data is materialized first and the GroupJoin is then performed in memory.