-1

I have a model class Patient:

    [Key]
    [Required(ErrorMessage = "")]
    [MinLength(3)]
    public string PatientCode { get; set; }

    [Required(ErrorMessage = "")]
    [MinLength(1)]
    public string Name { get; set; }

    [Required(ErrorMessage = "")]
    [MinLength(1)]
    public string Surname { get; set; }

I have a generic repository where I try to update my entity:

    public async Task<bool> UpdateAsync(T entity)
    {
        try
        {
            _applicationContext.Set<T>().Update(entity);
            await _applicationContext.SaveChangesAsync();

            return true;
        }
        catch (Exception)
        {
            return false;
        }
    }

which I call from my controller:

    private readonly IRepository<Patient> _patientRepository;
    ...
    [HttpPatch("update/")]
    public async Task<IActionResult> UpdateAsync([FromBody] Patient patient)
    {
        var result = await _patientRepository.UpdateAsync(patient);
    
        if (result)
            return Ok();
    
        return BadRequest();
    }

And when I do so, I get this error:

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

If I comment out setting the entity in my repository (comment line _applicationContext.Set<T>().Update(entity);) everything works just fine. It means that my entity has been attached to the ChangeTracker somehow. But how is it possible in such a straight forward code?

Slepoyi
  • 150
  • 1
  • 1
  • 12
  • When you load entity from DbContext, it become attached by default. Usually you do not need to call `Update` method explicitly. – Svyatoslav Danyliv Jan 16 '23 at 15:56
  • 2
    Did you see [this](https://stackoverflow.com/questions/36856073/the-instance-of-entity-type-cannot-be-tracked-because-another-instance-of-this-t) – huMpty duMpty Jan 16 '23 at 15:59
  • @SvyatoslavDanyliv I clarified part where I call `UpdateAsync` method. Basically I get an entity to update from request body in my controller, then I pass it as parameter into `UpdateAsync` method. That's it, I have no interactions with `ApplicationContext` except updating my entity. – Slepoyi Jan 16 '23 at 18:39
  • Usually it happens when you load entity before `Update`, as suggested by huMpty use proposed solutions, or more complicated created by me [UpdateSafe](https://github.com/linq2db/linq2db/issues/3915), which should work with any Entity Types. – Svyatoslav Danyliv Jan 16 '23 at 18:44
  • @SvyatoslavDanylivyes, I got the point that entity is being tracked since loading, but in my code I don't load this entity. I get it as JSON request body, ASP .NET Core deserializes it and then I try to update it. I don't understand how it can be tracked since I don't load it from `ApplicationContext` – Slepoyi Jan 16 '23 at 19:16
  • @huMptyduMpty yes, i did, and I don't see the connection between my case and this topic. – Slepoyi Jan 16 '23 at 19:45
  • @Progman yesterday I read this discussion and didn't understand how it can help me. Now I read Steve Py's answer, this discussion again and now I got the point. Thanks. – Slepoyi Jan 17 '23 at 08:16

1 Answers1

1

The error occurs when a DbContext instance is given an entity reference that it isn't tracking that has a same key value with an entity reference that it is tracking. This can be a common problem in application designs that use Dependency Injection for the DbContext since within the scope of a single repository that gets an injected DbContext it's an unknown what entity references that DbContext might already be tracking. For instance if your code happens to load a different entity that happens to have a reference to that particular Patient record that is either eager or lazy loaded, then passing a different entity reference, such as one that has been deserialized/mapped from a POST payload and calling Update will result in this exception, and it can be situational depending on whether the DbContext happens to be tracking that matching entity or not.

Passing entities around beyond the scope of their DbContext including serialization/deserialization requires extra care when dealing with updating entities and references to related entities. It also means you need to be prepared to deal with tracked references.

Since different entities will likely use different keys, I strongly recommend moving away from the Generic Repository pattern, or at least mitigate it to truly identical lowest-common-denominator type base code. Too many developers end up painting themselves into a tight corner far from the doorway with this anti-pattern.

public async Task<bool> UpdateAsync(Patient patient)
{
    try
    {   // Going to .Local checks for tracked references, it won't hit the DB.
        var trackedReference = _applicationContext.Patients.Local
            .SingleOrDefault(p => p.PatientCode == patient.PatientCode);

        if (trackedReference == null)
        { // We aren't already tracking an entity so we should be safe to attempt an Update.
            _applicationContext.Patients.Update(patient);
        }
        else if (!Object.ReferenceEquals(trackedReference, patient))
        { // Here we have a bit more work to do. The DbContext is already 
          // tracking a reference to this Patient and it's *not* the copy
          // you are passing in... Need to determine what to do, copy
          // values across, etc.
            Mapper.Map(patient, trackedReference); // This would use Automapper to copy values into the tracked reference, overwriting.
        }

        // If the patient passed in is the same reference as the tracked
        // instance, nothing to do, just call SaveChanges.
        
        await _applicationContext.SaveChangesAsync();

        return true;
    }
    catch (Exception)
    {
        return false;
    }
}

This may look simple enough, but you need to apply these checks to all entities in the entity graph. So for instance if a Patient references other entities like Address etc. you need to check the DbContext to see if it is already tracking a reference (where that address might be updated or inserted). For references to existing data rows you will want to check and replace references with existing tracked references, or Attach them if not tracked to avoid EF attempting to treat the untracked reference as a new entity and try inserting it.

Steve Py
  • 26,149
  • 3
  • 25
  • 43
  • Yes, that definitely makes sense. Can this be solved using transient DbContext? – Slepoyi Jan 17 '23 at 07:56
  • 1
    A transient DbContext scope can reduce occurrences, but it won't eliminate it. The graph and reference issues can still persist, but if your operations are against single entities with no relations then the errors would pretty much be eliminated. However, EF's strength is managing relational models, pretty much whenever working with detached references you need extra due diligence when re-attaching them. Personally I prefer to avoid detached entities all together where entities never leave the scope of the DbContext they were read from. (Mapped to ViewModels and back) – Steve Py Jan 17 '23 at 21:04