C# LINQ Left Outer Join Without Join-Equals-Into
Learn how to perform LINQ left outer joins without using join-on-equals-into clauses. Discover where clause alternatives and DefaultIfEmpty() patterns in C#.
How to perform a left outer join in C# LINQ to objects without using join-on-equals-into clauses? Can this be achieved using a where clause instead? I have a working inner join example but need help with the left outer join implementation.
Performing a left outer join in C# LINQ without using join-on-equals-into clauses can be achieved through alternative patterns, but not with a simple where clause alone. While a where clause filters existing results, it cannot create the default rows needed for left outer join functionality. The primary alternative involves using a combination of from clauses, filtering with where, and DefaultIfEmpty() to achieve the same result as a traditional left outer join.
Contents
- Understanding LINQ Left Outer Join Basics
- Why Where Alone Can’t Create Left Outer Join
- Alternative Approach: From + Where + DefaultIfEmpty
- Practical Implementation Examples
- Performance Considerations
- .NET 10+ LeftJoin Method
- Conclusion
Understanding LINQ Left Outer Join Basics
A left outer join in LINQ is a fundamental operation that returns all records from the left table (first sequence), and the matched records from the right table (second sequence). When there’s no match in the right table, the result still includes the left record with null or default values for the right-side properties.
The standard LINQ approach for left outer joins uses the join ... into ... pattern followed by DefaultIfEmpty():
var leftOuterJoin = from left in leftSequence
join right in rightSequence on left.Key equals right.Key into joinedGroup
from result in joinedGroup.DefaultIfEmpty()
select new { Left = left, Right = result };
This pattern works by first grouping matches together, then flattening those groups with DefaultIfEmpty() to ensure every element from the left sequence appears in the result.
Why Where Alone Can’t Create Left Outer Join
A common misconception is that a simple where clause can replace the join-on-equals-into pattern for left outer joins. However, this approach fundamentally misunderstands how LINQ queries work.
A where clause in LINQ operates as a filter that only affects records already present in the result set. It cannot create new records or rows - it can only exclude existing ones based on conditions. For example:
// This is NOT a left outer join - it's a filtered inner join
var incorrectLeftOuter = from left in leftSequence
from right in rightSequence
where left.Key == right.Key
select new { Left = left, Right = right };
The problem here is that this query only produces results where both leftSequence and rightSequence have matching elements. If an element exists in leftSequence but has no match in rightSequence, it never appears in the result at all.
As the official Microsoft documentation explains, “A where clause filters the result set after the join has been performed. It cannot bring in rows that have no match in the right sequence, which is the essence of a left-outer join.”
To understand this better, consider what happens when you have customers and orders, but some customers have no orders:
var customers = new List<Customer> {
new Customer { Id = 1, Name = "Alice" },
new Customer { Id = 2, Name = "Bob" },
new Customer { Id = 3, Name = "Charlie" }
};
var orders = new List<Order> {
new Order { Id = 100, CustomerId = 1, Amount = 50 },
new Order { Id = 101, CustomerId = 1, Amount = 75 }
};
// This will only return Alice's orders - Bob and Charlie won't appear
var customerOrders = from c in customers
from o in orders
where c.Id == o.CustomerId
select new { c.Name, o.Amount };
In this example, Bob and Charlie would be completely missing from the results because the where clause only processes matches that already exist. This is why a simple where approach cannot create a true left outer join.
Alternative Approach: From + Where + DefaultIfEmpty
While a simple where clause can’t create a left outer join, there is an alternative pattern that achieves the same result without using the traditional join ... into ... syntax. This approach uses multiple from clauses combined with DefaultIfEmpty():
var leftOuterJoin = from left in leftSequence
from right in rightSequence.Where(r => left.Key == r.Key).DefaultIfEmpty()
select new { Left = left, Right = right };
How this works might not be immediately obvious, so let’s break it down:
- The first
fromclause gets each element from the left sequence - The second
fromclause works differently - it applies aWherefilter to the right sequence for each left element DefaultIfEmpty()ensures that if no matches are found in the filtered right sequence, a default value (null for reference types) is provided instead- This effectively creates the same result as a traditional left outer join
Let’s revisit our customer and orders example using this approach:
var customerOrders = from c in customers
from o in orders.Where(o => o.CustomerId == c.Id).DefaultIfEmpty()
select new {
CustomerName = c.Name,
OrderAmount = o == null ? (decimal?)null : o.Amount
};
Now the results will include all customers:
- Alice with her two orders
- Bob with null for the order amount (no orders)
- Charlie with null for the order amount (no orders)
This pattern is particularly useful when you want to keep your LINQ query focused on “what” you’re fetching rather than “how” you’re joining the data. It can make the query more readable, especially for developers who find the join ... into ... pattern confusing.
The key insight is that we’re not using a where clause to filter the final result - we’re using Where() as part of the data retrieval process, combined with DefaultIfEmpty() to ensure we get a result for every left element, even when there’s no match.
Practical Implementation Examples
Let’s explore concrete examples of implementing left outer joins in LINQ to objects without using the traditional join syntax. We’ll start with a simple example and then move to more complex scenarios.
Example 1: Basic Left Outer Join with Objects
Consider we have two classes - Employee and Department:
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public int DepartmentId { get; set; }
}
public class Department
{
public int Id { get; set; }
public string Name { get; set; }
public string Location { get; set; }
}
Here’s how to perform a left outer join to get all employees with their department information:
var employees = new List<Employee>
{
new Employee { Id = 1, Name = "Alice", DepartmentId = 101 },
new Employee { Id = 2, Name = "Bob", DepartmentId = 102 },
new Employee { Id = 3, Name = "Charlie", DepartmentId = 999 } // Non-existent department
};
var departments = new List<Department>
{
new Department { Id = 101, Name = "Engineering", Location = "Building A" },
new Department { Id = 102, Name = "Marketing", Location = "Building B" }
};
// Left outer join using from + Where + DefaultIfEmpty
var employeeDepartments = from emp in employees
from dept in departments.Where(d => d.Id == emp.DepartmentId).DefaultIfEmpty()
select new
{
EmployeeName = emp.Name,
DepartmentName = dept?.Name ?? "Unassigned",
DepartmentLocation = dept?.Location ?? "N/A"
};
This will return:
- Alice with Engineering and Building A
- Bob with Marketing and Building B
- Charlie with “Unassigned” and “N/A” (since department 999 doesn’t exist)
Example 2: Multiple Properties and Complex Types
Let’s consider a more complex example with nested properties:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime LastPurchaseDate { get; set; }
}
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public DateTime OrderDate { get; set; }
public decimal Amount { get; set; }
public List<OrderItem> Items { get; set; }
}
public class OrderItem
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}
var customers = new List<Customer>
{
new Customer { Id = 1, Name = "Alice", LastPurchaseDate = new DateTime(2023, 1, 15) },
new Customer { Id = 2, Name = "Bob", LastPurchaseDate = new DateTime(2023, 2, 20) },
new Customer { Id = 3, Name = "Charlie", LastPurchaseDate = new DateTime(2023, 3, 10) }
};
var orders = new List<Order>
{
new Order
{
Id = 100,
CustomerId = 1,
OrderDate = new DateTime(2023, 1, 20),
Amount = 150,
Items = new List<OrderItem>
{
new OrderItem { ProductId = 1, ProductName = "Laptop", Quantity = 1, UnitPrice = 150 }
}
},
new Order
{
Id = 101,
CustomerId = 1,
OrderDate = new DateTime(2023, 1, 25),
Amount = 75,
Items = new List<OrderItem>
{
new OrderItem { ProductId = 2, ProductName = "Mouse", Quantity = 2, UnitPrice = 37.5m }
}
}
};
var customerOrderSummary = from cust in customers
from order in orders.Where(o => o.CustomerId == cust.Id).DefaultIfEmpty()
select new
{
CustomerName = cust.Name,
LastPurchaseDate = cust.LastPurchaseDate,
OrderCount = order == null ? 0 : 1,
TotalAmount = order == null ? 0 : order.Amount,
ItemsCount = order == null ? 0 : order.Items.Sum(i => i.Quantity),
HasOrders = order != null
};
This query returns customer information along with their order summary, including customers who have no orders.
Example 3: Using Method Syntax
If you prefer method syntax over query syntax, the same pattern applies:
var result = employees.SelectMany(
emp => departments
.Where(dept => dept.Id == emp.DepartmentId)
.DefaultIfEmpty(),
(emp, dept) => new
{
EmployeeName = emp.Name,
DepartmentName = dept?.Name ?? "Unassigned",
DepartmentLocation = dept?.Location ?? "N/A"
});
The SelectMany method is perfect for this scenario as it essentially does the same thing as the multiple from clauses in query syntax - it flattens the result of one-to-many relationships.
Example 4: Filtering After the Join
One advantage of this approach is that it’s easy to add additional filtering after the join:
var activeEmployeesWithDepartments = from emp in employees
where emp.IsActive // Filter employees first
from dept in departments.Where(d => d.Id == emp.DepartmentId).DefaultIfEmpty()
where dept == null || dept.IsActive // Filter departments after join
select new
{
EmployeeName = emp.Name,
DepartmentName = dept?.Name ?? "Unassigned"
};
This shows how you can combine filtering at different stages of the query while still maintaining the left outer join behavior.
Example 5: Handling Complex Key Matching
Sometimes your join keys might not be simple properties. Here’s how to handle more complex scenarios:
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
}
public class MedicalRecord
{
public int PatientId { get; set; }
public string RecordType { get; set; }
public DateTime VisitDate { get; set; }
public string Notes { get; set; }
}
var people = new List<Person>
{
new Person { Id = 1, FirstName = "Alice", LastName = "Smith", BirthDate = new DateTime(1980, 5, 15) },
new Person { Id = 2, FirstName = "Bob", LastName = "Johnson", BirthDate = new DateTime(1975, 8, 22) },
new Person { Id = 3, FirstName = "Charlie", LastName = "Brown", BirthDate = new DateTime(1990, 12, 3) }
};
var medicalRecords = new List<MedicalRecord>
{
new MedicalRecord { PatientId = 1, RecordType = "Annual", VisitDate = new DateTime(2023, 5, 16), Notes = "Annual checkup" },
new MedicalRecord { PatientId = 2, RecordType = "Emergency", VisitDate = new DateTime(2023, 8, 23), Notes = "Emergency visit" }
};
// Complex join where we match on multiple properties
var medicalHistory = from person in people
from record in medicalRecords
.Where(r => r.PatientId == person.Id &&
(r.RecordType == "Annual" ||
(r.RecordType == "Emergency" &&
(DateTime.Now - person.BirthDate).TotalDays < 365 * 30))) // Complex condition
.DefaultIfEmpty()
select new
{
FullName = $"{person.FirstName} {person.LastName}",
Age = (DateTime.Now - person.BirthDate).Days / 365,
LastVisit = record?.VisitDate,
VisitType = record?.RecordType,
Notes = record?.Notes ?? "No recent medical records"
};
This example shows how you can incorporate complex conditions while still maintaining the left outer join pattern.
Performance Considerations
When working with LINQ to objects, it’s important to understand the performance implications of different join approaches, especially when dealing with large datasets. Let’s compare the performance characteristics of the traditional join-on-equals-into approach versus the from + Where + DefaultIfEmpty pattern.
Performance Comparison
Both approaches ultimately produce the same results, but they may have different performance characteristics depending on your specific scenario:
- Traditional Join Approach:
var traditional = from left in leftSequence
join right in rightSequence on left.Key equals right.Key into joinedGroup
from result in joinedGroup.DefaultIfEmpty()
select new { Left = left, Right = result };
- From + Where + DefaultIfEmpty Approach:
var alternative = from left in leftSequence
from right in rightSequence.Where(r => left.Key == r.Key).DefaultIfEmpty()
select new { Left = left, Right = right };
In most cases, these approaches have similar performance characteristics because LINQ to objects translates both into similar underlying operations. However, there are some differences to consider:
Key Lookup Performance
The traditional join approach typically uses hash-based lookups when possible, which can be very efficient for large datasets. The alternative approach uses filtering, which might be less efficient if the right sequence is large and doesn’t have an optimal index.
// More efficient for large rightSequence due to potential hash lookup
var efficient = from left in leftSequence
join right in rightSequence on left.Key equals right.Key into joinedGroup
from result in joinedGroup.DefaultIfEmpty()
select new { Left = left, Right = result };
// Potentially less efficient for large rightSequence
var lessEfficient = from left in leftSequence
from right in rightSequence.Where(r => left.Key == r.Key).DefaultIfEmpty()
select new { Left = left, Right = right };
Compilation Overhead
The alternative approach (from + Where + DefaultIfEmpty) can sometimes have slightly higher compilation overhead because it needs to create a new filtered sequence for each element in the left sequence. This difference is usually negligible for small to medium datasets but might become noticeable with very large datasets.
Memory Usage
Both approaches need to maintain references to elements from both sequences, but the alternative approach might create additional intermediate collections during the filtering process, potentially increasing memory usage.
Optimization Strategies
Here are some strategies to optimize performance when using the from + Where + DefaultIfEmpty approach:
- Pre-filter the right sequence if possible:
// More efficient if rightSequence can be pre-filtered
var optimized = from left in leftSequence
from right in rightSequence
.Where(r => r.Key == left.Key) // Filter first
.DefaultIfEmpty()
select new { Left = left, Right = right };
- Use appropriate data structures:
// Convert to dictionary for O(1) lookups
var rightDict = rightSequence.ToDictionary(r => r.Key);
var optimized = from left in leftSequence
select new
{
Left = left,
Right = rightDict.TryGetValue(left.Key, out var right) ? right : default
};
- Consider parallel processing for very large datasets:
// Parallel processing can improve performance for large datasets
var parallel = leftSequence.AsParallel()
.SelectMany(left => rightSequence
.Where(r => r.Key == left.Key)
.DefaultIfEmpty()
.Select(right => new { Left = left, Right = right }));
- Use compiled queries for frequently executed operations:
// Compiled query for better performance
private static Func<IEnumerable<Left>, IEnumerable<Right>,
IEnumerable<AnonymousType>> GetLeftOuterJoin =
CompiledQuery.Compile((IEnumerable<Left> lefts, IEnumerable<Right> rights) =>
from left in lefts
from right in rights.Where(r => left.Key == r.Key).DefaultIfEmpty()
select new { Left = left, Right = right });
When to Use Which Approach
- Use traditional join when:
- You’re working with very large datasets
- Performance is critical
- You prefer the more explicit join syntax
- Your join keys are simple property matches
- Use from + Where + DefaultIfEmpty when:
- Readability is more important than micro-optimizations
- You have complex join conditions that are easier to express in a Where clause
- You’re working with smaller datasets where performance differences are negligible
- You want to keep the focus on “what” you’re fetching rather than “how” you’re joining
Real-world Performance Example
Let’s look at a concrete performance comparison with actual timing measurements:
// Setup test data
var leftSequence = Enumerable.Range(1, 10000).Select(i => new Left { Key = i, Value = $"Left{i}" }).ToList();
var rightSequence = Enumerable.Range(1, 5000).Select(i => new Right { Key = i, Value = $"Right{i}" }).ToList();
// Traditional join approach
var sw = Stopwatch.StartNew();
var traditional = from left in leftSequence
join right in rightSequence on left.Key equals right.Key into joinedGroup
from result in joinedGroup.DefaultIfEmpty()
select new { Left = left, Right = result };
var traditionalResult = traditional.ToList();
sw.Stop();
Console.WriteLine($"Traditional join: {sw.ElapsedMilliseconds}ms");
// Alternative approach
sw.Restart();
var alternative = from left in leftSequence
from right in rightSequence.Where(r => left.Key == r.Key).DefaultIfEmpty()
select new { Left = left, Right = right };
var alternativeResult = alternative.ToList();
sw.Stop();
Console.WriteLine($"Alternative approach: {sw.ElapsedMilliseconds}ms");
// Verify results are the same
Console.WriteLine($"Results match: {traditionalResult.SequenceEqual(alternativeResult)}");
In most test scenarios with this kind of data, both approaches will produce similar results with comparable execution times. The differences become more apparent with larger datasets or more complex join conditions.
Memory Profiling
For memory-sensitive applications, it’s worth profiling both approaches to understand their memory usage:
// Memory profiling example
var initialMemory = GC.GetTotalMemory(true);
// Traditional approach
var traditional = from left in leftSequence
join right in rightSequence on left.Key equals right.Key into joinedGroup
from result in joinedGroup.DefaultIfEmpty()
select new { Left = left, Right = result };
var traditionalResult = traditional.ToList();
var traditionalMemory = GC.GetTotalMemory(true) - initialMemory;
Console.WriteLine($"Traditional approach memory: {traditionalMemory} bytes");
// Alternative approach
initialMemory = GC.GetTotalMemory(true);
var alternative = from left in leftSequence
from right in rightSequence.Where(r => left.Key == r.Key).DefaultIfEmpty()
select new { Left = left, Right = right };
var alternativeResult = alternative.ToList();
var alternativeMemory = GC.GetTotalMemory(true) - initialMemory;
Console.WriteLine($"Alternative approach memory: {alternativeMemory} bytes");
Again, the differences are usually minimal unless you’re working with extremely large datasets or memory-constrained environments.
Conclusion on Performance
For most practical purposes, the choice between these two approaches should be based on readability and maintainability rather than performance micro-optimizations. The performance differences are typically negligible for small to medium datasets, and both approaches will produce identical results. The traditional join approach might have a slight edge in performance-critical scenarios with very large datasets, but the from + Where + DefaultIfEmpty approach often provides clearer, more maintainable code.
.NET 10+ LeftJoin Method
Looking ahead to future versions of .NET, there’s exciting news for LINQ enthusiasts: .NET 10 is expected to introduce a new LeftJoin method that will simplify left outer join operations. While this method isn’t available in current stable versions of .NET, it’s worth being aware of as it represents the evolution of LINQ toward more intuitive syntax.
What is the LeftJoin Method?
The proposed LeftJoin method is designed to be a more intuitive and readable way to perform left outer joins in LINQ. Instead of the current patterns that require understanding of join ... into ... or from ... Where ... DefaultIfEmpty(), the new method would provide a more direct approach.
Here’s how it might work:
// Proposed .NET 10+ syntax
var leftOuterJoin = leftSequence.LeftJoin(
rightSequence,
left => left.Key,
right => right.Key,
(left, right) => new { Left = left, Right = right }
);
This approach is much more explicit about what’s happening - it’s clearly a left join operation with key selectors and a result selector, making the intent of the code immediately obvious.
Benefits of the LeftJoin Method
- Readability: The syntax clearly indicates that this is a left join operation, eliminating the need to understand the nuances of
join ... into ...orfrom ... Where ... DefaultIfEmpty(). - Consistency: It follows the same pattern as other LINQ join methods, making it more consistent with the overall LINQ API design.
- Error Reduction: By providing a dedicated method for left joins, it reduces the chance of developers accidentally creating inner joins or misunderstanding how left joins work.
- Performance Potential: The dedicated method could potentially be optimized more effectively than the current patterns.
Comparison with Current Approaches
Let’s compare the proposed LeftJoin method with the current approaches:
// Current traditional approach
var traditional = from left in leftSequence
join right in rightSequence on left.Key equals right.Key into joinedGroup
from result in joinedGroup.DefaultIfEmpty()
select new { Left = left, Right = result };
// Current alternative approach
var alternative = from left in leftSequence
from right in rightSequence.Where(r => left.Key == r.Key).DefaultIfEmpty()
select new { Left = left, Right = right };
// Proposed .NET 10+ approach
var proposed = leftSequence.LeftJoin(
rightSequence,
left => left.Key,
right => right.Key,
(left, right) => new { Left = left, Right = right }
);
The proposed approach is the most readable and clearly expresses the intent of performing a left outer join. It eliminates the need for understanding the more complex LINQ patterns and makes the code more maintainable.
Method Syntax Equivalent
For those who prefer method syntax over query syntax, the LeftJoin method would provide a clean alternative:
// Method syntax equivalent
var leftOuterJoin = leftSequence.LeftJoin(
rightSequence,
left => left.Key,
right => right.Key,
(left, right) => new { Left = left, Right = right }
);
This is much cleaner than the current method syntax alternatives:
// Current method syntax using SelectMany
var currentMethod = leftSequence.SelectMany(
left => rightSequence.Where(right => left.Key == right.Key).DefaultIfEmpty(),
(left, right) => new { Left = left, Right = right }
);
Potential Implementation
While the exact implementation details aren’t public yet, we can speculate about how the LeftJoin method might be implemented:
public static IEnumerable<TResult> LeftJoin<TLeft, TRight, TKey, TResult>(
this IEnumerable<TLeft> left,
IEnumerable<TRight> right,
Func<TLeft, TKey> leftKeySelector,
Func<TRight, TKey> rightKeySelector,
Func<TLeft, TRight, TResult> resultSelector)
{
var rightLookup = right.ToLookup(rightKeySelector);
foreach (var leftItem in left)
{
var key = leftKeySelector(leftItem);
var rightItems = rightLookup[key];
if (rightItems.Any())
{
foreach (var rightItem in rightItems)
{
yield return resultSelector(leftItem, rightItem);
}
}
else
{
yield return resultSelector(leftItem, default(TRight));
}
}
}
This implementation uses a Lookup for efficient lookups and handles the case where no matching right items exist by using default(TRight).
Compatibility Considerations
When .NET 10+ is released with the LeftJoin method, developers will have three main approaches to choose from:
- Traditional join-on-equals-into: Still fully supported and appropriate for performance-critical scenarios
- From + Where + DefaultIfEmpty: Good for maintainability and complex conditions
- New LeftJoin method: Most readable and intuitive for simple left join scenarios
The choice between these approaches will likely depend on factors like:
- Target .NET version
- Team familiarity with different LINQ patterns
- Performance requirements
- Code readability preferences
Future-Proofing Your Code
If you’re planning long-term projects, it might be worth considering how to structure your LINQ queries to make future migration to the LeftJoin method easier:
// Structure your queries to be easily replaceable
var leftOuterJoin = leftSequence.LeftJoin(
rightSequence,
left => left.Key,
right => right.Key,
(left, right) => new { Left = left, Right = right }
);
// Current alternative that's easy to replace
var currentImplementation = from left in leftSequence
from right in rightSequence.Where(r => left.Key == r.Key).DefaultIfEmpty()
select new { Left = left, Right = right };
By keeping your queries in a consistent pattern, you’ll be able to easily upgrade to the new syntax when it becomes available.
Conclusion on LeftJoin
The upcoming LeftJoin method in .NET 10+ represents an exciting evolution in LINQ syntax, making left outer joins more intuitive and accessible to developers. While it’s not available yet, being aware of this upcoming feature can help you plan your LINQ strategies for future projects. For now, the from + Where + DefaultIfEmpty approach remains an excellent alternative to the traditional join syntax, offering good readability without sacrificing functionality.
Conclusion
Performing a left outer join in C# LINQ to objects without using join-on-equals-into clauses is indeed possible through the from + Where + DefaultIfEmpty pattern. While a simple where clause alone cannot create a left outer join (as it only filters existing results and cannot create rows for left elements that have no matching right element), combining from clauses with filtering and DefaultIfEmpty() provides an elegant alternative.
The primary approach involves using multiple from clauses where the second from applies a Where filter to the right sequence for each left element, followed by DefaultIfEmpty() to ensure every left element appears in the result. This pattern achieves the same functionality as the traditional join-on-equals-into approach but can be more readable and easier to understand, especially for developers who find the traditional syntax confusing.
For performance-critical applications with very large datasets, the traditional join approach might have a slight advantage due to potential hash-based optimizations. However, for most scenarios, the from + Where + DefaultIfEmpty approach provides excellent performance while offering better readability and maintainability.
Looking ahead, .NET 10+ is expected to introduce a dedicated LeftJoin method that will further simplify left outer join operations, providing a more intuitive syntax that clearly expresses the intent of the operation. Until then, the from + Where + DefaultIfEmpty pattern remains a powerful and flexible alternative to traditional join syntax for performing left outer joins in LINQ to objects.
Sources
- Join Operations - C# — Official Microsoft documentation explaining why where clauses can’t create left outer joins: https://learn.microsoft.com/en-us/dotnet/csharp/linq/perform-left-outer-joins
- LINQ Left Outer Join in C# With Examples — Comprehensive tutorial showing where clause limitations and alternative approaches: https://dotnettutorials.net/lesson/left-outer-join-in-linq/
- Left Outer Joins in LINQ with Entity Framework — Alternative approach using from + Where + DefaultIfEmpty pattern: https://www.thinqlinq.com/post.aspx/title/left-outer-joins-in-linq-with-entity-framework