Search code examples
c#entity-frameworkdatabase-migrationentity-framework-migrationsone-to-one

Irregular migration files for two one-to-one relations


I have three classes that are supposed to split a concept into 3 tables on one-to-one basis. Literally, a huge table that I want to chunk up.

class MainHolder
{
  public Guid Id { get; set; }
  public FirstPart FirstPart { get; set; }
  public SecondPart SecondPart { get; set; }
}
class FirstPart
{
  public Guid Id { get; set; }
  public string Title { get; set; }
}
class SecondPart
{
  public Guid Id { get; set; }
  public string Description { get; set; }
}

I'm setting up the context and relations as follows.

public DbSet<MainHolder> Holders { get; set; }
public DbSet<FirstPart> FirstParts { get; set; }
public DbSet<SecondPart> SecondParts { get; set; }

protected override void OnModelCreating(ModelBuilder model)
{
  model.Entity<FirstPart>()
    .HasOne<MainHolder>()
    .WithOne(a => a.FirstPart)
    .HasForeignKey<MainHolder>();
  model.Entity<SecondPart>()
    .HasOne<MainHolder>()
    .WithOne(a => a.SecondPart)
    .HasForeignKey<MainHolder>();
}

When I create a migration, the main holder contains 2 columns - first with ID of itself and one with the ID of the second entity but not the first one. The foreign keys are set up as expected: it's just the columns that are incomplete.

migrationBuilder.CreateTable(name: "Holders", columns: table => new
{
  Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
  SecondPartId = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
},
constraints: table =>
{
  table.PrimaryKey("PK_Holders", x => x.Id);
  table.ForeignKey(
    name: "FK_Holders_FirstParts_Id",
    column: x => x.Id,
    principalTable: "FirstParts",
    principalColumn: "Id",
    onDelete: ReferentialAction.Cascade);
  table.ForeignKey(
    name: "FK_Holders_SecondParts_SecondPartId",
    column: x => x.SecondPartId,
    principalTable: "SecondParts",
    principalColumn: "Id");
});

migrationBuilder.CreateTable(name: "FirstParts", columns: table => new
{
  Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
  Title = table.Column<string>(type: "nvarchar(max)", nullable: false)
},
constraints: table => { table.PrimaryKey("PK_FirstParts", x => x.Id); });

migrationBuilder.CreateTable(name: "SecondParts", columns: table => new
{
  Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
  Description = table.Column<string>(type: "nvarchar(max)", nullable: false)
},
constraints: table => { table.PrimaryKey("PK_SecondParts", x => x.Id); });

I notice a slight difference in the setup of the FK's in naming and cascade strategy. I can't for the life of me explain why this happens since both entities are declare and configured precisely the same way.

What am I missing?

If it's due to some convention for managing multiple has-one/with-one tables, then I failed miserably to find any kind of documentation on that. I fear that there actually is a deviation and I'm simply to dense to realize it. Help me understand.


Solution

  • Try removing the .HasForegnKey(). I think EF may be getting confused by this expecting a unique FK on the MainHolder for each relationship and using the default ID for the first case, then generating another one for the 2nd. By default a 1-to-1 relationship will be established between the PKs on both tables, which looks to be the way you are setting things up and aiming for. So, just having the HasOne().WithOne() defined for both should work as expected.

    HasForeignKey() is normally used in 1-to-1 relationships where you want to nominate a separate FK on one side for the relationship. For example if you wanted a FirstPartId on the MainHolder rather than joining the two on the PK. The default behavior is to use the PKs on both tables.

    The other change I would suggest is to define the 1-to-1 relationships from the MainHolder perspective rather than from both FirstPart and SecondPart.

    So instead of:

    model.Entity<FirstPart>()
      .HasOne<MainHolder>()
      .WithOne(a => a.FirstPart)
      .HasForeignKey<MainHolder>();
    model.Entity<SecondPart>()
      .HasOne<MainHolder>()
      .WithOne(a => a.SecondPart)
      .HasForeignKey<MainHolder>();
    

    use:

    model.Entity<MainHolder>()
      .HasOne(a => a.FirstPart)
      .WithOne();
    model.Entity<MainHolder>()
      .HasOne(a => a.SecondPart)
      .WithOne();
    

    The rules around support for optional bi-directional navigation properties are somewhat fluid with EF Core, so the WithOne() may not work as expected without a reference for MainHolder on the FirstPart and SecondPart. A work-around is typically to add a protected (or possibly private) reference if EF Core complains. 1-to-1 relationships commonly benefit from having bi-directional references where a MainHolder contains a reference to the FirstPart, and a FirstPart contains a reference back to its MainHolder. That navigation property does not need a ForeignKey attribute or mapping defined when part of a one-to-one relationship.