3

I have two entities, an entity Contact that has a navigation property Buyer that may exist, and an entity Buyer that has a navigation property Contact that must exist. All Buyer's have exactly one Contact, all Contact's may have zero or one Buyer's.

The problem that occurs is that when a Contact (that has a Buyer) is loaded, the Buyer cannot be loaded either through Eager or Explicit loading.

public class Contact
{
    public int ContactID { get; set; }
    public string FirstName { get; set; } = null!;
    public string LastName { get; set; } = null!;
    public string Email { get; set; } = null!;
    public virtual Buyer? Buyer { get; set; }
}
public class Buyer
{
    public int BuyerID { get; set; }
    public string CompanyName { get; set; } = default!;
    public string ProductName { get; set; } = default!;
    public int ContactID { get; set; }
    public virtual Contact Contact { get; set; } = new Contact();
}

When I create the entities:

 // existing Contact already initialized with Buyer == null and added
 var newBuyer = new Buyer() { CompanyName = "Acme", ProductName = "Anvil" };
 newBuyer.ContactID = contactID;
 // Load the reference to the Contact
 newBuyer.Contact = await _context.Contacts.SingleOrDefaultAsync(c => c.ContactID == contactID);
 // error checking elided (but in this test it is not failing)
 // newBuyer.Contact.Buyer is null if examined
 _context.Buyers.Add(newBuyer);
 // newBuyer.Contact.Buyer is now newBuyer, automatic fix-up
 await _context.SaveChangesAsync();

Looking at the underlying database everything is as expected.

Now I attempt to load the Contact and navigation properties two different ways expecting automatic fix-ups:

 Contact = await _context.Contacts.FindAsync(id);
 // The Contact.Buyer is null here as expected, so explicitly Load
 _context.Entry(Contact).Reference(c => c.Buyer).Load();
 // The Contact.Buyer is still null here, so try DetectChanges
 _context.ChangeTracker.DetectChanges();
 // The Contact.Buyer is still null here, so try again with Eager Loading
 Contact = await _context.Contacts.Include(c => c.Buyer).FirstOrDefaultAsync(m => m.ContactID == id);
 // The Contact.Buyer is still null here! What is wrong?

When tracing in a debugger, the first explicit Load() sees Buyer as a navigation property and successfully loads it into memory. Also looking at _contacts.Buyers shows that it is in memory.
The DetectChanges was added just in case, it makes no difference.
The Eager loading using Include also is not causing the fix-ups.
Lazy loading was also tried and failed.

Does anyone have any idea how to get the automatic fixup to work?

The fluent API:

 modelBuilder.Entity<Contact>()
             .HasKey("ContactID");
 modelBuilder.Entity<Buyer>()
             .HasKey(p => p.BuyerID);
 modelBuilder.Entity<Buyer>()
             .HasOne<Contact>(p => p.Contact)
             .WithOne("Buyer")
             .HasForeignKey("Buyer", "ContactID")
             .OnDelete(DeleteBehavior.Cascade)
             .IsRequired();

Notes: EF Core 3.1.3 Net Core API 3.1.0 Nullable Enable

[Edit] By adding the following line of code before the FindAsync it causes all Buyer's to be loaded into memory/cache, the Contact.Buyer buyer is then automatically fixed up after the first FindAsync(). This shows that fixups can occur. But I don't want to forcibly load the whole table.

var test = _context.Buyers.ToList();
  • just out of curiosity, not as a suggested solution, why don't you use the ConventionModelBuilder approach for edm building? – Michael Schönbauer Apr 27 '20 at 16:39
  • I have tried it both ways with no success, so I switched to the simple and explicit fluent to remove any hidden/unknown convention problems. – David Robinson Apr 27 '20 at 16:44
  • 2
    Possible duplicate of https://stackoverflow.com/questions/60488594/one-to-many-returns-empty-array-solved/60489370#60489370. Never initialize reference navigation properties with `new`. Either remove `= new Contact();` or if you want to suppress NRT warnings, replace it with `= null!;` – Ivan Stoev Apr 27 '20 at 17:12
  • 1
    @IvanStoev that solves the problem! Thank you! That was there for a previous validation problem. I will fix this and revisit that. – David Robinson Apr 27 '20 at 17:33

1 Answers1

2

@IvanStoev correctly comments that the problem is with the following line:

public virtual Contact Contact { get; set; } = new Contact();

When replaced with:

public virtual Contact Contact { get; set; } = null!;

All automatic fixups are working.

Also see: one-to-many-returns-empty-array-solved