diff --git a/src/EFCore/ChangeTracking/LocalView.cs b/src/EFCore/ChangeTracking/LocalView.cs index 6e10e920026..b35ed9fa09d 100644 --- a/src/EFCore/ChangeTracking/LocalView.cs +++ b/src/EFCore/ChangeTracking/LocalView.cs @@ -56,6 +56,7 @@ public class LocalView : private ObservableBackedBindingList? _bindingList; private ObservableCollection? _observable; private readonly DbContext _context; + private readonly IEntityType _entityType; private int _countChanges; private int? _count; private bool _triggeringStateManagerChange; @@ -72,6 +73,7 @@ public class LocalView : public LocalView(DbSet set) { _context = set.GetService().Context; + _entityType = set.EntityType; set.GetService().RegisterView(StateManagerChangedHandler); } @@ -208,7 +210,7 @@ public virtual void Add(TEntity item) // to Add it again since doing so would change its state to Added, which is probably not what // was wanted in this case. - var entry = _context.GetDependencies().StateManager.GetOrCreateEntry(item); + var entry = _context.GetDependencies().StateManager.GetOrCreateEntry(item, _entityType); if (entry.EntityState == EntityState.Deleted || entry.EntityState == EntityState.Detached) { diff --git a/src/EFCore/DbSet.cs b/src/EFCore/DbSet.cs index ef7a388bdf3..09baeb9bc41 100644 --- a/src/EFCore/DbSet.cs +++ b/src/EFCore/DbSet.cs @@ -590,6 +590,19 @@ public virtual void RemoveRange(IEnumerable entities) public virtual void UpdateRange(IEnumerable entities) => throw new NotSupportedException(); + /// + /// Gets an for the given entity. The entry provides + /// access to change tracking information and operations for the entity. + /// + /// + /// See Accessing tracked entities in EF Core for more information and + /// examples. + /// + /// The entity to get the entry for. + /// The entry for the given entity. + public virtual EntityEntry Entry(TEntity entity) + => throw new NotSupportedException(); + /// /// Returns an which when enumerated will execute a query against the database /// to load all entities from the database. diff --git a/src/EFCore/Internal/InternalDbSet.cs b/src/EFCore/Internal/InternalDbSet.cs index 06c44f8954a..ff9d7c9c625 100644 --- a/src/EFCore/Internal/InternalDbSet.cs +++ b/src/EFCore/Internal/InternalDbSet.cs @@ -370,7 +370,7 @@ public override async Task AddRangeAsync( foreach (var entity in entities) { await SetEntityStateAsync( - stateManager.GetOrCreateEntry(entity), + stateManager.GetOrCreateEntry(entity, EntityType), EntityState.Added, cancellationToken) .ConfigureAwait(false); @@ -426,6 +426,26 @@ public override void RemoveRange(IEnumerable entities) public override void UpdateRange(IEnumerable entities) => SetEntityStates(Check.NotNull(entities, nameof(entities)), EntityState.Modified); + /// + /// 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. + /// + public override EntityEntry Entry(TEntity entity) + { + Check.NotNull(entity, nameof(entity)); + + var entry = EntryWithoutDetectChanges(entity); + + if (_context.ChangeTracker.AutoDetectChangesEnabled) + { + entry.DetectChanges(); + } + + return entry; + } + private IEntityFinder Finder => (IEntityFinder)_context.GetDependencies().EntityFinderFactory.Create(EntityType); diff --git a/test/EFCore.Tests/DbSetTest.cs b/test/EFCore.Tests/DbSetTest.cs index 11e5847a297..09c702fa21d 100644 --- a/test/EFCore.Tests/DbSetTest.cs +++ b/test/EFCore.Tests/DbSetTest.cs @@ -133,37 +133,6 @@ public void Direct_use_of_Set_throws_if_context_disposed() Assert.Throws(() => context.Set()).Message); } - [ConditionalFact] - public void Direct_use_of_Set_for_shared_type_throws_if_context_disposed() - { - var context = new EarlyLearningCenter(); - context.Dispose(); - - Assert.StartsWith( - CoreStrings.ContextDisposed, - Assert.Throws(() => context.Set>("SharedTypeEntityTypeName")).Message); - } - - [ConditionalFact] - public void Using_shared_type_entity_type_db_set_with_incorrect_return_type_throws() - { - using var context = new EarlyLearningCenter(); - - var dbSet = context.Set>("SharedEntity"); - - Assert.NotNull(dbSet.Add(new Dictionary { { "Id", 1 } })); - Assert.NotNull(dbSet.ToList()); - - var wrongDbSet = context.Set("SharedEntity"); - - Assert.Equal( - CoreStrings.DbSetIncorrectGenericType("SharedEntity", "Dictionary", "Category"), - Assert.Throws(() => wrongDbSet.Add(new Category())).Message); - Assert.Equal( - CoreStrings.DbSetIncorrectGenericType("SharedEntity", "Dictionary", "Category"), - Assert.Throws(() => wrongDbSet.ToList()).Message); - } - [ConditionalFact] public void Use_of_LocalView_throws_if_context_is_disposed() { @@ -735,11 +704,6 @@ public async Task Can_enumerate_with_await_foreach_with_cancellation() } } - private class Curious - { - public string George { get; set; } - } - private class Category { public int Id { get; set; } @@ -769,12 +733,5 @@ protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBu => optionsBuilder .UseInMemoryDatabase(Guid.NewGuid().ToString()) .UseInternalServiceProvider(InMemoryTestHelpers.Instance.CreateServiceProvider()); - - protected internal override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.SharedTypeEntity>("SharedEntity").IndexerProperty("Id"); - } } } diff --git a/test/EFCore.Tests/SharedTypeDbSetTest.cs b/test/EFCore.Tests/SharedTypeDbSetTest.cs new file mode 100644 index 00000000000..cbcb8a05fa2 --- /dev/null +++ b/test/EFCore.Tests/SharedTypeDbSetTest.cs @@ -0,0 +1,742 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; + +namespace Microsoft.EntityFrameworkCore; + +public class SharedTypeDbSetTest +{ + [ConditionalFact] + public void DbSets_are_cached() + { + DbSet set1; + DbSet set2; + + using (var context = new EarlyLearningCenter()) + { + set1 = context.Category1s; + set2 = context.Category2s; + Assert.Same(set1, context.Set("Category1")); + Assert.Same(set2, context.Set("Category2")); + } + + using (var context = new EarlyLearningCenter()) + { + Assert.NotSame(set1, context.Category1s); + Assert.NotSame(set2, context.Category2s); + } + } + + [ConditionalFact] + public async Task Use_of_set_throws_if_context_is_disposed() + { + DbSet set; + + using (var context = new EarlyLearningCenter()) + { + set = context.Set("Category1"); + } + + Assert.StartsWith( + CoreStrings.ContextDisposed, + Assert.Throws(() => set.Entry(new Category())).Message); + + Assert.StartsWith( + CoreStrings.ContextDisposed, + Assert.Throws(() => set.Add(new Category())).Message); + + Assert.StartsWith( + CoreStrings.ContextDisposed, + Assert.Throws(() => set.Find(77)).Message); + + Assert.StartsWith( + CoreStrings.ContextDisposed, + Assert.Throws(() => set.Attach(new Category())).Message); + + Assert.StartsWith( + CoreStrings.ContextDisposed, + Assert.Throws(() => set.Update(new Category())).Message); + + Assert.StartsWith( + CoreStrings.ContextDisposed, + Assert.Throws(() => set.Remove(new Category())).Message); + + Assert.StartsWith( + CoreStrings.ContextDisposed, + Assert.Throws(() => set.ToList()).Message); + + Assert.StartsWith( + CoreStrings.ContextDisposed, + (await Assert.ThrowsAsync(() => set.AddAsync(new Category()).AsTask())).Message); + + Assert.StartsWith( + CoreStrings.ContextDisposed, + (await Assert.ThrowsAsync(() => set.FindAsync(77).AsTask())).Message); + + Assert.StartsWith( + CoreStrings.ContextDisposed, + (await Assert.ThrowsAsync(() => set.ToListAsync())).Message); + } + + [ConditionalFact] + public void Direct_use_of_Set_for_shared_type_throws_if_context_disposed() + { + var context = new EarlyLearningCenter(); + context.Dispose(); + + Assert.StartsWith( + CoreStrings.ContextDisposed, + Assert.Throws(() => context.Set>("SharedTypeEntityTypeName")).Message); + } + + [ConditionalFact] + public void Using_shared_type_entity_type_db_set_with_incorrect_return_type_throws() + { + using var context = new EarlyLearningCenter(); + + var dbSet = context.Set>("SharedEntity"); + + Assert.NotNull(dbSet.Add(new Dictionary { { "Id", 1 } })); + Assert.NotNull(dbSet.ToList()); + + var wrongDbSet = context.Set("SharedEntity"); + + Assert.Equal( + CoreStrings.DbSetIncorrectGenericType("SharedEntity", "Dictionary", "Category"), + Assert.Throws(() => wrongDbSet.Add(new Category())).Message); + Assert.Equal( + CoreStrings.DbSetIncorrectGenericType("SharedEntity", "Dictionary", "Category"), + Assert.Throws(() => wrongDbSet.ToList()).Message); + } + + [ConditionalFact] + public void Use_of_LocalView_throws_if_context_is_disposed() + { + LocalView view; + + using (var context = new EarlyLearningCenter()) + { + view = context.Category1s.Local; + } + + Assert.StartsWith( + CoreStrings.ContextDisposed, + Assert.Throws(() => view.Add(new Category())).Message); + + Assert.StartsWith( + CoreStrings.ContextDisposed, + Assert.Throws(() => view.Remove(new Category())).Message); + + Assert.StartsWith( + CoreStrings.ContextDisposed, + Assert.Throws(() => view.Contains(new Category())).Message); + + Assert.StartsWith( + CoreStrings.ContextDisposed, + Assert.Throws(() => view.CopyTo(Array.Empty(), 0)).Message); + + Assert.StartsWith( + CoreStrings.ContextDisposed, + Assert.Throws(() => view.Clear()).Message); + + Assert.StartsWith( + CoreStrings.ContextDisposed, + Assert.Throws(() => view.GetEnumerator()).Message); + } + + [ConditionalFact] + public Task Can_add_existing_entities_to_context_to_be_deleted() + => TrackEntitiesTest((c, e) => c.Remove(e), (c, e) => c.Remove(e), EntityState.Deleted); + + [ConditionalFact] + public Task Can_add_new_entities_to_context_graph() + => TrackEntitiesTest((c, e) => c.Add(e), (c, e) => c.Add(e), EntityState.Added); + + [ConditionalFact] + public Task Can_add_new_entities_to_context_graph_async() + => TrackEntitiesTest((c, e) => c.AddAsync(e), (c, e) => c.AddAsync(e), EntityState.Added); + + [ConditionalFact] + public Task Can_add_existing_entities_to_context_to_be_attached_graph() + => TrackEntitiesTest((c, e) => c.Attach(e), (c, e) => c.Attach(e), EntityState.Unchanged); + + [ConditionalFact] + public Task Can_add_existing_entities_to_context_to_be_updated_graph() + => TrackEntitiesTest((c, e) => c.Update(e), (c, e) => c.Update(e), EntityState.Modified); + + private static Task TrackEntitiesTest( + Func, Category, EntityEntry> categoryAdder, + Func, Product, EntityEntry> productAdder, + EntityState expectedState) + => TrackEntitiesTest( + (c, e) => new ValueTask>(categoryAdder(c, e)), + (c, e) => new ValueTask>(productAdder(c, e)), + expectedState); + + private static async Task TrackEntitiesTest( + Func, Category, ValueTask>> categoryAdder, + Func, Product, ValueTask>> productAdder, + EntityState expectedState) + { + using var context = new EarlyLearningCenter(); + var category11 = new Category { Id = 1, Name = "Beverages" }; + var category12 = new Category { Id = 2, Name = "Foods" }; + var product11 = new Product + { + Id = 1, + Name = "Marmite", + Price = 7.99m + }; + var product12 = new Product + { + Id = 2, + Name = "Bovril", + Price = 4.99m + }; + + var category21 = new Category { Id = 1, Name = "Beverages" }; + var category22 = new Category { Id = 2, Name = "Foods" }; + var product21 = new Product + { + Id = 1, + Name = "Marmite", + Price = 7.99m + }; + var product22 = new Product + { + Id = 2, + Name = "Bovril", + Price = 4.99m + }; + + var categoryEntry11 = await categoryAdder(context.Category1s, category11); + var categoryEntry12 = await categoryAdder(context.Category1s, category12); + var categoryEntry21 = await categoryAdder(context.Category2s, category21); + var categoryEntry22 = await categoryAdder(context.Category2s, category22); + var productEntry11 = await productAdder(context.Product1s, product11); + var productEntry12 = await productAdder(context.Product1s, product12); + var productEntry21 = await productAdder(context.Product2s, product21); + var productEntry22 = await productAdder(context.Product2s, product22); + + Assert.Same(category11, categoryEntry11.Entity); + Assert.Same(category12, categoryEntry12.Entity); + Assert.Same(category21, categoryEntry21.Entity); + Assert.Same(category22, categoryEntry22.Entity); + Assert.Same(product11, productEntry11.Entity); + Assert.Same(product12, productEntry12.Entity); + Assert.Same(product21, productEntry21.Entity); + Assert.Same(product22, productEntry22.Entity); + + Assert.Same(category11, categoryEntry11.Entity); + Assert.Equal(expectedState, categoryEntry12.State); + Assert.Same(category11, categoryEntry11.Entity); + Assert.Equal(expectedState, categoryEntry12.State); + Assert.Same(category12, categoryEntry12.Entity); + Assert.Equal(expectedState, categoryEntry12.State); + Assert.Same(category12, categoryEntry12.Entity); + Assert.Equal(expectedState, categoryEntry12.State); + + Assert.Same(product21, productEntry21.Entity); + Assert.Equal(expectedState, productEntry21.State); + Assert.Same(product21, productEntry21.Entity); + Assert.Equal(expectedState, productEntry21.State); + Assert.Same(product22, productEntry22.Entity); + Assert.Equal(expectedState, productEntry22.State); + Assert.Same(product22, productEntry22.Entity); + Assert.Equal(expectedState, productEntry22.State); + + Assert.Same(categoryEntry11.GetInfrastructure(), context.Entry(category11).GetInfrastructure()); + Assert.Same(categoryEntry12.GetInfrastructure(), context.Entry(category12).GetInfrastructure()); + Assert.Same(categoryEntry21.GetInfrastructure(), context.Entry(category21).GetInfrastructure()); + Assert.Same(categoryEntry22.GetInfrastructure(), context.Entry(category22).GetInfrastructure()); + Assert.Same(productEntry11.GetInfrastructure(), context.Entry(product11).GetInfrastructure()); + Assert.Same(productEntry12.GetInfrastructure(), context.Entry(product12).GetInfrastructure()); + Assert.Same(productEntry21.GetInfrastructure(), context.Entry(product21).GetInfrastructure()); + Assert.Same(productEntry22.GetInfrastructure(), context.Entry(product22).GetInfrastructure()); + } + + [ConditionalFact] + public Task Can_add_multiple_new_entities_to_set() + => TrackMultipleEntitiesTest( + (c, e) => c.Category1s.AddRange(e[0], e[1]), + (c, e) => c.Product2s.AddRange(e[0], e[1]), + EntityState.Added); + + [ConditionalFact] + public Task Can_add_multiple_new_entities_to_set_async() + => TrackMultipleEntitiesTest( + (c, e) => c.Category1s.AddRangeAsync(e[0], e[1]), + (c, e) => c.Product2s.AddRangeAsync(e[0], e[1]), + EntityState.Added); + + [ConditionalFact] + public Task Can_add_multiple_existing_entities_to_set_to_be_attached() + => TrackMultipleEntitiesTest( + (c, e) => c.Category1s.AttachRange(e[0], e[1]), + (c, e) => c.Product2s.AttachRange(e[0], e[1]), + EntityState.Unchanged); + + [ConditionalFact] + public Task Can_add_multiple_existing_entities_to_set_to_be_updated() + => TrackMultipleEntitiesTest( + (c, e) => c.Category1s.UpdateRange(e[0], e[1]), + (c, e) => c.Product2s.UpdateRange(e[0], e[1]), + EntityState.Modified); + + [ConditionalFact] + public Task Can_add_multiple_existing_entities_to_set_to_be_deleted() + => TrackMultipleEntitiesTest( + (c, e) => c.Category1s.RemoveRange(e[0], e[1]), + (c, e) => c.Product2s.RemoveRange(e[0], e[1]), + EntityState.Deleted); + + private static Task TrackMultipleEntitiesTest( + Action categoryAdder, + Action productAdder, + EntityState expectedState) + => TrackMultipleEntitiesTest( + (c, e) => + { + categoryAdder(c, e); + return Task.FromResult(0); + }, + (c, e) => + { + productAdder(c, e); + return Task.FromResult(0); + }, + expectedState); + + private static async Task TrackMultipleEntitiesTest( + Func categoryAdder, + Func productAdder, + EntityState expectedState) + { + using var context = new EarlyLearningCenter(); + var category1 = new Category { Id = 1, Name = "Beverages" }; + var category2 = new Category { Id = 2, Name = "Foods" }; + var product1 = new Product + { + Id = 1, + Name = "Marmite", + Price = 7.99m + }; + var product2 = new Product + { + Id = 2, + Name = "Bovril", + Price = 4.99m + }; + + await categoryAdder(context, new[] { category1, category2 }); + await productAdder(context, new[] { product1, product2 }); + + Assert.Same(category1, context.Entry(category1).Entity); + Assert.Same(category2, context.Entry(category2).Entity); + Assert.Same(product1, context.Entry(product1).Entity); + Assert.Same(product2, context.Entry(product2).Entity); + + Assert.Same(category1, context.Entry(category1).Entity); + Assert.Equal(expectedState, context.Entry(category1).State); + Assert.Same(category2, context.Entry(category2).Entity); + Assert.Equal(expectedState, context.Entry(category2).State); + + Assert.Same(product1, context.Entry(product1).Entity); + Assert.Equal(expectedState, context.Entry(product1).State); + Assert.Same(product2, context.Entry(product2).Entity); + Assert.Equal(expectedState, context.Entry(product2).State); + } + + [ConditionalFact] + public void Can_add_no_new_entities_to_set() + => TrackNoEntitiesTest(c => c.Category1s.AddRange(), c => c.Product2s.AddRange()); + + [ConditionalFact] + public async Task Can_add_no_new_entities_to_set_async() + { + using var context = new EarlyLearningCenter(); + await context.Category1s.AddRangeAsync(); + await context.Product2s.AddRangeAsync(); + Assert.Empty(context.ChangeTracker.Entries()); + } + + [ConditionalFact] + public void Can_add_no_existing_entities_to_set_to_be_attached() + => TrackNoEntitiesTest(c => c.Category1s.AttachRange(), c => c.Product2s.AttachRange()); + + [ConditionalFact] + public void Can_add_no_existing_entities_to_set_to_be_updated() + => TrackNoEntitiesTest(c => c.Category1s.UpdateRange(), c => c.Product2s.UpdateRange()); + + [ConditionalFact] + public void Can_add_no_existing_entities_to_set_to_be_deleted() + => TrackNoEntitiesTest(c => c.Category1s.RemoveRange(), c => c.Product2s.RemoveRange()); + + private static void TrackNoEntitiesTest(Action categoryAdder, Action productAdder) + { + using var context = new EarlyLearningCenter(); + categoryAdder(context); + productAdder(context); + Assert.Empty(context.ChangeTracker.Entries()); + } + + [ConditionalFact] + public Task Can_add_multiple_existing_entities_to_set_to_be_deleted_Enumerable() + => TrackMultipleEntitiesTestEnumerable( + (c, e) => c.Category1s.RemoveRange(e), + (c, e) => c.Product2s.RemoveRange(e), + EntityState.Deleted); + + [ConditionalFact] + public Task Can_add_multiple_new_entities_to_set_Enumerable_graph() + => TrackMultipleEntitiesTestEnumerable( + (c, e) => c.Category1s.AddRange(e), + (c, e) => c.Product2s.AddRange(e), + EntityState.Added); + + [ConditionalFact] + public Task Can_add_multiple_new_entities_to_set_Enumerable_graph_async() + => TrackMultipleEntitiesTestEnumerable( + (c, e) => c.Category1s.AddRangeAsync(e), + (c, e) => c.Product2s.AddRangeAsync(e), + EntityState.Added); + + [ConditionalFact] + public Task Can_add_multiple_existing_entities_to_set_to_be_attached_Enumerable_graph() + => TrackMultipleEntitiesTestEnumerable( + (c, e) => c.Category1s.AttachRange(e), + (c, e) => c.Product2s.AttachRange(e), + EntityState.Unchanged); + + [ConditionalFact] + public Task Can_add_multiple_existing_entities_to_set_to_be_updated_Enumerable_graph() + => TrackMultipleEntitiesTestEnumerable( + (c, e) => c.Category1s.UpdateRange(e), + (c, e) => c.Product2s.UpdateRange(e), + EntityState.Modified); + + private static Task TrackMultipleEntitiesTestEnumerable( + Action> categoryAdder, + Action> productAdder, + EntityState expectedState) + => TrackMultipleEntitiesTestEnumerable( + (c, e) => + { + categoryAdder(c, e); + return Task.FromResult(0); + }, + (c, e) => + { + productAdder(c, e); + return Task.FromResult(0); + }, + expectedState); + + private static async Task TrackMultipleEntitiesTestEnumerable( + Func, Task> categoryAdder, + Func, Task> productAdder, + EntityState expectedState) + { + using var context = new EarlyLearningCenter(); + var category1 = new Category { Id = 1, Name = "Beverages" }; + var category2 = new Category { Id = 2, Name = "Foods" }; + var product1 = new Product + { + Id = 1, + Name = "Marmite", + Price = 7.99m + }; + var product2 = new Product + { + Id = 2, + Name = "Bovril", + Price = 4.99m + }; + + await categoryAdder( + context, new List { category1, category2 }); + await productAdder( + context, new List { product1, product2 }); + + Assert.Same(category1, context.Entry(category1).Entity); + Assert.Same(category2, context.Entry(category2).Entity); + Assert.Same(product1, context.Entry(product1).Entity); + Assert.Same(product2, context.Entry(product2).Entity); + + Assert.Same(category1, context.Entry(category1).Entity); + Assert.Equal(expectedState, context.Entry(category1).State); + Assert.Same(category2, context.Entry(category2).Entity); + Assert.Equal(expectedState, context.Entry(category2).State); + + Assert.Same(product1, context.Entry(product1).Entity); + Assert.Equal(expectedState, context.Entry(product1).State); + Assert.Same(product2, context.Entry(product2).Entity); + Assert.Equal(expectedState, context.Entry(product2).State); + } + + [ConditionalFact] + public void Can_add_no_existing_entities_to_set_to_be_deleted_Enumerable() + => TrackNoEntitiesTestEnumerable((c, e) => c.Category2s.RemoveRange(e), (c, e) => c.Product1s.RemoveRange(e)); + + [ConditionalFact] + public void Can_add_no_new_entities_to_set_Enumerable_graph() + => TrackNoEntitiesTestEnumerable((c, e) => c.Category2s.AddRange(e), (c, e) => c.Product1s.AddRange(e)); + + [ConditionalFact] + public async Task Can_add_no_new_entities_to_set_Enumerable_graph_async() + { + using var context = new EarlyLearningCenter(); + await context.Category2s.AddRangeAsync(new HashSet()); + await context.Product1s.AddRangeAsync(new HashSet()); + Assert.Empty(context.ChangeTracker.Entries()); + } + + [ConditionalFact] + public void Can_add_no_existing_entities_to_set_to_be_attached_Enumerable_graph() + => TrackNoEntitiesTestEnumerable((c, e) => c.Category2s.AttachRange(e), (c, e) => c.Product1s.AttachRange(e)); + + [ConditionalFact] + public void Can_add_no_existing_entities_to_set_to_be_updated_Enumerable_graph() + => TrackNoEntitiesTestEnumerable((c, e) => c.Category2s.UpdateRange(e), (c, e) => c.Product1s.UpdateRange(e)); + + private static void TrackNoEntitiesTestEnumerable( + Action> categoryAdder, + Action> productAdder) + { + using var context = new EarlyLearningCenter(); + categoryAdder(context, new HashSet()); + productAdder(context, new HashSet()); + Assert.Empty(context.ChangeTracker.Entries()); + } + + [ConditionalFact] + public async Task Can_use_Add_to_change_entity_state() + { + await ChangeStateWithMethod(c => c.Category2s, (c, e) => c.Add(e), EntityState.Detached, EntityState.Added); + await ChangeStateWithMethod(c => c.Category2s, (c, e) => c.Add(e), EntityState.Unchanged, EntityState.Added); + await ChangeStateWithMethod(c => c.Category2s, (c, e) => c.Add(e), EntityState.Deleted, EntityState.Added); + await ChangeStateWithMethod(c => c.Category2s, (c, e) => c.Add(e), EntityState.Modified, EntityState.Added); + await ChangeStateWithMethod(c => c.Category2s, (c, e) => c.Add(e), EntityState.Added, EntityState.Added); + } + + [ConditionalFact] + public async Task Can_use_Add_to_change_entity_state_async() + { + await ChangeStateWithMethod(c => c.Category2s, async (c, e) => await c.AddAsync(e), EntityState.Detached, EntityState.Added); + await ChangeStateWithMethod(c => c.Category2s, async (c, e) => await c.AddAsync(e), EntityState.Unchanged, EntityState.Added); + await ChangeStateWithMethod(c => c.Category2s, async (c, e) => await c.AddAsync(e), EntityState.Deleted, EntityState.Added); + await ChangeStateWithMethod(c => c.Category2s, async (c, e) => await c.AddAsync(e), EntityState.Modified, EntityState.Added); + await ChangeStateWithMethod(c => c.Category2s, async (c, e) => await c.AddAsync(e), EntityState.Added, EntityState.Added); + } + + [ConditionalFact] + public async Task Can_use_Attach_to_change_entity_state() + { + await ChangeStateWithMethod(c => c.Category2s, (c, e) => c.Attach(e), EntityState.Detached, EntityState.Unchanged); + await ChangeStateWithMethod(c => c.Category2s, (c, e) => c.Attach(e), EntityState.Unchanged, EntityState.Unchanged); + await ChangeStateWithMethod(c => c.Category2s, (c, e) => c.Attach(e), EntityState.Deleted, EntityState.Unchanged); + await ChangeStateWithMethod(c => c.Category2s, (c, e) => c.Attach(e), EntityState.Modified, EntityState.Unchanged); + await ChangeStateWithMethod(c => c.Category2s, (c, e) => c.Attach(e), EntityState.Added, EntityState.Unchanged); + } + + [ConditionalFact] + public async Task Can_use_Update_to_change_entity_state() + { + await ChangeStateWithMethod(c => c.Category2s, (c, e) => c.Update(e), EntityState.Detached, EntityState.Modified); + await ChangeStateWithMethod(c => c.Category2s, (c, e) => c.Update(e), EntityState.Unchanged, EntityState.Modified); + await ChangeStateWithMethod(c => c.Category2s, (c, e) => c.Update(e), EntityState.Deleted, EntityState.Modified); + await ChangeStateWithMethod(c => c.Category2s, (c, e) => c.Update(e), EntityState.Modified, EntityState.Modified); + await ChangeStateWithMethod(c => c.Category2s, (c, e) => c.Update(e), EntityState.Added, EntityState.Modified); + } + + [ConditionalFact] + public async Task Can_use_Remove_to_change_entity_state() + { + await ChangeStateWithMethod(c => c.Category2s, (c, e) => c.Remove(e), EntityState.Detached, EntityState.Deleted); + await ChangeStateWithMethod(c => c.Category2s, (c, e) => c.Remove(e), EntityState.Unchanged, EntityState.Deleted); + await ChangeStateWithMethod(c => c.Category2s, (c, e) => c.Remove(e), EntityState.Deleted, EntityState.Deleted); + await ChangeStateWithMethod(c => c.Category2s, (c, e) => c.Remove(e), EntityState.Modified, EntityState.Deleted); + await ChangeStateWithMethod(c => c.Category2s, (c, e) => c.Remove(e), EntityState.Added, EntityState.Detached); + } + + private Task ChangeStateWithMethod( + Func> getSet, + Action, Category> action, + EntityState initialState, + EntityState expectedState) + => ChangeStateWithMethod( + getSet, + (s, e) => + { + action(s, e); + return Task.FromResult(0); + }, + initialState, + expectedState); + + private async Task ChangeStateWithMethod( + Func> getSet, + Func, Category, Task> action, + EntityState initialState, + EntityState expectedState) + { + using var context = new EarlyLearningCenter(); + var entity = new Category { Id = 1, Name = "Beverages" }; + var set = getSet(context); + var entry = set.Entry(entity); + + entry.State = initialState; + + await action(set, entity); + + Assert.Equal(expectedState, entry.State); + } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public async Task Can_add_new_entities_to_context_with_key_generation(bool async) + { + using var context = new EarlyLearningCenter(); + var gu1 = new TheGu { ShirtColor = "Red" }; + var gu2 = new TheGu { ShirtColor = "Still Red" }; + + if (async) + { + Assert.Same(gu1, (await context.Gu1s.AddAsync(gu1)).Entity); + Assert.Same(gu2, (await context.Gu1s.AddAsync(gu2)).Entity); + } + else + { + Assert.Same(gu1, context.Gu1s.Add(gu1).Entity); + Assert.Same(gu2, context.Gu1s.Add(gu2).Entity); + } + + Assert.NotEqual(default, gu1.Id); + Assert.NotEqual(default, gu2.Id); + Assert.NotEqual(gu1.Id, gu2.Id); + + var categoryEntry = context.Entry(gu1); + Assert.Same(gu1, categoryEntry.Entity); + Assert.Equal(EntityState.Added, categoryEntry.State); + + categoryEntry = context.Entry(gu2); + Assert.Same(gu2, categoryEntry.Entity); + Assert.Equal(EntityState.Added, categoryEntry.State); + } + + [ConditionalFact] + public void Can_get_scoped_service_provider() + { + using var context = new EarlyLearningCenter(); + Assert.Same( + ((IInfrastructure)context).Instance, + ((IInfrastructure)context.Product1s).Instance); + } + + [ConditionalFact] + public void Throws_when_using_with_IListSource() + { + using var context = new EarlyLearningCenter(); + Assert.Equal( + CoreStrings.DataBindingWithIListSource, + Assert.Throws(() => ((IListSource)context.Gu1s).GetList()).Message); + } + + [ConditionalFact] + public void Throws_when_using_query_with_IListSource() + { + using var context = new EarlyLearningCenter(); + Assert.Equal( + CoreStrings.DataBindingWithIListSource, + Assert.Throws(() => ((IListSource)context.Gu1s.Distinct()).GetList()).Message); + } + + [ConditionalFact] + public void Throws_when_using_Local_with_IListSource() + { + using var context = new EarlyLearningCenter(); + Assert.Equal( + CoreStrings.DataBindingToLocalWithIListSource, + Assert.Throws(() => ((IListSource)context.Gu1s.Local).GetList()).Message); + } + + [ConditionalFact] + public void Can_enumerate_with_foreach() + { + using var context = new EarlyLearningCenter(); + foreach (var _ in context.Category2s) + { + throw new Exception("DbSet should be empty"); + } + } + + [ConditionalFact] + public async Task Can_enumerate_with_await_foreach() + { + using var context = new EarlyLearningCenter(); + await foreach (var _ in context.Category2s) + { + throw new Exception("DbSet should be empty"); + } + } + + [ConditionalFact] + public async Task Can_enumerate_with_await_foreach_with_cancellation() + { + using var context = new EarlyLearningCenter(); + await foreach (var _ in context.Category2s.AsAsyncEnumerable().WithCancellation(default)) + { + throw new Exception("DbSet should be empty"); + } + } + + private class Category + { + public int Id { get; set; } + public string Name { get; set; } + } + + private class Product + { + public int Id { get; set; } + public string Name { get; set; } + public decimal Price { get; set; } + } + + private class TheGu + { + public Guid Id { get; set; } + public string ShirtColor { get; set; } + } + + private class EarlyLearningCenter : DbContext + { + public DbSet Product1s => Set("Product1"); + public DbSet Product2s => Set("Product2"); + public DbSet Category1s => Set("Category1"); + public DbSet Category2s => Set("Category2"); + public DbSet Gu1s => Set("TheGu1"); + public DbSet Gu2s => Set("TheGu2"); + + protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .UseInternalServiceProvider(InMemoryTestHelpers.Instance.CreateServiceProvider()); + + protected internal override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.SharedTypeEntity>("SharedEntity").IndexerProperty("Id"); + + modelBuilder.SharedTypeEntity("Product1"); + modelBuilder.SharedTypeEntity("Product2"); + + modelBuilder.SharedTypeEntity("Category1"); + modelBuilder.SharedTypeEntity("Category2"); + + modelBuilder.SharedTypeEntity("TheGu1"); + modelBuilder.SharedTypeEntity("TheGu2"); + } + } +}