I'm trying to build a library. On my Homepage I managed display all books with all details. When I click on Author's page I would like to display all his books(image, title and author). The problem is when mhy book has more than one author, I cannot display all of them.
My Homepage: Screenshot
Author: Screenshot
Models:
public class Book : IEntityBase
{
[Key]
public int Id { get; set; }
public string Title { get; set; }
public DateTime RelaseDate { get; set; }
public string Description { get; set; }
public string ImageURL { get; set; }
public bool IsBorrowed { get; set; }
public string ISBN { get; set; }
//Relationships
public int PublisherId { get; set; }
[ForeignKey("PublisherId")]
public virtual Publisher Publisher { get; set; }
public int CategoryId { get; set; }
[ForeignKey("CategoryId")]
public virtual Category Categories { get; set; }
public virtual ICollection <Author_Book> Authors_Books { get; set; }
}
public class Author_Book
{
public int AuthorId { get; set; }
public virtual Author Author { get; set; }
public int BookId { get; set; }
public virtual Book Book { get; set; }
}
public class Author : IEntityBase
{
[Key]
public int Id { get; set; }
[Display(Name = "Autor")]
[Required(ErrorMessage = "Author jest wymagany")]
public string FullName { get; set; }
[Display(Name = "Biografia")]
[Required(ErrorMessage = "Biografia jest wymagana")]
public string Bio { get; set; }
[Display(Name = "Zdjęcie")]
public string ImageURL { get; set; }
//Relationships
public virtual ICollection<Author_Book> Authors_Books { get; set; }
public virtual ICollection<Author_Publisher> Author_Publisher { get; set; }
public virtual ICollection<Author_Category> Author_Category { get; set; }
}
AuthorController:
//GET: author/details
public async Task<IActionResult> Details(int id)
{
var authorDetails = await _service.GetAuthorByIdAsync(id);
if (authorDetails == null) return View("NotFound");
return View(authorDetails);
}
Method GetAuthorbyIdAsync:
public async Task<Author> GetAuthorByIdAsync(int id)
{
var bookDetails = await _db.Authors
.Include(ab => ab.Authors_Books)
.ThenInclude(b => b.Book)
.FirstOrDefaultAsync(n => n.Id == id);
return bookDetails;
}
Author's View details
@model Author
<div >
<div >
<div >
<div >
<div >
<div >
<p >
<h1 >
@Model.FullName
</h1>
</p>
</div>
</div>
<div >
<img src="@Model.ImageURL" width="100%" alt="@Model.FullName">
</div>
<div >
<div >
<p >@Model.Bio</p>
</div>
</div>
<div >
<h2 > Wypożycz inne książki @Model.FullName:</h2>
<div >
@foreach (var book in Model.Authors_Books)
{
<div >
<div style="width: 250px;">
<div >
<img src="@book.Book.ImageURL" width="100%" alt="@book.Book.Title">
<br />
<br />
<h5 ><b>@book.Book.Title</b></h5>
<h6 ><b>@book.Author.FullName</b></h6>
<a asp-controller="Book" asp-action="Details" asp-route-id="@book.Book.Id">
Więcej
</a>
</div>
</div>
</div>
}
</div>
</div>
<div >
<div >
<p >
<a asp-action="Edit" asp-route-id="@Model.Id">Edytuj</a>
<a asp-controller="Book" asp-action="Index">Wróć</a>
</p>
</div>
</div>
</div>
</div>
</div>
CodePudding user response:
There are two significant issues here, the first is that you are not even attempting to show the Author
records that are related to the Book
, you are displaying the Book.Title
and but not the Author
records related to the book:
<h5 ><b>@book.Book.Title</b></h5>
<h6 ><b>@book.Author.FullName</b></h6>
At the very least you would expect
<h5 ><b>@book.Book.Title</b></h5>
<h6 ><b>@book.Book.Author.FullName</b></h6>
But thos wont work because there are many Author
records related to the book, which you have correctly demonstrated on the home page. You can use this simple C# expression to create a CSV string of the Authors related to the book:
String.Join(", ", book.Book.Authors_Books.Select(ba => ba.Author.FullName))
So that changes your template to this:
<h5 ><b>@book.Book.Title</b></h5>
<h6 ><b>@(String.Join(", ", book.Book.Authors_Books.Select(ba => ba.Author.FullName)))</b></h6>
But for this to work we need to tell EF to load the other references at the same time by appending an additional ThenInclude
which will load the correct data from the database.
public async Task<Author> GetAuthorByIdAsync(int id)
{
var authorAndBooks = await _db.Authors
.Include(author => author.Authors_Books)
.ThenInclude(authorBook => authorBook.Book)
.ThenInclude(book => book.Authors_Books)
.FirstOrDefaultAsync(author => author.Id == id);
return authorAndBooks;
}
*NOTE: I renamed some of the variables in your query to better represent the type of data they relate to, it looks like you misinterpret the lambda variable types
If your view is rendered server-side, or you have forced serialization to include references then you may not see any issues.
When it comes to views and serialization however, the results are not always quite what you expect, when the data is sent back to the view, in most cases it will be serialized using logic that will deliberately exclude recursive entity references. When that happens the Author
and its associated Author_Book
from the parent of the results graph will not be included in nested references.
See this fiddle: https://dotnetfiddle.net/JCF4dB
You can see that the serialized output of the authorDetails
only has a single record in the second array of Authors_Books
and this is to be expected. To explain why, have a look at the first instance of Authors_Books
, notice that the "Author"
property is omitted all together, even though if you queried it via code at runtime it would return the outer most, or parent Author
Waris Dirie
{
"Id": 1,
"FullName": "Waris Dirie",
...
"Authors_Books": [
{
"Id": 1,
"AuthorId": 1,
"BookId": 1,
"Book": {}
}
]
}
If it didn't exclude the reference, you would end up with a recursive loop like this:
{
"Id": 1,
"FullName": "Waris Dirie",
...
"Authors_Books": [
{
"Id": 1,
"AuthorId": 1,
"Author":
{
"Id": 1,
"FullName": "Waris Dirie",
...
"Authors_Books": [
{
"Id": 1,
"AuthorId": 1,
"Author":
{
"Id": 1,
"FullName": "Waris Dirie",
...
"Authors_Books": [
{
"Id": 1,
"AuthorId": 1,
"Author": { ... }
"BookId": 1,
"Book": {}
}
]
},
"BookId": 1,
"Book": {}
}
]
},
"BookId": 1,
"Book": {}
}
]
}
The same mechanism prevents an object in one array that is referenced in an entirely different array from being projected via a serialize payload.
One way to work around this is to recognise that if the Book
is already linked to the Author
via a parent Author_Book
record, then we should use that reference first in the rendering list, appended with the other authors. To visualise this, change your template:
<h6 >
<b>@book.Author.FullName, @(String.Join(", ", book.Book.Authors_Books.Select(ba => ba.Author.FullName)))</b>
</h6>
Or you can add the parent record into the child array, this handles cases where there is only a single author without any additional conditional logic. Use the Union
function to create a single enumerable that contains all the records:
book.Book.Authors_Books.Union(new[] { book }).Select(ba => ba.Author.FullName)
Putting that back into the original template:
<h6 >
<b>@book.Book.Authors_Books.Union(new[] { book }).Select(ba => ba.Author.FullName)))</b>
</h6>
The solution in these EF M:N (Many to Many) relationships is generally to avoid a recursive loop and only traverse the relationship in a single direction, but as you can see here, with a little bit of knowledge and understanding of how and why these data structure behave, we can work around the limitations imposed on us by the default serialization.
To further assist with identifying these data concepts as you are writing the code is to change your collection property names to be more meaningful, try either of these concepts:
public class Book : IEntityBase
{
[Key]
public int Id { get; set; }
...
public virtual ICollection <Author_Book> Book_Authors { get; set; }
}
public class Author : IEntityBase
{
[Key]
public int Id { get; set; }
...
public virtual ICollection <Author_Book> Author_Books { get; set; }
}
Or
public class Book : IEntityBase
{
[Key]
public int Id { get; set; }
...
public virtual ICollection <Author_Book> Authors { get; set; }
}
public class Author : IEntityBase
{
[Key]
public int Id { get; set; }
...
public virtual ICollection <Author_Book> Books { get; set; }
}