Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using .Count on a ICollection<T> does not produce EXISTS as it does when using List<T> #35365

Closed
alaatm opened this issue Dec 20, 2024 · 6 comments · Fixed by #35381
Closed

Comments

@alaatm
Copy link
Contributor

alaatm commented Dec 20, 2024

In the whats new in .net 9, it says that queries using .Count are now optimized to use EXISTS instead of COUNT. However, this seems to be true for collections of type List<T>. We use ICollection<T> in our projects and we would love to replace .Any() with .Count > 0 but this optimization isn't applied as shown below

public class Blog
{
    public int Id { get; set; }
    public ICollection<Post> Posts { get; set; } = default!;
}

public class Post
{
    public int Id { get; set; }
}

context.Blogs.Where(b => b.Posts.Count > 0);

produces:

SELECT [b].[Id]
FROM [Blogs] AS [b]
WHERE (
    SELECT COUNT(*)
    FROM [Post] AS [p]
    WHERE [b].[Id] = [p].[BlogId]) > 0

Using context.Blogs.Where(b => b.Posts.Count != 0); will make it even worse:

SELECT [b].[Id]
FROM [Blogs] AS [b]
WHERE (
    SELECT COUNT(*)
    FROM [Post] AS [p]
    WHERE [b].[Id] = [p].[BlogId]) <> 0 OR (
    SELECT COUNT(*)
    FROM [Post] AS [p]
    WHERE [b].[Id] = [p].[BlogId]) IS NULL

EF Core version: 9.0.0
Database provider: Microsoft.EntityFrameworkCore.SqlServer

@roji
Copy link
Member

roji commented Dec 21, 2024

@cincuranet interested in giving this a look?

@ChrisJollyAU
Copy link
Contributor

@roji See QueryableMethodNormalizingExpressionVisitor line 73/74

when (member.DeclaringType.GetGenericTypeDefinition().GetInterfaces().Any(

GetInterfaces only gets those inherited/implemented. Normally fine but if the type is also an interface that is NOT returned as part of the list. So in this case member.DeclaringType is an ICollection, however GetInterfaces only returns IEnumerable and its generic version.

Changing that section to the following should do the trick

when (member.DeclaringType.GetGenericTypeDefinition() == typeof(ICollection<>) || member.DeclaringType.GetGenericTypeDefinition().GetInterfaces().Any(
    x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(ICollection<>)))

@roji
Copy link
Member

roji commented Dec 22, 2024

@ChrisJollyAU thanks for looking into it... Are you interested in submitting a PR?

@ChrisJollyAU
Copy link
Contributor

Sure. I haven't got a test sorted yet so will do that sometime

@ChrisJollyAU
Copy link
Contributor

@roji PR done. There were a bunch of other tests that were affected so those are changed.

Do you still want a basic test for just this issue?

@roji
Copy link
Member

roji commented Dec 23, 2024

@ChrisJollyAU if we have other changes that already ensure this won't regress, then no, I think it's fine to submit without an additional test.

@roji roji added this to the 10.0.0 milestone Dec 24, 2024
@roji roji added the type-bug label Dec 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants