0

I'm getting the following error when trying to update to my database:

System.InvalidOperationException: 'The instance of entity type 'Expense' 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. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.'

I think I'm missing some fundamental understanding. Trying to google this error mostly gives me answers of changing the service from singleton to scoped but mine is scoped. Here's the relevant code:

private async void SaveExpensesToDatabase()
{
    foreach(Expense expense in expenses)
    {
        expense.CategoryName = expense.Category.Name;
        Expense check = await expenseService.GetExpensebyKey(expense.Id);
        if(check != null)
        {
            await expenseService.UpdateExpenseDetails(expense);
        }
    }
}

Here are the two functions in expenseService:

    public async Task<Expense> GetExpensebyKey(string Id)
    {
        Expense expense = await _dbContext.Expenses.FirstOrDefaultAsync(x => x.Id == Id);
        return expense;
    }

    public async Task<bool> UpdateExpenseDetails(Expense expense)
    {
        _dbContext.Expenses.Update(expense);
        await _dbContext.SaveChangesAsync();
        return true;
    }

This is how I have my service listed in my Program file:

builder.Services.AddScoped<ExpenseService>();

I'm not just looking for how to make this work although that would be wonderful. I'm also looking for what fundamental concepts I'm not understanding. I think it's centered around how db context works. Thanks in advance!

AGeist
  • 3
  • 2
  • What version of EF are you using? Depending on the version there are different ways to do this. But essentially you have retrieved an entity via the Get and introducing another entity with the same key with the update. Look at notracking and or updating the existing entity with the passed values in the update function. – Brian Parker Aug 12 '23 at 00:24

1 Answers1

0

In this line you're iterating through an `Expense`` collection.

foreach(Expense expense in expenses)

which you almost certainly got by doing something like this against the DbContext.

var expenses = _dbContext.Expenses.Where(....);

So at this point the DbContext is tracking all the entities you've retrieved. All your "copies" of expense are just references to the same object held by EF.

You then do this:

        Expense check = await expenseService.GetExpensebyKey(expense.Id);

So your now tracking two copies of the same entity.

You then do this:

        _dbContext.Expenses.Update(expense);

Telling it about changes it almost certainly already knows about. It's tracking the expense instance from the original list query, and you're just passing it a reference to that object.

And try and do:

        await _dbContext.SaveChangesAsync();

EF is now pretty confused. It has at least two entities with the same Id in different states. Which one does it persist back to the database. Don't know, so it throws an error.

In Blazor, the normal solution is to use per transaction DbContexts that you get from a DbContextFactory. This also overcomes the issue in trying to run async operations against a single DbContext.

How to set up the factory is explained here - https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/#using-a-dbcontext-factory-eg-for-blazor

And in many Stackoverflow answers:

Your service class looks like this:

    private readonly IDbContextFactory<MyDbContext> _factory;

    public MyService(IDbContextFactory<MyDbContext> factory)
    {
        _factory = factory;
    }

And methods:

    public async Task<bool> UpdateExpenseDetails(Expense expense)
    {
        // create a DbContext scoped to the method
        using var dbContext = _factory.CreateDbContext();

        //check if the record exists
        var exists = dbcontext.Expenses.Any(x => x.Id == expense.Id));

        //Exit it it doesn't.
        // ?? Don't you need to do a add here?
        if (!exist)
            return false;

        // pass the modified expense into the DbContext
        dbContext.Expenses.Update(expense);

        // commit the transaction
        var transactions = await _dbContext.SaveChangesAsync();

        // check we did 1 update
        // You should consider returning some sort of result object with status and a mnesage if failed 
        return transactions == 1 ? true: false;
    }

Your List query can look something like this. I've purposely broken the query down to show how you can construct a multipart query before actually [materializing] executing it in ToListAsync(). EF will build the query and get only the data requested from the server.

        using var dbContext = _factory.CreateDbContext();
        // turn off tracking as we are only doing queries during this transaction
        dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

        IQueryable<Expense> query = dbContext.Set<TRecord>();

        query = query.Where(your query here);

        // example of adding paging to a query
         // if (request.PageSize > 0)
         //   query = query
         //       .Skip(request.StartIndex)
         //       .Take(request.PageSize);

        var list = query.ToListAsync();
MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • Thank you this helps a lot! I'm running into other errors using the factory but if I can't figure it out that'll be for a new question. – AGeist Aug 15 '23 at 18:38