Search code examples
c#asp.netentity-framework-core

Multiple relationships to same entity with navigation to principal


Say we have this model of blog and posts:

public partial class Blog
{
    public virtual Post DefaultPost { get; set; }

    public virtual ICollection<Post> Posts{ get; set; }

    public static void OnModelCreating(ModelBuilder modelBuilder)
    {
        OnModelCreating<Blog>(modelBuilder);

        modelBuilder.Entity<Blog>()
            .HasOne(p => p.DefaultPost)
            .WithMany()
            .OnDelete(DeleteBehavior.ClientSetNull);

        modelBuilder.Entity<Blog>()
            .HasMany(p => p.Posts)
            .WithOne()
            .OnDelete(DeleteBehavior.Cascade);
    }
}

public class Post
{
     public int Id { get; set; }

     public virtual Blog Blog { get; set; }
}

As you can see post has the reverse relationship back to blog. But since blog has to relationships to post, efcore will see post as having a BlogId and a BlogId1 foreign key properties. Is it possible to define the reverse relationship for DefaultPost? Something like:

public virtual Blog BlogDefaultPost { get; set; }

Solution

  • Your relationships need to be explicit in this case. You will need a FK for the relationship between the Blog.Posts and Post (typically a BlogId on Post, but you will also need a FK for the Blog.DefaultPost for the one-to-one, which can either be on the Blog (i.e. a Blog.DefaultPostId) or it would be on the Post. (i.e. Post.BlogIAmADefaultFor, which EF is assuming by default and calling BlogId_1) Posts also "belong" to a blogk, so it is a one-to-one, not many-to-one. DefaultPost should be marked as Null-able as well:

    public virtual Post? DefaultPost { get; set; }
    
    public virtual ICollection<Post> Posts{ get; protected set; } = new List<Post>();
    

    To have a DefaultPostId on the Blog table, this can be a shadow property or an exposed as a FK property.

    Shadow property:

    modelBuilder.Entity<Blog>()
        .HasOne(p => p.DefaultPost)
        .WithOne()
        .HasForeignKey<Blog>("DefaultPostId")
        .OnDelete(DeleteBehavior.ClientSetNull);
    

    ... or with FK property:

    public int? DefaultPostId { get; set; }
    public virtual Post? DefaultPost { get; set; }
    
    // ...
    modelBuilder.Entity<Blog>()
        .HasOne(p => p.DefaultPost)
        .WithOne()
        .HasForeignKey<Blog>(p => p.DefaultPostId)
        .OnDelete(DeleteBehavior.ClientSetNull);
    

    The HasMany relationship for posts is probably also confusing EF as you have a Blog property on the Post which should be related to this, but you have WithOne() instead of WithOne(p => p.Blog).

        modelBuilder.Entity<Blog>()
            .HasMany(p => p.Posts)
            .WithOne(p => p.Blog)
            .OnDelete(DeleteBehavior.Cascade);
    

    Note that this is a form of de-normalization in the database as there is nothing that guarantees that the Post marked as the "default" for the blog is even associated to the blog. You could assign any arbitrary PostId in that FK, even Post IDs associated to a different blog. Alternatively you could add a "IsDefault" flag on the Post to identify the default post among all posts for a given blog, though there is no easy way to explicitly ensure that only one Post for a given blog has this flag set. (none may be set, or multiple) Enforcing this rule would need to be done programmatically, but it avoids the possiblity of a completely invalid Post ID being used.