I am migrating an ASP.NET Core 2.2 application using EF Core 2.2 to ASP.NET Core 3.0 anf EF Core 3.0.

On one particular method, the back-end receives a DTO from the front-end and, using Automapper, converts it to the equivalent entity type, attaches it to the DbContext, and saves. This works perfectly on EF Core 2.2, but fails with the following message on EF Core 3.0:

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

This is the code in question:

var sqresponse = _mapper.Map<SurveyQuestionResponse>(sqresponseDTO);

_context.Attach(sqresponse); // <=== Throws exception
_context.SaveChanges();

And here are the entities with the relevant classes:

public class SurveyQuestionResponse
{
    public int? Id { get; set; }
    public List<Answer> Answers { get; set; } = new List<Answer>();
}

public class Answer
{
    public int? Id { get; set; }
    public int SurveyQuestionResponseId { get; set; }
    public ServiceProvider ServiceProvider { get; set; }
}

The data passed to that method from the front-end has two Answer, both of them containing a ServiceProvider with Id = 56.

{
    answers: [
        {
            // some more properties here
            serviceProviderId: 56,
            serviceProvider: { id: 56, name: "Foo" },
        },
        {
            // some more properties here
            serviceProviderId: 56,
            serviceProvider: { id: 56, name: "Foo" },
        }
    ]
}

These two ServiceProvider entities are meant to exist in the database; I need both Answer in this case associated with the existing ServiceProvider entity with {Id: 56} This seems to be a problem for EntityFramework Core 3.0, but was perfectly fine for EF Core 2.2.

I suspect this has to do with this breaking change: DetectChanges honors store-generated key values, but I think the proposed Mitigation would fix this particular problem, but create another one when trying to insert a new ServiceProvider entity. Am I correct?

How can this be made to work with EF Core 3.0?

2

There are 2 best solutions below

0
On BEST ANSWER

I ended up solving it by making sure only answer.ServiceProviderId is filled in, and answer.ServiceProvider is null when attaching.

The JSON being passed to the backend now looks like this, then:

{
    answers: [
        {
            // some more properties here
            serviceProviderId: 56,
            serviceProvider: null,
        },
        {
            // some more properties here
            serviceProviderId: 56,
            serviceProvider: null,
        }
    ]
}

With this, the calls to _context.Attach(...) and _context.SaveChanges() work without problems.

2
On

This shouldn't work in EFCore 2.2 either so your usage/test case(s) must be sufficiently different.

The submitted JSON will be deserialized into your entity objects. The related property serviceProvider will be deserialized/materialize an entity object for each answer entity object - this means that, in your example, the two answer objects will reference two different ServiceProvider objects with the same ID. When their referencing answer objects are attached, EFCore will also attempt to attach their referenced ServiceProvider objects, but attempting to attached the second answer object will throw the error you received because the context is already tracking a ServiceProvider entity with the same ID that is not the same object.

You need to check if the entity is already being tracked before attempting to add it to the context, which means you can't blindly attach your DTO's to the context.

Before attaching the answer objects via the DTO, first check if there is an entity object of that type with the same ID already tracked by the context and assign that object to the serviceProvider property:

var attachedServiceProvider = dbContext.ServiceProviders
    .Local
    .FirstOrDefault(sp => sp.Id = answerObject.serviceProvider.Id);

if( null == attachedServiceProvider )
{
    dbContext.Attach(serviceProvider);
    attachedServiceProvider = serviceProvider;
}

answerObject.serviceProvider = attachedServiceProvider;