Optional Many-to-Many Relationship in Entity Framework Core 5+

Viewed 126

Context: .NET 5, EF Core 5, SQL Server

Problem: I need EF to LEFT JOIN an optional many-to-many relationship through the Include().ThenInclude() calls but it's only wanting to INNER JOIN. This results in missing rows when the relationship doesn't exist.

I have three tables: A, B, and AB that represents the many-to-many relationship between A and B. Below is a simplified version of the real tables but AB can have extra columns, so I don't want to use EF implicit relationships for it.

A

ID
1
2
...

B

ID
1
2
...

AB

ID AID BID
1 1 1
2 1 2
... ... ...

The C# models for them look like:

public class A
{
  public Guid ID { get; set; }

  public ICollection<AB> ABs { get; set; }
}
public class B
{
  public Guid ID { get; set; }

  public ICollection<AB> ABs { get; set; }
}
public class AB
{
  public Guid ID { get; set; }

  public Guid AID { get; set; }

  public Guid BID { get; set; }

  public A A { get; set; }

  public B B { get; set; }
}

Then the query I'm using to select the data out:

context.As
       .AsSplitQuery()
       .Include(f => f.ABs)
           .ThenInclude(f => f.B)
       .ToList();

These relationships all work fine when they exist. I can get from a given A to any of the Bs that are assigned to it in the AB table.

The problem happens when the A row doesn't have any corresponding rows in the AB table (ie. there are no Bs assigned to it). When that happens, EF is turning my LINQ query into an INNER JOIN, which excludes all the As that don't have rows in the AB table.

From my research, EF Core 5 did away with explicit mandatory vs optional relationship hinting and now infers it from whether the navigation property in the model is nullable. But ICollections are natively nullable, as is the B class I'm pointing it at. Even so, adding an ? to mark any part of the property nullable doesn't change the generated SQL (eg. public ICollection<AB>? ABs { get; set; })

I've tried specifying the relationship in the context, as per some other advice online but that didn't change the behavior:

modelBuilder.Entity<A>()
            .HasMany(f => f.Bs)
            .WithMany(f => f.As);

Is there any way (other than writing the joins manually) to force EF to treat the AB navigation property as optional?


Update:

This appears to be a side-effect of using AsSplitQuery(). Changing it to AsSingleQuery() correctly generates a LEFT JOIN.

I'm guessing it's something to do with my ThenInclude() loading a not-null one-to-many relationship in the split query. Would still be nice to know if this is a bug or if there's some way to tell EF that the upstream relationship is still optional, even though the downstream one isn't.

0 Answers
Related