Skip to content

Add change tracking support for complex collections #35962

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1497,40 +1497,45 @@ private void
var variableName = parameters.TargetName;
var mainBuilder = parameters.MainBuilder;
var unsafeAccessors = new HashSet<string>();
var isOnComplexCollection = property.DeclaringType is IReadOnlyComplexType complexType && complexType.ComplexProperty.IsCollection;

if (!property.IsShadowProperty()
&& !isOnComplexCollection
&& property is not IServiceProperty) // Service properties don't use property accessors
{
ClrPropertyGetterFactory.Instance.Create(
property,
out var getterExpression,
out var hasSentinelExpression,
out var structuralGetterExpression,
out var hasStructuralSentinelExpression);
out var getClrValueUsingContainingEntityExpression,
out var hasSentinelValueUsingContainingEntityExpression,
out var getClrValueExpression,
out var hasSentinelValueExpression);

mainBuilder
.Append(variableName).AppendLine(".SetGetter(")
.IncrementIndent()
.AppendLines(
_code.Expression(
getterExpression, parameters.Namespaces, unsafeAccessors,
(IReadOnlyDictionary<object, string>)parameters.ScopeVariables, memberAccessReplacements), skipFinalNewline: true)
.AppendLine(",")
.AppendLines(
_code.Expression(
hasSentinelExpression, parameters.Namespaces, unsafeAccessors,
(IReadOnlyDictionary<object, string>)parameters.ScopeVariables, memberAccessReplacements), skipFinalNewline: true)
.AppendLine(",")
.IncrementIndent();
if (property.DeclaringType is not IEntityType)
{
mainBuilder
.AppendLines(
_code.Expression(
getClrValueUsingContainingEntityExpression, parameters.Namespaces, unsafeAccessors,
(IReadOnlyDictionary<object, string>)parameters.ScopeVariables, memberAccessReplacements), skipFinalNewline: true)
.AppendLine(",")
.AppendLines(
_code.Expression(
hasSentinelValueUsingContainingEntityExpression, parameters.Namespaces, unsafeAccessors,
(IReadOnlyDictionary<object, string>)parameters.ScopeVariables, memberAccessReplacements), skipFinalNewline: true)
.AppendLine(",");
}

// For properties declared on entity types, use only two parameters
mainBuilder
.AppendLines(
_code.Expression(
structuralGetterExpression, parameters.Namespaces, unsafeAccessors,
getClrValueExpression, parameters.Namespaces, unsafeAccessors,
(IReadOnlyDictionary<object, string>)parameters.ScopeVariables, memberAccessReplacements), skipFinalNewline: true)
.AppendLine(",")
.AppendLines(
_code.Expression(
hasStructuralSentinelExpression, parameters.Namespaces, unsafeAccessors,
hasSentinelValueExpression, parameters.Namespaces, unsafeAccessors,
(IReadOnlyDictionary<object, string>)parameters.ScopeVariables, memberAccessReplacements), skipFinalNewline: true)
.AppendLine(");")
.DecrementIndent();
Expand Down Expand Up @@ -1558,10 +1563,38 @@ private void
(IReadOnlyDictionary<object, string>)parameters.ScopeVariables, memberAccessReplacements), skipFinalNewline: true)
.AppendLine(");")
.DecrementIndent();

if (property.IsCollection
&& property is IComplexProperty)
{
ClrIndexedCollectionAccessorFactory.Instance.Create(
property,
out _, out _, out _,
out var get, out var set, out var setForMaterialization);

mainBuilder
.Append(variableName).AppendLine(".SetIndexedCollectionAccessor(")
.IncrementIndent()
.AppendLines(
_code.Expression(
get!, parameters.Namespaces, unsafeAccessors,
(IReadOnlyDictionary<object, string>)parameters.ScopeVariables, memberAccessReplacements), skipFinalNewline: true)
.AppendLine(",")
.AppendLines(
_code.Expression(
set!, parameters.Namespaces, unsafeAccessors,
(IReadOnlyDictionary<object, string>)parameters.ScopeVariables, memberAccessReplacements), skipFinalNewline: true)
.AppendLine(",")
.AppendLines(
_code.Expression(
setForMaterialization!, parameters.Namespaces, unsafeAccessors,
(IReadOnlyDictionary<object, string>)parameters.ScopeVariables, memberAccessReplacements), skipFinalNewline: true)
.AppendLine(");")
.DecrementIndent();
}
}

if (property is not IServiceProperty
&& !isOnComplexCollection)
if (property is not IServiceProperty)
{
PropertyAccessorsFactory.Instance.Create(
property,
Expand Down
136 changes: 136 additions & 0 deletions src/EFCore/ChangeTracking/ComplexCollectionEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata.Internal;

namespace Microsoft.EntityFrameworkCore.ChangeTracking;

/// <summary>
/// Provides access to change tracking and loading information for a collection
/// navigation complexProperty that associates this entity to a collection of another entities.
/// </summary>
/// <remarks>
/// <para>
/// Instances of this class are returned from methods when using the <see cref="ChangeTracker" /> API and it is
/// not designed to be directly constructed in your application code.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-entity-entries">Accessing tracked entities in EF Core</see>,
/// <see href="https://aka.ms/efcore-docs-changing-relationships">Changing foreign keys and navigations</see>,
/// and <see href="https://aka.ms/efcore-docs-load-related-data">Loading related entities</see> for more information and examples.
/// </para>
/// </remarks>
public class ComplexCollectionEntry : MemberEntry
{
/// <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
/// 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>
[EntityFrameworkInternal]
public ComplexCollectionEntry(IInternalEntry internalEntry, IComplexProperty complexProperty)
: base(internalEntry, complexProperty)
{
if (!complexProperty.IsCollection)
{
throw new InvalidOperationException(
CoreStrings.ComplexCollectionIsReference(
internalEntry.StructuralType.DisplayName(), complexProperty.Name,
nameof(ChangeTracking.EntityEntry.ComplexCollection), nameof(ChangeTracking.EntityEntry.ComplexProperty)));
}

DetectChanges();
}

private void DetectChanges()
{
var context = InternalEntry.Context;
if (!context.ChangeTracker.AutoDetectChangesEnabled
|| ((IRuntimeModel)context.Model).SkipDetectChanges)
{
return;
}

var changeDetector = context.GetDependencies().ChangeDetector;
foreach (var complexEntry in InternalEntry.GetFlattenedComplexEntries())
{
changeDetector.DetectChanges(complexEntry);
}
}

/// <summary>
/// Gets or sets the value currently assigned to this complexProperty. If the current value is set using this complexProperty,
/// the change tracker is aware of the change and <see cref="ChangeTracker.DetectChanges" /> is not required
/// for the context to detect the change.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-entity-entries">Accessing tracked entities in EF Core</see>
/// and <see href="https://aka.ms/efcore-docs-changing-relationships">Changing foreign keys and navigations</see>
/// for more information and examples.
/// </remarks>
public new virtual IEnumerable? CurrentValue
{
get => this.GetInfrastructure().GetCurrentValue<IEnumerable?>(Metadata);
set => base.CurrentValue = value;
}

/// <summary>
/// Gets a <see cref="ComplexEntry"/> for the complex item at the specified ordinal.
/// </summary>
/// <param name="ordinal">The ordinal of the complex item to access.</param>
/// <returns>A <see cref="ComplexEntry"/> for the complex item at the specified ordinal.</returns>
public virtual ComplexEntry this[int ordinal]
=> new(InternalEntry.GetComplexCollectionEntry(Metadata, ordinal));

/// <summary>
/// Gets a <see cref="ComplexEntry"/> for the original complex item at the specified ordinal.
/// </summary>
/// <param name="ordinal">The original ordinal of the complex item to access.</param>
/// <returns>A <see cref="ComplexEntry"/> for the complex item at the specified original ordinal.</returns>
public virtual ComplexEntry GetOriginalEntry(int ordinal)
=> new(InternalEntry.GetComplexCollectionOriginalEntry(Metadata, ordinal));

/// <summary>
/// Gets the metadata that describes the facets of this property and how it maps to the database.
/// </summary>
public new virtual IComplexProperty Metadata
=> (IComplexProperty)base.Metadata;

/// <summary>
/// Gets or sets a value indicating whether any of foreign key complexProperty values associated
/// with this navigation complexProperty have been modified and should be updated in the database
/// when <see cref="DbContext.SaveChanges()" /> is called.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-entity-entries">Accessing tracked entities in EF Core</see>
/// and <see href="https://aka.ms/efcore-docs-changing-relationships">Changing foreign keys and navigations</see>
/// for more information and examples.
/// </remarks>
public override bool IsModified
{
get => InternalEntry.IsModified(Metadata);
set => InternalEntry.SetPropertyModified(Metadata, isModified: value, recurse: true);
}

/// <summary>
/// Gets an enumerator over all complex entries in this collection.
/// </summary>
/// <returns>An enumerator over all complex entries in this collection.</returns>
public virtual IEnumerator<ComplexEntry> GetEnumerator()
{
var currentValue = CurrentValue;
if (currentValue == null)
{
yield break;
}

foreach (var complexEntry in InternalEntry.GetFlattenedComplexEntries())
{
yield return new ComplexEntry(complexEntry);
}
}
}
84 changes: 84 additions & 0 deletions src/EFCore/ChangeTracking/ComplexCollectionEntry`.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;

namespace Microsoft.EntityFrameworkCore.ChangeTracking;

/// <summary>
/// Provides access to change tracking and loading information for a collection
/// navigation property that associates this entity to a collection of another entities.
/// </summary>
/// <remarks>
/// <para>
/// Instances of this class are returned from methods when using the <see cref="ChangeTracker" /> API and it is
/// not designed to be directly constructed in your application code.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-entity-entries">Accessing tracked entities in EF Core</see>,
/// <see href="https://aka.ms/efcore-docs-changing-relationships">Changing foreign keys and navigations</see>,
/// and <see href="https://aka.ms/efcore-docs-load-related-data">Loading related entities</see> for more information and examples.
/// </para>
/// </remarks>
/// <typeparam name="TEntity">The type of the entity the property belongs to.</typeparam>
/// <typeparam name="TElement">The element type.</typeparam>
public class ComplexCollectionEntry<TEntity, TElement> : ComplexCollectionEntry
where TEntity : class
where TElement : notnull
{
/// <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
/// 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>
[EntityFrameworkInternal]
public ComplexCollectionEntry(IInternalEntry internalEntry, IComplexProperty property)
: base(internalEntry, property)
{
}

/// <summary>
/// The <see cref="EntityEntry{TEntity}" /> to which this member belongs.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-entity-entries">Accessing tracked entities in EF Core</see> for more information and
/// examples.
/// </remarks>
/// <value> An entry for the entity that owns this member. </value>
public override EntityEntry<TEntity> EntityEntry
=> new(InternalEntry.EntityEntry);

/// <summary>
/// Gets or sets the value currently assigned to this property. If the current value is set using this property,
/// the change tracker is aware of the change and <see cref="ChangeTracker.DetectChanges" /> is not required
/// for the context to detect the change.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-entity-entries">Accessing tracked entities in EF Core</see>
/// and <see href="https://aka.ms/efcore-docs-changing-relationships">Changing foreign keys and navigations</see>
/// for more information and examples.
/// </remarks>
public new virtual IReadOnlyList<TElement>? CurrentValue
{
get => this.GetInfrastructure().GetCurrentValue<IReadOnlyList<TElement>?>(Metadata);
set => base.CurrentValue = value;
}

/// <summary>
/// Gets a <see cref="ComplexEntry{TEntity, TElement}"/> for the complex item at the specified ordinal.
/// </summary>
/// <param name="ordinal">The ordinal of the complex item to access.</param>
/// <returns>A <see cref="ComplexEntry{TEntity, TElement}"/> for the complex item at the specified ordinal.</returns>
public override ComplexEntry<TEntity, TElement> this[int ordinal]
=> new(InternalEntry.GetComplexCollectionEntry(Metadata, ordinal));

/// <summary>
/// Gets a <see cref="ComplexEntry{TEntity, TElement}"/> for the complex item at the specified original ordinal.
/// </summary>
/// <param name="ordinal">The original ordinal of the complex item to access.</param>
/// <returns>A <see cref="ComplexEntry{TEntity, TElement}"/> for the original complex item at the specified ordinal.</returns>
public override ComplexEntry<TEntity, TElement> GetOriginalEntry(int ordinal)
=> new(InternalEntry.GetComplexCollectionOriginalEntry(Metadata, ordinal));
}
Loading