efcore: Result of left joining a nullable nominal type should be null (i.e. no instance) and not an instance with all null property values
when Blog does not have a Post, following query does not work in 5.0.0-preview.8.* or 6.0.0-* nightly. but works in 5.0.0-preview.7.*
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
[NotMapped]
public Post Post { get; set; }
public List<Post> Posts { get; set; } = new List<Post>();
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
// this IQueryable would come from other API.
var dbPosts = from p in db.Posts
// select p;
select new Post
{
PostId = p.PostId,
BlogId = p.BlogId,
Content = p.Content
};
var query = from blog in db.Blogs
join post in dbPosts on blog.BlogId equals post.BlogId into posts
from xpost in posts.DefaultIfEmpty()
select new Blog
{
Url = blog.Url,
Post = xpost
};
Steps to reproduce
I have a repo to reproduce the bug.
https://github.com/skclusive/EFLeftJoinBug
Unhandled exception. System.InvalidOperationException: Nullable object must have a value.
at lambda_method17(Closure , QueryContext , DbDataReader , ResultContext , SingleQueryResultCoordinator )
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.MoveNext()
Further technical details
EF Core version: Database provider: (e.g. Microsoft.EntityFrameworkCore.Sqlite) Target framework: (e.g. .NET Core 5.0) Operating system: IDE: (e.g. Visual Studio Code)
About this issue
- Original URL
- State: open
- Created 4 years ago
- Reactions: 27
- Comments: 32 (13 by maintainers)
Links to this issue
Commits related to this issue
- fix(backend): potential bug in summary If the summary does not exist, SummaryId can be null. The bug has exist since the first day, but it had not been revealed until the fix in entity framework 5. ... — committed to Liu233w/acm-statistics by Liu233w 3 years ago
- fix(backend): potential bug in summary If the summary does not exist, SummaryId can be null. The bug has exist since the first day, but it had not been revealed until the fix in entity framework 5. ... — committed to Liu233w/acm-statistics by Liu233w 3 years ago
We’ve just hit this after (attempting) to upgrade from 3.1 to 5.0. From what I can tell, this is a significant behavioural change.
Suggestion: Mention this is Breaking changes in EF Core 5.0?
Googling stack traces isn’t much fun 😃
This is definitely a breaking change
It’s too bad LINQ expressions don’t support the null conditional operator ?. Then we could indicate a possible null in the navigation to the EF query and quiet the VS Analyzers that complain about a possible null dereference (CS8602).
I can’t believe that after years, this error is still present. I have tons of LINQ queries that work correctly in .NET Framework 3.1, and I’m not sure how they might behave after an update. Has anyone found any workarounds? Is there at least one way to obtain a compile-time error instead of a runtime error?
This is very inconvenient, and has been ‘punted’ for 2 versions now…
@Mijalski This issue is in the 6.0 milestone, which means we plan to fix it for EF Core 6.0.
https://stackoverflow.com/a/65207398/1181624
You can also do a cast to a nullable type to make this issue go away. I posted that stack overflow answer before I found this post.
@skclusive - Your query was giving incorrect results in 3.1, The bug fix for #20633 stopped generating incorrect results. In your case it throws exception because the query translation couldn’t figure out a way to generate right results. Your query working in LINQ is irrelevant as it worked in 3.1 also and EF Core generated results different than LINQ.
Tested that above work-around gives correct result on 5.0 rc2 nightly build.
@hhhuut - There is no one answer to the question. Essentially, when you do a left join, right side can have null values so accessing any property over that will give you back null (EF Core will make property access null safe). But in C# there is no concept of left join or null-safe access. So in C# the property access will take CLR type of the property which can/cannot accommodate null value. e.g.
entity.Propertywhere entity is coming from right side of left join andPropertyis of type int will return int value. If entity turns out to be null then it throws null-ref exception. Since EF Core makes the access null safe it is same as writingentity == null ? (int?)null : entity.Propertywhich will make the return value of typeint?So when actual null value is encountered then it cannot be put into a int value throwing above exception. This only happens at runtime, when you actually encounter a null value.So, as a user, you should carefully evaluate queries and see if you are expecting a null value appearing in left join and the property-accesses are able to accommodate that null value by being nullable type. In such cases you are required to put nullable type cast. It is not a “work-around”, it is required to materialize the results from the data you have. Though if your query will never get null from left join (be careful as data can change over the type in an app), or you are not accessing a property on an entity which can be nullable due to left join then you shouldn’t need it. The same exception can also arise when database contains null but client side property cannot take null values (even without left-joins) due to mismatch in model. There is also a possibility that there is a bug in EF Core somewhere. So if you have a query which has result types correct based on expected data and seeing this exception please file a new issue.
We just upgraded to .Net 6 and this is an embarrassing complete disaster. There’s no easy way to determine where this bug occurs other than scouring all 300+ LINQ expressions to see if we’re using a leftjoin, and selecting on the result. This isn’t even listed as a breaking change on the update page. The error message was completely useless and resulted in around 2 hours of debugging because I couldn’t figure out what was going wrong (why would I expect a simple SELECT to be the cause of the error?). Furthermore, this behaviour is completely inconsistent even within the LINQ query itself, and doesn’t follow expected C# paradigms. Consider the following query:
This query errors because
sis null, so it can’t cast the resultingbool?into the expected not-nullbool. However, why doesn’ts.suspendedin thewhereclause cause an error? Logically, the actualnullvalue iss, but this seems to be behaving against expectations, as though we’ve just got a completely blankObjectinstead of a row, and everything inside of it is evaluating tonull, but only sometimes. Not only that, but it’s not even evaluating todefaultas it should be based on theDefaultIfEmpty()name. This makes no sense at all, and honestly we’re seriously considering completely abandoning using Entity Framework LINQ because this isn’t the first time an update has broken queries across our application with no warning or logical way to fix it. I ended up coming to the same workaround conclusions of casting to(bool?)or using== truebut again this is crazy behaviour to any user and causes confusion.I know Microsoft doesn’t take suggestions, and this is a pipe dream, but the following syntax is what I would actually expect LINQ to work like.
Same problem in EF 7
@StrangeWill - This is what will happen in EF Core 6.0
Finally, this cannot be backported to 5.0 due to complexity of the fix.
Issues faced: When generating left join we need to add additional null check in selector so that we can generate correct default type if sequence is empty and not try to materialize result.
Due to unintended side effect of breaking working queries due to having a complex expression for entity now, we should postpone this.
Confirmed this works in 3.1, but fails in latest daily, on SQL Server and SQLite.