dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.66k stars 3.15k forks source link

Call to change tracker changes unchanged many to many relationship to deleted #26831

Open luizfbicalho opened 2 years ago

luizfbicalho commented 2 years ago

I have one update in some entities I clear a list of entities , then I update it with the new entities (in this examples the new list has the same old entities)

If I call context.ChangeTracker.Entries(); after the list list clear, and before the new entities added, I get that the relationship is deleted

Left {Id: 111} Modified Right {Id: 151} Unchanged Right {Id: 152} Unchanged Right {Id: 153} Unchanged LeftRight (Dictionary<string, object>) {LeftsId: 111, RightsId: 151} Deleted FK {LeftsId: 111} FK {RightsId: 151} LeftRight (Dictionary<string, object>) {LeftsId: 111, RightsId: 152} Deleted FK {LeftsId: 111} FK {RightsId: 152} LeftRight (Dictionary<string, object>) {LeftsId: 111, RightsId: 153} Deleted FK {LeftsId: 111} FK {RightsId: 153}

If I don't call the context.ChangeTracker.Entries(); the result is this

Left {Id: 114} Modified Right {Id: 155} Unchanged Right {Id: 156} Unchanged Right {Id: 157} Unchanged LeftRight (Dictionary<string, object>) {LeftsId: 114, RightsId: 155} Unchanged FK {LeftsId: 114} FK {RightsId: 155} LeftRight (Dictionary<string, object>) {LeftsId: 114, RightsId: 156} Unchanged FK {LeftsId: 114} FK {RightsId: 156} LeftRight (Dictionary<string, object>) {LeftsId: 114, RightsId: 157} Unchanged FK {LeftsId: 114} FK {RightsId: 157}

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace TesteManyToManyErro
{
    public class Program
    {
        static void Main(string[] args)
        {
            var optionsBuilder = new DbContextOptionsBuilder<ModelContext>().UseLazyLoadingProxies()
                .UseSqlServer("Server=.\\sql2019;Database=ManyToMany;Trusted_Connection=false;user id=sa;pwd=sa!2019;MultipleActiveResultSets=true");
            using (var context = new ModelContext(optionsBuilder.Options))
            {
                context.Database.EnsureCreated();
            }

            var leftid = 0;
            var rightid1 = 0;
            var rightid2= 0;
            var rightid3 = 0;

            using (var context = new ModelContext(optionsBuilder.Options))
            {
                var left1 = new Left();
                var left2 = new Left();
                var left3 = new Left();

               var right1 = new Right();
               var right2 = new Right();
               var right3 = new Right();
               var right4 = new Right();

                left1.Rights = new List<Right>();
                left2.Rights = new List<Right>();
                left3.Rights = new List<Right>();

                left1.Rights.Add(right1);
                left1.Rights.Add(right2);
                left1.Rights.Add(right3);

                left2.Rights.Add(right1);
                left2.Rights.Add(right2);
                left2.Rights.Add(right3);

                // create new item
                context.Left.Add(left1);
                context.Left.Add(left2);
                context.Left.Add(left3);

                context.Right.Add(right1);
                context.Right.Add(right2);
                context.Right.Add(right3);
                context.Right.Add(right4);
                // make relation

                //save
                context.SaveChanges();

                leftid = left1.Id;

                rightid1 = right1.Id;
                rightid2 = right2.Id;
                rightid3 = right3.Id;

            }

            using (var context = new ModelContext(optionsBuilder.Options))
            {
                var left = context.Left.Find(leftid);
                context.Entry(left).State = EntityState.Modified;
                var x = left.Rights;
                left.Rights = new List<Right>();

                context.ChangeTracker.Entries();// Comment this line to work fine

                var right1 = context.Right.Find(rightid1);

                left.Rights.Add(right1);

                var right2 = context.Right.Find(rightid2);

                left.Rights.Add(right2);

                var right3 = context.Right.Find(rightid3);

                left.Rights.Add(right3);

                foreach (var item in context.ChangeTracker.Entries())
                {
                    Console.WriteLine(item.ToString());
                }

                context.SaveChanges();

                Console.WriteLine($"Left {left.Id}");
                Console.WriteLine($"Rights {left.Rights.Count}");

            }
            // view result

            Console.ReadLine();
        }
    }

    public class ModelContext:DbContext
    {
        public ModelContext(DbContextOptions<ModelContext> options): base(options)
        {
        }

        public DbSet<Right> Right { get; set; }
        public DbSet<Left> Left { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity<Right>().HasMany(x => x.Lefts).WithMany(x => x.Rights).UsingEntity(a => a.ToTable("LeftRight"));
        }
    }

    [Table("Left")]
    public class Left
    {
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        [Key]
        public int Id { get; set; }

        public virtual IList<Right> Rights { get; set; }
    }
    [Table("Right")]
    public class Right
    {
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        [Key]
        public int Id { get; set; }

        public virtual IList<Left> Lefts { get; set; }

    }
}

Include provider and version information

EF Core version: 6.0 Database provider: (Microsoft.EntityFrameworkCore.SqlServer) Target framework: ( .NET 6.0) Operating system: Windows 10 IDE: (e.g. Visual Studio 2022 Version 17.1.0 Preview 1.1)

ajcvickers commented 2 years ago

@luizfbicalho This line:

left.Rights = new List<Right>();

removes all the existing entities from Rights, but requires change detection to happen. See Change Detection. It's possible that improving local detect changes when Find is called could make manual DetectChanges unnecessary here, although that would make it "not work" all the time from your perspective. We will inevstigate.

luizfbicalho commented 2 years ago

This issue I managed to use the change detection to reproduce another problem that I have

I have an extension on the Newtonsoft.Json to read from the json properties and load the correct entities from the database, this way when I finish loading the entity, it's clear for EF what I have to change.

The problem with the many to many relationship is that I can't refresh them, and it's weird, because I could imagine two scenarios. 1) the change tracker would have 2 entities for the many to many relation, one in removed state and one in added state 2) the change tracker would merge the changes onthis relation and end up with the relation or unchanged or modified

But my problem is that It end up as deleted. Is there something that I can do to bypass this problem?

luizfbicalho commented 1 year ago

Is there anything that I can do to spped things up in this issue?