问题
What I can't understand fully is exemplified in a unit test below. Basically I want to know why DbContext
overrides collection values currently stored in the database even if Reload()
and a new load for the collection has been called.
If I try to call dbContext.Entry(test.TestChildren).Reload();
I get the exception:
System.InvalidOperationException: 'The entity type 'List' was not found. Ensure that the entity type has been added to the model.'
I have also loaded TestChildren
explicitly via dbContext.Entry(test).Collection(x => x.TestChildren).Load();
before calling Assert
.
https://docs.microsoft.com/en-us/ef/core/querying/related-data/explicit#explicit-loading
I can get it working by creating a new context using (var context = new ApplicationDbContext(dbContextOptions))
and then execute a load on related entities.
However according to Microsoft Docs:
Queries are always executed against the database even if the entities returned in the result already exist in the context.
https://docs.microsoft.com/en-us/ef/core/querying/
The actual values are saved in LocalDb and SQLite file, SQLite in-memory and EF in-memory database looks correct.
https://docs.microsoft.com/en-us/ef/core/testing/sqlite
LocalDb after assert has failed:
EFC 3.1.9 with .NET Core 3.1:
EFC 5.0.2 with .NET 5:
As shown I can get simple properties to load correctly with Reload()
. I have tried the solution from @IvanStoev to reload collections but it does not work for me.
https://stackoverflow.com/a/57062852/3850405
I know that most examples have a new using statement per database call but it is normally not a problem to do several calls with one context and Microsoft has several examples with that as well.
https://docs.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs=netcore-cli#create-read-update--delete
My theory is that even though the values are in the database and then fetched DbContext
still overrides with the tracked entities because they are probably marked as Modified
. This would also explain why EntityEntry.Reload
method sets EntityState
to Unchanged
.
https://docs.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.changetracking.entityentry.reload?view=efcore-3.1
https://docs.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.entitystate?view=efcore-3.1#Microsoft_EntityFrameworkCore_EntityState_Unchanged
https://docs.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.changetracking.entityentry.collection?view=efcore-3.1
My three questions:
- How can I reload the collection for the entity? The answers I have already tried does not work.
https://stackoverflow.com/a/9084787/3850405 https://stackoverflow.com/a/57062852/3850405
Is my theory correct or why are values not updated when a new database call is made with
Collection(x => x.TestChildren).Load()
?Is there anyway to disable the need to use
Reload()
behavior or can that have dire consequences? I have seen examples withdbContext.ChangeTracker.QueryTrackingBehavior = Microsoft.EntityFrameworkCore.QueryTrackingBehavior.NoTracking;
but I have not dared to try it in production systems. I don't want to use Lazy loading.
https://docs.microsoft.com/en-us/ef/core/querying/related-data/lazy
https://stackoverflow.com/a/53099150/3850405
I have read a different question about a proper way to replace collection in one to many relationship in Entity Framework but I have not found an answer to my question there.
https://stackoverflow.com/a/31512648/3850405
It is a bit similar to the question on how to refresh an Entity Framework Core DBContext
but I would like to know why it happens and there is also no example with a collection.
How to refresh an Entity Framework Core DBContext?
To explain and keep it as simple as possible I have a model very similar to the Getting Started with EF Core
model.
https://docs.microsoft.com/en-us/ef/core/get-started/overview/first-app?tabs=netcore-cli#create-read-update--delete
ApplicationDbContext:
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(
DbContextOptions options) : base(options)
{
}
public DbSet<Test> Tests { get; set; }
public DbSet<TestChild> TestChildren { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}
Test model:
public class Test
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Name { get; set; }
public List<TestChild> TestChildren { get; set; } = new List<TestChild>();
}
public class TestChild
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Name { get; set; }
public int TestId { get; set; }
public Test Test { get; set; }
}
UnitTest1:
using System;
using Xunit;
using EFCoreTest;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using System.Linq;
using System.Data.Common;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore.ChangeTracking;
namespace XUnitTestEFCore
{
public static class Extensions
{
public static void Reload(this CollectionEntry source)
{
if (source.CurrentValue != null)
{
foreach (var item in source.CurrentValue)
source.EntityEntry.Context.Entry(item).State = EntityState.Detached;
source.CurrentValue = null;
}
source.IsLoaded = false;
source.Load();
}
}
public class UnitTest1
{
[Fact]
public void TestSqliteFile()
{
var dbContextOptions = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlite("Data Source=test.db")
.Options;
var dbContext = new ApplicationDbContext(dbContextOptions);
TestSingleProprtyMethod(dbContext, dbContextOptions);
TestCollectionMethod(dbContext, dbContextOptions);
}
[Fact]
public void TestSqliteInMemory()
{
var dbContextOptions = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlite(CreateInMemoryDatabase())
.Options;
var connection = RelationalOptionsExtension.Extract(dbContextOptions).Connection;
var dbContext = new ApplicationDbContext(dbContextOptions);
TestCollectionMethod(dbContext, dbContextOptions);
}
[Fact]
public void TestEFInMemoryDatabase()
{
var dbContextOptions = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
var dbContext = new ApplicationDbContext(dbContextOptions);
TestCollectionMethod(dbContext, dbContextOptions);
}
[Fact]
public void TestLocalDb()
{
var dbContextOptions = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=XUnitTestEFCore;Trusted_Connection=True;MultipleActiveResultSets=true")
.Options;
var dbContext = new ApplicationDbContext(dbContextOptions);
TestCollectionMethod(dbContext, dbContextOptions);
}
private static DbConnection CreateInMemoryDatabase()
{
var connection = new SqliteConnection("Data Source=:memory:");
connection.Open();
return connection;
}
private void TestCollectionMethod(ApplicationDbContext dbContext, DbContextOptions<ApplicationDbContext> dbContextOptions)
{
dbContext = new ApplicationDbContext(dbContextOptions);
Seed(dbContext);
//Works - Nothing in Db
Assert.Null(dbContext.Tests.FirstOrDefault());
dbContext.SaveChanges();
//Works - First item in Db
Assert.NotNull(dbContext.Tests.FirstOrDefault());
//Works - TestChildren are correctly added
Assert.Equal(2, dbContext.Tests.FirstOrDefault().TestChildren.Count);
using (var context = new ApplicationDbContext(dbContextOptions))
{
var usingTest = context.Tests.FirstOrDefault();
context.Entry(usingTest)
.Collection(x => x.TestChildren)
.Load();
//Works
Assert.Equal(2, usingTest.TestChildren.Count);
}
var test = dbContext.Tests.FirstOrDefault();
test.TestChildren = test.TestChildren.Where(x => x.Id == 1).ToList();
var count = dbContext.Entry(test)
.Collection(x => x.TestChildren)
.Query()
.Count();
//Works - Two items
Assert.Equal(2, count);
//Works - Two items
Assert.Equal(2, dbContext.TestChildren.Where(x => x.TestId == 1).Count());
using (var context = new ApplicationDbContext(dbContextOptions))
{
//Works - Two items
Assert.Equal(2, context.TestChildren.Where(x => x.TestId == 1).Count());
}
using (var context = new ApplicationDbContext(dbContextOptions))
{
var usingTest = context.Tests.FirstOrDefault();
context.Entry(usingTest)
.Collection(x => x.TestChildren)
.Load();
//Works - Two items
Assert.Equal(2, usingTest.TestChildren.Count);
}
dbContext.Entry(test).Reload();
dbContext.Entry(test)
.Collection(x => x.TestChildren)
.Load();
//Source - https://stackoverflow.com/a/57062852/3850405
dbContext.Entry(test).Collection(x => x.TestChildren).Reload();
//dbContext.Entry(test.TestChildren).Reload(); Causes exception System.InvalidOperationException: 'The entity type 'List<TestChild>' was not found. Ensure that the entity type has been added to the model.'
//Only one TestChild left even though no save has been performed, test is Reloaded and TestChildren has been explicitly loaded
Assert.Equal(2, test.TestChildren.Count);
}
private void TestSingleProprtyMethod(ApplicationDbContext dbContext, DbContextOptions<ApplicationDbContext> dbContextOptions)
{
Seed(dbContext);
Assert.Null(dbContext.Tests.FirstOrDefault());
dbContext.SaveChanges();
Assert.NotNull(dbContext.Tests.FirstOrDefault());
var test2 = dbContext.Tests.FirstOrDefault();
test2.Name = "test2";
//Will be test2
Assert.NotEqual("test1", dbContext.Tests.FirstOrDefault().Name);
dbContext.Entry(test2).Reload();
Assert.Equal("test1", dbContext.Tests.FirstOrDefault().Name);
using (var context = new ApplicationDbContext(dbContextOptions))
{
Assert.Equal("test1", context.Tests.FirstOrDefault().Name);
}
}
private void Seed(ApplicationDbContext dbContext)
{
dbContext.Database.EnsureDeleted();
dbContext.Database.EnsureCreated();
var test1 = new Test()
{
Name = "test1"
};
var testChild1 = new TestChild()
{
Name = "testChild1"
};
var testChild2 = new TestChild()
{
Name = "testChild2"
};
test1.TestChildren.Add(testChild1);
test1.TestChildren.Add(testChild2);
dbContext.Tests.Add(test1);
}
}
}
回答1:
The recommended way for dealing with changing data state is to keep the DbContext lifespan short, so dispose and use a new DbContext.
As for reloading a collection a DbContext is pretty "clingy" when it comes to navigation properties and collections. This occurs when making tracked changes that are not committed and trying to revert to data state, or trying to update DbContext state to reflect changes that may have been done in the database. Load
and Reload
work for refreshing fields, but when it comes to collections/references this isn't reliable.
To refresh a collection on an entity, about the only reliable option I have seen work outside of simply keeping DbContext lifespans short is something like this:
foreach(var child in test.TestChildren)
context.Entry(child).State = EntityState.Detached;
context.Entry(test).State = EntityState.Detached;
test.TestChildren.Clear();
context.Tests.Attach(test);
context.Entry(test).Collection(x => x.TestChildren).Query().Load();
We detach all of the children to stop the DbContext from tracking them, then also temporarily detach the parent from the DbContext as well. This is so that we can clear any object references from the parent's children collection. We then re-attach and tell the DbContext to reload the children. From there, test.TestChildren will reflect the persisted data state, so any changes made prior to reloading it are gone, and any database side changes since it was last loaded are brought up to date.
As a general rule with EF collections you should always avoid replacing the collection reference, instead adding, removing, and clearing via the EF proxies when tracking is happening.
I.e.
//(not good)
test.TestChildren = test.TestChildren.Where(x => x.Id == 1).ToList();
//(good)
var childrenToRemove = test.TestChildren.Where(x => x.Id != 1).ToList();
foreach(var child in childrenToRemove)
test.TestChildren.Remove(child);
Replacing collection references can lead to all kinds of unexpected behavior and will vary depending on whether any/all have been eager loaded / present in the DbContext cache. In the case of unit/integration test code we can break rules for a fixed, known state. Detaching the parent first helps avoid tracked state changes against the set.
来源:https://stackoverflow.com/questions/65723105/replacing-a-entity-collection-in-entity-framework-core-causes-dbcontext-to-fetch