-
Notifications
You must be signed in to change notification settings - Fork 3.2k
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
Many-to-many lazy (and explicit) loading erroneously sets inverse collection as loaded #23475
Comments
Note for triage: I am able to reproduce this; the issue is that the first Load also flags the inverse navigation as loaded. This isn't a valid thing to assume for many-to-many where one side and be fully loaded while the inverse is not. |
Underlying issue here is that the query used to do many-to-many loading uses a filtered Include: IQueryable<EntityTwo> loaded
= context.Set<EntityOne>()
.AsTracking()
.Where(e => e.Id == left.Id)
.SelectMany(e => e.TwoSkip)
.Include(e => e.OneSkip.Where(e => e.Id == left.Id)); Filtered Include, by-design, sets the navigation as loaded, but in this case we don't want that behavior. |
@smitpatel Proposal for the patch; a low-risk fix that doesn't introduce new surface. We should do something better in 6.0, but does this sound reasonable for the patch? New internal method: // A version of Include that doesn't set the navigation as loaded
internal static IIncludableQueryable<TEntity, TProperty> NotQuiteInclude<TEntity, TProperty>(
[NotNull] this IQueryable<TEntity> source,
[NotNull] Expression<Func<TEntity, TProperty>> navigationPropertyPath)
where TEntity : class
{
return new IncludableQueryable<TEntity, TProperty>(
source.Provider is EntityQueryProvider
? source.Provider.CreateQuery<TEntity>(
Expression.Call(
instance: null,
method: NotQuiteIncludeMethodInfo.MakeGenericMethod(typeof(TEntity), typeof(TProperty)),
arguments: new[] { source.Expression, Expression.Quote(navigationPropertyPath) }))
: source);
} Then flow this through translation such that we have a flag when setting IsLoaded. (Note that I don't currently know how to flow this through. :-D) For example: private static void InitializeIncludeCollection<TParent, TNavigationEntity>(
int collectionId,
QueryContext queryContext,
DbDataReader dbDataReader,
SingleQueryResultCoordinator resultCoordinator,
TParent entity,
Func<QueryContext, DbDataReader, object[]> parentIdentifier,
Func<QueryContext, DbDataReader, object[]> outerIdentifier,
INavigationBase navigation,
IClrCollectionAccessor clrCollectionAccessor,
bool trackingQuery,
bool setLoaded)
where TParent : class
where TNavigationEntity : class, TParent
{
object collection = null;
if (entity is TNavigationEntity)
{
if (setLoaded)
{
if (trackingQuery)
{
queryContext.SetNavigationIsLoaded(entity, navigation);
}
else
{
navigation.SetIsLoadedWhenNoTracking(entity);
}
}
collection = clrCollectionAccessor.GetOrCreate(entity, forMaterialization: true);
}
var parentKey = parentIdentifier(queryContext, dbDataReader);
var outerKey = outerIdentifier(queryContext, dbDataReader);
var collectionMaterializationContext = new SingleQueryCollectionContext(entity, collection, parentKey, outerKey);
resultCoordinator.SetSingleQueryCollectionContext(collectionId, collectionMaterializationContext);
} |
Flowing that flag would require some breaks in IncludeExpression. If we use another kind of expression to represent it, then it becomes higher risk in our codebase due to churn in code + anyone outside of EF Core (provider/plugins/libraries), if they are not expecting it and don't handle would break. |
@smitpatel What are the breaks required in IncludeExpression? |
A flag to flow the information to the shaper that don't mark this navigation as loaded |
…many navigation Fixes #23475 This is a targeted patch fix which special cases many-to-many loading. I will file an issue for a more general solution using an appropriate general update to query.
Using these model classes
and this program
the output is
as it should be. If the 'foreach' loop in 'Read' is uncommented (and the 'Write' is commented out), the output is
This is unexpected, the output should include 'Pascal' as well. Using expicit loading results in similar unexpected behaviour.
Version:
Entity Framework Core .NET Command-line Tools
5.0.0
The text was updated successfully, but these errors were encountered: