Skip to content

Commit

Permalink
Unidirectional many-to-many relationships
Browse files Browse the repository at this point in the history
Fixes #3864
  • Loading branch information
ajcvickers committed Jul 13, 2022
1 parent 43e0755 commit 447322c
Show file tree
Hide file tree
Showing 66 changed files with 9,451 additions and 482 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -308,8 +308,10 @@ private static void IncludeCollection<TEntity, TIncludingEntity, TIncludedEntity
{
if (entity is TIncludingEntity includingEntity)
{
var collectionAccessor = navigation.GetCollectionAccessor()!;
collectionAccessor.GetOrCreate(includingEntity, forMaterialization: true);
if (!navigation.IsShadowProperty())
{
navigation.GetCollectionAccessor()!.GetOrCreate(includingEntity, forMaterialization: true);
}

if (setLoaded)
{
Expand Down Expand Up @@ -370,14 +372,18 @@ private static LambdaExpression GenerateFixup(
{
var entityParameter = Expression.Parameter(entityType);
var relatedEntityParameter = Expression.Parameter(relatedEntityType);
var expressions = new List<Expression>
var expressions = new List<Expression>();

if (!navigation.IsShadowProperty())
{
navigation.IsCollection
? AddToCollectionNavigation(entityParameter, relatedEntityParameter, navigation)
: AssignReferenceNavigation(entityParameter, relatedEntityParameter, navigation)
expressions.Add(
navigation.IsCollection
? AddToCollectionNavigation(entityParameter, relatedEntityParameter, navigation)
: AssignReferenceNavigation(entityParameter, relatedEntityParameter, navigation));
};

if (inverseNavigation != null)
if (inverseNavigation != null
&& !inverseNavigation.IsShadowProperty())
{
expressions.Add(
inverseNavigation.IsCollection
Expand Down
37 changes: 20 additions & 17 deletions src/EFCore.Proxies/Proxies/Internal/ProxyBindingRewriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,30 +78,33 @@ public virtual void ProcessModelFinalizing(
foreach (var navigationBase in entityType.GetDeclaredNavigations()
.Concat<IConventionNavigationBase>(entityType.GetDeclaredSkipNavigations()))
{
if (navigationBase.PropertyInfo == null)
if (!navigationBase.IsShadowProperty())
{
throw new InvalidOperationException(
ProxiesStrings.FieldProperty(navigationBase.Name, entityType.DisplayName()));
}

if (_options.UseChangeTrackingProxies
&& navigationBase.PropertyInfo.SetMethod?.IsReallyVirtual() == false)
{
throw new InvalidOperationException(
ProxiesStrings.NonVirtualProperty(navigationBase.Name, entityType.DisplayName()));
}
if (navigationBase.PropertyInfo == null)
{
throw new InvalidOperationException(
ProxiesStrings.FieldProperty(navigationBase.Name, entityType.DisplayName()));
}

if (_options.UseLazyLoadingProxies)
{
if (!navigationBase.PropertyInfo.GetMethod!.IsReallyVirtual()
&& (!(navigationBase is INavigation navigation
&& navigation.ForeignKey.IsOwnership)))
if (_options.UseChangeTrackingProxies
&& navigationBase.PropertyInfo.SetMethod?.IsReallyVirtual() == false)
{
throw new InvalidOperationException(
ProxiesStrings.NonVirtualProperty(navigationBase.Name, entityType.DisplayName()));
}

navigationBase.SetPropertyAccessMode(PropertyAccessMode.Field);
if (_options.UseLazyLoadingProxies)
{
if (!navigationBase.PropertyInfo.GetMethod!.IsReallyVirtual()
&& (!(navigationBase is INavigation navigation
&& navigation.ForeignKey.IsOwnership)))
{
throw new InvalidOperationException(
ProxiesStrings.NonVirtualProperty(navigationBase.Name, entityType.DisplayName()));
}

navigationBase.SetPropertyAccessMode(PropertyAccessMode.Field);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,9 @@ protected override Expression VisitExtension(Expression extensionExpression)
Expression.Constant(parentIdentifierLambda.Compile()),
Expression.Constant(outerIdentifierLambda.Compile()),
Expression.Constant(navigation),
Expression.Constant(navigation.GetCollectionAccessor()),
Expression.Constant(navigation.IsShadowProperty()
? null
: navigation.GetCollectionAccessor(), typeof(IClrCollectionAccessor)),
Expression.Constant(_isTracking),
#pragma warning disable EF1001 // Internal EF Core API usage.
Expression.Constant(includeExpression.SetLoaded)));
Expand Down Expand Up @@ -937,14 +939,18 @@ private static LambdaExpression GenerateFixup(
{
var entityParameter = Expression.Parameter(entityType);
var relatedEntityParameter = Expression.Parameter(relatedEntityType);
var expressions = new List<Expression>
var expressions = new List<Expression>();

if (!navigation.IsShadowProperty())
{
navigation.IsCollection
? AddToCollectionNavigation(entityParameter, relatedEntityParameter, navigation)
: AssignReferenceNavigation(entityParameter, relatedEntityParameter, navigation)
};
expressions.Add(
navigation.IsCollection
? AddToCollectionNavigation(entityParameter, relatedEntityParameter, navigation)
: AssignReferenceNavigation(entityParameter, relatedEntityParameter, navigation));
}

if (inverseNavigation != null)
if (inverseNavigation != null
&& !inverseNavigation.IsShadowProperty())
{
expressions.Add(
inverseNavigation.IsCollection
Expand Down Expand Up @@ -1201,7 +1207,7 @@ private static void InitializeIncludeCollection<TParent, TNavigationEntity>(
Func<QueryContext, DbDataReader, object[]> parentIdentifier,
Func<QueryContext, DbDataReader, object[]> outerIdentifier,
INavigationBase navigation,
IClrCollectionAccessor clrCollectionAccessor,
IClrCollectionAccessor? clrCollectionAccessor,
bool trackingQuery,
bool setLoaded)
where TParent : class
Expand All @@ -1222,7 +1228,7 @@ private static void InitializeIncludeCollection<TParent, TNavigationEntity>(
}
}

collection = clrCollectionAccessor.GetOrCreate(entity, forMaterialization: true);
collection = clrCollectionAccessor?.GetOrCreate(entity, forMaterialization: true);
}

var parentKey = parentIdentifier(queryContext, dbDataReader);
Expand Down
9 changes: 7 additions & 2 deletions src/EFCore/ChangeTracking/CollectionEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ public CollectionEntry(InternalEntityEntry internalEntry, INavigationBase naviga

private void LocalDetectChanges()
{
if (Metadata.IsShadowProperty())
{
EnsureInitialized();
}

var collection = CurrentValue;
if (collection != null)
{
Expand Down Expand Up @@ -274,7 +279,7 @@ public override IQueryable Query()
}

private void EnsureInitialized()
=> Metadata.GetCollectionAccessor()!.GetOrCreate(InternalEntry.Entity, forMaterialization: true);
=> InternalEntry.GetOrCreateCollection(Metadata, forMaterialization: true);

/// <summary>
/// The <see cref="EntityEntry" /> of an entity this navigation targets.
Expand Down Expand Up @@ -302,7 +307,7 @@ private void EnsureInitialized()
[EntityFrameworkInternal]
protected virtual InternalEntityEntry? GetInternalTargetEntry(object entity)
=> CurrentValue == null
|| !Metadata.GetCollectionAccessor()!.Contains(InternalEntry.Entity, entity)
|| !InternalEntry.CollectionContains(Metadata, entity)
? null
: InternalEntry.StateManager.GetOrCreateEntry(entity, Metadata.TargetEntityType);

Expand Down
2 changes: 1 addition & 1 deletion src/EFCore/ChangeTracking/CollectionEntry`.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public CollectionEntry(InternalEntityEntry internalEntry, INavigationBase naviga
/// </remarks>
public new virtual IEnumerable<TRelatedEntity>? CurrentValue
{
get => this.GetInfrastructure().GetCurrentValue<IEnumerable<TRelatedEntity>>(Metadata);
get => (IEnumerable<TRelatedEntity>?)this.GetInfrastructure().GetCurrentValue(Metadata);
set => base.CurrentValue = value;
}

Expand Down
42 changes: 25 additions & 17 deletions src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -918,14 +918,15 @@ private void WritePropertyValue(
/// </summary>
public object GetOrCreateCollection(INavigationBase navigationBase, bool forMaterialization)
=> navigationBase.IsShadowProperty()
? GetOrCreateCollectionTyped(navigationBase)
? GetOrCreateShadowCollection(navigationBase)
: navigationBase.GetCollectionAccessor()!.GetOrCreate(Entity, forMaterialization);

private ICollection<object> GetOrCreateCollectionTyped(INavigationBase navigation)
private object GetOrCreateShadowCollection(INavigationBase navigation)
{
if (!(_shadowValues[navigation.GetShadowIndex()] is ICollection<object> collection))
var collection = _shadowValues[navigation.GetShadowIndex()];
if (collection == null)
{
collection = new HashSet<object>();
collection = navigation.GetCollectionAccessor()!.Create();
_shadowValues[navigation.GetShadowIndex()] = collection;
}

Expand All @@ -938,10 +939,13 @@ private ICollection<object> GetOrCreateCollectionTyped(INavigationBase navigatio
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public bool CollectionContains(INavigationBase navigationBase, InternalEntityEntry value)
=> navigationBase.IsShadowProperty()
? GetOrCreateCollectionTyped(navigationBase).Contains(value.Entity)
: navigationBase.GetCollectionAccessor()!.Contains(Entity, value.Entity);
public bool CollectionContains(INavigationBase navigationBase, object value)
{
var collectionAccessor = navigationBase.GetCollectionAccessor()!;
return navigationBase.IsShadowProperty()
? collectionAccessor.ContainsStandalone(GetOrCreateShadowCollection(navigationBase), value)
: collectionAccessor.Contains(Entity, value);
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand All @@ -951,18 +955,19 @@ public bool CollectionContains(INavigationBase navigationBase, InternalEntityEnt
/// </summary>
public bool AddToCollection(
INavigationBase navigationBase,
InternalEntityEntry value,
object value,
bool forMaterialization)
{
var collectionAccessor = navigationBase.GetCollectionAccessor()!;
if (!navigationBase.IsShadowProperty())
{
return navigationBase.GetCollectionAccessor()!.Add(Entity, value.Entity, forMaterialization);
return collectionAccessor.Add(Entity, value, forMaterialization);
}

var collection = GetOrCreateCollectionTyped(navigationBase);
if (!collection.Contains(value.Entity))
var collection = GetOrCreateShadowCollection(navigationBase);
if (!collectionAccessor.ContainsStandalone(collection, value))
{
collection.Add(value.Entity);
collectionAccessor.AddStandalone(collection, value);
return true;
}

Expand All @@ -975,10 +980,13 @@ public bool AddToCollection(
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public bool RemoveFromCollection(INavigationBase navigationBase, InternalEntityEntry value)
=> navigationBase.IsShadowProperty()
? GetOrCreateCollectionTyped(navigationBase).Remove(value.Entity)
: navigationBase.GetCollectionAccessor()!.Remove(Entity, value.Entity);
public bool RemoveFromCollection(INavigationBase navigationBase, object value)
{
var collectionAccessor = navigationBase.GetCollectionAccessor()!;
return navigationBase.IsShadowProperty()
? collectionAccessor.RemoveStandalone(GetOrCreateShadowCollection(navigationBase), value)
: collectionAccessor.Remove(Entity, value);
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,10 @@ private static INotifyCollectionChanged AsINotifyCollectionChanged(
IEntityType entityType,
ChangeTrackingStrategy changeTrackingStrategy)
{
if (navigation.GetCollectionAccessor()
?.GetOrCreate(entry.Entity, forMaterialization: false) is not INotifyCollectionChanged notifyingCollection)
var collection = entry.GetOrCreateCollection(navigation, forMaterialization: false);
if (collection is not INotifyCollectionChanged notifyingCollection)
{
var collectionType = navigation.GetCollectionAccessor()
?.GetOrCreate(entry.Entity, forMaterialization: false).GetType().DisplayName(fullName: false);
var collectionType = collection.GetType().DisplayName(fullName: false);
throw new InvalidOperationException(
CoreStrings.NonNotifyingCollection(navigation.Name, entityType.DisplayName(), collectionType, changeTrackingStrategy));
}
Expand Down
6 changes: 3 additions & 3 deletions src/EFCore/ChangeTracking/Internal/NavigationFixer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1022,7 +1022,7 @@ private void DelayedFixup(
{
if (navigation.IsCollection)
{
if (entry.CollectionContains(navigation, referencedEntry))
if (entry.CollectionContains(navigation, referencedEntry.Entity))
{
FixupToDependent(entry, referencedEntry, navigation.ForeignKey, setModified, fromQuery);
}
Expand Down Expand Up @@ -1491,7 +1491,7 @@ private void AddToCollection(InternalEntityEntry entry, INavigationBase? navigat
_inFixup = true;
try
{
if (entry.AddToCollection(navigation, value, fromQuery))
if (entry.AddToCollection(navigation, value.Entity, fromQuery))
{
entry.AddToCollectionSnapshot(navigation, value.Entity);
}
Expand All @@ -1508,7 +1508,7 @@ private void RemoveFromCollection(InternalEntityEntry entry, INavigationBase nav
_inFixup = true;
try
{
if (entry.RemoveFromCollection(navigation, value))
if (entry.RemoveFromCollection(navigation, value.Entity))
{
entry.RemoveFromCollectionSnapshot(navigation, value.Entity);
}
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore/ChangeTracking/Internal/StateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -803,7 +803,7 @@ public virtual bool ResolveToExistingEntry(
var navigationValue = referencedFromEntry![navigation];
if (navigationValue != null && navigation.IsCollection)
{
navigation.GetCollectionAccessor()!.Remove(referencedFromEntry.Entity, newEntry.Entity);
referencedFromEntry.RemoveFromCollection(navigation, newEntry.Entity);
}
}

Expand Down
10 changes: 0 additions & 10 deletions src/EFCore/Infrastructure/ModelValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,16 +115,6 @@ protected virtual void ValidateRelationships(
CoreStrings.SkipNavigationNoInverse(
skipNavigation.Name, skipNavigation.DeclaringEntityType.DisplayName()));
}

if (skipNavigation.IsShadowProperty())
{
throw new InvalidOperationException(
CoreStrings.ShadowManyToManyNavigation(
skipNavigation.DeclaringEntityType.DisplayName(),
skipNavigation.Name,
skipNavigation.Inverse.DeclaringEntityType.DisplayName(),
skipNavigation.Inverse.Name));
}
}
}
}
Expand Down
14 changes: 4 additions & 10 deletions src/EFCore/Internal/ManyToManyLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,31 +91,26 @@ public virtual IQueryable<TEntity> Query(InternalEntityEntry entry)

return Query(context, keyValues);
}

private object[]? PrepareForLoad(InternalEntityEntry entry)
{
if (entry.EntityState == EntityState.Detached)
{
throw new InvalidOperationException(CoreStrings.CannotLoadDetached(_skipNavigation.Name, entry.EntityType.DisplayName()));
}

var properties = _skipNavigation.ForeignKey.PrincipalKey.Properties;
var values = new object[properties.Count];

for (var i = 0; i < values.Length; i++)
{
var value = entry[properties[i]];
if (value == null)
{
return null;
}

values[i] = value;
}

return values;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down Expand Up @@ -159,13 +154,13 @@ private static Expression<Func<TEntity, IEnumerable<TSourceEntity>>> BuildInclud
{
var whereParameter = Expression.Parameter(typeof(TSourceEntity), "e");
var entityParameter = Expression.Parameter(typeof(TEntity), "e");

return Expression.Lambda<Func<TEntity, IEnumerable<TSourceEntity>>>(
Expression.Call(
EnumerableMethods.Where.MakeGenericMethod(typeof(TSourceEntity)),
Expression.MakeMemberAccess(
Expression.Call(
EF.PropertyMethod.MakeGenericMethod(skipNavigation.ClrType),
entityParameter,
skipNavigation.GetIdentifyingMemberInfo()!),
Expression.Constant(skipNavigation.Name, typeof(string))),
Expression.Lambda<Func<TSourceEntity, bool>>(
ExpressionExtensions.BuildPredicate(keyProperties, keyValues, whereParameter),
whereParameter)), entityParameter);
Expand All @@ -184,7 +179,6 @@ private static Expression<Func<TSourceEntity, bool>> BuildWhereLambda(
private static Expression<Func<TSourceEntity, IEnumerable<TEntity>>> BuildSelectManyLambda(INavigationBase navigation)
{
var entityParameter = Expression.Parameter(typeof(TSourceEntity), "e");

return Expression.Lambda<Func<TSourceEntity, IEnumerable<TEntity>>>(
Expression.MakeMemberAccess(
entityParameter,
Expand Down
Loading

0 comments on commit 447322c

Please sign in to comment.