-
Notifications
You must be signed in to change notification settings - Fork 11
Description
In certain cases, configuring enum lookups on an owned type (via the new ConfigureOwnedEnumLookup
extension method) causes EF Core to generate an enum lookup table with a redundant foreign key reference to itself. This results in a warning appearing within the Package Manager Console when creating migrations (given an enum called Month
):
Microsoft.EntityFrameworkCore.Model.Validation[10614]
The foreign key {'Id'} on entity type 'EnumWithNumberLookup<Month> targets itself, it should be removed since it serves no purpose.
This is a minor issue and it's really just to bring it to your attention in case you have any idea what might be causing it. I don't believe it will affect behaviour in any way.
Steps To Reproduce
It's difficult to reproduce this issue. I've set up both a dummy .NET Core console app project and my real ASP.NET Core-based project to create the exact same minimal set of entities that exposes the issue. The relevant classes are listed below.
Domain
A "product consultant" (not modelled) can optionally have an "annual target". An "annual target" owns zero or many "monthly targets", each of which correspond to a month of the year.
public class AnnualTarget
{
protected AnnualTarget()
{
// Required by EF
}
public int YearStartPeriod { get; private set; }
public int Target => MonthlyTargets.Sum(x => x.Target);
protected virtual IReadOnlyCollection<MonthlyTarget> MonthlyTargets { get; private set; } = null!;
private int ProductConsultantId { get; set; }
}
public class MonthlyTarget
{
protected MonthlyTarget()
{
// Required by EF
}
public MonthlyTarget(int yearStartPeriod, Month month, int target)
{
YearStartPeriod = yearStartPeriod;
Month = month;
Target = target;
}
public Month Month { get; private set; }
public int YearStartPeriod { get; private set; }
public int Target { get; private set; }
public int Adjustment { get; private set; }
private int ProductConsultantId { get; set; }
}
public enum Month
{
January = 1,
February = 2,
March = 3,
April = 4,
May = 5,
June = 6,
July = 7,
August = 8,
September = 9,
October = 10,
November = 11,
December = 12
}
Context
public class AnnualTargetConfiguration : IEntityTypeConfiguration<AnnualTarget>
{
private readonly ModelBuilder _modelBuilder;
private readonly EnumLookupOptions _enumLookupOptions;
public AnnualTargetConfiguration(ModelBuilder modelBuilder, EnumLookupOptions enumLookupOptions)
{
_modelBuilder = modelBuilder;
_enumLookupOptions = enumLookupOptions;
}
public void Configure(EntityTypeBuilder<AnnualTarget> builder)
{
builder.ToTable(nameof(AnnualTarget));
builder.HasKey(
"ProductConsultantId",
nameof(AnnualTarget.YearStartPeriod));
builder.OwnsMany<MonthlyTarget>("MonthlyTargets", b =>
{
b.ToTable(nameof(MonthlyTarget));
b.WithOwner();
b.HasKey(
"ProductConsultantId",
nameof(MonthlyTarget.YearStartPeriod),
nameof(MonthlyTarget.Month));
b.ConfigureOwnedEnumLookup(_enumLookupOptions, _modelBuilder);
});
}
}
public class CommissionsContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlServer("Data Source=tcp:dev.db.sales.mycompany.co.uk;Trusted_Connection=Yes;database=Commissions");
optionsBuilder.UseLazyLoadingProxies();
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var enumLookupOptions = EnumLookupOptions.Default.SetNamingScheme(x => x.Pascalize());
modelBuilder.ApplyConfiguration(new AnnualTargetConfiguration(modelBuilder, enumLookupOptions));
modelBuilder.ConfigureEnumLookup(enumLookupOptions);
}
}
Outputs
The generated migration file (..._InitialCreate.cs
) is identical between the dummy project and the real project:
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AnnualTarget",
columns: table => new
{
YearStartPeriod = table.Column<int>(nullable: false),
ProductConsultantId = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AnnualTarget", x => new { x.ProductConsultantId, x.YearStartPeriod });
});
migrationBuilder.CreateTable(
name: "Month",
columns: table => new
{
Id = table.Column<int>(nullable: false),
Name = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Month", x => x.Id);
});
migrationBuilder.CreateTable(
name: "MonthlyTarget",
columns: table => new
{
Month = table.Column<int>(nullable: false),
YearStartPeriod = table.Column<int>(nullable: false),
ProductConsultantId = table.Column<int>(nullable: false),
Target = table.Column<int>(nullable: false),
Adjustment = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MonthlyTarget", x => new { x.ProductConsultantId, x.YearStartPeriod, x.Month });
table.ForeignKey(
name: "FK_MonthlyTarget_Month_Month",
column: x => x.Month,
principalTable: "Month",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_MonthlyTarget_AnnualTarget_ProductConsultantId_YearStartPeriod",
columns: x => new { x.ProductConsultantId, x.YearStartPeriod },
principalTable: "AnnualTarget",
principalColumns: new[] { "ProductConsultantId", "YearStartPeriod" },
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.InsertData(
table: "Month",
columns: new[] { "Id", "Name" },
values: new object[,]
{
{ 1, "January" },
{ 2, "February" },
{ 3, "March" },
{ 4, "April" },
{ 5, "May" },
{ 6, "June" },
{ 7, "July" },
{ 8, "August" },
{ 9, "September" },
{ 10, "October" },
{ 11, "November" },
{ 12, "December" }
});
migrationBuilder.CreateIndex(
name: "IX_Month_Name",
table: "Month",
column: "Name",
unique: true,
filter: "[Name] IS NOT NULL");
migrationBuilder.CreateIndex(
name: "IX_MonthlyTarget_Month",
table: "MonthlyTarget",
column: "Month");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "MonthlyTarget");
migrationBuilder.DropTable(
name: "Month");
migrationBuilder.DropTable(
name: "AnnualTarget");
}
}
However, the generated designer scripts differ between the two projects.
Dummy .NET Console Project (working as intended)
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using experiment.Data;
namespace experiment.Migrations
{
[DbContext(typeof(CommissionsContext))]
[Migration("20200917121204_InitialCreate")]
partial class InitialCreate
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.8")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("SpatialFocus.EntityFrameworkCore.Extensions.EnumWithNumberLookup<experiment.Models.Month>", b =>
{
b.Property<int>("Id")
.HasColumnType("int");
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique()
.HasFilter("[Name] IS NOT NULL");
b.ToTable("Month");
b.HasData(
new
{
Id = 1,
Name = "January"
},
new
{
Id = 2,
Name = "February"
},
new
{
Id = 3,
Name = "March"
},
new
{
Id = 4,
Name = "April"
},
new
{
Id = 5,
Name = "May"
},
new
{
Id = 6,
Name = "June"
},
new
{
Id = 7,
Name = "July"
},
new
{
Id = 8,
Name = "August"
},
new
{
Id = 9,
Name = "September"
},
new
{
Id = 10,
Name = "October"
},
new
{
Id = 11,
Name = "November"
},
new
{
Id = 12,
Name = "December"
});
});
modelBuilder.Entity("experiment.Models.Targets.AnnualTarget", b =>
{
b.Property<int>("ProductConsultantId")
.HasColumnType("int");
b.Property<int>("YearStartPeriod")
.HasColumnType("int");
b.HasKey("ProductConsultantId", "YearStartPeriod");
b.ToTable("AnnualTarget");
});
modelBuilder.Entity("SpatialFocus.EntityFrameworkCore.Extensions.EnumWithNumberLookup<experiment.Models.Month>", b =>
{
b.HasOne("SpatialFocus.EntityFrameworkCore.Extensions.EnumWithNumberLookup<experiment.Models.Month>", null)
.WithMany()
.HasForeignKey("Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("experiment.Models.Targets.AnnualTarget", b =>
{
b.OwnsMany("experiment.Models.Targets.MonthlyTarget", "MonthlyTargets", b1 =>
{
b1.Property<int>("ProductConsultantId")
.HasColumnType("int");
b1.Property<int>("YearStartPeriod")
.HasColumnType("int");
b1.Property<int>("Month")
.HasColumnType("int");
b1.Property<int>("Adjustment")
.HasColumnType("int");
b1.Property<int>("Target")
.HasColumnType("int");
b1.HasKey("ProductConsultantId", "YearStartPeriod", "Month");
b1.HasIndex("Month");
b1.ToTable("MonthlyTarget");
b1.HasOne("SpatialFocus.EntityFrameworkCore.Extensions.EnumWithNumberLookup<experiment.Models.Month>", null)
.WithMany()
.HasForeignKey("Month")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b1.WithOwner()
.HasForeignKey("ProductConsultantId", "YearStartPeriod");
});
});
#pragma warning restore 612, 618
}
}
}
Real ASP.NET Core Project (has issue)
// <auto-generated />
using MyCompany.Commissions.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace MyCompany.Commissions.Infrastructure.Migrations
{
[DbContext(typeof(CommissionsContext))]
[Migration("20200917115338_InitialCreate")]
partial class InitialCreate
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.8")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
modelBuilder.Entity("MyCompany.Commissions.Domain.ProductConsultants.Targets.AnnualTarget", b =>
{
b.Property<int>("ProductConsultantId")
.HasColumnType("int");
b.Property<int>("YearStartPeriod")
.HasColumnType("int");
b.HasKey("ProductConsultantId", "YearStartPeriod");
b.ToTable("AnnualTarget");
});
modelBuilder.Entity("SpatialFocus.EntityFrameworkCore.Extensions.EnumWithNumberLookup<MyCompany.Commissions.Domain.ProductConsultants.Month>", b =>
{
b.Property<int>("Id")
.HasColumnType("int");
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique()
.HasFilter("[Name] IS NOT NULL");
b.ToTable("Month");
b.HasData(
new
{
Id = 1,
Name = "January"
},
new
{
Id = 2,
Name = "February"
},
new
{
Id = 3,
Name = "March"
},
new
{
Id = 4,
Name = "April"
},
new
{
Id = 5,
Name = "May"
},
new
{
Id = 6,
Name = "June"
},
new
{
Id = 7,
Name = "July"
},
new
{
Id = 8,
Name = "August"
},
new
{
Id = 9,
Name = "September"
},
new
{
Id = 10,
Name = "October"
},
new
{
Id = 11,
Name = "November"
},
new
{
Id = 12,
Name = "December"
});
});
modelBuilder.Entity("MyCompany.Commissions.Domain.ProductConsultants.Targets.AnnualTarget", b =>
{
b.OwnsMany("MyCompany.Commissions.Domain.ProductConsultants.Targets.MonthlyTarget", "MonthlyTargets", b1 =>
{
b1.Property<int>("ProductConsultantId")
.HasColumnType("int");
b1.Property<int>("YearStartPeriod")
.HasColumnType("int");
b1.Property<int>("Month")
.HasColumnType("int");
b1.Property<int>("Adjustment")
.HasColumnType("int");
b1.Property<int>("Target")
.HasColumnType("int");
b1.HasKey("ProductConsultantId", "YearStartPeriod", "Month");
b1.HasIndex("Month");
b1.ToTable("MonthlyTarget");
b1.HasOne("SpatialFocus.EntityFrameworkCore.Extensions.EnumWithNumberLookup<MyCompany.Commissions.Domain.ProductConsultants.Month>", null)
.WithMany()
.HasForeignKey("Month")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b1.WithOwner()
.HasForeignKey("ProductConsultantId", "YearStartPeriod");
});
});
modelBuilder.Entity("SpatialFocus.EntityFrameworkCore.Extensions.EnumWithNumberLookup<MyCompany.Commissions.Domain.ProductConsultants.Month>", b =>
{
b.HasOne("SpatialFocus.EntityFrameworkCore.Extensions.EnumWithNumberLookup<MyCompany.Commissions.Domain.ProductConsultants.Month>", null)
.WithMany()
.HasForeignKey("Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}
The problem lies at the end of the designer file for the ASP.NET Core project:
modelBuilder.Entity("SpatialFocus.EntityFrameworkCore.Extensions.EnumWithNumberLookup<MyCompany.Commissions.Domain.ProductConsultants.Month>", b =>
{
b.HasOne("SpatialFocus.EntityFrameworkCore.Extensions.EnumWithNumberLookup<MyCompany.Commissions.Domain.ProductConsultants.Month>", null)
.WithMany()
.HasForeignKey("Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});