Context
I have a simple link aggregator ASP.NET Core app built using Razor Pages.
The details page for each link shows the comments:

Users can comment on a link and reply to comments.
Models
Here's the Link
class:
public class Link
{
public int Id { get; set; }
public string UserId { get; set; }
[DataType(DataType.Url)]
public string Url { get; set; }
public string Title { get; set; }
[Display(Name = "Date")]
[DataType(DataType.Date)]
public DateTime DateTime { get; set; }
[ForeignKey("UserId")]
public IdentityUser User { get; set; }
public List<Vote> Votes { get; set; }
public int Score() => Votes.Sum(vote => vote.Score);
public Vote UserVote(string userId) =>
Votes.FirstOrDefault(vote => vote.UserId == userId);
public int UserScore(string userId)
{
var vote = UserVote(userId);
return vote == null ? 0 : vote.Score;
}
public async Task Vote(int score, string voterUserId)
{
var vote = UserVote(voterUserId);
if (vote == null)
{
vote = new Vote()
{
UserId = voterUserId,
LinkId = Id,
Score = score,
DateTime = DateTime.Now
};
Votes.Add(vote);
}
else
{
vote.Score = vote.Score == score ? 0 : score;
}
}
public List<Comment> Comments { get; set; }
public async Task AddComment(string text, string commenterUserId)
{
var comment = new Comment()
{
UserId = commenterUserId,
LinkId = Id,
Text = text,
DateTime = DateTime.Now
};
Comments.Add(comment);
}
}
Here's the Comment
class:
public class Comment
{
public int Id { get; set; }
public int? LinkId { get; set; }
//[ForeignKey("LinkId")]
public Link Link { get; set; }
public int? ParentCommentId { get; set; }
//[ForeignKey("ParentCommentId")]
public Comment ParentComment { get; set; }
public List<Comment> Comments { get; set; }
public string Text { get; set; }
public DateTime DateTime { get; set; }
public string UserId { get; set; }
[ForeignKey("UserId")]
public IdentityUser User { get; set; }
public List<CommentVote> Votes { get; set; }
public int Score() =>
Votes
.Where(vote => vote.CommentId == Id)
.Sum(vote => vote.Score);
public CommentVote UserVote(string userId) =>
Votes.FirstOrDefault(vote => vote.UserId == userId);
public int UserScore(string userId)
{
var vote = UserVote(userId);
return vote == null ? 0 : vote.Score;
}
public async Task Vote(int score, string voterUserId)
{
var vote = UserVote(voterUserId);
if (vote == null)
{
vote = new CommentVote()
{
UserId = voterUserId,
CommentId = Id,
Score = score,
DateTime = DateTime.Now
};
Votes.Add(vote);
}
else
{
vote.Score = vote.Score == score ? 0 : score;
}
}
public async Task AddComment(string text, string commenterUserId)
{
var comment = new Comment()
{
UserId = commenterUserId,
ParentCommentId = Id,
Text = text,
DateTime = DateTime.Now
};
Comments.Add(comment);
}
}
Details
Razor page
Here's the OnGetAsync
method for the Details
page.
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
Link = await _context.Link
.Include(link => link.User)
.Include(link => link.Votes)
.Include(link => link.Comments)
.FirstOrDefaultAsync(m => m.Id == id);
void load_comments(List<Comment> comments)
{
foreach (var comment in comments)
{
_context.Entry(comment).Reference(comment => comment.User).Load();
_context.Entry(comment).Collection(comment => comment.Comments).Load();
_context.Entry(comment).Collection(comment => comment.Votes).Load();
load_comments(comment.Comments);
}
}
load_comments(Link.Comments);
if (Link == null)
{
return NotFound();
}
return Page();
}
As you can see, I use Include
with various navigation properties of the Link
.
When a user adds a comment to a Link
, the LinkId
of the Comment
has a value. When a user adds a comment to a Comment
, the ParentCommentId
of the Comment
has a value.
Since comments can have comments, I use the recursive internal function load_comments
to retrieve the comments tree via explicit loading.
Suggested approaches
A Google search for ef core include recursive leads to various approaches to loading recursive entities.
This answer on stackoverflow recommends a recursive function approach, somewhat similar to what I've shown above.
However, this popular answer by an experienced user suggests that the whole tree can be retrieved via
Include
(eager loading), without needing to use explicit loading.
The question
Is there a way to get the comments for a Link
using eager loading instead of explicit loading? It seems like that would be a simpler implementation that the explicit recursive approach.
The second answer above suggests that it should work. However, implementing that suggestion (and some variations on it) leads to runtime errors when loading the comments page.
Full project
The full project is available on github: LinkAggregatorComments. This is just a snapshot of the project, meant for questions like this.
Links to some of the parts discussed above:
- Link
- Comment
- DetailsModel
- Details razor page