Skip to content
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

Stack overflow when saving multiple modified entities with the same key #23043

Closed
maliming opened this issue Oct 19, 2020 · 5 comments · Fixed by #25897
Closed

Stack overflow when saving multiple modified entities with the same key #23043

maliming opened this issue Oct 19, 2020 · 5 comments · Fixed by #25897
Labels
area-change-tracking area-save-changes closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-bug
Milestone

Comments

@maliming
Copy link

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.

SharedIdentityEntry?.AcceptChanges();

public virtual void AcceptChanges()

@ajcvickers
Copy link
Contributor

@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.

@maliming
Copy link
Author

hi @ajcvickers

I actually encountered another problem, I will try to reproduce it.

@ajcvickers ajcvickers reopened this Oct 21, 2020
@maliming
Copy link
Author

maliming commented Oct 21, 2020

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!");
      }
}

@ajcvickers
Copy link
Contributor

@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:

  • Posts with IDs 1 and 2 are queried from the database and so are tracked as Unchanged
  • These two post instances are then removed from the blog.Posts navigation. They are still tracked.
  • Two new instances also with IDs 1 and 2 are added to the blog.Posts navigation. (These are tracked as Modified, since they are configured with generated keys and have key values set. However, the behavior is the same when the keys are not generated and hence the new entities are in the Added state.) So the state manager has:
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'
  • At this point the state of the original two entities is changed to Modified. This results in a state manager like this:
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
  • SaveChanges then does this:
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();
  • So we generate two updates for each entity, which is wrong but doesn't fail. AcceptChanges then enters an infinite loop.

@ajcvickers ajcvickers changed the title [Bug report] Delete and try to add one-to-many entities. Stack overflow. Stack overflow when saving multiple modified entities with the same key Oct 23, 2020
@ajcvickers
Copy link
Contributor

Note from triage: this should throw as soon as the Deleted instances are changed back to Modified.

@ajcvickers ajcvickers added this to the 6.0.0 milestone Oct 23, 2020
@ajcvickers ajcvickers added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Sep 7, 2021
@ajcvickers ajcvickers modified the milestones: 6.0.0, 6.0.0-rc2 Sep 8, 2021
@ajcvickers ajcvickers modified the milestones: 6.0.0-rc2, 6.0.0 Nov 8, 2021
@ajcvickers ajcvickers removed their assignment Sep 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-change-tracking area-save-changes closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-bug
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants