Async Query on Many-To-Many with Projection and Where error & N+1 Queries
See original GitHub issueThere are three issues with the following query:
-
Doing a CountAsync() causes the query to fail, whereas Count() works
-
Doing Count() creates N+1 queries
-
If doing Count(): Where(x => x.Title.Contains(“united”)) - has 1 result Where(x => x.Title.Contains(“United”)) - has 1 result Where(x => x.CountryStates.Contains(“Florida”)) - has 1 result Where(x => x.CountryStates.Contains(“florida”)) - has no results
Seems like a bug.
Exception message: "Expression of type 'System.Collections.Generic.IAsyncEnumerable`1[System.String]' cannot be used for parameter of type 'System.Collections.Generic.IEnumerable`1[System.String]' of method 'System.Collections.Generic.List`1[System.String] ToList[String](System.Collections.Generic.IEnumerable`1[System.String])'
Parameter name: arg0"
Stack trace: " at System.Dynamic.Utils.ExpressionUtils.ValidateOneArgument(MethodBase method, ExpressionType nodeKind, Expression arguments, ParameterInfo pi, String methodParamName, String argumentParamName, Int32 index)
at System.Linq.Expressions.Expression.Call(MethodInfo method, Expression arg0)
at System.Linq.Expressions.MethodCallExpression1.Rewrite(Expression instance, IReadOnlyList`1 args)
at System.Linq.Expressions.ExpressionVisitor.VisitMethodCall(MethodCallExpression node)
at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at Microsoft.EntityFrameworkCore.Query.EntityQueryModelVisitor.ReplaceClauseReferences(Expression expression, IQuerySource querySource, Boolean inProjection)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryModelVisitor.CompileMainFromClauseExpression(MainFromClause mainFromClause, QueryModel queryModel)
at Microsoft.EntityFrameworkCore.Query.EntityQueryModelVisitor.VisitMainFromClause(MainFromClause fromClause, QueryModel queryModel)
at Remotion.Linq.QueryModelVisitorBase.VisitQueryModel(QueryModel queryModel)
at Microsoft.EntityFrameworkCore.Query.EntityQueryModelVisitor.VisitQueryModel(QueryModel queryModel)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryModelVisitor.VisitQueryModel(QueryModel queryModel)
at Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.RelationalEntityQueryableExpressionVisitor.VisitSubQuery(SubQueryExpression expression)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at Microsoft.EntityFrameworkCore.Query.EntityQueryModelVisitor.ReplaceClauseReferences(Expression expression, IQuerySource querySource, Boolean inProjection)
at Microsoft.EntityFrameworkCore.Query.EntityQueryModelVisitor.VisitWhereClause(WhereClause whereClause, QueryModel queryModel, Int32 index)
at Remotion.Linq.QueryModelVisitorBase.VisitBodyClauses(ObservableCollection`1 bodyClauses, QueryModel queryModel)
at Remotion.Linq.QueryModelVisitorBase.VisitQueryModel(QueryModel queryModel)
at Microsoft.EntityFrameworkCore.Query.EntityQueryModelVisitor.VisitQueryModel(QueryModel queryModel)
at Microsoft.EntityFrameworkCore.Query.RelationalQueryModelVisitor.VisitQueryModel(QueryModel queryModel)
at Microsoft.EntityFrameworkCore.Query.EntityQueryModelVisitor.CreateAsyncQueryExecutor[TResult](QueryModel queryModel)
at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQueryCore[TFunc](Object cacheKey, Func`1 compiler)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.CountAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
at ConsoleApp1.Program.<MainAsync>d__1.MoveNext() in C:\\Sources\\Playground\\NetCore.M2MTest\\NetCore.M2MTest\\Program.cs:line 36
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at ConsoleApp1.Program.Main(String[] args) in C:\\Sources\\Playground\\NetCore.M2MTest\\NetCore.M2MTest\\Program.cs:line 15"
Steps to reproduce
public class Country
{
public int ID { get; set; }
public string Title { get; set; }
public ICollection<CountryStates> CountryStates { get; set; }
}
public class State
{
public int ID { get; set; }
public string Title { get; set; }
public ICollection<CountryStates> CountryStates { get; set; }
}
public class CountryStates
{
public int CountryID { get; set; }
public int StateID { get; set; }
public Country Country { get; set; }
public State State { get; set; }
}
public class CountryDTO
{
public int ID { get; set; }
public string Title { get; set; }
public List<string> CountryStates { get; set; }
}
var country1 = new Country { Title = "United States" };
var country2 = new Country { Title = "India" };
var c1State1 = new State { Title = "Florida" };
var c1State2 = new State { Title = "California" };
var c2State1 = new State { Title = "Haryana" };
var c2State2 = new State { Title = "Maharashtra" };
_context.CountryStates.Add(new CountryStates { Country = country1, State = c1State1 });
_context.CountryStates.Add(new CountryStates { Country = country1, State = c1State2 });
_context.CountryStates.Add(new CountryStates { Country = country2, State = c2State1 });
_context.CountryStates.Add(new CountryStates { Country = country2, State = c2State2 });
_context.SaveChanges();
var result = await _context.Countries.Select(x => new CountryDTO {
ID = x.ID,
Title = x.Title,
CountryStates = x.CountryStates.Select(y => y.State.Title).ToList()
}).Where(x => x.CountryStates.Contains("Florida")).CountAsync();
Further technical details
EF Core version: 2.1.0-preview2-final Database Provider: Microsoft.EntityFrameworkCore.SqlServer Operating system: Windows 10 IDE: Visual Studio 2017 15.7-Preview
Issue Analytics
- State:
- Created 5 years ago
- Reactions:3
- Comments:9 (7 by maintainers)
Top Results From Across the Web
People asking me to fix an N+1 error? [duplicate]
In ORM terminology, the 'N+1 select problem' typically occurs when you have an entity that has nested collection properties.
Read more >Hibernate Search 6.2.1.Final: Reference Documentation
Whether the field can be projected on, i.e. whether the field value is stored in the index to allow retrieval later when querying....
Read more >Efficient Querying - EF Core
Querying efficiently is a vast subject, that covers subjects as wide-ranging as indexes, related entity loading strategies, and many others.
Read more >Making queries — Django 4.2.4 documentation
A QuerySet represents a collection of objects from your database. It can have zero, one or many filters. Filters narrow down the query...
Read more >Async Queries
Poll to check the status of the query, which can have the values of QUEUED , RUNNING , COMPLETED , ERROR , or...
Read more >
Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free
Top Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
SQL generated in 1 & 2
For 3 since Title is matched exactly (we are doing contains over a list of string values), United gives 0 results. Florida gives 1 result. Casing does not matter for default scenario as database are case insensitive.
This is the query model
CountAsync
should not throw exception. It should give correct result even with client eval version. (Hence this is bug)`Doing
Count()
causes N+1 queries because you are doingContains
on a list. There is no way to represent theToList()
call on server. Hence we have to callToList
client side. That means, we have to form the list for each Country and evaluate where on it. Hence N + 1 queries executed.When you do
Contains
on the list, we have to do client eval. So the Contains become case sensitive. When you do it onTitle
it is a string property so we can translate it to server. SqlServer is by default do case insensitive string comparison that is the reason for mis-match in result.Work-around: If you use
IEnumerable
instead ofList
(essentially removeToList()
call and modify your DTO object then, we actually can translate theCountryStates.Contains
to server using... IN ( subquery )
. It also works with Async. If you don’t want to modify the DTO then write theWhere
clause beforeSelect
like this.Generates SQL
Also if you are doing just
Count
thenSelect
is unnecessary.