Vladimir
Vladimir

Reputation: 1420

Adding a child entity to parent's collection fails during a second call to SaveChanges() in Entity Framework Core

I have a parent/child relationship in EF Core and I want to add a parent entity, save changes, then add a child to that parent and save the changes again (Note: This is part of a much more complex code that runs in a transaction and needs the intermediate SaveChanges, but here I have extracted only the code needed to show the problem).

If I add a child to the new parent before the first SaveChanges call, it works fine. However, if I add it after the first SaveChanges call, I get an error:

Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded. See https://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.

This worked fine in Entity Framework (non-Core), but now fails in EF Core.

If I manually add the child entity to the appropriate db set (instead of the children's collection), it works fine.

Is there anything I can do to fix this on a general level, e.g. by configuring the context in a different way?

Here is the full code to simulate the problem:

internal class Program
{
    static void Main(string[] args)
    {
        // Empty everything from previous runs
        {
            using var db = new BloggingContext();
            var posts = db.Posts.ToList();
            var blogs = db.Blogs.ToList();

            foreach (var post in posts)
                db.Remove(post);

            foreach (var blog in blogs)
                db.Remove(blog);

            db.SaveChanges();
        }

        try
        {
            using var db = new BloggingContext();

            var blog = new Blog
            {
                Id = 1,
                Name = "Blog 1",
                SiteUri = "http://blogs.msdn.com/adonet",
                Posts = new List<Post>()
            };

            db.Add(blog);

            var post1 = new Post
            {
                Id = 11,
                BlogId = 1,
                Title = "Hello World",
                Content = "I wrote an app using EF Core!"
            };

            blog.Posts.Add(post1);

            db.SaveChanges();

            var post2 = new Post
            {
                Id = 12,
                BlogId = 1,
                Title = "Hello World 2",
                Content = "I wrote an app using EF Core 2!"
            };

            blog.Posts.Add(post2);
            // db.Posts.Add(post2); --> With this line it works

            db.SaveChanges();

            Console.WriteLine("Done");
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }
}

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string SiteUri { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public DateTime PublishedOn { get; set; }
    public bool Archived { get; set; }

    public int BlogId { get; set; }
    public Blog Blog { get; set; }
}

public class BloggingContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    public BloggingContext()
    {
    }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlServer($"Server=.;Database=DbTests;Trusted_Connection=True;Encrypt=False");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .HasMany(e => e.Posts)
            .WithOne(e => e.Blog)
            .HasForeignKey(e => e.BlogId)
            .HasPrincipalKey(e => e.Id);
    }
}

And the SQL for the database model:

CREATE TABLE [Blogs] 
(
    [Id] int NOT NULL,
    [Name] nvarchar(max) NULL,
    [SiteUri] nvarchar(max) NULL,
    CONSTRAINT [PK_Blogs] PRIMARY KEY ([Id])
);

CREATE TABLE [Posts] 
(
    [Id] int NOT NULL,
    [Title] nvarchar(max) NULL,
    [Content] nvarchar(max) NULL,
    [PublishedOn] datetime2 NOT NULL,
    [Archived] bit NOT NULL,
    [BlogId] int NOT NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id]) ON DELETE CASCADE
);

Upvotes: 0

Views: 122

Answers (1)

For default, EF Core treat Primary Key as generated from database. In you sample code, PK is generated from client. The cause root is you forgot to config this.

In your case, use [DatabaseGenerated(DatabaseGeneratedOption.None)] for Id of Blog and Post, or using corresponding Fluent API configuration in DbContext.

Upvotes: 1

Related Questions