EntityFrameworkCore.Extensions icon indicating copy to clipboard operation
EntityFrameworkCore.Extensions copied to clipboard

Enum lookup table creating redundant foreign key reference to itself

Open TAGC opened this issue 5 years ago • 0 comments

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();
    });

TAGC avatar Sep 17 '20 12:09 TAGC