How to resolve the update problem tracking instance in many-to-many in EFCore 7.0.10

Introduction

There is a scene that 2 classes are related by a join-table-like class. For example:

A class defines some basic properties, one of which is a set of Project, which means an object may refers to many Project

/*AssemblyLanguage.cs*/
public class AssemblyLanguage{
   public int ID {get;set;}
   public string Mnemonic {get;set;}
   public List Projects {get;set;}
}

/*Project.cs*/
   public class Project
   {
       public int ID { get; set; }
       
       public string Name { get; set; }
       
       public int ParentID { get; set; }

       /*-----self-reference, not the point in the text----*/
       public Project Parent { get; set; }
   	public List Children { get; set; }
       /*-------------------------------------------------*/
       public List Nemonics { get; set; }
   }

The definition of DbContext, in which Entity Framework Core migrates and manipulates data from objects to database, is below.

public class AssemblyLangTranslatorDbContext : DbContext
{
        private string? DbPath = string.Empty;

        public AssemblyLangTranslatorDbContext()
        {
            DbPath = AppConfigurtaionServices.Configuration.GetSection("ConnectionStrings").GetSection("AssemblyLangTranslatorContext").Value;
        }


        protected override void OnConfiguring(DbContextOptionsBuilder options)
        {
            options.UseSqlServer(DbPath);
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity().Property(p=>p.Name).HasMaxLength(16);
            modelBuilder.Entity().HasData(new Project() {ID=1, Name = "项目", ParentID=1 });
            modelBuilder.Entity().HasMany(p => p.Mnemonics).WithMany(n => n.Projects)
                 .UsingEntity(
                    "AssemblyLanguageProject",
                    l => l.HasOne(typeof(AssemblyLanguage)).WithMany().HasForeignKey("AssemblyLanguageId").HasPrincipalKey(nameof(AssemblyLanguage.ID)).OnDelete(DeleteBehavior.Restrict),
                    r => r.HasOne(typeof(Project)).WithMany().HasForeignKey("ProjectId").HasPrincipalKey(nameof(Project.ID)).OnDelete(DeleteBehavior.Restrict),
                    j => j.HasKey("AssemblyLanguageId", "ProjectId"));

            modelBuilder.Entity().Property(al => al.Nemonic).HasMaxLength(32);

        }

        public DbSet Projects { get; set; } = default!;!;

        public DbSet AssemblyLanguages { get; set; } = default!;
}

As we can see, the name of the join-table defined by UsingEntity method is AssemblyLanguageProject, which combines the ProjectId and AssemblyLanguageId as PrimaryKey and specifies the foreign keys respectively on them.

How to resolve the update problem tracking instance in many-to-many in EFCore 7.0.10_第1张图片

By migration-tools from EntityFrameworkCore.Design package, database could be automatically built by two simple commands in PowerShell or terminal as mentioned at https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=vs

PS D:\src\cs\M2MUpdateSample\M2MUpdateSample\LibM2MUpdateSample> dotnet ef migrations add InitialCreate
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'

PS D:\src\cs\M2MUpdateSample\M2MUpdateSample\LibM2MUpdateSample> dotnet ef database update
Build started...
Build succeeded.
Applying migration '20230901010942_InitialCreate'.
Done.

Basic Db Operation

Insert operations will be implemented in their own class. For example, “jdk” added into table PROJECTS unit test is below.

/*Project.cs*/
[Test]
public void TestInsert()
{
    Project project = new Project();
    project.Name = "jdk";
    project.ParentID = 1;
    int result = project.Insert();

    Assert.AreEqual(1, result);
}

Assuming after several steps, some mnemonics have already been inserted into table AssemblyLanguage, and the manipulation, which will be introduced next, is to connect the ‘Project’ and ‘AssemblyLanguage’
How to resolve the update problem tracking instance in many-to-many in EFCore 7.0.10_第2张图片

figure 1. Table Projects

How to resolve the update problem tracking instance in many-to-many in EFCore 7.0.10_第3张图片

figure 2. Table AssemblyLanguages

To update the relations between projects and AssemblyLanguages, what firstly should be done is getting list of projects in AssemblyLanguage class. Certainly, it can be easily implemented by EFCore interfaces.

using (var context = new AssemblyLangTranslatorDbContext())
{
    var item = context.AssemblyLanguages.Include(al => al.Projects).First(r => r.ID == this.ID);
}

Next, Update is coming

using (var context = new AssemblyLangTranslatorDbContext())
{
	var item = context.AssemblyLanguages.Include(al => al.Projects).First(r => r.ID == this.ID);
    ...
    item.ID = this.ID;
    item.Mnemonic = this.Mnemonic;

    item.Projects = this.Projects;
    context.Update(item);
    result = context.SaveChanges();
}

An exception occurs.

Unexpected result and exception

Firstly, paying attention the pattern of assignment of List. Previous statement, this.Projects is assigned incautiously to the property of item without thinking the tracking state of item and its belongings-Projects, which from the Include(al=>al.Projects) .

So, Projects assigning operation is changed to one-by-one.

foreach (var project in Projects)
{
    item.Projects.Add(project);
}

exception still occurs. To find out what causes the exception, options.EnableSensitiveDataLogging();, which enables to output the information about application data, was add to the OnConfiguring method.

protected override void OnConfiguring(DbContextOptionsBuilder options)
{
    options.UseSqlServer(DbPath);
    options.EnableSensitiveDataLogging();
}

and restart the unit test again, the details of exception outputs.

SqlException: The UPDATE statement conflicted with the FOREIGN KEY SAME TABLE constraint "FK_Projects_Projects_ParentID". The conflict occurred in database "AssemblyLangProjectDb", table "dbo.Projects", column 'ID'.

Reason

Obviously , the exception is about the ParentID property of Project class, but the root of the problem is related to entity tracking, which comes from context. Every object’s operation for database must be from DbContext, which also means the object should be an entity tracked by the context, or attach to the database .

Resolution

Create an element from context, and add them to the Projects property of AssemblyLanguage, Everything is OK. Code as below.

foreach (var project in Projects)
{
    var pItem = context.Projects.Find(project.ID);
    item.Projects.Add(pItem);
}

Conclusion

In summary, every operation related to database should connects to the DbContext.

你可能感兴趣的:(Code,First:Entity,Framework编程,数据库,c#,.net)