One-way one-to-one relationship with EF Core

asp.net-core c# entity-framework-core

Question

What I want to achieve, is have a model CommentsThread that can be attached to any other model that needs comments, be it Chapter, BlogPost, UserProfile, or what have you. Basically, the structure I need is

CommentsThread
  int ID

Chapter
  int ID
  int Thread FK(CommentsThread.ID)

BlogPost
  int ID
  int Thread FK(CommentsThread.ID)

UserProfile
  int ID
  int Thread FK(CommentsThread.ID)

however I have no idea how to configure it properly in EF core, without having to add nullable references to Chapter, BlogPost and UserProfile to the CommentsThread model.

My code for CommentsThread is as follows:

public class CommentsThread
{
    [Key]
    [Required]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }
    public ICollection<Comment> Comments { get; set; }
}

And Chapter, omitting any irrelevant properties, is

public class Chapter
{
    [Key]
    [Required]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    public CommentsThread CommentsThread { get; set; }
}

With the following Fluent config (again, skipping the irrelevant bits):

builder.Entity<CommentsThread>()
    .HasMany(ct => ct.Comments)
    .WithOne()
    .OnDelete(DeleteBehavior.Cascade);

builder.Entity<Chapter>()
    .HasOne(c => c.CommentsThread)
    .WithOne()
    .OnDelete(DeleteBehavior.Cascade);

On dotnet ef migrations add X it throws this output into the console.


Update:

So, I tried explicitly adding public int CommentsThreadId { get; set; } to the Chapter and changing the config to

builder.Entity<Chapter>()
    .HasOne(c => c.CommentsThread)
    .WithOne()
    .HasForeignKey<Chapter>(c => c.CommentsThreadId)
    .OnDelete(DeleteBehavior.Cascade);

That did make the migration go through, but failed on database update with

The CREATE UNIQUE INDEX statement terminated because a duplicate key was found for the object name 'dbo.Chapters' and the index name 'IX_Chapters_CommentsThreadId'. The duplicate key value is (0).
The statement has been terminated.
1
0
2/18/2020 1:21:36 AM

Accepted Answer

Your database migration error;

The CREATE UNIQUE INDEX statement terminated because a duplicate key was found for the object name 'dbo.Chapters' and the index name 'IX_Chapters_CommentsThreadId'. The duplicate key value is (0).
The statement has been terminated.

Is due to adding a non-null column to a table that already has data in it. If you want to force a CommentsThread to exist for each record type, you'll also need to write a migration script to insert these records for existing data.

Or just change the foreign key column type to int? so it will allow nulls.

1
2/19/2020 11:58:19 PM

Popular Answer

AFAIK, there are two ways to configure 1-1 relationship:

  1. https://www.entityframeworktutorial.net/efcore/one-to-one-conventions-entity-framework-core.aspx
  2. https://weblogs.asp.net/manavi/inheritance-mapping-strategies-with-entity-framework-code-first-ctp5-part-2-table-per-type-tpt

the 1st post requires reference from each model. but you mentioned:

without having to add nullable references

then, only the 2nd concept (Table Per Type) can reach your goal, but the post is too old code to EF Core.

Here are new data models:

public class CommentsThread
{
    [Key]
    [Required]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    [Required]
    public string Title { get; set; }

    public ICollection<ThreadComment> Comments { get; set; }
}

[Table(nameof(Chapter))]
public class Chapter
{
    [Key]
    [Required]
    [ForeignKey(nameof(CommentsThread))]
    public int Id { get; set; }

    public int Number { get; set; }

    public CommentsThread CommentsThread { get; set; }
}
[Table(nameof(BlogPost))]
public class BlogPost
{
    [Key]
    [Required]
    [ForeignKey(nameof(CommentsThread))]
    public int Id { get; set; }

    public string Author { get; set; }

    public CommentsThread CommentsThread { get; set; }
}

[Table(nameof(UserProfile))]
public class UserProfile
{
    [Key]
    [Required]
    [ForeignKey(nameof(CommentsThread))]
    public int Id { get; set; }
    public string Bio { get; set; }

    public CommentsThread CommentsThread { get; set; }
}

public class ThreadComment
{
    [Key]
    [Required]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    [Required]
    public string Content { get; set; }

    public CommentsThread CommentsThread { get; set; }
}

Here is sample context:

public class ApplicationDbContext : IdentityDbContext
{
    public virtual DbSet<CommentsThread> CommentsThreads { get; set; }
    public virtual DbSet<Chapter> Chapters { get; set; }
    public virtual DbSet<BlogPost> BlogPosts { get; set; }
    public virtual DbSet<UserProfile> UserProfiles { get; set; }
    public virtual DbSet<ThreadComment> ThreadComments { get; set; }

    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }


    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.Entity<Chapter>().ToTable(nameof(Chapter));
        modelBuilder.Entity<BlogPost>().ToTable(nameof(BlogPost));
        modelBuilder.Entity<UserProfile>().ToTable(nameof(UserProfile));
    }
}

Here is generated migration code:

public partial class AddCommentsThread : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "CommentsThreads",
            columns: table => new
            {
                Id = table.Column<int>(nullable: false)
                    .Annotation("SqlServer:Identity", "1, 1"),
                Title = table.Column<string>(nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_CommentsThreads", x => x.Id);
            });

        migrationBuilder.CreateTable(
            name: "BlogPost",
            columns: table => new
            {
                Id = table.Column<int>(nullable: false),
                Author = table.Column<string>(nullable: true)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_BlogPost", x => x.Id);
                table.ForeignKey(
                    name: "FK_BlogPost_CommentsThreads_Id",
                    column: x => x.Id,
                    principalTable: "CommentsThreads",
                    principalColumn: "Id",
                    onDelete: ReferentialAction.Cascade);
            });

        migrationBuilder.CreateTable(
            name: "Chapter",
            columns: table => new
            {
                Id = table.Column<int>(nullable: false),
                Number = table.Column<int>(nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Chapter", x => x.Id);
                table.ForeignKey(
                    name: "FK_Chapter_CommentsThreads_Id",
                    column: x => x.Id,
                    principalTable: "CommentsThreads",
                    principalColumn: "Id",
                    onDelete: ReferentialAction.Cascade);
            });

        migrationBuilder.CreateTable(
            name: "ThreadComments",
            columns: table => new
            {
                Id = table.Column<int>(nullable: false)
                    .Annotation("SqlServer:Identity", "1, 1"),
                Content = table.Column<string>(nullable: false),
                CommentsThreadId = table.Column<int>(nullable: true)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_ThreadComments", x => x.Id);
                table.ForeignKey(
                    name: "FK_ThreadComments_CommentsThreads_CommentsThreadId",
                    column: x => x.CommentsThreadId,
                    principalTable: "CommentsThreads",
                    principalColumn: "Id",
                    onDelete: ReferentialAction.Restrict);
            });

        migrationBuilder.CreateTable(
            name: "UserProfile",
            columns: table => new
            {
                Id = table.Column<int>(nullable: false),
                Bio = table.Column<string>(nullable: true)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_UserProfile", x => x.Id);
                table.ForeignKey(
                    name: "FK_UserProfile_CommentsThreads_Id",
                    column: x => x.Id,
                    principalTable: "CommentsThreads",
                    principalColumn: "Id",
                    onDelete: ReferentialAction.Cascade);
            });

        migrationBuilder.CreateIndex(
            name: "IX_ThreadComments_CommentsThreadId",
            table: "ThreadComments",
            column: "CommentsThreadId");
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "BlogPost");

        migrationBuilder.DropTable(
            name: "Chapter");

        migrationBuilder.DropTable(
            name: "ThreadComments");

        migrationBuilder.DropTable(
            name: "UserProfile");

        migrationBuilder.DropTable(
            name: "CommentsThreads");
    }
}

Here is screenshot for result:

enter image description here



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow