entity-framework-coreasp.net-core-mvcrepositorymany-to-many

ASP.NET Core 8 with repository pattern unable to find navigation to many-to-many table


Using ASP.NET Core 8 MVC, I've implemented a many-to-many relationship between an Event table and Product table and then added a specific query in the Event repository to get an Event and include the list of Product associated with it.

The migration successfully created the EventProd many-to-many table, but I get an error with I run the query indicating the path is invalid:

Microsoft.EntityFrameworkCore.Query.InvalidIncludePathError: Unable to find navigation 'EventProd' specified in string based include path 'EventProd.Product'

I'm sure I don't fully understand all the magic is happening in ASP.NET Core with their implementation of many-to-many relationships, but I could get this done a lot quicker and with less frustration writing an SQL join and pulling the data out that way.

Any help is appreciated!

Here are the models (without all the gory details):

public class Event
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string? Description { get; set; }
    public List<EventProd> EventProducts { get; set; } = new List<EventProd>();
}    

public class Product
{
    public int Id { get; set; }
    [DisplayName("Product Name")]
    public string Title { get; set; }
    [DisplayName("Description")]
    public string Description { get; set; }
    
    public List<EventProd> EventProducts { get; set; }
}   

public class EventProd
{
     public int EventId { get; set; }
     public int ProductId { get; set; }
     public Event Event { get; set; }
     public Product Product { get; set; }
}

From my ApplicationDbContext file:

public DbSet<Event> Events { get; set; }
public DbSet<EventMeal> EventMeals { get; set; }
public DbSet<Product> Products { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<EventProd>()
                .HasKey(pt => new { pt.EventId, pt.ProductId });
    modelBuilder.Entity<EventProd>()
                .HasOne(pt => pt.Product)
                .WithMany(pt => pt.EventProducts)
                .HasForeignKey(pt => pt.ProductId);
    modelBuilder.Entity<EventProd>()
                .HasOne(pt => pt.Event)
                .WithMany(pt => pt.EventProducts)
                .HasForeignKey(P => P.EventId);
}

Here is the repository query that is failing:

public Event GetById(int id)
{
     //List<int> productIdlist = _db.EventProd 
     return _db.Events
               .Include("EventProd.Product")
               .FirstOrDefault(x => x.Id == id);
}

I've completed a tutorial I found on-line on many-to-many relationships in ASP.NET Core with a repository pattern, but of course the repository details differ from my existing site. I've tried to add the EventProd table to my ApplicationDbContext file but that didn't change anything.

Still looking for other examples out there to see if I find another clue...Thanks again for your help!


Solution

  • EF Core supports the string-based navigation, but it is property names, not table names. So .Include("EventProducts.Product") should work.

    In EF Core it is recommended to use the expressions to refer to the navigation properties since that is verified at compile time where string names would only be verified at runtime. .Include(x => x.EventProducts).ThenInclude(x => x.Product)

    Also note that if your EventProduct only serves to link the two, defining the navigation property of type EventProduct is optional. You can simplify the entities to:

    public class Event
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string? Description { get; set; }
    
        public virtual ICollection<Product> Products { get } = [];
    }    
    
    public class Product
    {
        public int Id { get; set; }
        [DisplayName("Product Name")]
        public string Title { get; set; }
        [DisplayName("Description")]
        public string Description { get; set; }
        
        public virtual ICollection<Event> Events { get; } = [];
    }   
    

    then in the configuration, if you define an EventProduct entity (without referencing it) you can use:

    modelBuilder.Entity<Product>()
        .HasMany(p => p.Events)
        .WithMany(e => e.Products)
        .UsingEntity<EventProduct>();
    

    Provided the properties in EventProduct follow the same naming convention, EF will work out the links. If you don't want to define an entity for EventProduct then you can map the entire table in the configuration.

    modelBuilder.Entity<Product>()
        .HasMany(p => p.Events)
        .WithMany(e => e.Products)
        .UsingEntity(
            "EventProd",
            l => l.HasOne(typeof(Event)).WithMany().HasForeignKey("EventId").HasPrincipalKey(nameof(Event.EventId)),
            r => r.HasOne(typeof(Product)).WithMany().HasForeignKey("ProductId").HasPrincipalKey(nameof(Product.ProductId)),
            j => j.HasKey("EventId", "ProductId"));
    

    Either way the simpler relationship is generally more practical using Product.Events and Event.Products without worrying about the joining entity. Where you would normally want the joining entity is if there are extra properties to map/use on the EventProduct.