heds1
heds1

Reputation: 3438

Accessing the properties of a many-to-many join entity's navigation properties

I have a many-to-many relationship between Post and Tag, represented by the join entity PostTag. I am currently able to access the PostTag table in my Details view of the Post class; for example, I can view a particular Post and display the associated PostTags. However, I have only been able to display the PostTag properties PostId and TagId, while I'd like to display properties related to the Tag class via the PostTag entity.

There are several posts online detailing ways to display many-to-many relationships, but they all seem to be broadly different to the approach I have taken, and so do not seem to be easily translatable to my project.

Models

public class Post
    {
        public int Id { get; set; } // primary key
        public string Title { get; set; }
        public string Description { get; set; }
        public DateTime PublicationDate { get; set; }

        public ICollection<PostTag> PostTags { get; } = new List<PostTag>();
    }

public class Tag
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }

        public ICollection<PostTag> PostTags { get; } = new List<PostTag>();
    }

public class PostTag
    {
        public int PostId { get; set; }
        public Post post { get; set; }

        public int TagId { get; set; }
        public Tag tag { get; set; }
    }

Relevant part of PostsController

public class PostsController : Controller
    {
        private readonly Website.Data.ApplicationDbContext _context;

        public PostsController(Website.Data.ApplicationDbContext context)
        {
            _context = context;
        }
        // GET: Post/Details/5
        public async Task<IActionResult> Details([Bind(Prefix = "Id")]int? id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var post = await _context.Post
                .Include(p => p.Author)
                .Include(p => p.PostTags)
                .FirstOrDefaultAsync(m => m.Id == id);
            if (post == null)
            {
                return NotFound();
            }

           return View(post);
        }
    }

Db context

public class ApplicationDbContext : IdentityDbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }
        public DbSet<Author> Author { get; set; }
        public DbSet<Post> Post { get; set; }
        public DbSet<Tag> Tag { get; set; }
        public DbSet<PostTag> PostTag { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
            builder.Entity<PostTag>()
                .HasKey(t => new { t.PostId, t.TagId });
        }
    }

View

@model Website.Models.Post
@foreach (var postTag in Model.PostTags)
{
<tr>
    <td>
        @Html.DisplayFor(modelItem => postTag)
    </td>
</tr>
}

This view displays the PostTags associated with a particular Post. For example, at https://localhost:xxxxx/Posts/Details/1, the Post with Id == 1 is accessed, and its associated Tags are displayed.

PostId
1
TagId
2
PostId
1
TagId
4
PostId
1
TagId
5

I can't seem to access Tag.Title; for example, @Html.DisplayFor(modelItem => postTag.tag.Title) does not display any view. My desired output would be, assuming Tag rows of Fiction, Nonfiction and Classic have Id values equal to 2, 4 and 5,

Fiction
Nonfiction
Classic

Appreciate any advice here.

edit: Upon loading the Details page in debug mode, the post variable contains all the expected properties including PostTags. PostTags contains the expected number of elements (three PostTag elements because this Post instance has three associated Tags). Each element contains properties of PostId, TagId, post and tag; however, tag is null. This is the issue, as I'm trying to access tag.Title. Screenshot of local variables below:

local variables

Upvotes: 0

Views: 652

Answers (1)

Ryan
Ryan

Reputation: 20116

You could include multiple levels of related data using the ThenInclude method.

var post= await _context.Post
                        .Include(p => p.Author)
                        .Include(p => p.PostTags)
                        .ThenInclude(pt => pt.Tag)
                        .FirstOrDefaultAsync(p => p.Id == id);

many-to-many DbContext:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(builder);
    modelBuilder.Entity<PostTag>()
        .HasKey(pt => new { pt.PostId, pt.TagId });

    modelBuilder.Entity<PostTag>()
        .HasOne(pt => pt.Post)
        .WithMany(p => p.PostTags)
        .HasForeignKey(pt => pt.PostId);

    modelBuilder.Entity<PostTag>()
        .HasOne(pt => pt.Tag)
        .WithMany(t => t.PostTags)
        .HasForeignKey(pt => pt.TagId);
}

Refer to https://learn.microsoft.com/en-us/ef/core/modeling/relationships?view=aspnetcore-2.1#other-relationship-patterns

Upvotes: 2

Related Questions