Update of navigation property in EF7 does not get saved

asp.net-core entity-framework-core

Question

I have simple model:

public class Post
{
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public long Id { set; get; }
        //non-important properties stripped, to focus on problem
        public virtual Resource Resource { set; get; }

        public virtual ICollection<Tag> Tags { set; get; }
}
public class Resource
{
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public Guid Id { set; get; }

        [Url]
        public string Url { set; get; }
}

and DbContext (I use ASP.NET identity in this project, if this is relevant):

public class DbContext : IdentityDbContext<User>
{
       protected override void OnModelCreating(ModelBuilder modelBuilder)
       {
            base.OnModelCreating(modelBuilder);
            var postEntity = modelBuilder.Entity<Post>();
            postEntity.Reference(p => p.Resource).InverseCollection(); //no navigation property on Resource side
            postEntity.Collection(p => p.Tags).InverseReference(tag => tag.Post);
            postEntity.ToTable("Posts");

            var resourceEntity = modelBuilder.Entity<Resource>();
            resourceEntity.ToTable("Resources");

            var tagEntity = modelBuilder.Entity<Tag>();
            tagEntity.Reference(t => t.Post).InverseCollection(p => p.Tags).Required(false);
            tagEntity.ToTable("Tags");
       }
}

After migration (SQL Server), database tables looks like expected - Post table has Foreign Key to ResourceId.

Creating Post's works fine, when I attach post.Resource (already created Resource).

My problem occurs when I want to replace post.Resource. By replace, I mean selecting one of already existing Resources and assigning it to post.

var resource2 = Database.Resources.First(r=>r.Url == "xyz");

I have tried:

  1. post.Resource = resource2; Database.Entry(post).State = EntityState.Modified;

  2. Database.Entry(post).Property(p => p.Resource).CurrentValue = resource2;

  3. post.Resource = null;

With different combinations of them also, but none of them works. After calling await SaveChangesAsync(); and looking up in database - there are no changes. How to perform the replace (update of foreign key) properly?

//Update 14.09.2015 Issue was caused by additional select, performed to update One-To-Many relationship. Full code:

        var Database new DbContext();
        var resource2 = Database.Resources.First(r=>r.Url == "xyz");
        var oldAssignedTags = Database.Posts.Include(p=>p.Tags).FirstOrDefault(p => p.Id == post.Id).Tags;
        var tags = newTags as List<Tag> ?? newTags.ToList();
        //TagComparer is irrelevant here
        var toRemove = oldAssignedTags.Except(tags, TagComparer);
        var toAdd = tags.Except(oldAssignedTags, TagComparer);

        foreach (var tag in toRemove)
        {
            Database.Entry(tag).State = EntityState.Deleted; //Database.Tags.Remove(tag);
        }

        foreach (var tag in toAdd)
        {
            tag.Post = post;
            post.Tags.Add(tag);
        }

        post.Resource = resource2;
        await Database.SaveChangesAsync();

Accepted Answer

I thought this may have something to do with Eager Loading, however I can't reproduce your issue with or without AspNet.Identity. Running the below code results in the Resource always being updated. Using EntityFramework 7.0.0-beta7.

Code

static void Main(string[] args)
{
    Init();
    WithEagerLoading();
    CleanUp();

    Init();
    WithoutEagerLoading();
    CleanUp();
}

private static void WithoutEagerLoading()
{
    var db = new MyContext();
    var post = db.Posts.First(); // no eager loading of child
    post.Resource = db.Resources.First(p => p.Url == "http://trend.atqu.in");
    db.SaveChanges();

    Console.WriteLine($"2nd Resource.Id: {new MyContext().Posts.Include(p => p.Resource).First().Resource.Id}");
}

private static void WithEagerLoading()
{
    var db = new MyContext();
    var post = db.Posts
        .Include(p => p.Resource) // eager loading
        .First();
    post.Resource = db.Resources.First(p => p.Url == "http://trend.atqu.in");
    db.SaveChanges();

    Console.WriteLine($"2nd Resource.Id: {new MyContext().Posts.Include(p => p.Resource).First().Resource.Id}");
}

private static void CleanUp()
{
    var db = new MyContext();
    db.Posts.RemoveRange(db.Posts);
    db.Resources.RemoveRange(db.Resources);
    db.SaveChanges();
}

private static void Init()
{
    var db = new MyContext();
    var resource = new Resource { Url = "http://atqu.in" };
    var resource2 = new Resource { Url = "http://trend.atqu.in" };
    var post = new Post { Resource = resource };
    db.Add(post);
    db.Add(resource);
    db.Add(resource2);
    db.SaveChanges();

    db = new MyContext();
    post = db.Posts.Include(p => p.Resource).First();
    resource = db.Resources.First(p => p.Url == "http://trend.atqu.in");
    Console.WriteLine($"1st Resource.Id: {post.Resource.Id}");
}

Result

1st Resource.Id: 0f4d222b-4184-4a4e-01d1-08d2bc9cea9b
2nd Resource.Id: 00ccae9c-f0da-43e6-01d2-08d2bc9cea9b
1st Resource.Id: 749f08f0-2426-4043-01d3-08d2bc9cea9b
2nd Resource.Id: 2e83b512-e8bd-4583-01d4-08d2bc9cea9b

Edit 16/9

The problem in the edited question's code is because you are instantiating Database after you have retrieved post. post is not attached to that instance of Database, so when you attach the Resource to it and call SaveChangesAsync it does nothing, because post at that time has noting to do with the Database you are saving against. That is why your workaround of selecting post post again (after the instantiation) causes it to be fixed - because then the instance of post is attached. If you don't want to select post again, you should use the same DbContext instance to do the work above that you used to originally retrieve post.




Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why