Say I have 2 simple models (below), where 'User' contains a navigation property to 'UserDetail' (one-to-one relationship):
Database
CREATE TABLE [dbo].[UserDetail]
(
[UserId] [int] NOT NULL PRIMARY KEY,
[Name] varchar(100) NULL
)
CREATE TABLE [dbo].[User]
(
[UserId] [int] NOT NULL PRIMARY KEY
)
C#.NET
[Table("UserDetail"), Schema="dbo"]
public class UserDetail
{
[Key, Required, DatabaseGenerated(DatabaseGeneratedOption.None)]
public int UserId { get; set; }
public string Name { get; set; }
// Additional Properties Not Listed
}
[Table("User"), Schema="dbo"]
public class User
{
[Key, Required]
public int UserId { get; set; }
// Additional Properties Not Listed
[ForeignKey("UserId")]
public virtual UserDetail UserDetail { get; set; }
}
Now, the .NET application is using a repository architecture, so I'm initially setting up a query for the 'User' table (default Entity Framework behavior; lazy-loading):
IQueryable<User> users = DataRepository.UserRepository.List(); // 'List()' returns an IQueryable
My ultimate goal from here is to simply add a filter that will only pull 'User'(s) that DO NOT have an associated 'UserDetail' record (i.e. navigation property would be NULL):
users = users.Where(m => m.UserDetail == null); // Also tried (m => m.UserDetail.Equals(null))
var testing = users.ToList();
But when inspecting the 'testing' variable, query ALWAYS returns empty (in the database, there are 'User' records with and without associated 'UserDetails'). Upon some initial research, it seemed like this was potentially due to the lazy-loading. To verify, I tried the following example:
users = users.Where(m => m.UserDetail.Name == "UserName");
var testing = users.ToList();
To my surprise, this worked as originally expected; the 'testing' variable contained 'User' records, which had an associated 'UserDetail' record with 'Name' property equaling 'UserName'. It now seems like the lazy-loading isn't a issue but rather a potential problem with IQueryable comparing NULL navigation properties (during the SQL query translation?).
In a final test, decided to try using the Include method to see if that made a difference but it produced the same result as the first test (i.e. empty):
users = users.Include(m => m.UserDetail).Where(m => m.UserDetail == null);
var testing = users.ToList();
So I'm now at a loss and obviously don't understand well enough to produce the results I'm looking for. Would love any advice/help from anyone who knows where I'm getting tripped up on this problem.
My best guess is that the IQueryable query translates navigation properties as 'JOIN'(s) rather than 'LEFT JOIN'(s) and therefore, cannot be used to compare empty/null associated records (the records are simply not there).
To recap questions:
- How can I write a query in .NET/Entity Framework that will be executed on the database to only pull 'User'(s) which have no associated 'UserDetail' record?
- Why does IQueryable not pull the records I'm expecting in my first test?
- Clarify any general points I'm misstating around IQueryable functionality
Thanks in advance for any help/advice!
Ultimately, the problem came down to an incorrect relationship configuration (data annotations) for Entity Framework. Was attempting to configure a one-to-(one or none) relationship that Entity Framework was interpreting as a one-to-one (both being required). Hence, IQueryable/Include functions were performing JOIN instead of the needed LEFT JOIN.
There were two articles I referenced that lead me down this line of thinking (this and this)
The required code changes were to the C#.NET models:
And now the IQueryable query works as expected/intended: