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.
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.
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’
figure 1. Table Projects
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.
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'.
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 .
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);
}
In summary, every operation related to database should connects to the DbContext
.