Skip to content

Commit c103ff1

Browse files
committed
Add change tracking support for complex collections
Part of #31237
1 parent 3e12e53 commit c103ff1

File tree

114 files changed

+8903
-3356
lines changed

Some content is hidden

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

114 files changed

+8903
-3356
lines changed

.editorconfig

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ csharp_style_expression_bodied_properties = true:suggestion
106106
csharp_style_expression_bodied_indexers = true:suggestion
107107
csharp_style_expression_bodied_accessors = true:suggestion
108108
csharp_style_expression_bodied_lambdas = true:suggestion
109-
csharp_style_expression_bodied_local_functions = false:suggestion
109+
csharp_style_expression_bodied_local_functions = true:suggestion
110110

111111
# Pattern matching
112112
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
@@ -144,6 +144,10 @@ csharp_style_prefer_top_level_statements = false:silent
144144
# Implementation preferences
145145
csharp_prefer_system_threading_lock = true:suggestion
146146

147+
# Local functions
148+
csharp_style_prefer_local_over_anonymous_function = false:silent
149+
csharp_prefer_static_local_function = true:suggestion
150+
147151
## Formatting conventions
148152
# Dotnet formatting settings:
149153
[*.{cs,vb}]

.github/copilot-instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ If you are not sure, do not guess, just tell that you don't know or ask clarifyi
4444
- File scoped namespace declarations
4545
- Use `var` for local variables
4646
- Use expression-bodied members where appropriate
47-
- Use collection initializers when possible
47+
- Prefer using collection expressions when possible
4848
- Use `is` pattern matching instead of `as` and null checks
4949
- Prefer `switch` expressions over `switch` statements when appropriate
5050
- Prefer field-backed property declarations using field contextual keyword instead of an explicit field.

src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,6 +1509,8 @@ private void
15091509
out var structuralGetterExpression,
15101510
out var hasStructuralSentinelExpression);
15111511

1512+
// TODO: Only use the last two for entity properties
1513+
15121514
mainBuilder
15131515
.Append(variableName).AppendLine(".SetGetter(")
15141516
.IncrementIndent()
@@ -1557,6 +1559,34 @@ private void
15571559
(IReadOnlyDictionary<object, string>)parameters.ScopeVariables, memberAccessReplacements), skipFinalNewline: true)
15581560
.AppendLine(");")
15591561
.DecrementIndent();
1562+
1563+
if (property.IsCollection)
1564+
{
1565+
ClrIndexedCollectionAccessorFactory.Instance.Create(
1566+
property,
1567+
out _, out _, out _,
1568+
out var get, out var set, out var setForMaterialization);
1569+
1570+
mainBuilder
1571+
.Append(variableName).AppendLine(".SetIndexedCollectionAccessor(")
1572+
.IncrementIndent()
1573+
.AppendLines(
1574+
_code.Expression(
1575+
get!, parameters.Namespaces, unsafeAccessors,
1576+
(IReadOnlyDictionary<object, string>)parameters.ScopeVariables, memberAccessReplacements), skipFinalNewline: true)
1577+
.AppendLine(",")
1578+
.AppendLines(
1579+
_code.Expression(
1580+
set!, parameters.Namespaces, unsafeAccessors,
1581+
(IReadOnlyDictionary<object, string>)parameters.ScopeVariables, memberAccessReplacements), skipFinalNewline: true)
1582+
.AppendLine(",")
1583+
.AppendLines(
1584+
_code.Expression(
1585+
setForMaterialization!, parameters.Namespaces, unsafeAccessors,
1586+
(IReadOnlyDictionary<object, string>)parameters.ScopeVariables, memberAccessReplacements), skipFinalNewline: true)
1587+
.AppendLine(");")
1588+
.DecrementIndent();
1589+
}
15601590
}
15611591

15621592
if (property is not IServiceProperty)

src/EFCore.Relational/Metadata/Internal/RelationalModel.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Reflection.Metadata;
54
using System.Text;
6-
using System.Text.Json;
75

86
namespace Microsoft.EntityFrameworkCore.Metadata.Internal;
97

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: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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 Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
6+
7+
namespace Microsoft.EntityFrameworkCore.ChangeTracking;
8+
9+
/// <summary>
10+
/// Provides access to change tracking and loading information for a collection
11+
/// navigation complexProperty that associates this entity to a collection of another entities.
12+
/// </summary>
13+
/// <remarks>
14+
/// <para>
15+
/// Instances of this class are returned from methods when using the <see cref="ChangeTracker" /> API and it is
16+
/// not designed to be directly constructed in your application code.
17+
/// </para>
18+
/// <para>
19+
/// See <see href="https://aka.ms/efcore-docs-entity-entries">Accessing tracked entities in EF Core</see>,
20+
/// <see href="https://aka.ms/efcore-docs-changing-relationships">Changing foreign keys and navigations</see>,
21+
/// and <see href="https://aka.ms/efcore-docs-load-related-data">Loading related entities</see> for more information and examples.
22+
/// </para>
23+
/// </remarks>
24+
public class ComplexCollectionEntry : MemberEntry, IEnumerable<ComplexEntry>
25+
{
26+
/// <summary>
27+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
28+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
29+
/// any release. You should only use it directly in your code with extreme caution and knowing that
30+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
31+
/// </summary>
32+
[EntityFrameworkInternal]
33+
public ComplexCollectionEntry(IInternalEntry internalEntry, IComplexProperty complexProperty)
34+
: base(internalEntry, complexProperty)
35+
{
36+
if (!complexProperty.IsCollection)
37+
{
38+
throw new InvalidOperationException(
39+
CoreStrings.ComplexCollectionIsReference(
40+
internalEntry.StructuralType.DisplayName(), complexProperty.Name,
41+
nameof(ChangeTracking.EntityEntry.ComplexCollection), nameof(ChangeTracking.EntityEntry.ComplexProperty)));
42+
}
43+
}
44+
45+
/// <summary>
46+
/// Gets or sets the value currently assigned to this complexProperty. If the current value is set using this complexProperty,
47+
/// the change tracker is aware of the change and <see cref="ChangeTracker.DetectChanges" /> is not required
48+
/// for the context to detect the change.
49+
/// </summary>
50+
/// <remarks>
51+
/// See <see href="https://aka.ms/efcore-docs-entity-entries">Accessing tracked entities in EF Core</see>
52+
/// and <see href="https://aka.ms/efcore-docs-changing-relationships">Changing foreign keys and navigations</see>
53+
/// for more information and examples.
54+
/// </remarks>
55+
public new virtual IEnumerable? CurrentValue
56+
{
57+
get => (IEnumerable?)base.CurrentValue;
58+
set => base.CurrentValue = value;
59+
}
60+
61+
/// <summary>
62+
/// Gets a <see cref="ComplexEntry"/> for the complex item at the specified ordinal.
63+
/// </summary>
64+
/// <param name="ordinal">The ordinal of the complex item to access.</param>
65+
/// <returns>A <see cref="ComplexEntry"/> for the complex item at the specified ordinal.</returns>
66+
public virtual ComplexEntry this[int ordinal]
67+
=> new(InternalEntry.GetComplexCollectionEntry(Metadata, ordinal));
68+
69+
/// <summary>
70+
/// Gets the metadata that describes the facets of this property and how it maps to the database.
71+
/// </summary>
72+
public new virtual IComplexProperty Metadata
73+
=> (IComplexProperty)base.Metadata;
74+
75+
/// <summary>
76+
/// Gets or sets a value indicating whether any of foreign key complexProperty values associated
77+
/// with this navigation complexProperty have been modified and should be updated in the database
78+
/// when <see cref="DbContext.SaveChanges()" /> is called.
79+
/// </summary>
80+
/// <remarks>
81+
/// See <see href="https://aka.ms/efcore-docs-entity-entries">Accessing tracked entities in EF Core</see>
82+
/// and <see href="https://aka.ms/efcore-docs-changing-relationships">Changing foreign keys and navigations</see>
83+
/// for more information and examples.
84+
/// </remarks>
85+
public override bool IsModified
86+
{
87+
get => InternalEntry.IsModified(Metadata);
88+
89+
set => InternalEntry.SetPropertyModified(Metadata, isModified: value, recurse: true);
90+
}
91+
92+
/// <summary>
93+
/// Gets an enumerator over all complex entries in this collection.
94+
/// </summary>
95+
/// <returns>An enumerator over all complex entries in this collection.</returns>
96+
public virtual IEnumerator<ComplexEntry> GetEnumerator()
97+
{
98+
var currentValue = CurrentValue;
99+
if (currentValue == null)
100+
{
101+
yield break;
102+
}
103+
104+
foreach (var complexEntry in InternalEntry.GetFlattenedComplexEntries())
105+
{
106+
yield return new ComplexEntry(complexEntry);
107+
}
108+
}
109+
110+
/// <summary>
111+
/// Gets an enumerator over all complex entries in this collection.
112+
/// </summary>
113+
/// <returns>An enumerator over all complex entries in this collection.</returns>
114+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
115+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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 Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
5+
6+
namespace Microsoft.EntityFrameworkCore.ChangeTracking;
7+
8+
/// <summary>
9+
/// Provides access to change tracking and loading information for a collection
10+
/// navigation property that associates this entity to a collection of another entities.
11+
/// </summary>
12+
/// <remarks>
13+
/// <para>
14+
/// Instances of this class are returned from methods when using the <see cref="ChangeTracker" /> API and it is
15+
/// not designed to be directly constructed in your application code.
16+
/// </para>
17+
/// <para>
18+
/// See <see href="https://aka.ms/efcore-docs-entity-entries">Accessing tracked entities in EF Core</see>,
19+
/// <see href="https://aka.ms/efcore-docs-changing-relationships">Changing foreign keys and navigations</see>,
20+
/// and <see href="https://aka.ms/efcore-docs-load-related-data">Loading related entities</see> for more information and examples.
21+
/// </para>
22+
/// </remarks>
23+
/// <typeparam name="TEntity">The type of the entity the property belongs to.</typeparam>
24+
/// <typeparam name="TElement">The element type.</typeparam>
25+
public class ComplexCollectionEntry<TEntity, TElement> : ComplexCollectionEntry, IEnumerable<ComplexEntry<TEntity, TElement>>
26+
where TEntity : class
27+
where TElement : notnull
28+
{
29+
/// <summary>
30+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
31+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
32+
/// any release. You should only use it directly in your code with extreme caution and knowing that
33+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
34+
/// </summary>
35+
[EntityFrameworkInternal]
36+
public ComplexCollectionEntry(IInternalEntry internalEntry, IComplexProperty property)
37+
: base(internalEntry, property)
38+
{
39+
}
40+
41+
/// <summary>
42+
/// The <see cref="EntityEntry{TEntity}" /> to which this member belongs.
43+
/// </summary>
44+
/// <remarks>
45+
/// See <see href="https://aka.ms/efcore-docs-entity-entries">Accessing tracked entities in EF Core</see> for more information and
46+
/// examples.
47+
/// </remarks>
48+
/// <value> An entry for the entity that owns this member. </value>
49+
public new virtual EntityEntry<TEntity> EntityEntry
50+
=> new(InternalEntry.EntityEntry);
51+
52+
/// <summary>
53+
/// Gets or sets the value currently assigned to this property. If the current value is set using this property,
54+
/// the change tracker is aware of the change and <see cref="ChangeTracker.DetectChanges" /> is not required
55+
/// for the context to detect the change.
56+
/// </summary>
57+
/// <remarks>
58+
/// See <see href="https://aka.ms/efcore-docs-entity-entries">Accessing tracked entities in EF Core</see>
59+
/// and <see href="https://aka.ms/efcore-docs-changing-relationships">Changing foreign keys and navigations</see>
60+
/// for more information and examples.
61+
/// </remarks>
62+
public new virtual IEnumerable<TElement>? CurrentValue
63+
{
64+
get => (IEnumerable<TElement>?)this.GetInfrastructure().GetCurrentValue(Metadata);
65+
set => base.CurrentValue = value;
66+
}
67+
68+
/// <summary>
69+
/// Gets a <see cref="ComplexEntry{TEntity, TElement}"/> for the complex item at the specified ordinal.
70+
/// </summary>
71+
/// <param name="ordinal">The ordinal of the complex item to access.</param>
72+
/// <returns>A <see cref="ComplexEntry{TEntity, TElement}"/> for the complex item at the specified ordinal.</returns>
73+
public new virtual ComplexEntry<TEntity, TElement> this[int ordinal]
74+
=> new(InternalEntry.GetComplexCollectionEntry(Metadata, ordinal));
75+
76+
/// <summary>
77+
/// Gets an enumerator over all complex entries in this collection.
78+
/// </summary>
79+
/// <returns>An enumerator over all complex entries in this collection.</returns>
80+
public new virtual IEnumerator<ComplexEntry<TEntity, TElement>> GetEnumerator()
81+
{
82+
var currentValue = CurrentValue;
83+
if (currentValue == null)
84+
{
85+
yield break;
86+
}
87+
88+
foreach (var complexEntry in InternalEntry.GetFlattenedComplexEntries())
89+
{
90+
yield return new ComplexEntry<TEntity, TElement>(complexEntry);
91+
}
92+
}
93+
}

0 commit comments

Comments
 (0)