4

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();
4
  • just out of curiosity, not as a suggested solution, why don't you use the ConventionModelBuilder approach for edm building? Commented Apr 27, 2020 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. Commented Apr 27, 2020 at 16:44
  • 2
    Possible duplicate of stackoverflow.com/questions/60488594/…. Never initialize reference navigation properties with new. Either remove = new Contact(); or if you want to suppress NRT warnings, replace it with = null!; Commented Apr 27, 2020 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. Commented Apr 27, 2020 at 17:33

1 Answer 1

3

@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

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.