Skip to content

Commit d56cde0

Browse files
committed
Add change tracking support for complex collections
Part of #31237
1 parent 62a94cb commit d56cde0

File tree

76 files changed

+7720
-2674
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+7720
-2674
lines changed

src/EFCore/ChangeTracking/CollectionEntry.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ public override bool IsModified
109109

110110
if (Metadata is ISkipNavigation skipNavigation)
111111
{
112-
if (InternalEntry.EntityState != EntityState.Unchanged
113-
&& InternalEntry.EntityState != EntityState.Detached)
112+
if (InternalEntityEntry.EntityState != EntityState.Unchanged
113+
&& InternalEntityEntry.EntityState != EntityState.Detached)
114114
{
115115
return true;
116116
}
@@ -231,7 +231,7 @@ public override void Load(LoadOptions options)
231231

232232
if (!IsLoaded)
233233
{
234-
TargetLoader.Load(InternalEntry, options);
234+
TargetLoader.Load(InternalEntityEntry, options);
235235
}
236236
}
237237

@@ -279,7 +279,7 @@ public override Task LoadAsync(LoadOptions options, CancellationToken cancellati
279279

280280
return IsLoaded
281281
? Task.CompletedTask
282-
: TargetLoader.LoadAsync(InternalEntry, options, cancellationToken);
282+
: TargetLoader.LoadAsync(InternalEntityEntry, options, cancellationToken);
283283
}
284284

285285
/// <summary>
@@ -300,11 +300,11 @@ public override IQueryable Query()
300300
{
301301
EnsureInitialized();
302302

303-
return TargetLoader.Query(InternalEntry);
303+
return TargetLoader.Query(InternalEntityEntry);
304304
}
305305

306306
private void EnsureInitialized()
307-
=> InternalEntry.GetOrCreateCollection(Metadata, forMaterialization: true);
307+
=> InternalEntityEntry.GetOrCreateCollection(Metadata, forMaterialization: true);
308308

309309
/// <summary>
310310
/// The <see cref="EntityEntry" /> of an entity this navigation targets.
@@ -332,7 +332,7 @@ private void EnsureInitialized()
332332
[EntityFrameworkInternal]
333333
protected virtual InternalEntityEntry? GetInternalTargetEntry(object entity)
334334
=> CurrentValue == null
335-
|| !InternalEntry.CollectionContains(Metadata, entity)
335+
|| !InternalEntityEntry.CollectionContains(Metadata, entity)
336336
? null
337337
: InternalEntry.StateManager.GetOrCreateEntry(entity, Metadata.TargetEntityType);
338338

src/EFCore/ChangeTracking/CollectionEntry`.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public CollectionEntry(InternalEntityEntry internalEntry, INavigationBase naviga
5959
/// </remarks>
6060
/// <value> An entry for the entity that owns this member. </value>
6161
public new virtual EntityEntry<TEntity> EntityEntry
62-
=> new(InternalEntry);
62+
=> new(InternalEntityEntry);
6363

6464
/// <summary>
6565
/// Gets or sets the value currently assigned to this property. If the current value is set using this property,
@@ -73,7 +73,7 @@ public CollectionEntry(InternalEntityEntry internalEntry, INavigationBase naviga
7373
/// </remarks>
7474
public new virtual IEnumerable<TRelatedEntity>? CurrentValue
7575
{
76-
get => (IEnumerable<TRelatedEntity>?)this.GetInfrastructure().GetCurrentValue(Metadata);
76+
get => (IEnumerable<TRelatedEntity>?)InternalEntry.GetCurrentValue(Metadata);
7777
set => base.CurrentValue = value;
7878
}
7979

@@ -93,7 +93,7 @@ public CollectionEntry(InternalEntityEntry internalEntry, INavigationBase naviga
9393
/// </remarks>
9494
public new virtual IQueryable<TRelatedEntity> Query()
9595
{
96-
InternalEntry.GetOrCreateCollection(Metadata, forMaterialization: true);
96+
InternalEntityEntry.GetOrCreateCollection(Metadata, forMaterialization: true);
9797

9898
return (IQueryable<TRelatedEntity>)base.Query();
9999
}
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections;
5+
using System.Collections.Generic;
6+
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
7+
8+
namespace Microsoft.EntityFrameworkCore.ChangeTracking;
9+
10+
/// <summary>
11+
/// Provides access to change tracking and loading information for a collection
12+
/// navigation complexProperty that associates this entity to a collection of another entities.
13+
/// </summary>
14+
/// <remarks>
15+
/// <para>
16+
/// Instances of this class are returned from methods when using the <see cref="ChangeTracker" /> API and it is
17+
/// not designed to be directly constructed in your application code.
18+
/// </para>
19+
/// <para>
20+
/// See <see href="https://aka.ms/efcore-docs-entity-entries">Accessing tracked entities in EF Core</see>,
21+
/// <see href="https://aka.ms/efcore-docs-changing-relationships">Changing foreign keys and navigations</see>,
22+
/// and <see href="https://aka.ms/efcore-docs-load-related-data">Loading related entities</see> for more information and examples.
23+
/// </para>
24+
/// </remarks>
25+
public class ComplexCollectionEntry : MemberEntry, IEnumerable<ComplexEntry>
26+
{
27+
/// <summary>
28+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
29+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
30+
/// any release. You should only use it directly in your code with extreme caution and knowing that
31+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
32+
/// </summary>
33+
[EntityFrameworkInternal]
34+
public ComplexCollectionEntry(IInternalEntry internalEntry, IComplexProperty complexProperty)
35+
: base(internalEntry, complexProperty)
36+
{
37+
if (!complexProperty.IsCollection)
38+
{
39+
throw new InvalidOperationException(
40+
CoreStrings.ComplexCollectionIsReference(
41+
internalEntry.StructuralType.DisplayName(), complexProperty.Name,
42+
nameof(ChangeTracking.EntityEntry.ComplexCollection), nameof(ChangeTracking.EntityEntry.ComplexProperty)));
43+
}
44+
}
45+
46+
/// <summary>
47+
/// Gets or sets the value currently assigned to this complexProperty. If the current value is set using this complexProperty,
48+
/// the change tracker is aware of the change and <see cref="ChangeTracker.DetectChanges" /> is not required
49+
/// for the context to detect the change.
50+
/// </summary>
51+
/// <remarks>
52+
/// See <see href="https://aka.ms/efcore-docs-entity-entries">Accessing tracked entities in EF Core</see>
53+
/// and <see href="https://aka.ms/efcore-docs-changing-relationships">Changing foreign keys and navigations</see>
54+
/// for more information and examples.
55+
/// </remarks>
56+
public new virtual IEnumerable? CurrentValue
57+
{
58+
get => (IEnumerable?)base.CurrentValue;
59+
set => base.CurrentValue = value;
60+
}
61+
62+
/// <summary>
63+
/// Gets a <see cref="ComplexEntry"/> for the complex item at the specified index.
64+
/// </summary>
65+
/// <param name="index">The index of the complex item to access.</param>
66+
/// <returns>A <see cref="ComplexEntry"/> for the complex item at the specified index.</returns>
67+
public virtual ComplexEntry this[int index]
68+
{
69+
get
70+
{
71+
var currentValue = CurrentValue;
72+
if (currentValue == null)
73+
{
74+
throw new InvalidOperationException(
75+
CoreStrings.CollectionNotFound(Metadata.Name, InternalEntry.EntityType.DisplayName()));
76+
}
77+
78+
var enumerableValue = currentValue as IList ?? currentValue.Cast<object>().ToList();
79+
80+
if (index < 0 || index >= enumerableValue.Count)
81+
{
82+
throw new ArgumentOutOfRangeException(nameof(index));
83+
}
84+
85+
var element = enumerableValue[index];
86+
87+
return new ComplexEntry(InternalEntry.GetComplexCollectionEntry(Metadata, index));
88+
}
89+
}
90+
91+
/// <summary>
92+
/// Gets the metadata that describes the facets of this property and how it maps to the database.
93+
/// </summary>
94+
public new virtual IComplexProperty Metadata
95+
=> (IComplexProperty)base.Metadata;
96+
97+
/// <summary>
98+
/// Gets or sets a value indicating whether any of foreign key complexProperty values associated
99+
/// with this navigation complexProperty have been modified and should be updated in the database
100+
/// when <see cref="DbContext.SaveChanges()" /> is called.
101+
/// </summary>
102+
/// <remarks>
103+
/// See <see href="https://aka.ms/efcore-docs-entity-entries">Accessing tracked entities in EF Core</see>
104+
/// and <see href="https://aka.ms/efcore-docs-changing-relationships">Changing foreign keys and navigations</see>
105+
/// for more information and examples.
106+
/// </remarks>
107+
public override bool IsModified
108+
{
109+
get
110+
{
111+
var stateManager = InternalEntry.StateManager;
112+
113+
if (Metadata is ISkipNavigation skipNavigation)
114+
{
115+
if (InternalEntry.EntityState != EntityState.Unchanged
116+
&& InternalEntry.EntityState != EntityState.Detached)
117+
{
118+
return true;
119+
}
120+
121+
var joinEntityType = skipNavigation.JoinEntityType;
122+
var foreignKey = skipNavigation.ForeignKey;
123+
var inverseForeignKey = skipNavigation.Inverse.ForeignKey;
124+
foreach (var joinEntry in stateManager.Entries)
125+
{
126+
if (joinEntry.EntityType == joinEntityType
127+
&& stateManager.FindPrincipal(joinEntry, foreignKey) == InternalEntry
128+
&& (joinEntry.EntityState == EntityState.Added
129+
|| joinEntry.EntityState == EntityState.Deleted
130+
|| foreignKey.Properties.Any(joinEntry.IsModified)
131+
|| inverseForeignKey.Properties.Any(joinEntry.IsModified)
132+
|| (stateManager.FindPrincipal(joinEntry, inverseForeignKey)?.EntityState == EntityState.Deleted)))
133+
{
134+
return true;
135+
}
136+
}
137+
}
138+
else
139+
{
140+
var navigationValue = CurrentValue;
141+
if (navigationValue != null)
142+
{
143+
var targetEntityType = Metadata.TargetEntityType;
144+
var foreignKey = ((INavigation)Metadata).ForeignKey;
145+
146+
foreach (var relatedEntity in navigationValue)
147+
{
148+
var relatedEntry = stateManager.TryGetEntry(relatedEntity, targetEntityType);
149+
150+
if (relatedEntry != null
151+
&& (relatedEntry.EntityState == EntityState.Added
152+
|| relatedEntry.EntityState == EntityState.Deleted
153+
|| foreignKey.Properties.Any(relatedEntry.IsModified)))
154+
{
155+
return true;
156+
}
157+
}
158+
}
159+
}
160+
161+
return false;
162+
}
163+
set
164+
{
165+
var stateManager = InternalEntry.StateManager;
166+
167+
if (Metadata is ISkipNavigation skipNavigation)
168+
{
169+
var joinEntityType = skipNavigation.JoinEntityType;
170+
var foreignKey = skipNavigation.ForeignKey;
171+
foreach (var joinEntry in stateManager
172+
.GetEntriesForState(added: !value, modified: !value, deleted: !value, unchanged: value).Where(
173+
e => e.EntityType == joinEntityType
174+
&& stateManager.FindPrincipal(e, foreignKey) == InternalEntry)
175+
.ToList())
176+
{
177+
joinEntry.SetEntityState(value ? EntityState.Modified : EntityState.Unchanged);
178+
}
179+
}
180+
else
181+
{
182+
var foreignKey = ((INavigation)Metadata).ForeignKey;
183+
var navigationValue = CurrentValue;
184+
if (navigationValue != null)
185+
{
186+
foreach (var relatedEntity in navigationValue)
187+
{
188+
var relatedEntry = InternalEntry.StateManager.TryGetEntry(relatedEntity, Metadata.TargetEntityType);
189+
if (relatedEntry != null)
190+
{
191+
var anyNonPk = foreignKey.Properties.Any(p => !p.IsPrimaryKey());
192+
foreach (var property in foreignKey.Properties)
193+
{
194+
if (anyNonPk
195+
&& !property.IsPrimaryKey())
196+
{
197+
relatedEntry.SetPropertyModified(property, isModified: value, acceptChanges: false);
198+
}
199+
}
200+
}
201+
}
202+
}
203+
}
204+
}
205+
}
206+
207+
/// <summary>
208+
/// Gets an enumerator over all complex entries in this collection.
209+
/// </summary>
210+
/// <returns>An enumerator over all complex entries in this collection.</returns>
211+
public virtual IEnumerator<ComplexEntry> GetEnumerator()
212+
{
213+
var currentValue = CurrentValue;
214+
if (currentValue == null)
215+
{
216+
yield break;
217+
}
218+
219+
foreach (var _ in currentValue)
220+
{
221+
yield return new ComplexEntry(InternalEntry, (IComplexProperty)Metadata);
222+
}
223+
}
224+
225+
/// <summary>
226+
/// Gets an enumerator over all complex entries in this collection.
227+
/// </summary>
228+
/// <returns>An enumerator over all complex entries in this collection.</returns>
229+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
230+
}

0 commit comments

Comments
 (0)