1
votes

I'm trying to store older versions of entities in my database. To do that I am copying the existing values before I update them. For some reason EF Core won't let me use the same batch.Values property twice.

public async Task<Batch> UpdateBatch(Batch batch, Batch updatedBatch)
{
    foreach (var valueParameter in batch.Values)
    {
        batch.ValuesHistory.Add(new ParameterValueHistory
        {
            Parameter = valueParameter.Parameter,
            ParameterBatchNumber = valueParameter.ParameterBatchNumber,
            Value = valueParameter.Value
        });
    }

    batch.Values = updatedBatch.Values;

    batch.Version++;

    await this.context.SaveChangesAsync();

    return batch;
}

The foreach loop and batch.Values = updatedBatch.Values; work exactly like they should when only one of them exists. But whenever they're both active I get the following error:

The instance of entity type 'ParameterValue' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.

These are the relevant models: ParameterValue:

public class ParameterValue
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public long Id { get; set; }

    [Required]
    public virtual RecipeParameter Parameter { get; set; }

    public string Value { get; set; }


    public string? ParameterBatchNumber { get; set; }
}

ParameterValueHistory:

public class ParameterValueHistory
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public long Id { get; set; }

    [Required]
    public virtual RecipeParameter Parameter { get; set; }

    public string Value { get; set; }


    public string? ParameterBatchNumber { get; set; }
}

RecipeParameter for context:

public class RecipeParameter
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public long Id { get; set; }

    [Required]
    public string Name { get; set; }

    [Required]
    public string Type { get; set; }

    public string Unit { get; set; }

    public string Value { get; set; }

    public bool BatchRequired { get; set; }
}

Batch:

public class Batch
{
    [Key]
    [MaxLength(12)]
    public string BatchNumber { get; set; }

    public virtual List<ParameterValue> Values { get; set; }

    public virtual List<ParameterValueHistory> ValuesHistory { get; set; }

    public int Version { get; set; }

    [Required]
    public bool IsResearch { get; set; }

    [Required]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public DateTime CreatedOn { get; set; } = DateTime.UtcNow;
}

This is my DbContext class:

public class ApplicationDataContext : DbContext
{
    public ApplicationDataContext(DbContextOptions<ApplicationDataContext> options)
        : base(options)
    {
    }

    public DbSet<Product> Product { get; set; }

    public DbSet<Batch> Batch { get; set; }

    public DbSet<ParameterValue> ParameterValue { get; set; }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseLazyLoadingProxies();

        base.OnConfiguring(optionsBuilder);
    }
}

Why does this error keep showing up? Even when I am just accessing the propety as batch.Values more than once, it gives me this error.

UPDATE: This is the controller method that calls the UpdateBatch method.

[HttpPut("{productId}/batches/{batchNumber}")]
public async Task<ActionResult<Batch>> PutBatch(string batchNumber, Batch updatedBatch)
{
    Batch batch = await this.repository.GetBatchByBatchNumber(batchNumber);

    if (batch == null)
    {
        return NotFound()
    }

    return await this.repository.UpdateBatch(batch, updatedBatch);
}
2
Each class instance can only be tracked once. If you want to duplicate it, you will need to perform a deep copy.Jeremy Lakeman
Hi Kevin!, what is important here is that you can mantain only one reference to a given entity with a given Id, because when an entity is retrieved from the database or a entity is created with a given id, entity framework says "a ha!" im gonna track this entity with id (i dont know..) 1 to be aware the changes on its properties for future reference!. So when you later on the code create, o re-retrieve the same entity with the same Id from the database, EF says: "wait a minute, i can only remember one of you guys, get your *&&%$ together"Rod Ramírez
Batch batch, Batch updatedBatch are the same, show how they were passed to this function, usually u fetch the entity... this would be the one which is tracked, the other is not, in your case for some reason it is... its like you did fetch twice and modified one of themSeabizkit
you include a lot but the part we need to see is where UpdateBatch is called... it needs to be tweeked somewhere there... where the instances of Batch batch, Batch updatedBatch originateSeabizkit
I've included the controller method where UpdateBatch is called.Kevin

2 Answers

0
votes

When you use batch.Values = updatedBatch.Values;, because batch.Values contains the foreign key of Batch, and if the value in updatedBatch.Values also contains the key value,if the equal operation is performed directly, due to the foreign key constraint, the foreign key cannot be modified directly, which will cause your error.

Therefore, you cannot include the key value in the Values in your updateBatch.

Regarding your question. I did a simple test. You can see the following code(updateBatch.Values have no Id).

 var batch = _context.Batches.Include(c => c.Values)
                                    .ThenInclude(c => c.Parameter)
                                    .Include(b => b.ValuesHistory)
                                    .ThenInclude(c => c.Parameter)
                                    .Where(c => c.BatchNumber == "1")
                                    .FirstOrDefault();

        var updateBatch = new Batch
        {
            Version = 3,
            CreatedOn = new DateTime(),
            IsResearch = true,
            Values = new List<ParameterValue>
            {
                new ParameterValue 
                {   
                    Value = "hello", 
                    Parameter = new RecipeParameter 
                    {  
                        BatchRequired = true, 
                        Name = "h", 
                        Type = "e", 
                        Unit = "l", 
                        Value = "o" 
                    } 
                },
            },
            ValuesHistory = new List<ParameterValueHistory>()
        };
        foreach (var valueParameter in batch.Values)
        {
            batch.ValuesHistory.Add(new ParameterValueHistory
            {
                Parameter = valueParameter.Parameter,
                ParameterBatchNumber = valueParameter.ParameterBatchNumber,
                Value = valueParameter.Value
            });
        }

        batch.Values = updateBatch.Values;

        batch.Version++;

        _context.SaveChanges();

Test result: enter image description here

0
votes

start by making these changes..

[DatabaseGenerated(DatabaseGeneratedOption.Identity)] 

should not be on

 [Required]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public DateTime CreatedOn { get; set; } = DateTime.UtcNow;

instead model like

public class Batch
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public long Id { get; set; }

    //you can add index on this
    [MaxLength(12)]
    public string BatchNumber { get; set; }

    public int Version { get; set; }

    [Required]
    public bool IsResearch { get; set; }

    [Required]
    public DateTime CreatedOn { get; set; };// set this in the repo or create do another way

    //you add this but don't see the linkage aka ParameterValue does not have a BatchId
    public virtual List<ParameterValue> Values { get; set; }

    public virtual List<ParameterValueHistory> ValuesHistory { get; set; } 

}