4

I'm trying to connect two objects with one-to-many relationship, Workplace can hold multiple People, while one Person can only have one Workplace.

When I execute this code snippet and check to see the results, p1 has properly assigned w1 as the p1.Workplace, but the list of people of the w1.employees is always empty.

Do I have to manually add p1 to w1.Employees even after I assigned w1 to p1.Workplace?

SeedData.cs (code snippet)

var w1 = new Workplace
{
   EntryCode = "1111"
   //[...] other data I cut out for brievity
};
context.Workplaces.Add(w1);

Person p1 = new Person
{
    Name = "Spencer",
    Workplace = w1
   //[...] other data I cut out for brievity
};
context.Person.Add(p1)
context.SaveChanges();

Workplace.cs

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

namespace Counsel.Models
{
    [Table("Workplace")]
    public class Workplace
    {
        [Column("WorkplaceId")]
        public int WorkplaceId {get;set;}

        [Column("EntryCode")]
        public string EntryCode {get;set;}

        [Column("ConfirmationCode")]
        public string ConfirmationCode {get;set;}

        [Column("Employees")]
        public ICollection<Person> Employees {get;set;}
    }
}

Person.cs

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

namespace Counsel.Models
{
    public class Person
    {
        [Column("PersonId")]
        public int PersonId {get;set;}

        [Column("Image")]
        public string Image {get;set;}

        [Column("FName")]
        public string FName { get; set; }

        [Column("LName")]
        public string LName {get;set;}

        [Column("Role")]
        public string Role {get;set;}

        [Column("Email")]
        public string Email {get;set;}

        [Column("Password")]
        public string Password {get;set;}

        public Workplace Workplace {get;set;} = new Workplace();
        public ICollection<ChatPerson> Chats {get;set;}
    }
}

DataContext.cs

using Counsel.Models;
using Microsoft.EntityFrameworkCore;

namespace Counsel.Models {
    public class DataContext : DbContext
    {
        public DataContext(DbContextOptions<DataContext> opts): base(opts){}

        public DbSet<Person> People {get;set;}
        public DbSet<Workplace> Workplaces {get;set;}


        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {

            modelBuilder.Entity<Person>((item)=>{
                item.HasKey(p => p.PersonId);
                item.HasOne<Workplace>(p => p.Workplace).WithMany(w => w.Employees).OnDelete(DeleteBehavior.SetNull);
            });


            modelBuilder.Entity<Workplace>((item)=>{
                item.HasKey(p => p.WorkplaceId);
                item.HasMany<Person>(w => w.Employees).WithOne(p => p.Workplace).OnDelete(DeleteBehavior.Cascade);
            });
        }
    }
}

WorkplaceController.cs (code snippet)

[HttpGet("{id}")]
        public Workplace GetWorkplace(int id)
        {
           return context.Workplaces
                .Where(c => c.WorkplaceId == id)
                .Include(c => c.Employees).ThenInclude(c => c.Workplace)
                .FirstOrDefault();

        }

GetWorplace() output

[
  {
    "workplaceId": 1,
    "entryCode": "1111",
    "confirmationCode": "1111",
    "employees": [

    ]
  }
]

As you can see, the "employees" array is empty, even though p1 should be there.

Syscall
  • 19,327
  • 10
  • 37
  • 52
kzmijak
  • 77
  • 6
  • I'm by no means an expert (and can be wrong), but when I usually run into this error I check that there is a save after adding the entities I'm going to link to something else (before linking them to something else). And I do it in transaction. – mishan Mar 02 '20 at 12:21
  • 1
    everything looks good. can you please check whether the seed data saved successfully – Krishna Varma Mar 02 '20 at 12:26

3 Answers3

7

The problem is caused by the reference navigation property initializer:

public Workplace Workplace { get; set; } = new Workplace(); // <--

Never do that - it confuses EF navigation property fixup and leads to unexpected runtime behaviors.

Note that initializing collection navigation properties is ok, although not required. It's because null in reference navigation property have a special meaning and really provides a link to the principal entity, which may or may not contain collection of dependent entities.

Shortly, remove the initializer

public Workplace Workplace { get; set; }

and the issue will be solved.

Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • This should do it, but I'd also recommend adding WorkplaceId to the Person object, with a [ForeignKey] attribute added to it that links to the Workplace object. – Chris Dixon Mar 02 '20 at 13:06
  • @ChrisDixon Explicit FK property is not mandatory. So recommending it might be for other reasons (applying disconnected entity changes w/o loading the related entity, although even in such scenario EF Core allows setting the shadow property), but in general it is optional (not needed) and I see no reason recommending it in unrelated issue. Moreover (I've tested it) the OP issue happens with explicit FK property as well, so adding it doesn't solve the issue and still requires removing the initializer. – Ivan Stoev Mar 02 '20 at 13:12
  • @IvanStoev For sure, it's not 100% required, but I believe it gives better readability overall in your objects - you have more of an idea of key relations. It was more of a recommendation for code maintainability, rather than a fix for this specific problem (of which I agreed you'd highlighted). Either or, sounds like it's fixed! – Chris Dixon Mar 02 '20 at 13:16
1

If you are using lazy loading of the data you need to enumerate the data in order for them to be pulled into the EF context. Child elements are not retrieved by default. Another thing you can do is explicitly include the children when constructing the query. You can see how the include operation works here: https://entityframework.net/include-multiple-levels

And you can also see the related issue here: Include children in EF

DNakevski
  • 300
  • 1
  • 7
0

Workplace has a relationship property Employees, but it is mapped [Column("Employees")] which I'm guessing causes EF to stop treating it as a relationship to find the other entities through, and starts treating it as a column that exists in the Workplace table. Remove the column attribute and, if necessary, provide more information to EF so that it can properly map the Person * ↔ 1 Workplace relationship.

Jesper
  • 7,477
  • 4
  • 40
  • 57
  • That wasn't directly causing the issue, but I appreciate pointing that out, since it genuinely helps me understand EF better – kzmijak Mar 02 '20 at 13:15