0

I have a GamingLimit class with a composite key that uses the properties PersonalNationalIdHash and Timeframe as composite key.

// GamingLimit.cs
public class GamingLimit
{
    public string? PlayerNationalIdHash { get; set; }
    public decimal Limit { get; set; }
    public string? Currency { get; set; }
    public GamingLimitTimeframe Timeframe { get; set; }
}

// GamingLimitTimeframe.cs
public enum GamingLimitTimeframe
{
    All,
    Daily,
    Weekly,
    Monthly,
    Quarterly,
    Yearly
}

I set up EF Core to use the mentioned properties as a composite key with the OnModelCreating override with dbContext:

// ApplicationDbContext.cs
protected override void OnModelCreating(ModelBuilder builder)
{
    builder.Entity<GamingLimit>()
           .HasKey(nameof(GamingLimit.PlayerNationalIdHash), 
                   nameof(GamingLimit.Timeframe));
}

I seed the database like so

// SeedDb.cs
public static void InitializeDbForTests(ApplicationDbContext db)
{
    var gamingLimitSeedData = new List<GamingLimit>
    {
        new()
        {
           PlayerNationalIdHash = "1",
           Limit = 2500m,
           Currency = "SHILLINGS",
           Timeframe = GamingLimitTimeframe.Monthly,
        },
        new()
        {
           PlayerNationalIdHash = "1",
           Limit = 500m,
           Currency = "SHILLINGS",
           Timeframe = GamingLimitTimeframe.Daily,
        }
   };
   
   gamingLimitSeedData.ForEach(x => db.GamingLimits?.Add(x));

   db.SaveChanges();
}

Whenever I have more than one entity going into the context I get the error:

The instance of entity type 'GamingLimit' cannot be tracked because another instance with the same key value for {'TempId'} is already being tracked

This happens at the line

gamingLimitSeedData.ForEach(x => db.GamingLimits?.Add(x));

I have no idea why, since all the other answers I have seen don't have this specific issue.

I have tried to to add an integer field with the name Id like so

// GamingLimit.cs
public class GamingLimit
{
    public int Id { get; set; }
    public string? PlayerNationalIdHash { get; set; }
    public decimal Limit { get; set; }
    public string? Currency { get; set; }
    public GamingLimitTimeframe Timeframe { get; set; }
}

while keeping the OnModelCreating override with the composite key.

I am using Postgres 9.6

Any idea what could be the issue?

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459

1 Answers1

2

All I had to do was to add the Key and Column attributes to the desired composite key properties to the GamingLimit class.

public class GamingLimit : AuditableEntity
{
    [Key, Column(Order = 0)]
    public string? PlayerNationalIdHash { get; set; }

    public decimal Limit { get; set; }
    public string? Currency { get; set; }

    [Key, Column(Order = 1)]
    public GamingLimitTimeframe Timeframe { get; set; }
}

Had to do some cross-referencing between the official documentation and some StackOverflow answers

Sources

Edit: So this was kind of a workaround, but since I am using DDD (Domain Driven Design) in my project this was adding dependencies to my domain, which I do not want. I continued my search for an answer and found that when migrating the initial migration EF adds the TempId column to my entity. I assume this is some kind of bug, but not really sure. The line

builder.Entity<GamingLimit>()
                .HasKey(nameof(GamingLimit.PlayerNationalIdHash), nameof(GamingLimit.Timeframe));

should in theory add a composite key to my entity table, and use that to identify the entity when working with it, however, it did not for some reason. Looking at the migration files I see this

migrationBuilder.CreateTable(name: "GamingLimits", columns: table => new
    {
        PlayerNationalIdHash = table.Column<string>(type: "text", nullable: false),
        Timeframe = table.Column<int>(type: "integer", nullable: false),
        Limit = table.Column<decimal>(type: "numeric", nullable: false),
        Currency = table.Column<string>(type: "text", nullable: true),
        TempId = table.Column<int>(type: "integer", nullable: false),
    },
    constraints: table =>
    {
        table.PrimaryKey("PK_GamingLimits", x => new { x.PlayerNationalIdHash, x.Timeframe });
        table.UniqueConstraint("AK_GamingLimits_TempId", x => x.TempId);
    });

The migration is adding some TempId as a unique constraint to the entity, which I do not need. After some hours of poking the code with a stick I settled on this solution:

protected override void OnModelCreating(ModelBuilder builder)
{
   builder.Entity<GamingLimit>().HasNoKey();
   builder.Entity<GamingLimit>()
       .HasKey(nameof(GamingLimit.PlayerNationalIdHash), nameof(GamingLimit.Timeframe));
}

By setting the entity to have no key at first with

builder.Entity<GamingLimit>().HasNoKey();

and the setting the composite key with

builder.Entity<GamingLimit>()
       .HasKey(nameof(GamingLimit.PlayerNationalIdHash), nameof(GamingLimit.Timeframe));

I was able to achieve the result I wanted.

Now I can do all of my operations with the entity as I had intended to do.