简体   繁体   中英

The instance of entity type cannot be tracked because another instance with the same key value for is already being tracked

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

When attaching existing entities, ensure that only one entity instance with a given key value is attached.

Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.

I encounter this error when we call data from a table and update it.

I solved it by calling a view which calls the table. Why does this happen? How can I solve without creating additional views?

The simplest answer: Don't pass entities around outside of the scope they were read. Pass view models (POCO objects rather than entities) and fetch entities on update to copy expected values across.

The complex answer is that when updating entity references, all entity references including child collections and many-to-1 references, you need to check if the DbContext is tracking a matching reference, and either replace the references with the tracked entity, or tell the DbContext to dump the tracked reference before attaching.

For example, an update method that accepts a detatched or deserialized "entity". What works sometimes, but then craps other times:

public void UpdateOrder(Order order)
{
    context.Update(order);

    // OR 
    context.Attach(order);
    context.Entry(order).State = EntityState.Modified;

    context.SaveChanges();
}

Looks simple and clean, but craps out when the DbContext instance might already be tracking a matching Order instance. When it is, you get that exception

The safety check:

public void UpdateOrder(Order order)
{
    var existingOrder = context.Orders.Local.SingleOrDefault(o => o.OrderId == order.OrderId);
    if (existingOrder != null)
        context.Entry(existingOrder).State = EntityState.Detatched;

    context.Update(order);

    // OR 
    context.Attach(order);
    context.Entry(order).State = EntityState.Modified;

    context.SaveChanges();
}

That example checks the local tracking cache for a matching order and dumps any tracked instance. The key here is searching the .Local with the DbSet to search the local tracking cache, not hitting the DB.

Where this gets more complex is where Order contains other entity references like OrderLines, or a reference to a Customer, etc. When dealing with detached entities you need to check over the entire object graph for tracked references.

public void UpdateOrder(Order order)
{
    var existingOrder = context.Orders.Local.SingleOrDefault(o => o.OrderId == order.OrderId);
    if (existingOrder != null)
        context.Entry(existingOrder).State = EntityState.Detatched;

    var customer = context.Customers.Local.SingleOrDefault(c => c.CustomerId = order.Customer.CustomerId);
    if (customer != null)
        order.Customer = customer; // Replace our Customer reference with the tracked one.
    else
        context.Attach(order.Customer);

    context.Update(order);

    // OR 
    context.Attach(order);
    context.Entry(order).State = EntityState.Modified;

    context.SaveChanges();
}

As you can see, this starts to get complex and cumbersome pretty quick as you need to check every reference. Hence, it's simpler to avoid passing detached or serialized entities around. Using a View Model offers many benefits for performance and simplifying issues like this. Coupled with Automapper or a similar mapper that supports projection can make operations with view models very simple:

Selecting Orders:

var orders = context.Orders.Where(/* suitable conditions */)
    .ProjectTo<OrderViewModel>(_mapperConfig)
    .ToList();

Where _mapperConfig is an Automapper configuration that tells Automapper how to convert an Order into an OrderViewModel. This can follow conventions or optionally contain mapping rules to build a flattened view model for an Order and it's relative details. ProjectTo works with EF's IQueryable to build an SQL SELECT statement across the entity graph to return only the data needed to populate the view model. This is far more efficient than using Map which would require all related entities to be eager loaded.

When updating:

public void UpdateOrder(UpdateOrderViewModel orderVM)
{
    var order = context.Orders.Single(o => o.OrderId == orderVM.OrderId);
    if (orderVM.RowVersion != order.RowVersion)
        throw new StaleDataException(); // placeholder to handle the situation where the data has changed since our view got the order details.

    var mapper = _mapperConfig.CreateMapper();
    mapper.Map(orderVM, order);
    context.SaveChanges();
}

orderVM could be an OrderViewModel returned, but typically I would recommend packaging just the fields that can be updated into a dedicated view model. The "magic" is in the Automapper configuration which governs what fields get copied from the view model back into the entity. If can include child data such as OrderLines or such, in which case you would want to ensure those child entities are eager loaded /w .Include in your DB fetch. Automapper's Map method in this case is the variant that copies mapped values from a source to a destination, so values are copied across directly into the tracked entity instance. EF will build an SQL UPDATE statement based on what values actually charge rather than overwriting the entire record.

You can also use the same technique with detached entities to avoid your issue. The benefit of using Automapper is that you can configure which values can be legally copied over from the deserialized/detached entity provided into the real data:

public void UpdateOrder(Order updatedOrder)
{
    var order = context.Orders.Single(o => o.OrderId == orderVM.OrderId);
    if (updatedOrder.RowVersion != order.RowVersion)
        throw new StaleDataException(); // placeholder to handle the situation where the data has changed since our view got the order details.

    var mapper = _mapperConfig.CreateMapper();
    mapper.Map(updatedOrder, order);
    context.SaveChanges();
}

This ensures we only change what is allowed to change, and avoids the whole crapshoot of tracked references. In our mapper configuration we literally have an entry like:

cfg.CreateMap<Order, Order>(...)

which will hold explicit rules to ignore copying across fields and related entities we don't want copied across on an Update.

The downside of doing this is the overhead of sending entire entities and potentially their related entities across the wire back and forth, plus to be "safe" from tampering, a lot more effort needs to go into the mapper configuration or copying across allowed values explicitly.

I had the same issue with EF Core and Blazor Server. Switching the scope in the service collection to "Transient" and using a ServiceScopeFactory for the queries/updates did the trick. You'll see below I'm using the Blazor style dependency injection, but constructor injection will still work the same way for an IServiceScopeFactory

            [Inject]
        IServiceScopeFactory _serviceScopeFactory { get; set; }
private async Task UpdateItem(GridCommandEventArgs args)
        {
            var utilityItem = (EntityModelSample)args.Item;
            using (var scope1 = _serviceScopeFactory.CreateScope())
            {
                var dbContext = scope1.ServiceProvider.GetService<SampleDbContext>();
                dbContext.Update(utilityItem);
                await dbContext.SaveChangesAsync();
            }

            LoadData();
        }

In the startup code:

builder.Services.AddDbContext<InternalUtilitiesDbContext>(option => option.UseSqlServer(connectionString), ServiceLifetime.Transient);

this code fix your problems:: builder.Services.AddDbContext(option => option.UseSqlServer(connectionString), ServiceLifetime.Transient);

ServiceLifetime.Transient

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

Related Question The instance of entity type 'Item' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked The instance of entity type cannot be tracked because another instance with the same key value is already being tracked The instance of entity type 'Bookmark' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked The instance of entity type <T> cannot be tracked because another instance with the same key value for {'Id'} is already being tracked The instance of entity type Model cannot be tracked because another instance with the same key value for {'Id'} is already being tracked Getting 'The instance of entity type cannot be tracked because another instance with same key value for is already being tracked' with AsNoTracking() The instance of entity type ‘Bus’ cannot be tracked because another instance with the same key value for {‘Id’} is already being tracked The instance of entity type 'AppUser' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked The instance of entity type 'Product' cannot be tracked because another instance with the same key value is already being tracked The instance of entity type 'Article' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM