Efcore: Included entity in let clause is not included

Created on 27 Oct 2018  路  6Comments  路  Source: dotnet/efcore

Project attached.

var query = (from w in db.WatchListItems
                        let s = db.Series.Where(x => x.Symbol == w.Symbol && x.DataProviderID == w.DataProviderID).Include(x => x.NativeFrequency).First() // NativeFrequency not included
                        where w.UserID == user.ID && w.DataProviderID == dataProvider.ID
                        select new WatchListItem
                        {
                            ID = w.ID,
                            DataProviderID = w.DataProviderID,
                            Symbol = w.Symbol,
                            UserID = w.UserID,
                            Series = s
                        }).ToList();

EFAddTest.zip

Tested using EF 2.1.4 and 2.2.0-preview3-35497
Visual Studio 15.8.8

area-query closed-fixed customer-reported type-bug

All 6 comments

When I inspect the sql for the above query I see this:

SELECT [w].[ID], [w].[DataProviderID], [w].[Symbol], [w].[UserID]
FROM [WatchListItems] AS [w]
WHERE ([w].[UserID] = 1) AND ([w].[DataProviderID] = 1)

I do not see the SQL for the Series object even though the Series object is populated correctly.
Why is that? Is this itself an issue or a byproduct of the problem that is reported?

Using IQueryableHelper found here: https://github.com/aspnet/EntityFrameworkCore/issues/6482

@sam-wheat Translation of First is currently problematic. You might try FirstOrDefault instead. /cc @maumar

Beyond that, we will revisit this after 3.0 query changes.

Problem happens because IncludeReplacingExpressionVisitor does not replace qsres inside a subquery. However the fix is more involved - when we simply apply include replacing visitor recursively, additional issues/assumptions we had in the code are exposed. e.g. this would fail:

(from w in db.WatchListItems
 let s = db.Series.Where(x => x.Symbol == w.Symbol && x.DataProviderID == w.DataProviderID).Include(x => x.NativeFrequency)//.FirstOrDefault()
 where w.UserID == user.ID && w.DataProviderID == dataProvider.ID
 select new
 {
     ID = w.ID,
     DataProviderID = w.DataProviderID,
     Symbol = w.Symbol,
     UserID = w.UserID,
     Series = s
 }).ToList();

Exception:

System.InvalidCastException: 'Unable to cast object of type 'QueryableAdapter`1[Repro13787.Series]' to type 'Microsoft.EntityFrameworkCore.Query.IIncludableQueryable`2[Repro13787.Series,Repro13787.NativeFrequency]'.'

   at lambda_method(Closure , QueryContext , ValueBuffer )
   at Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal.ProjectionShaper.TypedProjectionShaper`3.Shape(QueryContext queryContext, ValueBuffer& valueBuffer) in D:\git\EntityFrameworkCore\src\EFCore.Relational\Query\ExpressionVisitors\Internal\ProjectionShaper.cs:line 101
   at Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal.ProjectionShaper.TypedProjectionShaper`3.Microsoft.EntityFrameworkCore.Query.ExpressionVisitors.Internal.IShaper<TOut>.Shape(QueryContext queryContext, ValueBuffer& valueBuffer)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.Enumerator.BufferlessMoveNext(DbContext _, Boolean buffer) in D:\git\EntityFrameworkCore\src\EFCore.Relational\Query\Internal\QueryingEnumerable.cs:line 132
   at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded) in D:\git\EntityFrameworkCore\src\EFCore.SqlServer\Storage\Internal\SqlServerExecutionStrategy.cs:line 47
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.Enumerator.MoveNext() in D:\git\EntityFrameworkCore\src\EFCore.Relational\Query\Internal\QueryingEnumerable.cs:line 83
   at Microsoft.EntityFrameworkCore.Query.Internal.LinqOperatorProvider.ExceptionInterceptor`1.EnumeratorExceptionInterceptor.MoveNext() in D:\git\EntityFrameworkCore\src\EFCore\Query\Internal\LinqOperatorProvider.cs:line 143

@sam-wheat because the query in the end projects entity and a scalar EF produces N+1 queries (see #11186).

We issue a query for WatchListItems (the one you see), and then for each matching watch list item we issue another query to fetch the relevant series:

SELECT [x].[ID], [x].[DataProviderID], [x].[Name], [x].[NativeFrequencyID], [x].[Symbol]
FROM [Series] AS [x]
WHERE ([x].[Symbol] = @_outer_Symbol) AND ([x].[DataProviderID] = @_outer_DataProviderID)

Wrt the actual issue, you can work around it by rewriting it to group joins, like so:

(from w in db.WatchListItems
 join s in db.Series.Include(x => x.NativeFrequency) on new { w.Symbol, w.DataProviderID } equals new { s.Symbol, s.DataProviderID } into grouping
 where w.UserID == user.ID && w.DataProviderID == dataProvider.ID
 select new //WatchListItem
 {
     ID = w.ID,
     DataProviderID = w.DataProviderID,
     Symbol = w.Symbol,
     UserID = w.UserID,
     Series = grouping
 }).ToList();

if (rewritten.First().Series.First().NativeFrequency == null)
   throw new Exception("NativeFrequency is null");

The problem is that we will bring all the matching series for a given watchlistitem, not only the first one.
However everything is performed in a single query, so it should be faster for most cases. Generated query is as follows:

SELECT [w].[ID] AS [ID0], [w].[DataProviderID] AS [DataProviderID0], [w].[SeriesID], [w].[Symbol] AS [Symbol0], [w].[UserID], [s].[ID], [s].[DataProviderID], [s].[Name], [s].[NativeFrequencyID], [s].[Symbol]
FROM [WatchListItems] AS [w]
LEFT JOIN [Series] AS [s] ON (([w].[Symbol] = [s].[Symbol]) OR ([w].[Symbol] IS NULL AND [s].[Symbol] IS NULL)) AND ([w].[DataProviderID] = [s].[DataProviderID])
WHERE ([w].[UserID] = 1) AND ([w].[DataProviderID] = 1)
ORDER BY [Symbol0], [DataProviderID0])

@maumar Great explanation and revised query. Thanks very much.

This works in 3.1 correctly.
Generated SQL

exec sp_executesql N'SELECT [w].[ID], [w].[DataProviderID], [w].[Symbol], [w].[UserID], [t0].[ID], [t0].[DataProviderID], [t0].[Name], [t0].[NativeFrequencyID], [t0].[Symbol], [t0].[ID0], [t0].[DataProviderID0], [t0].[NativeFrequencyName], [t0].[ReportingFrequencyID], [t0].[c]
FROM [WatchListItems] AS [w]
LEFT JOIN (
    SELECT [t].[ID], [t].[DataProviderID], [t].[Name], [t].[NativeFrequencyID], [t].[Symbol], [t].[ID0], [t].[DataProviderID0], [t].[NativeFrequencyName], [t].[ReportingFrequencyID], [t].[c]
    FROM (
        SELECT [s].[ID], [s].[DataProviderID], [s].[Name], [s].[NativeFrequencyID], [s].[Symbol], [n].[ID] AS [ID0], [n].[DataProviderID] AS [DataProviderID0], [n].[NativeFrequencyName], [n].[ReportingFrequencyID], 1 AS [c], ROW_NUMBER() OVER(PARTITION BY [s].[Symbol], [s].[DataProviderID] ORDER BY [s].[ID], [n].[ID]) AS [row]
        FROM [Series] AS [s]
        INNER JOIN [NativeFrequencies] AS [n] ON [s].[NativeFrequencyID] = [n].[ID]
    ) AS [t]
    WHERE [t].[row] <= 1
) AS [t0] ON (([w].[Symbol] = [t0].[Symbol]) OR ([w].[Symbol] IS NULL AND [t0].[Symbol] IS NULL)) AND ([w].[DataProviderID] = [t0].[DataProviderID])
WHERE ([w].[UserID] = @__user_ID_0) AND ([w].[DataProviderID] = @__dataProvider_ID_1)',N'@__user_ID_0 int,@__dataProvider_ID_1 int',@__user_ID_0=1,@__dataProvider_ID_1=1
Was this page helpful?
0 / 5 - 0 ratings