Entity Framework : reference loop in many to many relationship - c#

I have this three entities Customer, Product and Review.
A customer can have many products, and a product can have only one customer as owner. A customer can also have many reviews, and one review can have only one customer. A product can have many reviews.
It seems like I am having a reference loop and below is the JsonException that I get when trying to get all customers:
Error message
System.Text.Json.JsonException: A possible object cycle was detected. This can either be due to a cycle or if the object depth is larger than the maximum allowed depth of 32. Consider using ReferenceHandler.Preserve on JsonSerializerOptions to support cycles.
Path: $.rows.Reviews.Product.Reviews.Product.Reviews.Product.Reviews.Product.Reviews.Product.Reviews.Product.Reviews.Product.Reviews.Product.Reviews.Product.Reviews.Id.
Code:
namespace Domain.Entities
{
public partial class Customer
{
public int Id { get; set; }
public string? Name { get; set; }
public virtual ICollection<Review> Reviews { get; set; }
}
public partial class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public int Price { get; set; }
public int CustomerId { get; set; }
public Customer Customer { get; set; }
public virtual ICollection<Review> Reviews { get; set; }
}
public partial class Review
{
public int Id { get; set; }
public int Stars { get; set; }
public string Description { get; set; }
public int CustomerId { get; set; }
public int ProductId { get; set; }
public Customer Customer { get; set; }
public Product Product { get; set; }
}
}
ModelBuilder configurations:
// Products configurations
builder.Ignore(e => e.DomainEvents);
builder.HasKey(t => t.Id);
// Customers configurations
builder.Ignore(e => e.DomainEvents);
builder.HasMany(e => e.Reviews)
.WithOne(e => e.Customer)
.HasForeignKey(uc => uc.Id);
builder.HasMany(e => e.MessagesSent)
.WithOne(e => e.Receiver)
.HasForeignKey(uc => uc.SenderId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(e => e.MessagesReceived)
.WithOne(e => e.Sender)
.HasForeignKey(uc => uc.ReceiverId)
.OnDelete(DeleteBehavior.Cascade);
// Reviews configurations
builder.HasKey(t => t.Id);
builder.HasOne(d => d.Customer)
.WithMany(p => p.Reviews)
.HasForeignKey(t => t.CustomerId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne(d => d.Product)
.WithMany(p => p.Reviews)
.HasForeignKey(t => t.ProductId)
.OnDelete(DeleteBehavior.Cascade);
Any idea on how to fix this error?
Thanks in advance and if you need any more information please do let me know and I will provide asap.
Edit: this is the query that I am using for getting all customers:
public async Task<PaginatedData<CustomerDto>> Handle(CustomersWithPaginationQuery request)
{
var filters = PredicateBuilder.FromFilter<Customer>("");
var data = await _context.Customers
.Where(filters)
.OrderBy("Id desc")
.ProjectTo<CustomerDto>(_mapper.ConfigurationProvider)
.PaginatedDataAsync(1, 15);
return data;
}
Edit #2: CustomerDto
namespace Application.Customers.DTOs
{
public partial class CustomerDto : IMapFrom<Customer>
{
public int Id { get; set; }
public string Name { get; set; }
public List<Review> Reviews { get; set; }
}
}

To fix this issue you need to add a ReviewDto class like this:
public partial class ReviewDto
{
public int Id { get; set; }
public int Stars { get; set; }
public string Description { get; set; }
// ...
}
And update the CustomerDto:
public partial class CustomerDto : IMapFrom<Customer>
{
public int Id { get; set; }
public string Name { get; set; }
public List<ReviewDto> Reviews { get; set; }
}

As the comments suggest, the problem is not with EF; it is with the default mechanism of System.Text.Json to serialize everything, even if there are loops. The problem with that is you eventually hit a limit giving you that exception. It is probably not your intent to send such a bloated payload back to API clients.
You can prevent that a number of different ways. You can null out the properties that would lead to cycles, but this "sort of" destroys data and could be misinterpreted by clients.
Another way would be to map your classes with cycles to DTOs that explicitly suppress the loop by not including that data, or substituting a reference property (e.g. an ID or some other reference value) to data that has been repeated.
If you don't want to do that, you can prevent the exception by using a ReferenceHandler set to ignore cycles.
This documentation explains how to do that. The effect is equivalent to the first solution of nulling out the values manually. An excerpt from that page
Employee tyler = new()
{
Name = "Tyler Stein"
};
Employee adrian = new()
{
Name = "Adrian King"
};
tyler.DirectReports = new List<Employee> { adrian };
adrian.Manager = tyler;
JsonSerializerOptions options = new()
{
ReferenceHandler = ReferenceHandler.IgnoreCycles,
WriteIndented = true
};
string tylerJson = JsonSerializer.Serialize(tyler, options);
...
Really, though, you're missing a step. It makes more sense to map your returned entities to DTOs. The purpose of the DTOs is to shape the response content to the needs of the API clients. That makes Ghassen's answer a good one.

Related

Many to Many, self join - how to do this, code first in Fluent Api, EF Core 6?

https://i.imgur.com/rvWQVQt.png
So basically, I want to be able to define a User and have them be able to have a list of other Users that I designate as their friends - for some reason I'm stumped
Here are my classes and attempt so far:
public class User : BaseEntity, IUser
{
public string UserName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public Guid PhotoId { get; set; }
public string Mobile { get; set; }
public IList<ClubbrEvent> ClubbrEvents { get; set; }
public bool ProfileComplete { get; set; }
public List<UserFriends> Friends { get; set; }
public List<UserFriends> FriendsOf { get; set; }
}
public class UserFriends
{
public Long UserId { get; set; }
public User User { get; set; }
public Long FriendId { get; set; }
public User Friend { get; set; }
}
public class UserFriendsConfiguration: IEntityTypeConfiguration<UserFriends>
{
public void Configure(EntityTypeBuilder<UserFriends> builder)
{
builder.HasOne(f => f.Friend)
.WithMany(fo => fo.FriendsOf)
.HasForeignKey(fk => fk.FriendId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(u => u.User)
.WithMany(f => f.Friends)
.HasForeignKey(fk => fk.UserId);
}
}
But when I try to add a migration I get the following error:
The entity type 'UserFriends' requires a primary key to be defined. If you intended to use a keyless entity type, call 'HasNoKey' in 'OnModelCreating'. For more information on keyless entity types, see https://go.microsoft.com/fwlink/?linkid=2141943.
Ok, I figured it out, so leaving this here for anyone else in the same situation.
First, I had made a mistake in my join table properties - I had made them long but they should have been guid
Second, I defined the key in the config like so:
builder.HasKey(k => new { k.UserId, k.FriendId });
So in full:
public void Configure(EntityTypeBuilder<UserFriends> builder)
{
builder.HasKey(k => new { k.UserId, k.FriendId });
builder.HasOne(f => f.Friend)
.WithMany(fo => fo.FriendsOf)
.HasForeignKey(fk => fk.FriendId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(u => u.User)
.WithMany(f => f.Friends)
.HasForeignKey(fk => fk.UserId);
}
Running the migration and update now gives me what I need:
https://i.imgur.com/my674wx.png

entity framework core nested object with self parentId

I have a treetable structure and this data comes to me from the frontend.
In this treetable structure, there is IssueActivity and IssueActivityDetail for details of this issue.
Now my question is, more than one IssueActivityDetail field can be added to this IssueActivity field. How can I do this on the c# ef core side?
I tried to do it with the logic of ParentId. My Entity structures are as follows. I did not add the parentId in FluenApi because I did not fully understand it.
My IssueActivity table.
public partial class IssueActivitiy
{
public int Id { get; set; }
public int IssueId { get; set; }
public byte Type { get; set; }
public short SubActivityNo { get; set; }
public string SubActivityTitle { get; set; }
public virtual Issue Issue { get; set; }
public virtual List<IssueActivitiyDetail> IssueActivitiyDetails { get; set; }
}
My IssueActivityDetail table.
public partial class IssueActivitiyDetail
{
public int Id { get; set; }
public int IssueActivityId { get; set; }
public short LineNo { get; set; }
public string Definition { get; set; }
public byte RoleId { get; set; }
public byte Medium { get; set; }
public string Explanation { get; set; }
public int? ParentId { get; set; }
public virtual IssueActivitiy IssueActivity { get; set; }
}
FluentApi Configuration.
public void Configure(EntityTypeBuilder<IssueActivitiy> modelBuilder)
{
modelBuilder.ToTable("IssueActivitiy");
modelBuilder.HasKey(a => a.Id);
modelBuilder.Property(e => e.SubActivityNo).HasComment("Sıra No");
modelBuilder.Property(e => e.SubActivityTitle).HasMaxLength(256).IsUnicode(false);
modelBuilder.Property(e => e.Type).HasDefaultValueSql("((1))").HasComment("1) Temel Aktivite\r\n2) Alternatif Aktivite\r\n3) İşlem İptal Aktivite");
modelBuilder.HasOne(d => d.Issue).WithMany(p => p.IssueActivitiys).HasForeignKey(d => d.IssueId).OnDelete(DeleteBehavior.ClientSetNull).HasConstraintName("FK_Issue_IssueActivitiy_Id");
}
public void Configure(EntityTypeBuilder<IssueActivitiyDetail> modelBuilder)
{
modelBuilder.ToTable("IssueActivitiyDetail");
modelBuilder.Property(e => e.Definition).IsRequired().HasMaxLength(2048).IsUnicode(false).HasComment("Açıklama");
modelBuilder.Property(e => e.Explanation).HasMaxLength(2048).IsUnicode(false).HasComment("Açıklama");
modelBuilder.Property(e => e.IssueActivityId).HasComment("Konu Id");
modelBuilder.Property(e => e.LineNo).HasComment("Sıra No");
modelBuilder.Property(e => e.Medium).HasComment("Ortam (Excel, Mail vb.)");
modelBuilder.Property(e => e.RoleId).HasComment("Rol");
modelBuilder.Property(e => e.ParentId);
modelBuilder.HasOne(d => d.IssueActivity).WithMany(p => p.IssueActivitiyDetails).HasForeignKey(d => d.IssueActivityId).OnDelete(DeleteBehavior.ClientSetNull).HasConstraintName("FK_IssueActivitiy_IssueActivitiyDetail_");
}
Web Api is also the place where I try to receive and process the data, but I played a lot and couldn't do it correctly.
var vIssueActivity = issueInfo.IssueActivitiyInfos
.Select(a => new IssueActivitiy
{
Type = a.Type,
SubActivityNo = a.SubActivityNo,
SubActivityTitle = a.SubActivityTitle,
IssueActivitiyDetails = a.IssueActivitiyDetailInfos
.Select(x => new IssueActivitiyDetail
{
LineNo = x.LineNo,
Definition = x.Definition,
RoleId = vUser.RoleId,
Medium = x.Medium,
Explanation = x.Explanation,
IssueActivityDetail = new List<IssueActivitiyDetail> { }
}).ToList()
});
You don't need to keep ParentId property in IssueActivityDetail.
public partial class IssueActivitiy
{
...
public virtual List<IssueActivitiyDetail> IssueActivitiyDetails { get; set; }
}
public partial class IssueActivitiyDetail
{
...
public virtual IssueActivitiy IssueActivity { get; set; }
}
Your configuration looks not wrong.
Maybe you can use Include when getting the entity from db context.
var issueActivity = context.IssueActivities.Include(x => x.IssueActivityDetails).FirstOrDefault();
You can accomplish this by retrieving all the entries from the database. Then select the Root node and then let EF Core mapping do the rest.
public class TreeNode
{
public bool IsRoot { get; set; }
public int? ParentNodeId {get; set;}
public virtual List<TreeNode> ChildNodes {get; set;}
}
public class TreeNodeRepository
{
public async Task<TreeNode> GetTreeStructure()
{
var allNodes = await _context.TreeNodes.ToListAsync();
return allNodes.FirstOrDefault(t => t.IsRoot);
}
}
You could argue that ParentId == null would also imply that it's a parent node. this just makes the example given more tuitive imo.
You should consider performance, how many nodes will become an issue, is it exposed through a web-api and would iterating over the nodes be more efficient. So you wouldn't have to load the entire Tree into memory each time but let clients handle that instead.

Map components of DTO which are DTOs as well

This is my class which holds database data:
public partial class PermissionGroup
{
public int Id { get; set; }
public string Name { get; set; }
// other database properties
public virtual ICollection<GroupActionPermission> GroupActionPermissions { get; set; }
}
And that's my dto's:
public class PermissionGroupDTO
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<GroupActionPermissionDTO> ActionPermissions { get; set; }
}
public class GroupActionPermissionDTO
{
public int Id { get; set; }
public int GroupId { get; set; }
public int PermissionActionId { get; set; }
public PermissionGroupDTO Group { get; set; }
}
Now, I am making mapping:
public IEnumerable<PermissionGroupDTO> GetGroups()
{
return OnConnect<IEnumerable<PermissionGroupDTO>>(db =>
{
return db.PermissionGroups
.Include(i => i.GroupActionPermissions)
.ProjectTo<PermissionGroupDTO>()
.ToList();
});
}
And I am getting collection of PermissionGroupDTO which should contains collection of GroupActionPermissionDTO, but that collection stays null. Is there something wrong with my code? I am afraid that automapper can map collections from foreign keys.
Also, thats my automapper initializer:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<PermissionGroup, PermissionGroupDTO>();
cfg.CreateMap<GroupActionPermission, GroupActionPermissionDTO>();
});
I believe the reason is desribed here http://docs.automapper.org/en/stable/Queryable-Extensions.html
Note that for this feature to work, all type conversions must be explicitly handled in your Mapping.
So that means you should manually configure the mapping:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<PermissionGroup, PermissionGroupDTO>()
.ForMember(dto => dto.ActionPermissions , conf => conf.MapFrom(ol => ol.GroupActionPermissions )));;
cfg.CreateMap<GroupActionPermission, GroupActionPermissionDTO>();
});
BTW, note that fields are named differently: GroupActionPermissions vs. ActionPermissions. This is also the reason why automapper doesn't map it automatically and then you should use the manual configuration I wrote.

Many to many relationship mapping in EF Core

I have a problem with many to many relationship in EF core.
I have the following model classes:
public class Meal
{
public int Id { get; set; }
[Required]
public int Insulin { get; set; }
public MealType Type { get; set; }
public ICollection<MealFood> MealFoods { get; set; }
public Meal()
{
MealFoods = new Collection<MealFood>();
}
}
public class Food
{
public int Id { get; set; }
[StringLength(255)]
public string Name { get; set; }
[Required]
public int Carbohydrates { get; set; }
public ICollection<MealFood> MealFoods { get; set; }
public Food()
{
MealFoods = new Collection<MealFood>();
}
}
public class MealFood
{
public int MealId { get; set; }
public Meal Meal { get; set; }
public int FoodId { get; set; }
public Food Food { get; set; }
}
I have the following API resource class:
public class MealResource
{
public int Id { get; set; }
public int Insulin { get; set; }
public MealType Type { get; set; }
public ICollection<FoodResource> Foods { get; set; }
public MealResource()
{
Foods = new Collection<FoodResource>();
}
}
I have done the mapping in my DbContext:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<MealFood>().HasKey(mf => new { mf.MealId, mf.FoodId });
modelBuilder.Entity<MealFood>().HasOne(mf => mf.Meal).WithMany(m => m.MealFoods).HasForeignKey(mf => mf.MealId);
modelBuilder.Entity<MealFood>().HasOne(mf => mf.Food).WithMany(f => f.MealFoods).HasForeignKey(mf => mf.FoodId);
}
I've got a problem with this call:
var meals = await context.Meals.Include(m => m.MealFoods).ToListAsync();
This returns almost everything I need, except the navigation properties from MealFoods
The reason why I want those properties, because I want to do the following mapping:
CreateMap<Meal, MealResource>().ForMember(mr => mr.Foods, opt => opt.MapFrom(x => x.MealFoods.Select(y => y.Food).ToList()));
I have already found this:
Automapper many to many mapping
but (maybe I don't get something) this doesn't work because the property called Food in MealFood is null.
I hope I didn't explain too complex.
When you include navigation property, EF Core automatically fills the inverse navigation property, e.g. including Meal.MealFoods will automatically fill MealFood.Meal, including Food.MealFoods will automatically populate MealFood.Food etc. In order to populate other navigation properties you need to use additional ThenInclude. E.g.
var meals = await context.Meals
.Include(m => m.MealFoods)
.ThenInclude(mf => mf.Food) // <--
.ToListAsync();
or
var foods = await context.Foods
.Include(f => f.MealFoods)
.ThenInclude(mf => mf.Meal) // <--
.ToListAsync();

Is there a way I can avoid getting another copy of a child data when I include a parent in EF5?

I have the following classes and I am having a problem with getting data from them:
public partial class Exam {
public Exam()
{
this.Objectives = new List<Objective>();
}
public int ExamId { get; set; }
public int SubjectId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public virtual ICollection<Objective> Objectives { get; set; }
}
public partial class Objective {
public int ObjectiveId { get; set; }
public int ExamId { get; set; }
public string Name { get; set; }
public string Text { get; set; }
public virtual Exam Exam { get; set; }
}
I am getting a list of Objectives and I want to include Exam.Name.
Here is the query that I created so I could get Exam which would give me a way to get the Exam name.
public IList<Objective> GetObjectives(int examId)
{
var objectives = _objectivesRepository
.GetAll()
.Include(o => o.Exam)
.ToList();
return objectives;
}
Here is the mapping that I am using:
public ObjectiveMap()
{
this.HasRequired(t => t.Exam)
.WithMany(t => t.Objectives)
.HasForeignKey(d => d.ExamId)
.WillCascadeOnDelete(false);
}
Unfortunately this small query returns over 6MB of data. When I check with Fiddler I see:
Objective objects > Exam objects > Objective objects
What I need to have is:
Objective objects > Exam objects
Is there a solution for this. How can I stop EF5 from getting another layer of objectives?
JSOn serialization is a different issue. Not related to entity framework here. You can use ScriptIgnore attribute to avoid serializing it,
public partial class Exam {
//....
[ScriptIgnore]
public virtual ICollection<Objective> Objectives { get; set; }
}

Categories