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 9, 2022
1 parent 983112b commit ce2054e
Show file tree
Hide file tree
Showing 63 changed files with 7,070 additions and 368 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
38 changes: 23 additions & 15 deletions src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -921,11 +921,12 @@ public object GetOrCreateCollection(INavigationBase navigationBase, bool forMate
? GetOrCreateCollectionTyped(navigationBase)
: navigationBase.GetCollectionAccessor()!.GetOrCreate(Entity, forMaterialization);

private ICollection<object> GetOrCreateCollectionTyped(INavigationBase navigation)
private object GetOrCreateCollectionTyped(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(GetOrCreateCollectionTyped(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))
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(GetOrCreateCollectionTyped(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 @@ -1020,7 +1020,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 @@ -1476,7 +1476,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 @@ -1493,7 +1493,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
Loading

0 comments on commit ce2054e

Please sign in to comment.