I'm encoutering an InvalidCastException that I don't understand, related to the Entity Framework Plus library and its IncludeFilter extension method in particular.
To summarize, I have 3 entities: Project, Test and TestRun: - each Project has a collection of Tests; - each Test has a collection of TestRuns. I have a ProjectService class which implements a method to retrieve a Project from database, with options to select which includes are desired. Here is this method's code (I stripped it down to the smallest piece of code that still produces the same exception, plus another piece that works for comparison):
private IQueryable<Project> NewQuery(ProjectIncludeOptions includes = ProjectIncludeOptions.NONE)
{
IQueryable<Project> query = base.NewQuery();
/* NO PROBLEM HERE: just left it for comparison. */
if (includes.HasFlag(ProjectIncludeOptions.DOMAINS))
{
query =
query
.IncludeFilter(p => p.TestDomains.Where(td => !td.IsArchived).Select(td => td.Children.Where(tdc => !tdc.IsArchived)))
.IncludeFilter(p => p.TestDomains.Where(td => !td.IsArchived).Select(td => td.Parent));
}
/* EXCEPTION CAUSED BY THE CODE BELOW */
if (includes.HasFlag(ProjectIncludeOptions.TESTS))
{
query =
query
/* In the below code, if I remove the Where clause, or use a non-calculated property in it, then the exception disappears. */
.IncludeFilter(p => p.Tests.Where(t => !t.IsArchived).Select(t => t.TestRuns));
}
return query;
}
The implementation of the IsArchived property (in the Test class) is as follows:
[NotMapped]
public virtual bool IsArchived
{
get { return ArchivingDate.HasValue; }
set { ArchivingDate = value ? System.DateTime.Now : (System.DateTime?)null; }
}
And the place where I actually get the InvalidCastException (coming from the SingleOrDefault call):
Project project = NewQuery(includes).SingleOrDefault(p => p.Id == projectId);
The complete exception message is:
System.InvalidCastException : 'Unable to cast object of type 'System.String' to type 'System.Int32'.'
And the stack trace when the error occurs:
at System.Data.SqlClient.SqlBuffer.get_Int32() at System.Data.SqlClient.SqlDataReader.GetInt32(Int32 i) at lambda_method(Closure , DbDataReader ) at Microsoft.EntityFrameworkCore.Storage.Internal.TypedRelationalValueBufferFactory.Create(DbDataReader dataReader) at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable
1.Enumerator.BufferlessMoveNext(DbContext _, Boolean buffer) at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.Execute[TState,TResult](TState state, Func
3 operation, Func3 verifySucceeded) at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable
1.Enumerator.MoveNext() at System.Linq.Lookup2.CreateForJoin(IEnumerable
1 source, Func2 keySelector, IEqualityComparer
1 comparer) at System.Linq.Enumerable.JoinIterator[TOuter,TInner,TKey,TResult](IEnumerable1 outer, IEnumerable
1 inner, Func2 outerKeySelector, Func
2 innerKeySelector, Func3 resultSelector, IEqualityComparer
1 comparer)+MoveNext() at System.Linq.Enumerable.GroupJoinIterator[TOuter,TInner,TKey,TResult](IEnumerable1 outer, IEnumerable
1 inner, Func2 outerKeySelector, Func
2 innerKeySelector, Func3 resultSelector, IEqualityComparer
1 comparer)+MoveNext() at System.Linq.Enumerable.SelectManyIterator[TSource,TCollection,TResult](IEnumerable1 source, Func
2 collectionSelector, Func3 resultSelector)+MoveNext() at System.Linq.Enumerable.SelectEnumerableIterator
2.MoveNext() at Microsoft.EntityFrameworkCore.Query.Internal.LinqOperatorProvider._TrackEntities[TOut,TIn](IEnumerable1 results, QueryContext queryContext, IList
1 entityTrackingInfos, IList1 entityAccessors)+MoveNext() at Microsoft.EntityFrameworkCore.Query.Internal.LinqOperatorProvider.ExceptionInterceptor
1.EnumeratorExceptionInterceptor.MoveNext() at Z.EntityFramework.Plus.QueryFutureEnumerable1.SetResult(IEnumerator
1 enumerator) at Z.EntityFramework.Plus.QueryFutureEnumerable1.SetResult(DbDataReader reader) at Z.EntityFramework.Plus.QueryFutureBatch.ExecuteQueries() at Z.EntityFramework.Plus.QueryFutureValue
1.get_Value() at Z.EntityFramework.Plus.QueryIncludeFilterProvider1.Execute[TResult](Expression expression) at System.Linq.Queryable.SingleOrDefault[TSource](IQueryable
1 source, Expression`1 predicate) at Tresse.Service.Impl.ProjectService.Get(Int32 projectId, ProjectIncludeOptions includes)
The thing that baffles me most is that I don't get any exception when I use the ProjectIncludeOptions.DOMAINS part, which is apparently implemented the exact same way (the IsArchived property is also identical on TestDomain objects).
What's more, all my entities (Project, Test, TestRun and TestDomain) have DateTime properties, and they seem to play a role in this problem. Indeed, if I mark all the DateTime properties from Test and TestRun as [NotMapped]
(while keeping all the code in ProjectService as shown above), then the exception disappears! If I leave just one DateTime property mapped (no matter which one), then the exception is triggered.
However, they don't seem to cause any kind of problem with TestDomain and the code above.
Does that make any kind of sense to any of you?
I managed to work around this situation by just removing the WHERE clause inside the IncludeFilter altogether (because it is not critical for my project), but I would be happy to at least understand what is going on and have a solution for it. :)
I'm not sure if that is exactly the problem, but the goal of IncludeFilter
is to generate a query and make the filtering on the database side.
However, the IsArchived
property is not mapped. This means, that it's impossible to create a query that will be executed on the database side (could maybe be possible in EF Core 2.x due to client-side evaluation).
Make sure that the filtering part can be all done in the database.
It looks to be possible by using directly the ArchivingDate
property.