Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enum lookup table creating redundant foreign key reference to itself #17

Open
TAGC opened this issue Sep 17, 2020 · 0 comments
Open

Enum lookup table creating redundant foreign key reference to itself #17

TAGC opened this issue Sep 17, 2020 · 0 comments

Comments

@TAGC
Copy link
Contributor

TAGC commented Sep 17, 2020

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();
    });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant