I have the following entity models set up in my EF Core project:
public class ForumPost
{
[Key]
public int Id { get; set; }
public string Title { get; set; }
// ...some other properties
public List<Tag> Tags { get; set; }
}
public class Tag
{
[Key]
public string Name { get; set; }
public string Description { get; set; }
// ...some other properties
public List<ForumPost> Posts { get; set; }
}
So I have a many to many relationship between posts and tags. I have no relationship configuration in my OnModelCreating.
My context is setup as follows:
public class ForumsContext : DbContext
{
public DbSet<ForumPost> Posts { get; set; }
public DbSet<Tag> Tags { get; set; }
}
When I query my ForumsContext as follows:
var posts = await context.Posts.Include(p => p.Tags).ToListAsync();
EF automatically populates the Posts field for each of the Tag objects in each of the ForumPost objects in posts. I cannot figure out why this is happening and if I was to serialize the posts list above to JSON it would throw an exception due to the object cycle (Post->Tags->Posts->Post).
Why are the tag's post lists being populated? How can I tell EF to not populate this list.
That's just how Entity Framework works. The Include call populates the navigation property on both sides of the relationship.
Entity Framework conventions dictate that you must have navigation properties on both ends of a many-to-many relationship.
It does not appear to be possible to only have one navigation property using the fluent API, either. The code below does not work, and in asking whether there is a way to avoid defining the property for one of the entities, in this thread the team answered with "Not yet".
modelBuilder.Entity<ForumPost>().HasMany<Tag>(post => post.Tags).WithMany();
As for object cycle errors when serializing to JSON, these can be handled or ignored in .NET 5 or later.
This article goes into more detail on the issue, and shows a solution for NewtonSoft.Json.
I'm not a fan of this solution. You'll end up with null properties where the serializer stopped because it encountered a cycle.
But all of that is beside the point. You don't need to change your data model or JSON serializer settings to get around this issue. The issue is that you're exposing your data model in the first place.
The proper solution is to map your entities to response/view models or DTOs instead. This allows you to format proper responses, and it allows your data model to evolve without interfering with your response model.
Example:
public class ForumPostResponse
{
public int Id { get; set; }
public string Title { get; set; }
public List<Tag> Tags { get; set; }
}
public class TagResponse
{
public string Name { get; set; }
public string Description { get; set; }
}
You could also format ForumPostResponse.Tags as a list of string instead, in case you only want a list of tag names on forum posts, and include the description in other responses. Anything's possible.
You do need to write extra code to handle the mapping between objects, but it's a small tradeoff for a much nicer and more maintainable design. And besides, there are several libraries that simplify the process, the most popular being AutoMapper.
I know this might not be the best solution for circular reference on serialization, but an workaround can be initializing the list property:
public class ForumPost
{
[Key]
public int Id { get; set; }
public string Title { get; set; }
// ...some other properties
public List<Tag> Tags { get; set; } = new List<Tag>();
}
public class Tag
{
[Key]
public string Name { get; set; }
public string Description { get; set; }
// ...some other properties
public List<ForumPost> Posts { get; set; } = new List<ForumPost>();
}
Also, due to initialization, you can manipulate those lists without worrying about null exeptions and all that stuff. But note, there might be a better solution for this problem.