Efcore: Query: Include for derived types fails when source does not contain any of the derived type

Created on 11 Oct 2019  路  5Comments  路  Source: dotnet/efcore

Consider a model with a base type and two different derived types. "Include for derived types" works well when the source of the query contains only the first derived type or a mix of both derived types. It fails when the source only does not contain any of the first derived type. Please look at the code example below:

``` C#
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;

namespace DerivedIncludeSelectiveTest
{
class Program
{
static void Main(string[] args)
{
using (var myContext = new MyContext())
{
myContext.Database.EnsureDeleted();
myContext.Database.EnsureCreated();
GetSchoolsWithInclude(myContext.Schools);
GetSchoolsWithInclude(myContext.PrimarySchools);
GetSchoolsWithInclude(myContext.Colleges); // this line throws an exception despite the "include on derived types" being supposed to apply only to the specified derived type
}
Console.ReadLine();
}

    static School[] GetSchoolsWithInclude(IQueryable<School> source) =>
                source
                .Include(s => ((PrimarySchool)s).Students) // loading students only for PrimarySchools
                .ToArray();
}

public class MyContext : DbContext
{
    public DbSet<PrimaryStudent> PrimaryStudents { get; set; }
    public DbSet<School> Schools { get; set; }
    public DbSet<PrimarySchool> PrimarySchools { get; set; }
    public DbSet<College> Colleges { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(@"Data Source=" + System.Environment.MachineName +
            ";Initial Catalog=derivedIncludeSelectiveTest;Integrated Security=True");
        optionsBuilder.EnableSensitiveDataLogging();
        optionsBuilder.ConfigureWarnings(w => w.Log(
            Microsoft.EntityFrameworkCore.Diagnostics.CoreEventId.SensitiveDataLoggingEnabledWarning));
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<PrimarySchool>().HasMany(s => s.Students).WithOne(s => s.School);
        modelBuilder.Entity<PrimarySchool>().HasData(new PrimarySchool { Id = 1 });
        modelBuilder.Entity<College>().HasData(new College { Id = 2 });
    }
}

public class PrimaryStudent
{
    public int Id { get; set; }
    public PrimarySchool School { get; set; }
}

public abstract class School // base type
{
    public int Id { get; set; }
}

public class PrimarySchool : School // first derived type
{
    public List<PrimaryStudent> Students { get; set; }
}

public class College : School // second derived type
{
    // does not have an association to students
}

}

In EF 3.0, it throws:

System.InvalidOperationException
HResult=0x80131509
Message=Invalid include.
Source=Microsoft.EntityFrameworkCore
StackTrace:
at Microsoft.EntityFrameworkCore.Query.Internal.NavigationExpandingExpressionVisitor.PopulateIncludeTree(IncludeTreeNode includeTreeNode, Expression expression)
at Microsoft.EntityFrameworkCore.Query.Internal.NavigationExpandingExpressionVisitor.ProcessInclude(NavigationExpansionExpression source, Expression expression, Boolean thenInclude)
at Microsoft.EntityFrameworkCore.Query.Internal.NavigationExpandingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
at Microsoft.EntityFrameworkCore.Query.Internal.NavigationExpandingExpressionVisitor.ExpandAndReduce(Expression query, Boolean applyInclude)
at Microsoft.EntityFrameworkCore.Query.Internal.NavigationExpandingExpressionVisitor.Expand(Expression query)
at Microsoft.EntityFrameworkCore.Query.QueryTranslationPreprocessor.Process(Expression query)
at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutorTResult
at Microsoft.EntityFrameworkCore.Storage.Database.CompileQueryTResult
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCoreTResult
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass9_01.<Execute>b__0() at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQueryCore[TFunc](Object cacheKey, Func1 compiler)
at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func1 compiler) at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query) at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression) at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable1.GetEnumerator()
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.IncludableQueryable2.GetEnumerator() at System.Collections.Generic.LargeArrayBuilder1.AddRange(IEnumerable1 items) at System.Collections.Generic.EnumerableHelpers.ToArray[T](IEnumerable1 source)
at System.Linq.Enumerable.ToArrayTSource


In EF 2.2.6, it throws:

System.InvalidOperationException
HResult=0x80131509
Message=The property 'Students' is not a navigation property of entity type 'College'. The 'Include(string)' method can only be used with a '.' separated list of navigation property names.
Source=Microsoft.EntityFrameworkCore
StackTrace:
at Microsoft.EntityFrameworkCore.Query.Internal.IncludeCompiler.WalkNavigations(IEntityType entityType, IReadOnlyList1 navigationPropertyPaths, IncludeLoadTree includeLoadTree, Boolean shouldThrow) at Microsoft.EntityFrameworkCore.Query.Internal.IncludeCompiler.TryPopulateIncludeLoadTree(IncludeResultOperator includeResultOperator, IncludeLoadTree includeLoadTree, Boolean shouldThrow) at Microsoft.EntityFrameworkCore.Query.Internal.IncludeCompiler.CreateIncludeLoadTrees(QueryModel queryModel, Boolean shouldThrow) at Microsoft.EntityFrameworkCore.Query.Internal.IncludeCompiler.CompileIncludes(QueryModel queryModel, Boolean trackingQuery, Boolean asyncQuery, Boolean shouldThrow) at Microsoft.EntityFrameworkCore.Query.EntityQueryModelVisitor.OptimizeQueryModel(QueryModel queryModel, Boolean asyncQuery) at Microsoft.EntityFrameworkCore.Query.RelationalQueryModelVisitor.OptimizeQueryModel(QueryModel queryModel, Boolean asyncQuery) at Microsoft.EntityFrameworkCore.Query.EntityQueryModelVisitor.CreateQueryExecutor[TResult](QueryModel queryModel) at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](QueryModel queryModel) at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](Expression query, IQueryModelGenerator queryModelGenerator, IDatabase database, IDiagnosticsLogger1 logger, Type contextType)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass13_01.<Execute>b__0() at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQueryCore[TFunc](Object cacheKey, Func1 compiler)
at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func1 compiler) at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query) at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression) at Remotion.Linq.QueryableBase1.GetEnumerator()
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.IncludableQueryable2.GetEnumerator() at System.Collections.Generic.LargeArrayBuilder1.AddRange(IEnumerable1 items) at System.Collections.Generic.EnumerableHelpers.ToArray[T](IEnumerable1 source)
at System.Linq.Enumerable.ToArrayTSource
```

Further technical details

EF Core version: 2.2.6 and 3.0 (different errors in each)
Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: NET Core 3.0
Operating system: Windows 10 x64 Pro 1903
IDE: Visual Studio 2019 16.3.4

closed-question customer-reported

Most helpful comment

```C#
static School[] GetSchoolsWithInclude(IQueryable source)
where T : School
{
if (typeof(T) == typeof(School)
|| typeof(T) == typeof(PrimarySchool))
{
source = source.Include(e => (e as PrimarySchool).Students);
}

        return source.ToArray();
    }

// Use as
GetSchoolsWithInclude(db.Schools);
GetSchoolsWithInclude(db.PrimarySchools);
GetSchoolsWithInclude(db.Colleges);
```

All 5 comments

@vslee Thanks for reporting this. We discussed this behavior at length and while it can be viewed from different perspectives, the general feeling on the team is that validating that the Include could be valid is more important than allowing use of all types in the hierarchy consistently. This allows an error to be reported in cases where the code might be wrong.

In other words, the method GetSchoolsWithInclude should validate that the cast in Include might be valid and should not issue the Include if it is known to not be valid.

What is the recommended way to validate the cast, something like this?
C# static School[] GetSchoolsWithInclude(IQueryable<School> source) { if (source.Any(s => s is PrimarySchool)) return source .Include(s => ((PrimarySchool)s).Students) // loading students only for PrimarySchools .ToArray(); else return source.ToArray(); }

@vslee No, that would result in sending two queries to the database.

@smitpatel @roji @bricelam Any idea how to do what was suggested in triage without using reflection on the actual type of the IQueryable?

```C#
static School[] GetSchoolsWithInclude(IQueryable source)
where T : School
{
if (typeof(T) == typeof(School)
|| typeof(T) == typeof(PrimarySchool))
{
source = source.Include(e => (e as PrimarySchool).Students);
}

        return source.ToArray();
    }

// Use as
GetSchoolsWithInclude(db.Schools);
GetSchoolsWithInclude(db.PrimarySchools);
GetSchoolsWithInclude(db.Colleges);
```

Great, thanks for the guidance

Was this page helpful?
0 / 5 - 0 ratings