Defining Self Referencing Foreign-Key-Relationship Using Entity Framework 7 Code First

ef-fluent-api entity-framework-core

Question

I have an ArticleComment entity as you can see below:

public class ArticleComment
 {
    public int ArticleCommentId { get; set; }
    public int? ArticleCommentParentId { get; set; }

    //[ForeignKey("ArticleCommentParentId")]
    public virtual ArticleComment Comment { get; set; }
    public DateTime ArticleDateCreated  { get; set; }
    public string ArticleCommentName { get; set; }
    public string ArticleCommentEmail { get; set; }
    public string ArticleCommentWebSite { get; set; }
    public string AricleCommentBody { get; set; }

    //[ForeignKey("UserIDfk")]   
    public virtual ApplicationUser ApplicationUser { get; set; }
    public Guid? UserIDfk { get; set; }

    public int ArticleIDfk { get; set; }
    //[ForeignKey("ArticleIDfk")]   
    public virtual Article Article { get; set; }


}

I want to define a foreign key relationship in such a way that one comment can have many reply or child, I've tried to create the relationship using fluent API like this:

builder.Entity<ArticleComment>()
            .HasOne(p => p.Comment)
            .WithMany()
            .HasForeignKey(p => p.ArticleCommentParentId)
            .OnDelete(DeleteBehavior.Restrict)
            .IsRequired(false);

I followed the solution that was proposed here and here, but I get an error with the message:

Introducing FOREIGN KEY constraint 'FK_ArticleComment_ArticleComment_ArticleCommentParentId' on table 'ArticleComment' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints. Could not create constraint or index. See previous errors.

First I though by setting the OnDelete(DeleteBehavior.Restrict) this would go away, but the problem persist, also I've tried to use the data annotation [ForeignKey("ArticleCommentParentId")] as you can see the commented code in the ArticleComment definition, but it didn't work, I'd appreciate any though on this.

Accepted Answer

You are not modeling correctly your entity. Each comment needs a Set of replies, which are of type ArticleComment too, and each of those replies are the ones that point back to its parent (Note the added ICollection Replies property):

  public class ArticleComment
     {
        public ArticleComment()
        {
            Replies = new HashSet<ArticleComment>();
        }

        public int ArticleCommentId { get; set; }
        public int? ParentArticleCommentId { get; set; }
        public virtual ArticleComment ParentArticleComment{ get; set; }
        public virtual ICollection<ArticleComment> Replies { get; set; }

        //The rest of the properties omitted for clarity...
    }

...and the fluent Mapping:

modelBuilder.Entity<ArticleComment>(entity =>
{
    entity
        .HasMany(e => e.Replies )
        .WithOne(e => e.ParentArticleComment) //Each comment from Replies points back to its parent
        .HasForeignKey(e => e.ParentArticleCommentId );
});

With the above setup you get an open-ended tree structure.

EDIT: Using attributes you just need to decorate ParentArticleComment property. Take into account that in this case EF will resolve all the relations by convention.

[ForeignKey("ParentArticleCommentId")]
public virtual ArticleComment ParentArticleComment{ get; set; } 

For collection properties EF is intelligent enough to understand the relation.


Popular Answer

I simplified the class (removing foreign key support fields) and it works.
It could be an issue of your EF version (I've just installed it but actually I think I'm using rc1 but I'm not sure because I had several dependency issues) or it could be your model.
Anyway, this source works fine

public class ArticleComment
{
    public int ArticleCommentId { get; set; }

    public virtual ArticleComment Comment { get; set; }
    public DateTime ArticleDateCreated { get; set; }
    public string ArticleCommentName { get; set; }
    public string ArticleCommentEmail { get; set; }
    public string ArticleCommentWebSite { get; set; }
    public string AricleCommentBody { get; set; }
}

class Context : DbContext
{
    public Context(DbContextOptions dbContextOptions) : base(dbContextOptions)
    {}

    public DbSet<ArticleComment> Comments { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<ArticleComment>()
            .HasOne(p => p.Comment)
            .WithMany();

    }
}

static class SampleData
{
    public static void Initialize(Context context)
    {
        if (!context.Comments.Any())
        {
            var comment1 = new ArticleComment()
            {
                AricleCommentBody = "Article 1"
            };

            var comment2 = new ArticleComment()
            {
                AricleCommentBody = "Article 2 that referes to 1",
                Comment = comment1
            };

            context.Comments.Add(comment2);
            context.Comments.Add(comment1);

            context.SaveChanges();
        }
    }
}


Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why