Why does EF Core create a new shadow foreign key instead of using an existing property?

3 weeks ago 9
ARTICLE AD BOX

I have two model classes, Entity and EntityVersion (the name Entity here is unrelated to EF, it's just coincidence given my company's domain). There (simplified) models have a few relationships:

One Entity has many EntityVersions - EntityVersion.EntityId

One Entity has one (optional) EntityVersion - Entity.LatestEntityVersionId

One changeset has many EntityVersions - EntityVersion.ChangesetId (changeset model omitted for brevity - I only included this detail due to the composite index in the model builder)

public class Entity { public Guid Id { get; set; } public Guid? LatestEntityVersionId { get; set; } } public class EntityVersion { public Guid Id { get; set; } public Guid EntityId { get; set; } public Guid ChangesetId { get; set; } public string? InternalName { get; set; } public string? MigrationSource { get; set; } // More unrelated properties }

The model builder for these classes looks like this:

protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Entity>(b => { b.HasOne<EntityVersion>() .WithMany() .HasForeignKey(x => x.LatestEntityVersionId); b.ToTable("Entities"); }); modelBuilder.Entity<EntityVersion>(b => { b.HasOne<Entity>() .WithMany() .HasForeignKey(x => x.EntityId) .OnDelete(DeleteBehavior.Restrict); b.HasOne<ChangeSet>() .WithMany() .HasForeignKey(x => x.ChangeSetId); b.Property(x => x.InternalName).HasColumnName("InternalName"); b.Property(x => x.MigrationSource).HasColumnName("MigrationSource"); b.HasIndex(x => x.ChangeSetId); b.HasIndex(x => x.EntityId); b.HasIndex(x => new { x.ChangeSetId, x.EntityId }) .IsUnique(); b.ToTable("EntityVersions"); }); }

This all works fine. The story I'm working on was to move the two nullable strings in the EntityVersion model into Entity. Since these properties were each used in around 80 places I decided to implement a navigation property for easy access:

public class EntityVersion { // ... public virtual Entity Entity { get; set; } = null!; // ... }

And the model builder:

protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<EntityVersion>(b => { //... b.HasOne<Entity>(x => x.Entity) .WithMany() .HasForeignKey(x => x.EntityId) .OnDelete(DeleteBehavior.Restrict); b.Navigation(x => x.Entity).AutoInclude(); //... } }

This meant that it was easy to change references of entityVersion.InternalName to entityVersion.Entity.InternalName.

However, when I posted the PR the story creator asked me to remove the EntityVersion.EntityId property in favour of a convention based shadow property. No problem, I thought, just remove the property and point all references to entityVersion.Entity.Id.

However what actually happened was the migration dropped the existing EntityId foreign keys and indexes (but left the column itself) and added a new column called EntityVersion_EntityId and added the foreign key and indexes back for that new column.

Here's the created migration:

public partial class EntityRelationshipsFixed : Migration { /// <inheritdoc /> protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.DropForeignKey( name: "FK_EntityVersions_Entities_EntityId", table: "EntityVersions"); migrationBuilder.DropIndex( name: "IX_EntityVersions_ChangeSetId_EntityId", table: "EntityVersions"); migrationBuilder.DropIndex( name: "IX_EntityVersions_EntityId", table: "EntityVersions"); migrationBuilder.AddColumn<Guid>( name: "EntityVersion_EntityId", table: "EntityVersions", type: "uniqueidentifier", nullable: false, defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); migrationBuilder.CreateIndex( name: "IX_EntityVersions_ChangeSetId_EntityVersion_EntityId", table: "EntityVersions", columns: new[] { "ChangeSetId", "EntityVersion_EntityId" }, unique: true); migrationBuilder.CreateIndex( name: "IX_EntityVersions_EntityVersion_EntityId", table: "EntityVersions", column: "EntityVersion_EntityId"); migrationBuilder.AddForeignKey( name: "FK_EntityVersions_Entities_EntityVersion_EntityId", table: "EntityVersions", column: "EntityVersion_EntityId", principalTable: "Entities", principalColumn: "Id", onDelete: ReferentialAction.Cascade); } /// <inheritdoc /> protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropForeignKey( name: "FK_EntityVersions_Entities_EntityVersion_EntityId", table: "EntityVersions"); migrationBuilder.DropIndex( name: "IX_EntityVersions_ChangeSetId_EntityVersion_EntityId", table: "EntityVersions"); migrationBuilder.DropIndex( name: "IX_EntityVersions_EntityVersion_EntityId", table: "EntityVersions"); migrationBuilder.DropColumn( name: "EntityVersion_EntityId", table: "EntityVersions"); migrationBuilder.CreateIndex( name: "IX_EntityVersions_ChangeSetId_EntityId", table: "EntityVersions", columns: new[] { "ChangeSetId", "EntityId" }); migrationBuilder.CreateIndex( name: "IX_EntityVersions_EntityId", table: "EntityVersions", column: "EntityId"); migrationBuilder.AddForeignKey( name: "FK_EntityVersions_Entities_EntityId", table: "EntityVersions", column: "EntityId", principalTable: "Entities", principalColumn: "Id", onDelete: ReferentialAction.Cascade); } }

This became apparent when running unit tests and I kept getting errors saying that I cannot inset NULLs into column EntityId which is non-nullable - this is because EF Core is trying to insert the Entity.Id into the new EntityVersion_EntityId property.

I have tried explicitly defining the shadow property in the model builder, but this makes no difference:

b.HasOne<Entity>(x => x.Entity) .WithMany() .HasForeignKey("EntityId") .OnDelete(DeleteBehavior.NoAction);

Why is EF Core creating this new property instead of using the existing EntityId?

Read Entire Article