EF6: saving to same entity using dbContext in async tasks

async-await asynchronous c# entity-framework-6

Question

I load an object/entity from the database:

var db = new DbContext();

//...

var item = db.Items.First();

Then I want to perform two asynchronous tasks, that when returned, update data on the item:

var task1 = Function1(db, item); 
var task2 = Function2(db, item); 
await Task.WhenAll(new Task[] { task1 , task2 });

The two functions will have some code that gets, sets & saves a (different) property on the item, like so:

var orderId = await CallApi();
item.OrderId = orderId;
db.Entry(item).State = EntityState.Modified;
await db.SaveChangesAsync();

However, as they are running asynchonously, I'm getting the error: A second operation started on this context before a previous asynchronous operation completed.

I tried newing up a dbContext in the Functions, but then I get the error An entity object cannot be referenced by multiple instances of IEntityChangeTracker.

I understand why I'm getting both of these errors, my question is: what coding pattern would best resolve this?


EDIT Ok, so the above was a simplified example. In reality, there is a lot more work that goes on in the functions, so it would be difficult to move all that logic out of the function and into the calling method. I also want that all that work to be self-contained.

The properties on the item that Function1 and Function2 update are discrete. I'm using this library to ensure that the save doesn't overwrite all the fields.

1
0
7/28/2018 7:20:45 AM

Accepted Answer

You can move the code

db.Entry(item).State = EntityState.Modified; 
await db.SaveChangesAsync();

after the Task.Whenall(). The job of those 2 functions is to update the properties of that object.

1
4/15/2019 3:17:17 PM

Popular Answer

Don't let two threads perform operations on the same dbContext. A dbContext keeps track of the fetched items. If two threads are fetching and changing items on the same DbContext at the same time, it might lead to undesired results.

Furthermore: if you fetched an Item, just change the properties you want. You don't need to set the state to Modified. A dbContext can check whether it has the original value or not.

So create functions that create their own DbContext, Fetch the requested data, change the data, save the data and finally Dispose the dbContext.

async Task Function1(...)
{
    using (var dbcontext = new MyDbContext(...))
    {
        // Start Fetching the item that must be changed; don't await yet
        var taskFetchItemToChange = dbContext.Items
            .Where(...)
            .FirstOrDefaultAsync();

        // Start fetching the orderId that must be changed; don't await yet
        var taskFetchOrderId = this.CallApiAsync();

        // await until both item and orderId are fetched:
        await Task.WhenAll(new Task[] {taskFetchItemToChange, taskFetchOrderId});

        var fetchedItemToChange = taskFetchItemToChange.Result;
        var fetchedOrderId = taskFetchOrderId.Result;
        fetchedItemToChange.OrderId = fetchedOrderId;
        // or do this in one big unreadable unmaintainable untestable step

        await dbContext.SaveChangesAsync();
    }
}

You will have a similar Function2, or maybe the same Function1 with different parameters.

var taskFunction1 = Function1();
var taskFunction2 = Function2();
await Task.WhenAll( new Task[] {taskFunction1, taskFunction2});


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow