Search code examples
asp.netasp.net-mvcdatabaseentity-frameworkmany-to-one

EF fails when Inserting only one out of two properties of the same type


I have this closure table that is supposed to make easier to get all replies of a comment, their replies, all replies to the comment ( even distant inheritants), but when I try to insert the parent - it inserts the CHILD as the parent. I don't recall if I had ever encountered such issue.

public class ReplyClosure
 {
     [Key]
     public int Id { get; set; }

     public int ParentId { get; set; }

     [ForeignKey(nameof(ParentId))]

     public PostReply Parent { get; set; } = null!;

     public int ChildId { get; set; }
     [ForeignKey(nameof(ChildId))]
     public PostReply Child { get; set; } = null!;

     public int Depth { get; set; } 
     
 }

public class PostReply
{
    [Key]
    public int Id { get; set; }

    public string ForumUserId { get; set; } = string.Empty;

    [ForeignKey(nameof(ForumUserId))]
    public ForumUser ForumUser { get; set; } = null!;

    public int PostId { get; set; }
    [ForeignKey(nameof(PostId))]

    public Post Post { get; set; } = null!;

    [Column(TypeName = "Text")]
    public string Content { get; set; } = String.Empty;


    public DateTime CreatedOn { get; set; } 

    public DateTime ModifiedOn { get; set; }

    public bool IsDeleted { get; set; }

    [InverseProperty(nameof(ReplyClosure.Parent))]
    public virtual List<ReplyClosure> Parents { get; set; } = new List<ReplyClosure>();


    [InverseProperty(nameof(ReplyClosure.Child))]
    public virtual List<ReplyClosure> Children { get; set; } = new List<ReplyClosure>();

}

The problematic code is this: I tried to write this in too many ways and none worked and for some reason I insert the child in the place of parent. I don't know why it is happening. It probably has something to do when trying to create a reply at the same time as reply closure. I tried using the other way around - insert the reply and use the navigation property to add the parent. It did not work either.

PostReply postReply = new PostReply
 {
     CreatedOn = DateTime.Now,
     PostId = replyViewModel.PostId,
     UserId = GetUserId(),
     Content = replyViewModel.Content,
     IsDeleted = false
 };
 var closure = new ReplyClosure
 {
     ParentId = replyViewModel.ParentId, // this is the correct id, confirmed
     Depth = replyViewModel.Depth,
     Child = postReply
 };
          
 await _context.RepliesClosures.AddAsync(closure);
 await _context.SaveChangesAsync();

The context contains the following db relationship description:

builder.Entity<PostReply>()
.HasOne(entry => entry.Post)
.WithMany(entry => entry.PostsReplies)
.OnDelete(DeleteBehavior.Restrict);
  
builder.Entity<ReplyClosure>()
.HasOne(entry => entry.Parent)
.WithMany(entry => entry.Children)
.HasForeignKey(entry => entry.ParentId)
.OnDelete(DeleteBehavior.Restrict);
 
builder.Entity<ReplyClosure>()
.HasOne(entry => entry.Child)
.WithMany(entry => entry.Parents)
.HasForeignKey(entry => entry.ChildId)
.OnDelete(DeleteBehavior.Restrict);

Ps. I know there is probably a better way to design a comment area in a simulated forum app, but this was the bare minimum I could think of. I know I might be forgetting something, just not sure what it is. Thanks

I tried to use the inverse navigation properties - to no avail, I can't use a raw sql transaction and I also tried chat gpt, all my db relations seem correct. I want to create the two entities at the same time and use saveChanges just once. I tried first inserting the reply, saving and then inserting the closure. It still inserts the child in both places - the parentId and the childId, so it must be the way I define the relations in the dbcontext or something.


Solution

  • This configuration:

    [InverseProperty(nameof(ReplyClosure.Parent))]
    public virtual List<ReplyClosure> Parents { get; set; } = new List<ReplyClosure>();
    

    Does not match this config:

    builder.Entity<ReplyClosure>()
        .HasOne(entry => entry.Parent)
        .WithMany(entry => entry.Children)
        .HasForeignKey(entry => entry.ParentId)
        .OnDelete(DeleteBehavior.Restrict);
    

    The second fluent config looks correct, using the attributes instead should be:

    [InverseProperty(nameof(ReplyClosure.Child))]
    public virtual List<ReplyClosure> Parents { get; set; } = new List<ReplyClosure>();
    
    [InverseProperty(nameof(ReplyClosure.Parent))]
    public virtual List<ReplyClosure> Children { get; set; } = new List<ReplyClosure>();
    

    If you are seeing the wrong FK updated I think EF is using the attributes and ignoring the correct fluent config. I'd recommend using one or the other to avoid this possible confusion.