EF Core version: 5.0.0-rc.2.20475.6
Database provider: Microsoft.EntityFrameworkCore.Sqlite
Target framework: net5.0
Example.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.0-rc.2.20475.6" />
</ItemGroup>
</Project>
Program.cs
public class BloggingContext : DbContext
{
private DbConnection _connection;
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite(CreateDatabaseAndGetConnection());
}
private DbConnection CreateDatabaseAndGetConnection()
{
_connection = new SqliteConnection("Data Source=:memory:");
_connection.Open();
using (var context = new BloggingContext())
{
context.GetService<IRelationalDatabaseCreator>().CreateTables();
}
return _connection;
}
public override void Dispose()
{
base.Dispose();
_connection.Dispose();
}
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public int BlogId { get; set; }
}
class Program
{
static void Main(string[] args)
{
using (var db = new BloggingContext())
{
db.Database.Migrate();
}
using (var db = new BloggingContext())
{
var blog = new Blog
{
BlogId = 1,
Url = "http://sample.com",
Posts = new List<Post>()
{
new Post()
{
PostId = 1,
Title = "title1"
},
new Post()
{
PostId = 2,
Title = "title2"
}
}
};
db.Blogs.Add(blog);
db.SaveChanges();
}
using (var db = new BloggingContext())
{
var blog = db.Blogs.Include(x => x.Posts).First(x => x.BlogId == 1);
blog.Posts.Clear();
blog.Posts.AddRange(new List<Post>()
{
new Post()
{
PostId = 1,
Title = "title1"
},
new Post()
{
PostId = 2,
Title = "title2"
}
});
db.SaveChanges();
}
Console.WriteLine("Hello World!");
}
}
dotnet build
C:\Example>dotnet build
Microsoft (R) Build Engine version 16.8.0-preview-20475-05+aed5e7ed0 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.
Determining projects to restore...
All projects are up-to-date for restore.
You are using a preview version of .NET. See: https://aka.ms/dotnet-core-preview
Example -> C:\Example\bin\Debug\net5.0\Example.dll
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:00.83
dotnet run
C:\Example>dotnet run
Stack overflow.
It seems that the AcceptChanges method caused the endless loop.
https://github.com/dotnet/efcore/blob/3199752b8785feec835e81455d12182306b1563f/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs#L1293
https://github.com/dotnet/efcore/blob/3199752b8785feec835e81455d12182306b1563f/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs#L1264
@maliming You're creating a new BloggingContext in OnConfiguring. Using this context in turn calls OnConfiguring, which then creates a new BloggingContext, and so on.
Note for triage: we detect and throw a better message when the _same context instance_ is used while it is being configured. In this case it is a different context instance each time. We may be able to detect and throw a better message in this case too.
hi @ajcvickers
I actually encountered another problem, I will try to reproduce it.
hi
The following code will cause an endless loop:
Output a lot of at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.AcceptChanges()
Although my code is unconventional, better error messages will help. : )
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.AcceptChanges()
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.AcceptChanges()
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.AcceptAllChanges(System.Collections.Generic.IReadOnlyList`1<Microsoft.EntityFrameworkCore.Update.IUpdateEntry>)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(Microsoft.EntityFrameworkCore.DbContext, Boolean)
at Microsoft.EntityFrameworkCore.Storage.NonRetryingExecutionStrategy.Execute[[System.Boolean, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Int32, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](Boolean, Syste
m.Func`3<Microsoft.EntityFrameworkCore.DbContext,Boolean,Int32>, System.Func`3<Microsoft.EntityFrameworkCore.DbContext,Boolean,Microsoft.EntityFrameworkCore.Storage.ExecutionResult`1<Int32>>)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(Boolean)
at Microsoft.EntityFrameworkCore.DbContext.SaveChanges(Boolean)
at Microsoft.EntityFrameworkCore.DbContext.SaveChanges()
at Example.Program.Main(System.String[])
public static class SqliteMemoryConnection
{
public static readonly SqliteConnection Connection;
static SqliteMemoryConnection()
{
Connection = new SqliteConnection("Data Source=:memory:");
Connection.Open();
}
}
public class BloggingContext : DbContext
{
public static DbContextOptions Options;
public BloggingContext(DbContextOptions options)
:base(options)
{
}
public BloggingContext()
{
}
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite(SqliteMemoryConnection.Connection);
}
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public int BlogId { get; set; }
}
class Program
{
static void Main(string[] args)
{
using (var context = new BloggingContext(new DbContextOptionsBuilder().UseSqlite(SqliteMemoryConnection.Connection).Options))
{
context.GetService<IRelationalDatabaseCreator>().CreateTables();
}
using (var db = new BloggingContext())
{
var blog = new Blog
{
BlogId = 1,
Url = "http://sample.com",
Posts = new List<Post>()
{
new Post()
{
PostId = 1,
Title = "title1"
},
new Post()
{
PostId = 2,
Title = "title2"
}
}
};
db.Blogs.Add(blog);
db.SaveChanges();
}
using (var db = new BloggingContext())
{
var blog = db.Blogs.Include(x => x.Posts).First(x => x.BlogId == 1);
var post1 = blog.Posts.First(x => x.PostId == 1);
var post2 = blog.Posts.First(x => x.PostId == 2);
blog.Posts.Clear();
blog.Posts.AddRange(new List<Post>()
{
new Post()
{
PostId = 1,
Title = "title1"
},
new Post()
{
PostId = 2,
Title = "title2"
}
});
db.Entry(post1).State = EntityState.Modified;
db.Entry(post2).State = EntityState.Modified;
db.SaveChanges();
}
Console.WriteLine("Hello World!");
}
}
@maliming Thanks; I am able to reproduce this now.
Notes for team triage: The behavior is the same in 3.1; not a regression. What is happening is this:
Blog {BlogId: 1} Unchanged
BlogId: 1 PK
Url: 'http://sample.com'
Posts: [{PostId: 1}, {PostId: 2}]
Post (Shared) {PostId: 1} Modified
PostId: 1 PK
BlogId: 1 FK Modified Originally 0
Title: 'title1' Modified
Post (Shared) {PostId: 1} Deleted
PostId: 1 PK
BlogId: 1 FK
Title: 'title1'
Post (Shared) {PostId: 2} Modified
PostId: 2 PK
BlogId: 1 FK Modified Originally 0
Title: 'title2' Modified
Post (Shared) {PostId: 2} Deleted
PostId: 2 PK
BlogId: 1 FK
Title: 'title2'
Post (Shared) {PostId: 1} Modified
PostId: 1 PK
BlogId: 1 FK Modified Originally 0
Title: 'title1' Modified
Post (Shared) {PostId: 1} Modified
PostId: 1 PK
BlogId: 1 FK Modified
Title: 'title1' Modified
Post (Shared) {PostId: 2} Modified
PostId: 2 PK
BlogId: 1 FK Modified Originally 0
Title: 'title2' Modified
Post (Shared) {PostId: 2} Modified
PostId: 2 PK
BlogId: 1 FK Modified
Title: 'title2' Modified
info: 10/21/2020 16:38:28.237 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (1ms) [Parameters=[@p2='1' (DbType = String), @p0='1' (DbType = String), @p1='title1' (Size = 6)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Title" = @p1
WHERE "PostId" = @p2;
SELECT changes();
info: 10/21/2020 16:38:28.237 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (0ms) [Parameters=[@p2='1' (DbType = String), @p0='1' (DbType = String), @p1='title1' (Size = 6)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Title" = @p1
WHERE "PostId" = @p2;
SELECT changes();
info: 10/21/2020 16:38:28.237 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (0ms) [Parameters=[@p2='2' (DbType = String), @p0='1' (DbType = String), @p1='title2' (Size = 6)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Title" = @p1
WHERE "PostId" = @p2;
SELECT changes();
info: 10/21/2020 16:38:28.237 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (0ms) [Parameters=[@p2='2' (DbType = String), @p0='1' (DbType = String), @p1='title2' (Size = 6)], CommandType='Text', CommandTimeout='30']
UPDATE "Posts" SET "BlogId" = @p0, "Title" = @p1
WHERE "PostId" = @p2;
SELECT changes();
Note from triage: this should throw as soon as the Deleted instances are changed back to Modified.