简体   繁体   中英

Foreign key not populating in Entity Framework

I cannot get a table to update correctly that should be linking two of my entities. To explain in more detail...I have two entities, Class and Teacher , with a relationship in the form of:

  1. Teacher can be assigned to many classes
  2. Class can only have one teacher.

Below are these two entities.

public class Teacher
{
    [Required, Key]
    public Guid Id { get; private set; }
    [StringLength(50)]
    public string Name { get; set; }
    public string Email { get; set; }
    public List<Class> Classes = new List<Class>();

    public Teacher()
    {
        Id = new Guid();
    }

    public Teacher(Guid id)
    {
        Id = id;
    }

    public void AssignClass(Class newClass)
    {
        Classes.Add(newClass);
    }
}

public class Class
{
    [Required, Key]
    public Guid Id { get; private set; }
    [Required, StringLength(20)]
    public string Name { get; set; }
    [Required, Range(5, 30)]
    public int Capacity { get; set; }
    public Teacher Teacher { get; set; }
    public IEnumerable<StudentClass> StudentClasses { get; set; }

    public Class()
    {
        Id = new Guid();
    }

    public Class(Guid id)
    {
        Id = id;
    }
}

When I generate my migrations I get a foreign key of TeacherId in the Classes table as expected. Here is the SQL:

CREATE TABLE [dbo].[Classes] (
[Id]        UNIQUEIDENTIFIER NOT NULL,
[Name]      NVARCHAR (20)    NOT NULL,
[Capacity]  INT              NOT NULL,
[TeacherId] UNIQUEIDENTIFIER NULL,
CONSTRAINT [PK_Classes] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_Classes_Teachers_TeacherId] FOREIGN KEY ([TeacherId]) REFERENCES [dbo].[Teachers] ([Id])
);
GO
CREATE NONCLUSTERED INDEX [IX_Classes_TeacherId]
ON [dbo].[Classes]([TeacherId] ASC);

My class derived of DBContext looks like:

public class SchoolDatabaseContext : DbContext
{
    public DbSet<Student> Students { get; private set; }
    public DbSet<Class> Classes { get; private set; }
    public DbSet<Teacher> Teachers { get; private set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {

    }

    public SchoolDatabaseContext(DbContextOptions<SchoolDatabaseContext> options) : base(options)
    {
    }
}

No configuration for those entities yet. I use DI to serve the DbContext to the controller and that all seems fine.

I have aimed for a DDD type structure, but to make this issue easier to debug I have stripped everything all the way back to the controller so it is basically... controller => DbContext .

Here is my code in the controller:

[HttpPost]
    [Route("assign-teacher-to-class")]
    public async Task<IActionResult> AssignClass([FromBody] AssignTeacherToClass assignTeacherToClass)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        var teacher = await schoolDatabaseContext.Teachers.FindAsync(assignTeacherToClass.TeacherId);

        var classToAssign = await schoolDatabaseContext.Classes.FindAsync(assignTeacherToClass.ClassId);


        teacher.AssignClass(classToAssign);

        schoolDatabaseContext.Entry(teacher).State = EntityState.Modified;

        await schoolDatabaseContext.SaveChangesAsync();

        return Ok(teacher);
}

When I debug through the ids are fine from the post body, they are assigned correctly to the DTO AssignClass and the calls to the DbContext to find the data for each type (teacher and class) are fine. I then call a method in my teacher type to add the class to the List Classes property (see teachers entity code at beginning for reference), I then Save the changes with the DbContext method and Problem Defined Here: at no stage does the TeacherId in the database update whilst debugging/completing. I have tried all I can think of like instantiating collections in different ways, changing collection types, looking for config that might help map these entities in this way, stripping out all extra layers, changing accessibility of properties and classes and few more.

Any help would really be appreciated as I am getting a bit defeated on this one and I feel like this relationship should be fairly straight forward. I actually was able to get my many to many working with a bridge class so I was surprised to get stuck on this one :(

Thanks

try this:

 var teacher = await schoolDatabaseContext.Teachers.Include(x => x.Classes).SingleOrDefaultAsync(x => x.Id == assignTeacherToClass.TeacherId);

I don't think teacher.Classes gets tracked by DbContext otherwise.

There are multiple ways to accomplish this with EF Core. It is easiest to find if you call it what the docs call it "Related Data".

Here is the parent doc:Related Data

Specifically as @Y Stroli has illustrated the Eager Loading method.

The below example is shown on the eager loading reference to load multiple levels of related data:

using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
            .ThenInclude(post => post.Author)
                .ThenInclude(author => author.Photo)
        .ToList();
}

As of EF Core 5.0 you can also do filtered includes:

using (var context = new BloggingContext())
{
    var filteredBlogs = context.Blogs
        .Include(blog => blog.Posts
            .Where(post => post.BlogId == 1)
            .OrderByDescending(post => post.Title)
            .Take(5))
        .ToList();
}

As the suggestion from lvan, you should change public List<Class> Classes = new List<Class>(); to public List<Class> Classes { get; set; } = new List<Class>(); public List<Class> Classes { get; set; } = new List<Class>(); .

For your current code, it seems you want to add Class and return the teacher , if so, you need to include the exsiting classes to teacher like below, otherwise, it will only return the new adding class.

    public async Task<IActionResult> AssignClass()
    {
        var assignTeacherToClass = new AssignTeacherToClass {
            TeacherId = new Guid("52abe5e0-bcd4-4827-893a-26b24ca7b1c4"),
            ClassId =new Guid("50354c76-c9e8-4fc3-a7c9-7644d47a6854")
        };
        var teacher = await _context.Teachers.Include(t => t.Classes).FirstOrDefaultAsync(t => t.Id == assignTeacherToClass.TeacherId);
        var classToAssign = await _context.Classes.FindAsync(assignTeacherToClass.ClassId);
        teacher.AssignClass(classToAssign);
        _context.Entry(teacher).State = EntityState.Modified;
        await _context.SaveChangesAsync();
        return Ok(teacher);
    }

One more note, you need to configure SerializerSettings.ReferenceLoopHandling like

services.AddMvc()
        .AddJsonOptions(opt => {
            opt.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
        }).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

You need to define the connection between Teacher and Class.

protected override void OnModelCreating(Modelbuilder modelBuilder)
{
    modelBuilder.Entity<Class>()
        .HasOne<Teacher>(p => p.Teacher)
        .WithMany(q => q.Classes)
        .HasForeignKey(r => r.TeacherId);
}

Also add TeacherId prop to Class.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM