Skip to content
Merged
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
67 changes: 67 additions & 0 deletions src/EFCore/ChangeTracking/ChangeTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,73 @@ public virtual IEnumerable<EntityEntry<TEntity>> Entries<TEntity>()
.Select(e => new EntityEntry<TEntity>(e));
}

/// <summary>
/// Gets an <see cref="EntityEntry" /> for each entity being tracked by the context that has the given state(s).
/// The entries provide access to change tracking information and operations for each entity.
/// </summary>
/// <remarks>
/// <para>
/// This method does not call <see cref="DetectChanges" /> and returns entries based on the current state
/// of the change tracker. This makes it suitable for use in high-performance scenarios or in contexts
/// that track many entities, where calling <see cref="DetectChanges" /> may be expensive.
/// </para>
/// <para>
/// Note that modification of entity state while iterating over the returned enumeration may result in
/// an <see cref="InvalidOperationException" /> indicating that the collection was modified while enumerating.
/// To avoid this, create a defensive copy using <see cref="Enumerable.ToList{TSource}" /> or similar before iterating.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-change-tracking">EF Core change tracking</see> for more information and examples.
/// </para>
/// </remarks>
/// <param name="added">If <see langword="true" />, entries with <see cref="EntityState.Added" /> state are included.</param>
/// <param name="modified">If <see langword="true" />, entries with <see cref="EntityState.Modified" /> state are included.</param>
/// <param name="deleted">If <see langword="true" />, entries with <see cref="EntityState.Deleted" /> state are included.</param>
/// <param name="unchanged">If <see langword="true" />, entries with <see cref="EntityState.Unchanged" /> state are included.</param>
/// <returns>An entry for each entity that has the given state(s).</returns>
public virtual IEnumerable<EntityEntry> GetEntriesForState(
bool added = false,
bool modified = false,
bool deleted = false,
bool unchanged = false)
=> StateManager.GetEntriesForState(added, modified, deleted, unchanged, returnSharedIdentity: true)
.Select(e => new EntityEntry(e));

/// <summary>
/// Gets an <see cref="EntityEntry{TEntity}" /> for each entity of a given type being tracked by the context
/// that has the given state(s). The entries provide access to change tracking information and operations for each entity.
/// </summary>
/// <remarks>
/// <para>
/// This method does not call <see cref="DetectChanges" /> and returns entries based on the current state
/// of the change tracker. This makes it suitable for use in high-performance scenarios or in contexts
/// that track many entities, where calling <see cref="DetectChanges" /> may be expensive.
/// </para>
/// <para>
/// Note that modification of entity state while iterating over the returned enumeration may result in
/// an <see cref="InvalidOperationException" /> indicating that the collection was modified while enumerating.
/// To avoid this, create a defensive copy using <see cref="Enumerable.ToList{TSource}" /> or similar before iterating.
/// </para>
/// <para>
/// See <see href="https://aka.ms/efcore-docs-change-tracking">EF Core change tracking</see> for more information and examples.
/// </para>
/// </remarks>
/// <typeparam name="TEntity">The type of entities to get entries for.</typeparam>
/// <param name="added">If <see langword="true" />, entries with <see cref="EntityState.Added" /> state are included.</param>
/// <param name="modified">If <see langword="true" />, entries with <see cref="EntityState.Modified" /> state are included.</param>
/// <param name="deleted">If <see langword="true" />, entries with <see cref="EntityState.Deleted" /> state are included.</param>
/// <param name="unchanged">If <see langword="true" />, entries with <see cref="EntityState.Unchanged" /> state are included.</param>
/// <returns>An entry for each entity of the given type that has the given state(s).</returns>
public virtual IEnumerable<EntityEntry<TEntity>> GetEntriesForState<TEntity>(
bool added = false,
bool modified = false,
bool deleted = false,
bool unchanged = false)
where TEntity : class
=> StateManager.GetEntriesForState(added, modified, deleted, unchanged, returnSharedIdentity: true)
.Where(e => e.Entity is TEntity)
.Select(e => new EntityEntry<TEntity>(e));

private void TryDetectChanges()
{
if (AutoDetectChangesEnabled)
Expand Down
74 changes: 74 additions & 0 deletions test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3484,6 +3484,80 @@ public void Auto_DetectChanges_for_Entries_can_be_switched_off(bool useGenericOv
Assert.Equal(EntityState.Unchanged, entry.State);
}

[ConditionalTheory,
InlineData(false),
InlineData(true)]
public void GetEntriesForState_does_not_call_DetectChanges(bool useGenericOverload)
{
using var context = new EarlyLearningCenter();
var entry = context.Attach(
new Product { Id = 1, CategoryId = 66 });

entry.Entity.CategoryId = 77;

Assert.Equal(EntityState.Unchanged, entry.State);

if (useGenericOverload)
{
_ = context.ChangeTracker.GetEntriesForState<Product>(unchanged: true).ToList();
}
else
{
_ = context.ChangeTracker.GetEntriesForState(unchanged: true).ToList();
}

Assert.Equal(EntityState.Unchanged, entry.State);
}

[ConditionalTheory,
InlineData(false),
InlineData(true)]
public void GetEntriesForState_returns_only_entries_with_matching_state(bool useGenericOverload)
{
using var context = new EarlyLearningCenter();

context.Add(new Product { Id = 1, CategoryId = 1 });
context.Attach(new Product { Id = 2, CategoryId = 2 });
var modifiedEntry = context.Attach(new Product { Id = 3, CategoryId = 3 });
modifiedEntry.State = EntityState.Modified;
var deletedEntry = context.Attach(new Product { Id = 4, CategoryId = 4 });
deletedEntry.State = EntityState.Deleted;

if (useGenericOverload)
{
Assert.Single(context.ChangeTracker.GetEntriesForState<Product>(added: true));
Assert.Single(context.ChangeTracker.GetEntriesForState<Product>(unchanged: true));
Assert.Single(context.ChangeTracker.GetEntriesForState<Product>(modified: true));
Assert.Single(context.ChangeTracker.GetEntriesForState<Product>(deleted: true));
Assert.Equal(2, context.ChangeTracker.GetEntriesForState<Product>(added: true, modified: true).Count());
Assert.Equal(4, context.ChangeTracker.GetEntriesForState<Product>(added: true, modified: true, deleted: true, unchanged: true).Count());
Assert.Empty(context.ChangeTracker.GetEntriesForState<Product>());
}
else
{
Assert.Single(context.ChangeTracker.GetEntriesForState(added: true));
Assert.Single(context.ChangeTracker.GetEntriesForState(unchanged: true));
Assert.Single(context.ChangeTracker.GetEntriesForState(modified: true));
Assert.Single(context.ChangeTracker.GetEntriesForState(deleted: true));
Assert.Equal(2, context.ChangeTracker.GetEntriesForState(added: true, modified: true).Count());
Assert.Equal(4, context.ChangeTracker.GetEntriesForState(added: true, modified: true, deleted: true, unchanged: true).Count());
Assert.Empty(context.ChangeTracker.GetEntriesForState());
}
}

[ConditionalFact]
public void GetEntriesForState_generic_filters_by_type()
{
using var context = new EarlyLearningCenter();

context.Add(new Product { Id = 1, CategoryId = 1 });
context.Add(new Category { Id = 1 });

Assert.Single(context.ChangeTracker.GetEntriesForState<Product>(added: true));
Assert.Single(context.ChangeTracker.GetEntriesForState<Category>(added: true));
Assert.Equal(2, context.ChangeTracker.GetEntriesForState(added: true).Count());
}

[ConditionalTheory,
InlineData(false),
InlineData(true)]
Expand Down
Loading